diff --git a/enrollments-events/app/certs/fonts/SF-Pro.ttf b/enrollments-events/app/certs/fonts/SF-Pro.ttf
deleted file mode 100755
index 1e8aa63..0000000
Binary files a/enrollments-events/app/certs/fonts/SF-Pro.ttf and /dev/null differ
diff --git a/enrollments-events/app/certs/sample.html b/enrollments-events/app/certs/sample.html
index 65d1ef3..930cc3c 100644
--- a/enrollments-events/app/certs/sample.html
+++ b/enrollments-events/app/certs/sample.html
@@ -22,8 +22,8 @@
}
@font-face {
- font-family: "SF-Pro";
- src: url("fonts/SF-Pro.ttf") format("truetype");
+ font-family: "Arial";
+ src: url("fonts/Arial.ttf") format("truetype");
}
@page {
@@ -32,7 +32,7 @@
}
html {
- font-family: SF-Pro;
+ font-family: Arial;
font-size: 13pt;
line-height: 1.4;
}
@@ -115,7 +115,7 @@
-
diff --git a/order-events/Makefile b/order-events/Makefile
index dc7246f..b2a2b85 100644
--- a/order-events/Makefile
+++ b/order-events/Makefile
@@ -2,4 +2,4 @@ build:
sam build --use-container
deploy: build
- sam deploy --debug
+ sam deploy --debug
\ No newline at end of file
diff --git a/order-events/app/boto3clients.py b/order-events/app/boto3clients.py
index 3be85c6..b8cf4f5 100644
--- a/order-events/app/boto3clients.py
+++ b/order-events/app/boto3clients.py
@@ -11,3 +11,4 @@ def get_dynamodb_client():
dynamodb_client = get_dynamodb_client()
+s3_client = boto3.client('s3')
diff --git a/order-events/app/config.py b/order-events/app/config.py
index 5d3ab5d..f2203df 100644
--- a/order-events/app/config.py
+++ b/order-events/app/config.py
@@ -5,6 +5,8 @@ ORDER_TABLE: str = os.getenv('ORDER_TABLE') # type: ignore
COURSE_TABLE: str = os.getenv('COURSE_TABLE') # type: ignore
ENROLLMENT_TABLE: str = os.getenv('ENROLLMENT_TABLE') # type: ignore
+BUCKET_NAME: str = os.getenv('BUCKET_NAME') # type: ignore
+
# Post-migration: Remove the following lines
if os.getenv('AWS_LAMBDA_FUNCTION_NAME'):
SQLITE_DATABASE = 'courses_export_2025-06-18_110214.db'
diff --git a/order-events/app/events/billing/append_enrollment.py b/order-events/app/events/billing/append_enrollment.py
index 9d1fba0..81ca3d4 100644
--- a/order-events/app/events/billing/append_enrollment.py
+++ b/order-events/app/events/billing/append_enrollment.py
@@ -1,3 +1,5 @@
+import json
+import sqlite3
from datetime import datetime, time, timedelta
from aws_lambda_powertools import Logger
@@ -14,9 +16,16 @@ from layercake.dynamodb import (
TransactKey,
)
from layercake.funcs import pick
+from sqlite_utils import Database
from boto3clients import dynamodb_client
-from config import COURSE_TABLE, ENROLLMENT_TABLE, ORDER_TABLE
+from config import (
+ COURSE_TABLE,
+ ENROLLMENT_TABLE,
+ ORDER_TABLE,
+ SQLITE_DATABASE,
+ SQLITE_TABLE,
+)
from utils import get_billing_period
logger = Logger(__name__)
@@ -24,6 +33,8 @@ order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
course_layer = DynamoDBPersistenceLayer(COURSE_TABLE, dynamodb_client)
+sqlite3.register_converter('json', json.loads)
+
@event_source(data_class=EventBridgeEvent)
@logger.inject_lambda_context
@@ -41,6 +52,11 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
logger.debug('Enrollment not found')
return False
+ # Keep it until the migration has been completed
+ old_course = _get_course(data['course']['id'])
+ if old_course:
+ data['course'] = old_course
+
start_date, end_date = get_billing_period(
new_image['billing_day'],
year=created_at.year,
@@ -52,6 +68,8 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
end=end_date.isoformat(),
)
+ logger.info('Enrollment found', data=data)
+
try:
with order_layer.transact_writer() as transact:
transact.put(
@@ -78,47 +96,44 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
pass
try:
- with order_layer.transact_writer() as transact:
- author = data['author']
- course_id = data['course']['id']
- course = course_layer.collection.get_items(
- KeyPair(
- pk=course_id,
- sk=SortKey('0', path_spec='metadata__unit_price'),
- rename_key='unit_price',
- )
- + KeyPair(
- pk=f'CUSTOM_PRICING#ORG#{org_id}',
- sk=SortKey(f'COURSE#{course_id}', path_spec='unit_price'),
- rename_key='unit_price',
- ),
- flatten_top=False,
+ author = data['author']
+ course_id = data['course']['id']
+ course = course_layer.collection.get_items(
+ KeyPair(
+ pk=course_id,
+ sk=SortKey('0', path_spec='metadata__unit_price'),
+ rename_key='unit_price',
)
-
- transact.condition(
- key=KeyPair(pk, sk),
- cond_expr='attribute_exists(sk)',
- exc_cls=BillingNotFoundError,
- )
- transact.put(
- item={
- 'id': pk,
- 'sk': f'{sk}#ENROLLMENT#{enrollment_id}',
- 'user': pick(('id', 'name'), data['user']),
- 'course': pick(('id', 'name'), data['course']),
- 'unit_price': course['unit_price'],
- 'author': {
- 'id': author['user_id'],
- 'name': author['name'],
- },
- # Post-migration: uncomment the following line
- # 'enrolled_at': data['created_at'],
- 'enrolled_at': data['create_date'],
- 'created_at': now_,
+ + KeyPair(
+ pk=f'CUSTOM_PRICING#ORG#{org_id}',
+ sk=SortKey(f'COURSE#{course_id}', path_spec='unit_price'),
+ rename_key='unit_price',
+ ),
+ flatten_top=False,
+ )
+ order_layer.put_item(
+ item={
+ 'id': pk,
+ 'sk': f'{sk}#ENROLLMENT#{enrollment_id}',
+ 'user': pick(('id', 'name'), data['user']),
+ 'course': pick(('id', 'name'), data['course']),
+ 'unit_price': course['unit_price'],
+ 'author': {
+ 'id': author['user_id'],
+ 'name': author['name'],
},
- cond_expr='attribute_not_exists(sk)',
- )
- except Exception:
+ # Post-migration: uncomment the following line
+ # 'enrolled_at': data['created_at'],
+ 'enrolled_at': data['create_date'],
+ 'created_at': now_,
+ },
+ cond_expr='attribute_not_exists(sk)',
+ )
+ except Exception as exc:
+ logger.exception(
+ exc,
+ keypair={'pk': pk, 'sk': sk},
+ )
return False
else:
return True
@@ -128,3 +143,18 @@ class ExistingBillingConflictError(Exception): ...
class BillingNotFoundError(Exception): ...
+
+
+def _get_course(course_id: str) -> dict | None:
+ with sqlite3.connect(
+ database=SQLITE_DATABASE, detect_types=sqlite3.PARSE_DECLTYPES
+ ) as conn:
+ db = Database(conn)
+ rows = db[SQLITE_TABLE].rows_where(
+ "json->>'$.metadata__betaeducacao_id' = ?", [course_id]
+ )
+
+ for row in rows:
+ return row['json']
+
+ return None
diff --git a/order-events/app/events/billing/close_window.py b/order-events/app/events/billing/close_window.py
index 2797841..f53fc3e 100644
--- a/order-events/app/events/billing/close_window.py
+++ b/order-events/app/events/billing/close_window.py
@@ -1,68 +1,93 @@
-import locale
-import os
-from datetime import date
+import json
+import requests
from aws_lambda_powertools import Logger
+from aws_lambda_powertools.shared.json_encoder import Encoder
from aws_lambda_powertools.utilities.data_classes import (
EventBridgeEvent,
event_source,
)
from aws_lambda_powertools.utilities.typing import LambdaContext
-from jinja2 import Environment, FileSystemLoader
-from layercake.dateutils import fromisoformat
+from layercake.dateutils import now
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
-from weasyprint import HTML
-from boto3clients import dynamodb_client
-from config import ORDER_TABLE
+from boto3clients import dynamodb_client, s3_client
+from config import BUCKET_NAME, ORDER_TABLE
logger = Logger(__name__)
order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
-curdir = os.path.dirname(__file__)
-env = Environment(loader=FileSystemLoader(curdir))
-locale.setlocale(locale.LC_ALL, 'pt_BR.UTF-8')
-
-
-def currency(value: float | int) -> str:
- return locale.currency(value, grouping=True)
-
-
-def datetime_format(dt: date, fmt='%H:%M %d-%m-%y'):
- if isinstance(dt, str):
- dt = fromisoformat(dt) # type: ignore
-
- return dt.strftime(fmt)
-
-
-env.filters['datetime_format'] = datetime_format
-env.filters['currency'] = currency
@event_source(data_class=EventBridgeEvent)
@logger.inject_lambda_context
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
- new_image = event.detail['new_image']
- _, start_date, _, end_date, *_ = new_image['sk'].split('#')
+ keys = event.detail['keys']
+ now_ = now()
+ # Key pattern `BILLING#ORG#{org_id}`
+ *_, org_id = keys['id'].split('#')
+ # Key pattern `START#{start_date}#END#{end_date}#SCHEDULE#AUTO_CLOSE`
+ _, start_date, _, end_date, *_ = keys['sk'].split('#')
result = order_layer.collection.query(
KeyPair(
- pk=new_image['id'],
+ pk=keys['id'],
sk=f'START#{start_date}#END#{end_date}#ENROLLMENT',
),
)
- template = env.get_template('tmpl.html')
- html_rendered = template.render(
- start_date=start_date,
- end_date=end_date,
- items=result['items'],
+ r = requests.post(
+ 'https://weasyprint.saladeaula.digital',
+ data=json.dumps(
+ {
+ 'template_s3_uri': 's3://saladeaula.digital/billing/template.html',
+ 'template_vars': {
+ 'start_date': start_date,
+ 'end_date': end_date,
+ 'items': result['items'],
+ },
+ },
+ cls=Encoder,
+ ),
)
+ r.raise_for_status()
- HTML(string=html_rendered, base_url='').write_pdf('cert.pdf')
+ object_key = f'billing/{org_id}/{start_date}_{end_date}.pdf'
+ s3_uri = f's3://{BUCKET_NAME}/{object_key}'
- return order_layer.update_item(
- key=KeyPair(new_image['id'], new_image['sk']),
- update_expr='SET #status = :status',
- expr_attr_names={'#status': 'status'},
- expr_attr_values={':status': 'CLOSED'},
- )
+ try:
+ s3_client.put_object(
+ Bucket=BUCKET_NAME,
+ Key=object_key,
+ Body=r.content,
+ ContentType='application/pdf',
+ )
+ except Exception as exc:
+ logger.exception(exc)
+ raise
+
+ with order_layer.transact_writer() as transact:
+ transact.update(
+ key=KeyPair(
+ pk=keys['id'],
+ sk=f'START#{start_date}#END#{end_date}',
+ ),
+ update_expr='SET #status = :status, s3_uri = :s3_uri, \
+ updated_at = :updated_at',
+ expr_attr_names={'#status': 'status'},
+ expr_attr_values={
+ ':status': 'CLOSED',
+ ':s3_uri': s3_uri,
+ ':updated_at': now_,
+ },
+ cond_expr='attribute_exists(sk)',
+ )
+ transact.put(
+ item={
+ 'id': keys['id'],
+ 'sk': '{sk}#EXECUTED'.format(sk=keys['sk']),
+ 'created_at': now_,
+ }
+ )
+
+ logger.info(f'PDF uploaded successfully to {s3_uri}')
+ return True
diff --git a/order-events/app/events/billing/fonts/Arial.ttf b/order-events/app/events/billing/fonts/Arial.ttf
deleted file mode 100644
index ff0815c..0000000
Binary files a/order-events/app/events/billing/fonts/Arial.ttf and /dev/null differ
diff --git a/order-events/app/events/billing/tmpl.html b/order-events/app/events/billing/tmpl.html
deleted file mode 100644
index 8eeed4b..0000000
--- a/order-events/app/events/billing/tmpl.html
+++ /dev/null
@@ -1,200 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Matrículas realizadas entre
- {{ start_date|datetime_format('%d/%m/%Y') }} e
- {{ end_date|datetime_format('%d/%m/%Y') }}
-
-
-
-
-
- | Curso |
- Colaborador |
- Matrículado em |
- Valor unit. |
- Autor |
-
-
-
- {% for x in items %}
-
- | {{ x.course.name }} |
- {{ x.user.name }} |
-
- {{ x.enrolled_at|datetime_format('%d/%m/%Y, %H:%M')
- }}
- |
- {{ x.unit_price|currency }} |
- {{ x.author.name }} |
-
- {% endfor %}
-
-
-
-
-
diff --git a/order-events/template.yaml b/order-events/template.yaml
index 8f643c3..609c1d6 100644
--- a/order-events/template.yaml
+++ b/order-events/template.yaml
@@ -2,6 +2,9 @@ AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31
Parameters:
+ BucketName:
+ Type: String
+ Default: saladeaula.digital
UserTable:
Type: String
Default: betaeducacao-prod-users_d2o3r5gmm4it7j
@@ -35,6 +38,7 @@ Globals:
ORDER_TABLE: !Ref OrderTable
ENROLLMENT_TABLE: !Ref EnrollmentTable
COURSE_TABLE: !Ref CourseTable
+ BUCKET_NAME: !Ref BucketName
Resources:
EventLog:
@@ -45,7 +49,7 @@ Resources:
EventBillingAddEnrollmentFunction:
Type: AWS::Serverless::Function
Properties:
- Handler: events.billing.add_enrollment.lambda_handler
+ Handler: events.billing.append_enrollment.lambda_handler
LoggingConfig:
LogGroup: !Ref EventLog
Policies:
@@ -70,11 +74,14 @@ Resources:
Type: AWS::Serverless::Function
Properties:
Handler: events.billing.close_window.lambda_handler
+ Timeout: 26
LoggingConfig:
LogGroup: !Ref EventLog
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref OrderTable
+ - S3WritePolicy:
+ BucketName: !Ref BucketName
Events:
Event:
Type: EventBridgeRule
@@ -83,7 +90,7 @@ Resources:
resources: [!Ref OrderTable]
detail-type: [EXPIRE]
detail:
- new_image:
+ keys:
sk:
- suffix: SCHEDULE#AUTO_CLOSE
diff --git a/order-events/tests/conftest.py b/order-events/tests/conftest.py
index 50a2a2a..804e4bc 100644
--- a/order-events/tests/conftest.py
+++ b/order-events/tests/conftest.py
@@ -20,6 +20,7 @@ def pytest_configure():
os.environ['ENROLLMENT_TABLE'] = PYTEST_TABLE_NAME
os.environ['ORDER_TABLE'] = PYTEST_TABLE_NAME
os.environ['LOG_LEVEL'] = 'DEBUG'
+ os.environ['BUCKET_NAME'] = 'saladeaula.digital'
@dataclass
diff --git a/order-events/tests/events/billing/test_close_window.py b/order-events/tests/events/billing/test_close_window.py
index c5dd035..bb1cdba 100644
--- a/order-events/tests/events/billing/test_close_window.py
+++ b/order-events/tests/events/billing/test_close_window.py
@@ -1,10 +1,14 @@
from aws_lambda_powertools.utilities.typing import LambdaContext
-from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey
+from layercake.dynamodb import (
+ DynamoDBPersistenceLayer,
+ SortKey,
+ TransactKey,
+)
import events.billing.close_window as app
-def test_append_enrollment(
+def test_close_window(
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
@@ -22,8 +26,15 @@ def test_append_enrollment(
assert app.lambda_handler(event, lambda_context) # type: ignore
- # r = dynamodb_persistence_layer.collection.query(
- # PartitionKey('BILLING#ORG#edp8njvgQuzNkLx2ySNfAD')
+ # r = dynamodb_persistence_layer.collection.get_items(
+ # TransactKey('BILLING#ORG#BES6dmWgTMXRYmfDyYYXUF')
+ # + SortKey('START#2025-07-01#END#2025-07-31')
+ # + SortKey('START#2025-07-01#END#2025-07-31#SCHEDULE#AUTO_CLOSE#EXECUTED'),
+ # flatten_top=False,
# )
- # print(r)
+ # assert 's3_uri' in r['START#2025-07-01#END#2025-07-31']
+ # assert 'created_at' in r['START#2025-07-01#END#2025-07-31']
+ # assert 'updated_at' in r['START#2025-07-01#END#2025-07-31']
+ # assert r['START#2025-07-01#END#2025-07-31']['status'] == 'CLOSED'
+ # assert 'START#2025-07-01#END#2025-07-31#SCHEDULE#AUTO_CLOSE#EXECUTED' in r
diff --git a/streams-events/app/events/docs_into_eventbus.py b/streams-events/app/events/docs_into_eventbus.py
index d0166ab..c176fb1 100644
--- a/streams-events/app/events/docs_into_eventbus.py
+++ b/streams-events/app/events/docs_into_eventbus.py
@@ -68,7 +68,7 @@ def record_handler(record: DynamoDBRecord):
]
)
- logger.info('Event result', result=result)
+ logger.info('Event result', detail=detail, detail_type=detail_type, result=result)
@tracer.capture_lambda_handler
diff --git a/streams-events/template.yaml b/streams-events/template.yaml
index da18415..6d7674e 100644
--- a/streams-events/template.yaml
+++ b/streams-events/template.yaml
@@ -11,7 +11,7 @@ Globals:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:83
Environment:
Variables:
- LOG_LEVEL: DEBUG
+ LOG_LEVEL: INFO
TZ: America/Sao_Paulo
POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1
POWERTOOLS_LOGGER_LOG_EVENT: true