diff --git a/enrollments-events/app/events/stopgap/set_terms_if_subscribed.py b/enrollments-events/app/events/stopgap/set_subscription_covered.py
similarity index 55%
rename from enrollments-events/app/events/stopgap/set_terms_if_subscribed.py
rename to enrollments-events/app/events/stopgap/set_subscription_covered.py
index cb03736..73dc34f 100644
--- a/enrollments-events/app/events/stopgap/set_terms_if_subscribed.py
+++ b/enrollments-events/app/events/stopgap/set_subscription_covered.py
@@ -22,6 +22,7 @@ enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
@logger.inject_lambda_context
def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
new_image = event.detail['new_image']
+ now_ = now()
data = user_layer.get_item(
# Post-migration: uncomment the following line
# KeyPair(new_image['org_id'], 'METADATA#BILLING_TERMS'),
@@ -32,16 +33,27 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
return False
try:
- enrollment_layer.put_item(
- item={
- 'id': new_image['id'],
- 'sk': 'METADATA#BILLING_TERMS',
- 'org_id': new_image['tenant_id'],
- 'billing_day': data['billing_day'],
- 'created_at': now(),
- },
- cond_expr='attribute_not_exists(sk)',
- )
+ with enrollment_layer.transact_writer() as transact:
+ transact.update(
+ key=KeyPair(new_image['id'], '0'),
+ update_expr='SET subscription_covered = :subscription_covered, \
+ updated_at = :updated_at',
+ expr_attr_values={
+ ':subscription_covered': True,
+ ':updated_at': now_,
+ },
+ cond_expr='attribute_exists(sk)',
+ )
+ transact.put(
+ item={
+ 'id': new_image['id'],
+ 'sk': 'METADATA#SUBSCRIPTION_COVERED',
+ 'org_id': new_image['tenant_id'],
+ 'billing_day': data['billing_day'],
+ 'created_at': now(),
+ },
+ cond_expr='attribute_not_exists(sk)',
+ )
except Exception:
return False
else:
diff --git a/enrollments-events/template.yaml b/enrollments-events/template.yaml
index 2777b2c..9a6810d 100644
--- a/enrollments-events/template.yaml
+++ b/enrollments-events/template.yaml
@@ -42,10 +42,10 @@ Resources:
Properties:
RetentionInDays: 90
- EventSetTermsIfSubscribedFunction:
+ EventSetSubscriptionCoveredFunction:
Type: AWS::Serverless::Function
Properties:
- Handler: events.stopgap.set_terms_if_subscribed.lambda_handler
+ Handler: events.stopgap.set_subscription_covered.lambda_handler
LoggingConfig:
LogGroup: !Ref EventLog
Policies:
diff --git a/order-events/app/events/billing/append_enrollment.py b/order-events/app/events/billing/append_enrollment.py
index bfd3d23..9d1fba0 100644
--- a/order-events/app/events/billing/append_enrollment.py
+++ b/order-events/app/events/billing/append_enrollment.py
@@ -6,7 +6,7 @@ from aws_lambda_powertools.utilities.data_classes import (
event_source,
)
from aws_lambda_powertools.utilities.typing import LambdaContext
-from layercake.dateutils import now, ttl
+from layercake.dateutils import fromisoformat, now, ttl
from layercake.dynamodb import (
DynamoDBPersistenceLayer,
KeyPair,
@@ -35,12 +35,17 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
data = enrollment_layer.collection.get_items(
TransactKey(enrollment_id) + SortKey('0') + SortKey('author')
)
+ created_at: datetime = fromisoformat(data['create_date']) # type: ignore
if not data:
logger.debug('Enrollment not found')
return False
- start_date, end_date = get_billing_period(new_image['billing_day'])
+ start_date, end_date = get_billing_period(
+ new_image['billing_day'],
+ year=created_at.year,
+ month=created_at.month,
+ )
pk = 'BILLING#ORG#{org_id}'.format(org_id=org_id)
sk = 'START#{start}#END#{end}'.format(
start=start_date.isoformat(),
diff --git a/order-events/app/events/billing/close_window.py b/order-events/app/events/billing/close_window.py
new file mode 100644
index 0000000..2797841
--- /dev/null
+++ b/order-events/app/events/billing/close_window.py
@@ -0,0 +1,68 @@
+import locale
+import os
+from datetime import date
+
+from aws_lambda_powertools import Logger
+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.dynamodb import DynamoDBPersistenceLayer, KeyPair
+from weasyprint import HTML
+
+from boto3clients import dynamodb_client
+from config import 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('#')
+
+ result = order_layer.collection.query(
+ KeyPair(
+ pk=new_image['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'],
+ )
+
+ HTML(string=html_rendered, base_url='').write_pdf('cert.pdf')
+
+ 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'},
+ )
diff --git a/order-events/app/events/billing/fonts/SF-Pro.ttf b/order-events/app/events/billing/fonts/SF-Pro.ttf
new file mode 100755
index 0000000..1e8aa63
Binary files /dev/null and b/order-events/app/events/billing/fonts/SF-Pro.ttf differ
diff --git a/order-events/app/events/billing/tmpl.html b/order-events/app/events/billing/tmpl.html
new file mode 100644
index 0000000..74e5f49
--- /dev/null
+++ b/order-events/app/events/billing/tmpl.html
@@ -0,0 +1,200 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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 be407a4..8f643c3 100644
--- a/order-events/template.yaml
+++ b/order-events/template.yaml
@@ -64,7 +64,28 @@ Resources:
detail-type: [INSERT]
detail:
new_image:
- sk: ["METADATA#BILLING_TERMS"]
+ sk: ["METADATA#SUBSCRIPTION_COVERED"]
+
+ EventBillingCloseWindowFunction:
+ Type: AWS::Serverless::Function
+ Properties:
+ Handler: events.billing.close_window.lambda_handler
+ LoggingConfig:
+ LogGroup: !Ref EventLog
+ Policies:
+ - DynamoDBCrudPolicy:
+ TableName: !Ref OrderTable
+ Events:
+ Event:
+ Type: EventBridgeRule
+ Properties:
+ Pattern:
+ resources: [!Ref OrderTable]
+ detail-type: [EXPIRE]
+ detail:
+ new_image:
+ sk:
+ - suffix: SCHEDULE#AUTO_CLOSE
EventAppendOrgIdFunction:
Type: AWS::Serverless::Function
@@ -128,7 +149,7 @@ Resources:
Policies:
- DynamoDBWritePolicy:
TableName: !Ref OrderTable
- - DynamoDBWritePolicy:
+ - DynamoDBCrudPolicy:
TableName: !Ref EnrollmentTable
Events:
Event:
diff --git a/order-events/tests/events/reporting/__init__.py b/order-events/tests/events/billing/__init__.py
similarity index 100%
rename from order-events/tests/events/reporting/__init__.py
rename to order-events/tests/events/billing/__init__.py
diff --git a/order-events/tests/events/reporting/test_add_item.py b/order-events/tests/events/billing/test_append_enrollment.py
similarity index 63%
rename from order-events/tests/events/reporting/test_add_item.py
rename to order-events/tests/events/billing/test_append_enrollment.py
index 9644500..0f0b79a 100644
--- a/order-events/tests/events/reporting/test_add_item.py
+++ b/order-events/tests/events/billing/test_append_enrollment.py
@@ -1,10 +1,10 @@
from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey
-import events.reporting.add_item as app
+import events.billing.append_enrollment as app
-def test_add_item(
+def test_append_enrollment(
dynamodb_seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
lambda_context: LambdaContext,
@@ -13,7 +13,7 @@ def test_add_item(
'detail': {
'new_image': {
'id': '945e8672-1d72-45c6-b76c-ac06aa8b52ab',
- 'sk': 'METADATA#BILLING_TERMS',
+ 'sk': 'METADATA#SUBSCRIPTION_COVERED',
'billing_day': 5,
'created_at': '2025-07-23T18:09:22.785678-03:00',
'org_id': 'edp8njvgQuzNkLx2ySNfAD',
@@ -26,5 +26,11 @@ def test_add_item(
r = dynamodb_persistence_layer.collection.query(
PartitionKey('BILLING#ORG#edp8njvgQuzNkLx2ySNfAD')
)
+ items = r['items']
- print(r)
+ assert items[0]['sk'] == 'START#2025-06-05#END#2025-07-04#SCHEDULE#AUTO_CLOSE'
+ assert (
+ items[1]['sk']
+ == 'START#2025-06-05#END#2025-07-04#ENROLLMENT#945e8672-1d72-45c6-b76c-ac06aa8b52ab'
+ )
+ assert items[2]['sk'] == 'START#2025-06-05#END#2025-07-04'
diff --git a/order-events/tests/events/billing/test_close_window.py b/order-events/tests/events/billing/test_close_window.py
new file mode 100644
index 0000000..c5dd035
--- /dev/null
+++ b/order-events/tests/events/billing/test_close_window.py
@@ -0,0 +1,29 @@
+from aws_lambda_powertools.utilities.typing import LambdaContext
+from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey
+
+import events.billing.close_window as app
+
+
+def test_append_enrollment(
+ dynamodb_seeds,
+ dynamodb_persistence_layer: DynamoDBPersistenceLayer,
+ lambda_context: LambdaContext,
+):
+ event = {
+ 'detail': {
+ 'new_image': {
+ 'id': 'BILLING#ORG#BES6dmWgTMXRYmfDyYYXUF',
+ 'sk': 'START#2025-07-01#END#2025-07-31#SCHEDULE#AUTO_CLOSE',
+ 'created_at': '2025-07-24T15:20:52.464244-03:00',
+ 'ttl': 1754017200,
+ }
+ }
+ }
+
+ assert app.lambda_handler(event, lambda_context) # type: ignore
+
+ # r = dynamodb_persistence_layer.collection.query(
+ # PartitionKey('BILLING#ORG#edp8njvgQuzNkLx2ySNfAD')
+ # )
+
+ # print(r)
diff --git a/order-events/tests/seeds.jsonl b/order-events/tests/seeds.jsonl
index 5d7a647..fae2423 100644
--- a/order-events/tests/seeds.jsonl
+++ b/order-events/tests/seeds.jsonl
@@ -14,4 +14,7 @@
{"id": {"S": "945e8672-1d72-45c6-b76c-ac06aa8b52ab"}, "sk": {"S": "0"}, "course": {"M": {"id": {"S": "123"}, "name": {"S": "pytest"}}}, "user": {"M": {"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "name": {"S": "Sérgio R Siqueira"}}}, "create_date": {"S": "2025-06-05T12:13:54.371416+00:00"}}
{"id": {"S": "945e8672-1d72-45c6-b76c-ac06aa8b52ab"}, "sk": {"S": "author"}, "name": {"S": "Carolina Brand"}, "user_id": {"S": "SMEXYk5MQkKCzknJpxqr8n"}}
{"id": {"S": "CUSTOM_PRICING#ORG#edp8njvgQuzNkLx2ySNfAD"},"sk": {"S": "COURSE#123"},"created_at": {"S": "2025-07-24T16:10:09.304073-03:00"},"unit_price": {"N": "79.2"}}
-{"id": {"S": "123"},"sk": {"S": "0"},"access_period": {"N": "360"},"cert": {"M": {"exp_interval": {"N": "360"}}},"created_at": {"S": "2024-12-30T00:33:33.088916-03:00"},"metadata__konviva_class_id": {"N": "194"},"metadata__unit_price": {"N": "99"},"name": {"S": "Direção Defensiva (08 horas)"},"tenant_id": {"S": "*"},"updated_at": {"S": "2025-07-24T00:00:24.639003-03:00"}}
\ No newline at end of file
+{"id": {"S": "123"},"sk": {"S": "0"},"access_period": {"N": "360"},"cert": {"M": {"exp_interval": {"N": "360"}}},"created_at": {"S": "2024-12-30T00:33:33.088916-03:00"},"metadata__konviva_class_id": {"N": "194"},"metadata__unit_price": {"N": "99"},"name": {"S": "Direção Defensiva (08 horas)"},"tenant_id": {"S": "*"},"updated_at": {"S": "2025-07-24T00:00:24.639003-03:00"}}
+{"id": {"S": "BILLING#ORG#BES6dmWgTMXRYmfDyYYXUF"},"sk": {"S": "START#2025-07-01#END#2025-07-31"},"created_at": {"S": "2025-07-24T15:20:52.464244-03:00"},"status": {"S": "PENDING"}}
+{"id": {"S": "BILLING#ORG#BES6dmWgTMXRYmfDyYYXUF"},"sk": {"S": "START#2025-07-01#END#2025-07-31#ENROLLMENT#a08c94a2-7ee4-45fd-bfe7-73568c738b8b"},"author": {"M": {"id": {"S": "SMEXYk5MQkKCzknJpxqr8n"},"name": {"S": "Carolina Brand"}}},"course": {"M": {"id": {"S": "7f7905aa-ec6d-4189-b884-50fa9b1bd0b8"},"name": {"S": "NR-10 Reciclagem: 08 horas"}}},"created_at": {"S": "2025-07-24T16:38:33.095216-03:00"},"enrolled_at": {"S": "2025-07-24T11:26:56.975207-03:00"},"unit_price": {"N": "169"},"user": {"M": {"id": {"S": "iPWidwn4HsYtikiZD33smV"},"name": {"S": "William da Silva Nascimento"}}}}
+{"id": {"S": "BILLING#ORG#BES6dmWgTMXRYmfDyYYXUF"},"sk": {"S": "START#2025-07-01#END#2025-07-31#ENROLLMENT#ac09e8da-6cb2-4e31-84e7-238df2647a7a"},"author": {"M": {"id": {"S": "SMEXYk5MQkKCzknJpxqr8n"},"name": {"S": "Carolina Brand"}}},"course": {"M": {"id": {"S": "7f7905aa-ec6d-4189-b884-50fa9b1bd0b8"},"name": {"S": "NR-10 Reciclagem: 08 horas"}}},"created_at": {"S": "2025-07-24T16:38:58.694031-03:00"},"enrolled_at": {"S": "2025-07-24T11:26:56.913746-03:00"},"unit_price": {"N": "169"},"user": {"M": {"id": {"S": "ca8c9fca-b508-4842-8a48-fd5cc5632ac0"},"name": {"S": "Geovane Soares De Lima"}}}}
\ No newline at end of file