From 208c43f398534f2cb3bdc1c905af45199f8e173e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Rafael=20Siqueira?= Date: Thu, 3 Jul 2025 15:36:30 -0300 Subject: [PATCH] fix --- README.md | 29 ++++--- enrollments-events/app/config.py | 1 - http-api/app/rules/enrollment.py | 8 +- order-events/app/config.py | 9 +++ .../app/courses_export_2025-06-18_110214.db | Bin 0 -> 159744 bytes order-events/app/events/assign_tenant_cnpj.py | 30 ++++--- ...anceled.py => remove_slots_if_canceled.py} | 9 ++- .../app/events/stopgap/patch_items.py | 73 ++++++++++++++++++ .../app/events/stopgap/remove_slots.py | 4 +- .../app/events/stopgap/set_as_paid.py | 9 ++- order-events/template.yaml | 30 ++++++- order-events/tests/conftest.py | 1 + .../tests/events/stopgap/test_patch_items.py | 35 +++++++++ .../tests/events/test_assign_tenant.py | 28 ------- .../tests/events/test_assign_tenant_cnpj.py | 3 +- ...ed.py => test_remove_slots_if_canceled.py} | 4 +- order-events/tests/seeds.jsonl | 6 +- order-events/uv.lock | 60 +++++++++++++- 18 files changed, 261 insertions(+), 78 deletions(-) create mode 100644 order-events/app/courses_export_2025-06-18_110214.db rename order-events/app/events/{remove_slots_on_canceled.py => remove_slots_if_canceled.py} (91%) create mode 100644 order-events/app/events/stopgap/patch_items.py create mode 100644 order-events/tests/events/stopgap/test_patch_items.py delete mode 100644 order-events/tests/events/test_assign_tenant.py rename order-events/tests/events/{test_remove_slots_on_canceled.py => test_remove_slots_if_canceled.py} (89%) diff --git a/README.md b/README.md index d926f08..c8461c9 100644 --- a/README.md +++ b/README.md @@ -7,18 +7,17 @@ Toda compra é relacionada a empresa responsável, que é definida como o `tenan O gestor responsável pela ação também é relacionado à compra, com base no email presente na compra. ```json -{"id": "10", "sk": "0", "user": {"id": "123", "name": "Sérgio"}, "tenant": "100"} -{"id": "10", "sk": "linked_entities#org", "org_id": "111"} -{"id": "10", "sk": "linked_entities#user", "user_id": "123"} -{"id": "10", "sk": "slots", "status": "PENDING", "mode": "BATCH"} -{"id": "10", "sk": "slots#enrollment#11", "status": "SUCCESS"} -{"id": "10", "sk": "slots#enrollment#12", "status": "ROLLBACK"} +{"id": "101", "sk": "0", "name": "EDUSEG", "cnpj": "15608435000190", "tenant": "100"} +{"id": "101", "sk": "author", "name": "Sérgio", "email": "sergio@somosbeta.com.br", "user_id": "123"} +{"id": "101", "sk": "slots", "status": "PENDING", "mode": "BATCH"} +{"id": "101", "sk": "slots#enrollment#9omWNKymwU5U4aeun6mWzZ", "status": "SUCCESS"} +{"id": "101", "sk": "slots#enrollment#12", "status": "ROLLBACK"} ``` Quando o responsável é uma pessoa física (CPF). ```json -{"id": "20", "sk": "0", "user": {"id": "123", "name": "Sérgio"}, "tenant": "self"} +{"id": "20", "sk": "0", "name": "Sérgio", "email": "sergio@somosbeta.com.br", "cpf": "07879819908", "tenant": "123"} {"id": "20", "sk": "slots", "status": "PENDING", "mode": "STANDALONE"} {"id": "20", "sk": "slots#enrollment#1123", "status": "SUCCESS"} ``` @@ -38,7 +37,15 @@ Quando o responsável é uma pessoa física (CPF). {"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "failed", "failed_at": "2025-04-06T11:07:32.762178-03:00"} {"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "canceled", "canceled_at": "2025-04-06T11:07:32.762178-03:00"} {"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "archived", "archived_at": "2025-04-06T11:07:32.762178-03:00"} -{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "expired", "expired": "2025-04-06T11:07:32.762178-03:00"} +{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "expired", "expired_at": "2025-04-06T11:07:32.762178-03:00"} +{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "linked_entities#order", "order_id": "101"} +``` + +### Vagas + +```json +{"id": "slots#org#100", "sk": "order#101#faa8a547-bb9b-4103-bd8c-8fbe96b4056f", "course": {"name": "pytest"}} +{"id": "slots#org#100", "sk": "order#101#afffbdde-fe58-4df7-b4d5-7553a571d32a", "course": {"name": "pytest"}} ``` ### Emails/eventos agendados @@ -80,14 +87,14 @@ Se um certificado for emitido para a matrícula, o período de proteção será ### Política de cancelamento -Apenas matrículas com `metadata#cancel_policy` podem ser canceladas. +Apenas matrículas com `cancel_policy` podem ser canceladas. -Se houver `metadata#parent_slot`, +Se houver `metadata#parent_slot`, deve ser devolvido. ```json {"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "0"} {"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "cancel_policy"} -{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "metadata#parent_slot", "slot": {"id": "slots#123", "sk": "1221#f7120daf-96d2-4639-b8f4-d736fd99e4ee"}} +{"id": "9omWNKymwU5U4aeun6mWzZ", "sk": "metadata#parent_slot", "slot": {"id": "slots#org#123", "sk": "1221#f7120daf-96d2-4639-b8f4-d736fd99e4ee"}} ``` # Cursos diff --git a/enrollments-events/app/config.py b/enrollments-events/app/config.py index 9aff6e7..bdf9d6d 100644 --- a/enrollments-events/app/config.py +++ b/enrollments-events/app/config.py @@ -13,4 +13,3 @@ else: SQLITE_DATABASE = 'app/courses_export_2025-06-18_110214.db' SQLITE_TABLE = 'courses' -OLD_ENROLLMENT_TABLE: str = os.getenv('OLD_ENROLLMENT_TABLE') # type: ignore diff --git a/http-api/app/rules/enrollment.py b/http-api/app/rules/enrollment.py index 30515d6..c09b9b1 100644 --- a/http-api/app/rules/enrollment.py +++ b/http-api/app/rules/enrollment.py @@ -263,13 +263,13 @@ def set_status_as_canceled( with persistence_layer.transact_writer() as transact: transact.update( key=KeyPair(id, '0'), - update_expr='SET #status = :canceled, updated_at = :update', + update_expr='SET #status = :canceled, updated_at = :updated_at', expr_attr_names={ '#status': 'status', }, expr_attr_values={ ':canceled': 'CANCELED', - ':update': now_, + ':updated_at': now_, }, ) transact.put( @@ -321,13 +321,13 @@ def set_status_as_canceled( # Post-migration: uncomment the following line # key=KeyPair(order_id, f'slots#enrollment#{enrollment_id}'), key=KeyPair(order_id, f'generated_items#{enrollment_id}'), - update_expr='SET #status = :status, update_date = :update', + update_expr='SET #status = :status, updated_at = :updated_at', expr_attr_names={ '#status': 'status', }, expr_attr_values={ ':status': 'ROLLBACK', - ':update': now_, + ':updated_at': now_, }, cond_expr='attribute_exists(sk)', table_name=ORDER_TABLE, diff --git a/order-events/app/config.py b/order-events/app/config.py index ad958ca..7b13358 100644 --- a/order-events/app/config.py +++ b/order-events/app/config.py @@ -2,4 +2,13 @@ import os USER_TABLE: str = os.getenv('USER_TABLE') # type: ignore 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 + +# Post-migration: remove the lines below +if os.getenv('AWS_LAMBDA_FUNCTION_NAME'): + SQLITE_DATABASE = 'courses_export_2025-06-18_110214.db' +else: + SQLITE_DATABASE = 'app/courses_export_2025-06-18_110214.db' + +SQLITE_TABLE = 'courses' diff --git a/order-events/app/courses_export_2025-06-18_110214.db b/order-events/app/courses_export_2025-06-18_110214.db new file mode 100644 index 0000000000000000000000000000000000000000..97681f070d34341fc1bb29dc7414e11e13609496 GIT binary patch literal 159744 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 bool: with order_layer.transact_writer() as transact: transact.update( key=KeyPair(new_image['id'], '0'), - update_expr='SET metadata__tenant_id = :tenant_id, \ - metadata__related_ids = :related_ids, \ - update_date = :update_date', + update_expr='SET tenant = :tenant_id, \ + updated_at = :updated_at', expr_attr_values={ ':tenant_id': ids['org_id'], - ':related_ids': set(ids.values()), - ':update_date': now_, + ':updated_at': now_, }, ) + + transact.update( + key=KeyPair(new_image['id'], 'author'), + update_expr='SET user_id = :user_id, updated_at = :updated_at', + expr_attr_values={ + ':user_id': ids['user_id'], + ':updated_at': now_, + }, + ) + + # Post-migration: remove the following line transact.put( item={ 'id': new_image['id'], @@ -64,15 +73,4 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: } ) - for k, v in ids.items(): - kind = k.removesuffix('_id') - transact.put( - item={ - 'id': new_image['id'], - 'sk': f'related_ids#{kind}', # e.g. related_ids#user - 'create_date': now_, - k: v, - } - ) - return True diff --git a/order-events/app/events/remove_slots_on_canceled.py b/order-events/app/events/remove_slots_if_canceled.py similarity index 91% rename from order-events/app/events/remove_slots_on_canceled.py rename to order-events/app/events/remove_slots_if_canceled.py index 51823a4..22726e4 100644 --- a/order-events/app/events/remove_slots_on_canceled.py +++ b/order-events/app/events/remove_slots_if_canceled.py @@ -29,13 +29,17 @@ class TenantDoesNotExistError(Exception): def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: new_image = event.detail['new_image'] order_id = new_image['id'] + # Post-migration: remove the following line tenant_id = order_layer.collection.get_item( KeyPair( order_id, SortKey('metadata#tenant', path_spec='tenant_id'), ), exc_cls=TenantDoesNotExistError, - ) + ).removeprefix('ORG#') + + # Post-migration: uncomment the following line + # tenant_id = new_image['tenant'] result = enrollment_layer.collection.query( KeyPair( @@ -45,11 +49,12 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: order_id, ) ) + with enrollment_layer.batch_writer() as batch: for pair in result['items']: batch.delete_item( Key={ - # Post-migration: rename `vacancies` to `slots` + # Post-migration: rename `vacancies` to `slots#org` 'id': {'S': ComposeKey(pair['id'], prefix='vacancies')}, 'sk': {'S': pair['sk']}, } diff --git a/order-events/app/events/stopgap/patch_items.py b/order-events/app/events/stopgap/patch_items.py new file mode 100644 index 0000000..0b4203c --- /dev/null +++ b/order-events/app/events/stopgap/patch_items.py @@ -0,0 +1,73 @@ +import json +import sqlite3 + +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 layercake.dateutils import now +from layercake.dynamodb import ( + DynamoDBPersistenceLayer, + KeyPair, +) +from sqlite_utils import Database + +from boto3clients import dynamodb_client +from config import ORDER_TABLE, SQLITE_DATABASE, SQLITE_TABLE + +sqlite3.register_converter('json', json.loads) + +logger = Logger(__name__) +order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client) + + +@event_source(data_class=EventBridgeEvent) +@logger.inject_lambda_context +def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: + new_image = event.detail['new_image'] + now_ = now() + items = new_image['items'] + new_items: list[dict] = [] + + for item in items: + course = _get_course(item['id']) + + new_items.append( + item + | ( + { + 'id': course.get('metadata__betaeducacao_id'), + } + if course + else {} + ) + ) + + order_layer.update_item( + key=KeyPair(new_image['id'], 'items'), + update_expr='SET #items = :items, updated_at = :updated_at', + expr_attr_names={'#items': 'items'}, + expr_attr_values={':items': new_items, ':updated_at': now_}, + ) + + return True + + +class CourseNotFoundError(Exception): + def __init__(self, *args): + super().__init__('Course not found') + + +def _get_course(course_id: str) -> dict: + with sqlite3.connect( + database=SQLITE_DATABASE, detect_types=sqlite3.PARSE_DECLTYPES + ) as conn: + db = Database(conn) + rows = db[SQLITE_TABLE].rows_where('id = ?', [course_id]) + + for row in rows: + return row['json'] + + raise CourseNotFoundError diff --git a/order-events/app/events/stopgap/remove_slots.py b/order-events/app/events/stopgap/remove_slots.py index b1e2c70..ae5207d 100644 --- a/order-events/app/events/stopgap/remove_slots.py +++ b/order-events/app/events/stopgap/remove_slots.py @@ -24,8 +24,8 @@ enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) @event_source(data_class=EventBridgeEvent) @logger.inject_lambda_context def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: - """Remove slots if the Tenant has a `metadata#billing_policy` and - the order is positive.""" + """Remove slots if the tenant has a `metadata#billing_policy` and + the total is greater than zero.""" new_image = event.detail['new_image'] order_id = new_image['id'] data = order_layer.collection.get_items( diff --git a/order-events/app/events/stopgap/set_as_paid.py b/order-events/app/events/stopgap/set_as_paid.py index 1edb1c2..605ec67 100644 --- a/order-events/app/events/stopgap/set_as_paid.py +++ b/order-events/app/events/stopgap/set_as_paid.py @@ -20,26 +20,27 @@ order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client) @event_source(data_class=EventBridgeEvent) @logger.inject_lambda_context def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: + """Set to `PAID` if the status is `PENDING` and the total is zero.""" new_image = event.detail['new_image'] now_ = now() with order_layer.transact_writer() as transact: transact.update( key=KeyPair(new_image['id'], '0'), - update_expr='SET #status = :status, update_date = :update_date', + update_expr='SET #status = :status, updated_at = :updated_at', expr_attr_names={ '#status': 'status', }, expr_attr_values={ ':status': 'PAID', - ':update_date': now_, + ':updated_at': now_, }, ) transact.put( item={ 'id': new_image['id'], - 'sk': 'paid_date', - 'create_date': now_, + 'sk': 'paid_at', + 'paid_at': now_, } ) diff --git a/order-events/template.yaml b/order-events/template.yaml index dad47bb..8aa5f29 100644 --- a/order-events/template.yaml +++ b/order-events/template.yaml @@ -11,6 +11,9 @@ Parameters: OrderTable: Type: String Default: betaeducacao-prod-orders + CourseTable: + Type: String + Default: saladeaula_courses Globals: Function: @@ -31,6 +34,7 @@ Globals: USER_TABLE: !Ref UserTable ORDER_TABLE: !Ref OrderTable ENROLLMENT_TABLE: !Ref EnrollmentTable + COURSE_TABLE: !Ref CourseTable Resources: EventLog: @@ -64,10 +68,10 @@ Resources: metadata__tenant_id: - exists: false - EventRemoveSlotsOnCanceledFunction: + EventRemoveSlotsIfCanceledFunction: Type: AWS::Serverless::Function Properties: - Handler: events.delete_slots_on_canceled.lambda_handler + Handler: events.delete_slots_if_canceled.lambda_handler LoggingConfig: LogGroup: !Ref EventLog Policies: @@ -89,6 +93,28 @@ Resources: - exists: true status: [CANCELED, EXPIRED] + EventPatchItemsFunction: + Type: AWS::Serverless::Function + Properties: + Handler: events.stopgap.patch_items.lambda_handler + LoggingConfig: + LogGroup: !Ref EventLog + Policies: + - DynamoDBWritePolicy: + TableName: !Ref OrderTable + - DynamoDBReadPolicy: + TableName: !Ref CourseTable + Events: + Event: + Type: EventBridgeRule + Properties: + Pattern: + resources: [!Ref OrderTable] + detail-type: [INSERT] + detail: + new_image: + sk: ["items"] + EventSetAsPaidFunction: Type: AWS::Serverless::Function Properties: diff --git a/order-events/tests/conftest.py b/order-events/tests/conftest.py index 0a35607..fc62764 100644 --- a/order-events/tests/conftest.py +++ b/order-events/tests/conftest.py @@ -18,6 +18,7 @@ def pytest_configure(): os.environ['COURSE_TABLE'] = PYTEST_TABLE_NAME os.environ['ENROLLMENT_TABLE'] = PYTEST_TABLE_NAME os.environ['ORDER_TABLE'] = PYTEST_TABLE_NAME + os.environ['COURSE_TABLE'] = PYTEST_TABLE_NAME @dataclass diff --git a/order-events/tests/events/stopgap/test_patch_items.py b/order-events/tests/events/stopgap/test_patch_items.py new file mode 100644 index 0000000..f8bce53 --- /dev/null +++ b/order-events/tests/events/stopgap/test_patch_items.py @@ -0,0 +1,35 @@ +from decimal import Decimal + +import app.events.stopgap.patch_items as app +from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair + +from ...conftest import LambdaContext + + +def test_patch_items( + dynamodb_seeds, + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + lambda_context: LambdaContext, +): + event = { + 'detail': { + 'new_image': { + 'id': '9omWNKymwU5U4aeun6mWzZ', + 'items': [ + { + 'id': 'a810dd22-56c0-4d9b-8cd2-7e2ee9c45839', + 'name': 'pytest', + 'quantity': 17, + 'unit_price': Decimal('87.2'), + }, + ], + } + }, + } + assert app.lambda_handler(event, lambda_context) # type: ignore + + result = dynamodb_persistence_layer.collection.get_item( + KeyPair('9omWNKymwU5U4aeun6mWzZ', 'items') + ) + + assert result['items'][0]['id'] == 'dc1a0428-47bf-4db1-a5da-24be49c9fda6' diff --git a/order-events/tests/events/test_assign_tenant.py b/order-events/tests/events/test_assign_tenant.py deleted file mode 100644 index ccb36cb..0000000 --- a/order-events/tests/events/test_assign_tenant.py +++ /dev/null @@ -1,28 +0,0 @@ -from aws_lambda_powertools.utilities.typing import LambdaContext -from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey - -import events.assign_tenant_cnpj as app - - -def test_assign_tenant_cnpj( - dynamodb_seeds, - dynamodb_persistence_layer: DynamoDBPersistenceLayer, - lambda_context: LambdaContext, -): - event = { - 'detail': { - 'new_image': { - 'id': '9omWNKymwU5U4aeun6mWzZ', - 'cnpj': '15608435000190', - 'email': 'sergio@somosbeta.com.br', - } - } - } - - assert app.lambda_handler(event, lambda_context) # type: ignore - - result = dynamodb_persistence_layer.collection.query( - PartitionKey('9omWNKymwU5U4aeun6mWzZ') - ) - - assert 4 == len(result['items']) diff --git a/order-events/tests/events/test_assign_tenant_cnpj.py b/order-events/tests/events/test_assign_tenant_cnpj.py index bdbb2c9..93094bd 100644 --- a/order-events/tests/events/test_assign_tenant_cnpj.py +++ b/order-events/tests/events/test_assign_tenant_cnpj.py @@ -25,5 +25,4 @@ def test_assign_tenant_cnpj( PartitionKey('9omWNKymwU5U4aeun6mWzZ') ) - assert 4 == len(result['items']) - print(result['items']) + assert 3 == len(result['items']) diff --git a/order-events/tests/events/test_remove_slots_on_canceled.py b/order-events/tests/events/test_remove_slots_if_canceled.py similarity index 89% rename from order-events/tests/events/test_remove_slots_on_canceled.py rename to order-events/tests/events/test_remove_slots_if_canceled.py index ff921fe..709d200 100644 --- a/order-events/tests/events/test_remove_slots_on_canceled.py +++ b/order-events/tests/events/test_remove_slots_if_canceled.py @@ -1,10 +1,10 @@ from aws_lambda_powertools.utilities.typing import LambdaContext from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey -import events.remove_slots_on_canceled as app +import events.remove_slots_if_canceled as app -def test_delete_slots_on_canceled( +def test_remove_slots_if_canceled( dynamodb_seeds, dynamodb_persistence_layer: DynamoDBPersistenceLayer, lambda_context: LambdaContext, diff --git a/order-events/tests/seeds.jsonl b/order-events/tests/seeds.jsonl index 2f87c24..bd070f3 100644 --- a/order-events/tests/seeds.jsonl +++ b/order-events/tests/seeds.jsonl @@ -1,10 +1,12 @@ {"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "metadata#payment_policy"}, "due_days": {"N": "90"}} {"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "metadata#billing_policy"}, "billing_day": {"N": "1"}, "payment_method": {"S": "PIX"}} {"id": {"S": "9omWNKymwU5U4aeun6mWzZ"}, "sk": {"S": "0"}, "total": {"N": "398"}, "status": {"S": "PENDING"}} -{"id": {"S": "9omWNKymwU5U4aeun6mWzZ"}, "sk": {"S": "metadata#tenant"}, "tenant_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}} +{"id": {"S": "9omWNKymwU5U4aeun6mWzZ"}, "sk": {"S": "metadata#tenant"}, "tenant_id": {"S": "ORG#cJtK9SsnJhKPyxESe7g3DG"}} {"id": {"S": "cnpj"}, "sk": {"S": "15608435000190"}, "user_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}} {"id": {"S": "email"}, "sk": {"S": "sergio@somosbeta.com.br"}, "user_id": {"S": "5OxmMjL-ujoR5IMGegQz"}} {"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "0"}, "name": {"S": "Sérgio R Siqueira"}} {"id": {"S": "vacancies#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "9omWNKymwU5U4aeun6mWzZ#1"}} {"id": {"S": "vacancies#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "9omWNKymwU5U4aeun6mWzZ#2"}} -{"id": {"S": "vacancies#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "9omWNKymwU5U4aeun6mWzZ#3"}} \ No newline at end of file +{"id": {"S": "vacancies#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "9omWNKymwU5U4aeun6mWzZ#3"}} +{"id": {"S": "6a60d026-d383-4707-b093-b6eddea1a24e"}, "sk": {"S": "items"},"items": {"L": [{"M": {"id": {"S": "a810dd22-56c0-4d9b-8cd2-7e2ee9c45839"}, "name": {"S": "pytest"},"quantity": {"N": "1"},"unit_price": {"N": "109"}}}]}} +{"id": {"S": "a810dd22-56c0-4d9b-8cd2-7e2ee9c45839"}, "sk": {"S": "metadata#betaeducacao"},"course_id": {"S": "dc1a0428-47bf-4db1-a5da-24be49c9fda6"},"create_date": {"S": "2025-06-05T12:13:54.371416+00:00"}} \ No newline at end of file diff --git a/order-events/uv.lock b/order-events/uv.lock index e5424f7..7a12739 100644 --- a/order-events/uv.lock +++ b/order-events/uv.lock @@ -484,6 +484,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + [[package]] name = "jmespath" version = "1.0.1" @@ -519,7 +531,7 @@ wheels = [ [[package]] name = "layercake" -version = "0.6.11" +version = "0.6.12" source = { directory = "../layercake" } dependencies = [ { name = "arnparse" }, @@ -528,12 +540,14 @@ dependencies = [ { name = "elasticsearch-dsl" }, { name = "ftfy" }, { name = "glom" }, + { name = "jinja2" }, { name = "meilisearch" }, { name = "orjson" }, { name = "pycpfcnpj" }, { name = "pydantic", extra = ["email"] }, { name = "pydantic-extra-types" }, { name = "pytz" }, + { name = "qrcode" }, { name = "requests" }, { name = "smart-open", extra = ["s3"] }, { name = "sqlite-utils" }, @@ -548,12 +562,14 @@ requires-dist = [ { name = "elasticsearch-dsl", specifier = ">=8.17.1" }, { name = "ftfy", specifier = ">=6.3.1" }, { name = "glom", specifier = ">=24.11.0" }, + { name = "jinja2", specifier = ">=3.1.6" }, { name = "meilisearch", specifier = ">=0.34.0" }, { name = "orjson", specifier = ">=3.10.15" }, { name = "pycpfcnpj", specifier = ">=1.8" }, { name = "pydantic", extras = ["email"], specifier = ">=2.10.6" }, { name = "pydantic-extra-types", specifier = ">=2.10.3" }, { name = "pytz", specifier = ">=2025.1" }, + { name = "qrcode", specifier = ">=8.2" }, { name = "requests", specifier = ">=2.32.3" }, { name = "smart-open", extras = ["s3"], specifier = ">=7.1.0" }, { name = "sqlite-utils", specifier = ">=3.38" }, @@ -570,6 +586,34 @@ dev = [ { name = "ruff", specifier = ">=0.11.1" }, ] +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +] + [[package]] name = "meilisearch" version = "0.34.1" @@ -584,7 +628,7 @@ wheels = [ ] [[package]] -name = "order-management" +name = "orders-events" version = "0.1.0" source = { virtual = "." } dependencies = [ @@ -859,6 +903,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] +[[package]] +name = "qrcode" +version = "8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/b2/7fc2931bfae0af02d5f53b174e9cf701adbb35f39d69c2af63d4a39f81a9/qrcode-8.2.tar.gz", hash = "sha256:35c3f2a4172b33136ab9f6b3ef1c00260dd2f66f858f24d88418a015f446506c", size = 43317, upload-time = "2025-05-01T15:44:24.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/b8/d2d6d731733f51684bbf76bf34dab3b70a9148e8f2cef2bb544fccec681a/qrcode-8.2-py3-none-any.whl", hash = "sha256:16e64e0716c14960108e85d853062c9e8bba5ca8252c0b4d0231b9df4060ff4f", size = 45986, upload-time = "2025-05-01T15:44:22.781Z" }, +] + [[package]] name = "requests" version = "2.32.3"