From a9f6a89d54f924dc8d713717574f6e238a4a8209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Rafael=20Siqueira?= Date: Tue, 1 Jul 2025 17:45:17 -0300 Subject: [PATCH] fix --- .../app/events/assign_tenant_cpf.py | 23 ------ .../app/events/remove_slots_on_canceled.py | 57 +++++++++++++++ .../app/events/stopgap/__init__.py | 4 ++ .../app/events/stopgap/remove_slots.py | 71 +++++++++++++++++++ order-management/template.yaml | 52 +++++++++++++- .../tests/events/stopgap/test_remove_slots.py | 30 ++++++++ .../tests/events/test_assign_tenant_cnpj.py | 29 ++++++++ .../events/test_remove_slots_on_canceled.py | 27 +++++++ order-management/tests/seeds.jsonl | 9 ++- order-management/uv.lock | 63 +++++++++++++++- 10 files changed, 337 insertions(+), 28 deletions(-) delete mode 100644 order-management/app/events/assign_tenant_cpf.py create mode 100644 order-management/app/events/remove_slots_on_canceled.py create mode 100644 order-management/app/events/stopgap/remove_slots.py create mode 100644 order-management/tests/events/stopgap/test_remove_slots.py create mode 100644 order-management/tests/events/test_assign_tenant_cnpj.py create mode 100644 order-management/tests/events/test_remove_slots_on_canceled.py diff --git a/order-management/app/events/assign_tenant_cpf.py b/order-management/app/events/assign_tenant_cpf.py deleted file mode 100644 index 782fcd2..0000000 --- a/order-management/app/events/assign_tenant_cpf.py +++ /dev/null @@ -1,23 +0,0 @@ -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.dynamodb import ( - DynamoDBPersistenceLayer, -) - -from boto3clients import dynamodb_client -from config import ORDER_TABLE, USER_TABLE - -logger = Logger(__name__) -user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) -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'] - return True diff --git a/order-management/app/events/remove_slots_on_canceled.py b/order-management/app/events/remove_slots_on_canceled.py new file mode 100644 index 0000000..cedd453 --- /dev/null +++ b/order-management/app/events/remove_slots_on_canceled.py @@ -0,0 +1,57 @@ +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.dynamodb import ( + ComposeKey, + DynamoDBPersistenceLayer, + KeyPair, + SortKey, +) + +from boto3clients import dynamodb_client +from config import ENROLLMENT_TABLE, ORDER_TABLE + +logger = Logger(__name__) +enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) +order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client) + + +class TenantDoesNotExistError(Exception): + def __init__(self, *args): + super().__init__('Tenant does not exist') + + +@event_source(data_class=EventBridgeEvent) +@logger.inject_lambda_context +def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: + new_image = event.detail['new_image'] + order_id = new_image['id'] + tenant_id = order_layer.collection.get_item( + KeyPair( + order_id, + SortKey('metadata#tenant', path_spec='tenant_id'), + ), + exc_cls=TenantDoesNotExistError, + ) + + result = enrollment_layer.collection.query( + KeyPair( + # Post-migration: rename `vacancies` to `slots` + ComposeKey(tenant_id, prefix='vacancies'), + order_id, + ) + ) + with enrollment_layer.batch_writer() as batch: + for pair in result['items']: + batch.delete_item( + Key={ + # Post-migration: rename `vacancies` to `slots` + 'id': {'S': ComposeKey(pair['id'], prefix='vacancies')}, + 'sk': {'S': pair['sk']}, + } + ) + + return True diff --git a/order-management/app/events/stopgap/__init__.py b/order-management/app/events/stopgap/__init__.py index e69de29..5a77fb0 100644 --- a/order-management/app/events/stopgap/__init__.py +++ b/order-management/app/events/stopgap/__init__.py @@ -0,0 +1,4 @@ +""" +Stopgap events. Everything here is a quick fix and should be replaced with +proper solutions. +""" diff --git a/order-management/app/events/stopgap/remove_slots.py b/order-management/app/events/stopgap/remove_slots.py new file mode 100644 index 0000000..b1e2c70 --- /dev/null +++ b/order-management/app/events/stopgap/remove_slots.py @@ -0,0 +1,71 @@ +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.dynamodb import ( + ComposeKey, + DynamoDBPersistenceLayer, + KeyPair, + SortKey, + TransactKey, +) + +from boto3clients import dynamodb_client +from config import ENROLLMENT_TABLE, ORDER_TABLE, USER_TABLE + +logger = Logger(__name__) +user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) +order_layer = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client) +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.""" + new_image = event.detail['new_image'] + order_id = new_image['id'] + data = order_layer.collection.get_items( + TransactKey(order_id) + + SortKey('0') + + KeyPair( + pk=order_id, + sk=SortKey( + sk='metadata#tenant', + path_spec='tenant_id', + remove_prefix='metadata#', + ), + rename_key='tenant_id', + ) + ) + tenant_id = data['tenant_id'].removeprefix('ORG#') + + policy = user_layer.collection.get_item( + KeyPair(pk=tenant_id, sk='metadata#billing_policy'), + raise_on_error=False, + default=False, + ) + + # Skip if missing billing policy or order is zero/negative + if not policy or data['total'] <= 0: + return False + + result = enrollment_layer.collection.query( + KeyPair( + ComposeKey(tenant_id, prefix='vacancies'), + order_id, + ) + ) + with enrollment_layer.batch_writer() as batch: + for pair in result['items']: + batch.delete_item( + Key={ + 'id': {'S': ComposeKey(pair['id'], prefix='vacancies')}, + 'sk': {'S': pair['sk']}, + } + ) + + return True diff --git a/order-management/template.yaml b/order-management/template.yaml index 4d0c9c4..ffe58be 100644 --- a/order-management/template.yaml +++ b/order-management/template.yaml @@ -20,7 +20,7 @@ Globals: Architectures: - x86_64 Layers: - - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:72 + - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:78 Environment: Variables: TZ: America/Sao_Paulo @@ -30,6 +30,7 @@ Globals: POWERTOOLS_LOGGER_LOG_EVENT: true USER_TABLE: !Ref UserTable ORDER_TABLE: !Ref OrderTable + ENROLLMENT_TABLE: !Ref EnrollmentTable Resources: EventLog: @@ -89,6 +90,31 @@ Resources: metadata__tenant_id: - exists: false + EventRemoveSlotsOnCanceledFunction: + Type: AWS::Serverless::Function + Properties: + Handler: events.delete_slots_on_canceled.lambda_handler + LoggingConfig: + LogGroup: !Ref EventLog + Policies: + - DynamoDBWritePolicy: + TableName: !Ref OrderTable + - DynamoDBWritePolicy: + TableName: !Ref EnrollmentTable + Events: + Event: + Type: EventBridgeRule + Properties: + Pattern: + resources: [!Ref OrderTable] + detail-type: [MODIFY] + detail: + new_image: + sk: ["0"] + cnpj: + - exists: true + status: [CANCELED, EXPIRED] + EventSetAsPaidFunction: Type: AWS::Serverless::Function Properties: @@ -113,3 +139,27 @@ Resources: total: [0] status: [CREATING, PENDING] payment_method: [MANUAL] + + EventRemoveSlotsFunction: + Type: AWS::Serverless::Function + Properties: + Handler: events.stopgap.remove_slots.lambda_handler + LoggingConfig: + LogGroup: !Ref EventLog + Policies: + - DynamoDBReadPolicy: + TableName: !Ref UserTable + - DynamoDBReadPolicy: + TableName: !Ref OrderTable + - DynamoDBCrudPolicy: + TableName: !Ref EnrollmentTable + Events: + DynamoDBEvent: + Type: EventBridgeRule + Properties: + Pattern: + resources: [!Ref OrderTable] + detail: + new_image: + sk: [generated_items] + status: [SUCCESS] diff --git a/order-management/tests/events/stopgap/test_remove_slots.py b/order-management/tests/events/stopgap/test_remove_slots.py new file mode 100644 index 0000000..28b18d2 --- /dev/null +++ b/order-management/tests/events/stopgap/test_remove_slots.py @@ -0,0 +1,30 @@ +from layercake.dynamodb import PartitionKey + +import events.stopgap.remove_slots as app + +from ...conftest import LambdaContext + + +def test_remove_slots( + dynamodb_seeds, + dynamodb_persistence_layer, + lambda_context: LambdaContext, +): + event = { + 'detail': { + 'new_image': { + 'id': '9omWNKymwU5U4aeun6mWzZ', + 'sk': 'generated_items', + 'create_date': '2024-07-23T20:43:37.303418-03:00', + 'status': 'SUCCESS', + 'scope': 'MILTI_USER', + } + }, + } + assert app.lambda_handler(event, lambda_context) # type: ignore + + result = dynamodb_persistence_layer.collection.query( + PartitionKey('vacancies#cJtK9SsnJhKPyxESe7g3DG') + ) + + assert len(result['items']) == 0 diff --git a/order-management/tests/events/test_assign_tenant_cnpj.py b/order-management/tests/events/test_assign_tenant_cnpj.py new file mode 100644 index 0000000..bdbb2c9 --- /dev/null +++ b/order-management/tests/events/test_assign_tenant_cnpj.py @@ -0,0 +1,29 @@ +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']) + print(result['items']) diff --git a/order-management/tests/events/test_remove_slots_on_canceled.py b/order-management/tests/events/test_remove_slots_on_canceled.py new file mode 100644 index 0000000..ff921fe --- /dev/null +++ b/order-management/tests/events/test_remove_slots_on_canceled.py @@ -0,0 +1,27 @@ +from aws_lambda_powertools.utilities.typing import LambdaContext +from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey + +import events.remove_slots_on_canceled as app + + +def test_delete_slots_on_canceled( + dynamodb_seeds, + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + lambda_context: LambdaContext, +): + event = { + 'detail': { + 'new_image': { + 'id': '9omWNKymwU5U4aeun6mWzZ', + 'status': 'CANCELED', + } + } + } + + assert app.lambda_handler(event, lambda_context) # type: ignore + + result = dynamodb_persistence_layer.collection.query( + PartitionKey('vacancies#cJtK9SsnJhKPyxESe7g3DG') + ) + + assert len(result['items']) == 0 diff --git a/order-management/tests/seeds.jsonl b/order-management/tests/seeds.jsonl index b1124e4..2f87c24 100644 --- a/order-management/tests/seeds.jsonl +++ b/order-management/tests/seeds.jsonl @@ -1,7 +1,10 @@ -{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "0"}, "name": {"S": "EDUSEG"}} {"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"}, "org_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}} +{"id": {"S": "9omWNKymwU5U4aeun6mWzZ"}, "sk": {"S": "metadata#tenant"}, "tenant_id": {"S": "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"}} \ No newline at end of file +{"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 diff --git a/order-management/uv.lock b/order-management/uv.lock index 759c70d..e5424f7 100644 --- a/order-management/uv.lock +++ b/order-management/uv.lock @@ -221,6 +221,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "click-default-group" +version = "1.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/ce/edb087fb53de63dad3b36408ca30368f438738098e668b78c87f93cd41df/click_default_group-1.2.4.tar.gz", hash = "sha256:eb3f3c99ec0d456ca6cd2a7f08f7d4e91771bef51b01bdd9580cc6450fe1251e", size = 3505, upload-time = "2023-08-04T07:54:58.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/1a/aff8bb287a4b1400f69e09a53bd65de96aa5cee5691925b38731c67fc695/click_default_group-1.2.4-py2.py3-none-any.whl", hash = "sha256:9b60486923720e7fc61731bdb32b617039aba820e22e1c88766b1125592eaa5f", size = 4123, upload-time = "2023-08-04T07:54:56.875Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -495,7 +519,7 @@ wheels = [ [[package]] name = "layercake" -version = "0.6.2" +version = "0.6.11" source = { directory = "../layercake" } dependencies = [ { name = "arnparse" }, @@ -512,6 +536,7 @@ dependencies = [ { name = "pytz" }, { name = "requests" }, { name = "smart-open", extra = ["s3"] }, + { name = "sqlite-utils" }, { name = "weasyprint" }, ] @@ -531,6 +556,7 @@ requires-dist = [ { name = "pytz", specifier = ">=2025.1" }, { name = "requests", specifier = ">=2.32.3" }, { name = "smart-open", extras = ["s3"], specifier = ">=7.1.0" }, + { name = "sqlite-utils", specifier = ">=3.38" }, { name = "weasyprint", specifier = ">=65.0" }, ] @@ -911,6 +937,41 @@ s3 = [ { name = "boto3" }, ] +[[package]] +name = "sqlite-fts4" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/6d/9dad6c3b433ab8912ace969c66abd595f8e0a2ccccdb73602b1291dbda29/sqlite-fts4-1.0.3.tar.gz", hash = "sha256:78b05eeaf6680e9dbed8986bde011e9c086a06cb0c931b3cf7da94c214e8930c", size = 9718, upload-time = "2022-07-30T01:14:26.943Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/29/0096e8b1811aaa78cfb296996f621f41120c21c2f5cd448ae1d54979d9fc/sqlite_fts4-1.0.3-py3-none-any.whl", hash = "sha256:0359edd8dea6fd73c848989e1e2b1f31a50fe5f9d7272299ff0e8dbaa62d035f", size = 9972, upload-time = "2022-07-30T01:14:24.942Z" }, +] + +[[package]] +name = "sqlite-utils" +version = "3.38" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "click-default-group" }, + { name = "pluggy" }, + { name = "python-dateutil" }, + { name = "sqlite-fts4" }, + { name = "tabulate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/43/ce9183a21911e0b73248c8fb83f8b8038515cb80053912c2a009e9765564/sqlite_utils-3.38.tar.gz", hash = "sha256:1ae77b931384052205a15478d429464f6c67a3ac3b4eafd3c674ac900f623aab", size = 214449, upload-time = "2024-11-23T22:49:40.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/eb/f8e8e827805f810838efff3311cccd2601238c5fa3fc35c1f878709e161b/sqlite_utils-3.38-py3-none-any.whl", hash = "sha256:8a27441015c3b2ef475f555861f7a2592f73bc60d247af9803a11b65fc605bf9", size = 68183, upload-time = "2024-11-23T22:49:38.289Z" }, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, +] + [[package]] name = "tinycss2" version = "1.4.0"