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 @@ @@ -181,8 +181,8 @@ de {{ progress }}%

-

Realizado entre {{ started_date }} e {{ finished_date }}

-

Florianópolis, SC, {{ today }}

+

Realizado entre {{ started_at }} e {{ completed_at }}

+

São José, SC, {{ today }}

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

- - - - - - - - - - - - - {% 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 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