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') }} +

+ + + + + + + + + + + + + {% for x in items %} + + + + + + + + {% endfor %} + +
CursoColaboradorMatrículado emValor unit.Autor
{{ x.course.name }}{{ x.user.name }} + {{ x.enrolled_at|datetime_format('%d/%m/%Y, %H:%M') + }} + {{ x.unit_price|currency }}{{ x.author.name }}
+
+ + 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