From 2d191c5fc8bdd7c23bf35a0c1673eea19e3a8739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Rafael=20Siqueira?= Date: Mon, 26 Jan 2026 13:00:36 -0300 Subject: [PATCH] add tests to canceled enrollment or scheduled with seat --- .../app/routes/enrollments/enroll.py | 1 + .../enrollments.tsx | 6 +- enrollments-events/app/enrollment.py | 4 +- enrollments-events/app/events/enroll.py | 3 + .../app/events/enroll_scheduled.py | 4 +- ...oll_if_failed.py => reenroll_on_failed.py} | 0 ...re_seat.py => restore_seat_on_canceled.py} | 2 +- .../restore_seat_on_scheduled_canceled.py | 59 +++++++++++++++++++ enrollments-events/template.yaml | 37 ++++++++++-- ...f_failed.py => test_reenroll_on_failed.py} | 2 +- .../events/test_restore_seat_on_canceled.py | 20 +++++++ ...test_restore_seat_on_scheduled_canceled.py | 45 ++++++++++++++ enrollments-events/tests/seeds.jsonl | 5 +- orders-events/app/events/start_fulfillment.py | 1 + 14 files changed, 176 insertions(+), 13 deletions(-) rename enrollments-events/app/events/{reenroll_if_failed.py => reenroll_on_failed.py} (100%) rename enrollments-events/app/events/{restore_seat.py => restore_seat_on_canceled.py} (100%) create mode 100644 enrollments-events/app/events/restore_seat_on_scheduled_canceled.py rename enrollments-events/tests/events/{test_reenroll_if_failed.py => test_reenroll_on_failed.py} (97%) create mode 100644 enrollments-events/tests/events/test_restore_seat_on_canceled.py create mode 100644 enrollments-events/tests/events/test_restore_seat_on_scheduled_canceled.py diff --git a/api.saladeaula.digital/app/routes/enrollments/enroll.py b/api.saladeaula.digital/app/routes/enrollments/enroll.py index f1d8298..fad2e25 100644 --- a/api.saladeaula.digital/app/routes/enrollments/enroll.py +++ b/api.saladeaula.digital/app/routes/enrollments/enroll.py @@ -343,6 +343,7 @@ def _enroll_later(enrollment: Enrollment, context: Context): 'user': user.model_dump(), 'course': course.model_dump(), 'org_name': org.name, + 'enrollment_id': enrollment.id, 'created_by': { 'id': created_by.id, 'name': created_by.name, diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.payments.$id._index/enrollments.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.payments.$id._index/enrollments.tsx index 73bfb18..e95e60d 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.payments.$id._index/enrollments.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.payments.$id._index/enrollments.tsx @@ -162,9 +162,9 @@ const statuses: Record = { const labels: Record = { PENDING: 'Aguardando', - EXECUTED: 'Executado', - SCHEDULED: 'Agendado', - ROLLBACK: 'Revogado' + EXECUTED: 'Executada', + SCHEDULED: 'Agendada', + ROLLBACK: 'Revogada' } function Status({ status: s }: { status: string }) { diff --git a/enrollments-events/app/enrollment.py b/enrollments-events/app/enrollment.py index 260e899..81c8dab 100644 --- a/enrollments-events/app/enrollment.py +++ b/enrollments-events/app/enrollment.py @@ -38,7 +38,7 @@ class Course(BaseModel): class Enrollment(BaseModel): - id: UUID4 | str = Field(default_factory=uuid4) + id: UUID4 | str user: User course: Course progress: int = Field(default=0, ge=0, le=100) @@ -61,7 +61,7 @@ Org = TypedDict('Org', {'org_id': str, 'name': str}) CreatedBy = TypedDict('CreatedBy', {'id': str, 'name': str}) -Seat = TypedDict('Seat', {'order_id': str}) +Seat = TypedDict('Seat', {'order_id': str, 'enrollment_id': NotRequired[str]}) DeduplicationWindow = TypedDict('DeduplicationWindow', {'offset_days': int}) diff --git a/enrollments-events/app/events/enroll.py b/enrollments-events/app/events/enroll.py index 6de9450..6057ca3 100644 --- a/enrollments-events/app/events/enroll.py +++ b/enrollments-events/app/events/enroll.py @@ -1,3 +1,5 @@ +from uuid import uuid4 + from aws_lambda_powertools import Logger from aws_lambda_powertools.utilities.data_classes import ( EventBridgeEvent, @@ -84,6 +86,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: def _handler(course: Course, context: dict) -> Enrollment: enrollment = Enrollment( + id=uuid4(), user=context['user'], course=course, ) diff --git a/enrollments-events/app/events/enroll_scheduled.py b/enrollments-events/app/events/enroll_scheduled.py index 44e9929..a565fc3 100644 --- a/enrollments-events/app/events/enroll_scheduled.py +++ b/enrollments-events/app/events/enroll_scheduled.py @@ -1,4 +1,5 @@ from datetime import datetime +from uuid import uuid4 from aws_lambda_powertools import Logger from aws_lambda_powertools.utilities.data_classes import ( @@ -37,8 +38,9 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: offset_days = old_image.get('dedup_window_offset_days') billing_day = old_image.get('subscription_billing_day') created_by = old_image.get('created_by') - seat: Seat | None = old_image.get('seat') + seat: Seat = old_image.get('seat', {}) enrollment = Enrollment( + id=old_image['enrollment_id'], course=old_image['course'], user=old_image['user'], ) diff --git a/enrollments-events/app/events/reenroll_if_failed.py b/enrollments-events/app/events/reenroll_on_failed.py similarity index 100% rename from enrollments-events/app/events/reenroll_if_failed.py rename to enrollments-events/app/events/reenroll_on_failed.py diff --git a/enrollments-events/app/events/restore_seat.py b/enrollments-events/app/events/restore_seat_on_canceled.py similarity index 100% rename from enrollments-events/app/events/restore_seat.py rename to enrollments-events/app/events/restore_seat_on_canceled.py index e912aba..e2a65dc 100644 --- a/enrollments-events/app/events/restore_seat.py +++ b/enrollments-events/app/events/restore_seat_on_canceled.py @@ -37,7 +37,6 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: rollback_at = :now, \ reason = :reason', cond_expr='attribute_exists(sk) AND #status = :executed', - table_name=ORDER_TABLE, expr_attr_names={ '#status': 'status', }, @@ -47,6 +46,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: ':reason': 'CANCELLATION', ':now': now_, }, + table_name=ORDER_TABLE, ) transact.put( item={ diff --git a/enrollments-events/app/events/restore_seat_on_scheduled_canceled.py b/enrollments-events/app/events/restore_seat_on_scheduled_canceled.py new file mode 100644 index 0000000..ee03b5c --- /dev/null +++ b/enrollments-events/app/events/restore_seat_on_scheduled_canceled.py @@ -0,0 +1,59 @@ +from uuid import uuid4 + +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 boto3clients import dynamodb_client +from config import ENROLLMENT_TABLE, ORDER_TABLE + +logger = Logger(__name__) +dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) + + +@event_source(data_class=EventBridgeEvent) +@logger.inject_lambda_context +def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: + old_image = event.detail['old_image'] + order_id = old_image['seat']['order_id'] + enrollment_id = old_image['enrollment_id'] + *_, org_id = old_image['id'].split('#') + now_ = now() + + with dyn.transact_writer() as transact: + transact.update( + key=KeyPair( + pk=order_id, + sk=f'ENROLLMENT#{enrollment_id}', + table_name=ORDER_TABLE, + ), + cond_expr='attribute_exists(sk) AND #status = :scheduled', + update_expr='SET #status = :rollback, \ + rollback_at = :now, \ + reason = :reason', + expr_attr_names={ + '#status': 'status', + }, + expr_attr_values={ + ':rollback': 'ROLLBACK', + ':scheduled': 'SCHEDULED', + ':reason': 'CANCELLATION', + ':now': now_, + }, + table_name=ORDER_TABLE, + ) + transact.put( + item={ + 'id': f'SEAT#ORG#{org_id}', + 'sk': f'ORDER#{order_id}#ENROLLMENT#{uuid4()}', + 'course': old_image['course'], + 'created_at': now_, + } + ) + + return True diff --git a/enrollments-events/template.yaml b/enrollments-events/template.yaml index 41dedc1..0af7b0b 100644 --- a/enrollments-events/template.yaml +++ b/enrollments-events/template.yaml @@ -211,11 +211,14 @@ Resources: keys: id: - prefix: SCHEDULED#ORG# + old_image: + enrollment_id: + - exists: true - EventReenrollIfFailedFunction: + EventReenrollOnFailedFunction: Type: AWS::Serverless::Function Properties: - Handler: events.reenroll_if_failed.lambda_handler + Handler: events.reenroll_on_failed.lambda_handler LoggingConfig: LogGroup: !Ref EventLog Policies: @@ -240,10 +243,10 @@ Resources: old_image: status: [IN_PROGRESS] - EventRestoreSeatFunction: + EventRestoreSeatOnCanceledFunction: Type: AWS::Serverless::Function Properties: - Handler: events.restore_seat.lambda_handler + Handler: events.restore_seat_on_canceled.lambda_handler LoggingConfig: LogGroup: !Ref EventLog Policies: @@ -265,6 +268,32 @@ Resources: order_id: - exists: true + EventRestoreSeatOnScheduledCanceledFunction: + Type: AWS::Serverless::Function + Properties: + Handler: events.restore_seat_on_scheduled_canceled.lambda_handler + LoggingConfig: + LogGroup: !Ref EventLog + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref EnrollmentTable + - DynamoDBCrudPolicy: + TableName: !Ref OrderTable + Events: + DynamoDBEvent: + Type: EventBridgeRule + Properties: + Pattern: + resources: [!Ref EnrollmentTable] + detail-type: [REMOVE] + detail: + old_image: + id: + - prefix: SCHEDULED#ORG# + seat: + order_id: + - exists: true + # DEPRECATED EventAllocateSlotsFunction: Type: AWS::Serverless::Function diff --git a/enrollments-events/tests/events/test_reenroll_if_failed.py b/enrollments-events/tests/events/test_reenroll_on_failed.py similarity index 97% rename from enrollments-events/tests/events/test_reenroll_if_failed.py rename to enrollments-events/tests/events/test_reenroll_on_failed.py index 719d6de..4937566 100644 --- a/enrollments-events/tests/events/test_reenroll_if_failed.py +++ b/enrollments-events/tests/events/test_reenroll_on_failed.py @@ -1,7 +1,7 @@ from aws_lambda_powertools.utilities.typing import LambdaContext from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair -import events.reenroll_if_failed as app +import events.reenroll_on_failed as app def test_reenroll_custom_dedup_window( diff --git a/enrollments-events/tests/events/test_restore_seat_on_canceled.py b/enrollments-events/tests/events/test_restore_seat_on_canceled.py new file mode 100644 index 0000000..cc5be37 --- /dev/null +++ b/enrollments-events/tests/events/test_restore_seat_on_canceled.py @@ -0,0 +1,20 @@ +from aws_lambda_powertools.utilities.typing import LambdaContext +from layercake.dynamodb import DynamoDBPersistenceLayer + +import events.restore_seat_on_canceled as app + + +def test_restore_seat_on_canceled( + dynamodb_seeds, + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + lambda_context: LambdaContext, +): + event = { + 'detail': { + 'old_image': { + 'id': '', + 'seat': {'order_id': ''}, + }, + } + } + assert app.lambda_handler(event, lambda_context) # type: ignore diff --git a/enrollments-events/tests/events/test_restore_seat_on_scheduled_canceled.py b/enrollments-events/tests/events/test_restore_seat_on_scheduled_canceled.py new file mode 100644 index 0000000..2e42447 --- /dev/null +++ b/enrollments-events/tests/events/test_restore_seat_on_scheduled_canceled.py @@ -0,0 +1,45 @@ +from aws_lambda_powertools.utilities.typing import LambdaContext +from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey + +import events.restore_seat_on_scheduled_canceled as app + + +def test_restore_seat_on_scheduled_canceled( + dynamodb_seeds, + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + lambda_context: LambdaContext, +): + org_id = 'cJtK9SsnJhKPyxESe7g3DG' + event = { + 'detail': { + 'old_image': { + 'seat': { + 'order_id': 'f1ecaa69-8054-4cdc-ba13-a6680e18df21', + }, + 'enrollment_id': '19c0aa75-473e-4d4c-822d-2d42d46d2167', + 'course': { + 'name': 'Gestão da Cultura de Segurança', + 'id': 'c19cd7ee-3cc8-4f9c-95ff-dad7993f49b1', + 'access_period': 365, + }, + 'org_name': 'Beta Educação', + 'user': { + 'name': 'Sérgio Rafael de Siqueira', + 'cpf': '07879819908', + 'id': '5OxmMjL-ujoR5IMGegQz', + 'email': 'sergio@somosbeta.com.br', + }, + 'ttl': 1769828760, + 'sk': '2026-01-31T00:00:00-03:06#addf2b5f2cbf30080df8582e6a95eb96', + 'id': f'SCHEDULED#ORG#{org_id}', + 'scheduled_at': '2026-01-25T14:58:09.772660-03:00', + 'created_by': { + 'name': 'Sérgio Rafael de Siqueira', + 'id': '5OxmMjL-ujoR5IMGegQz', + }, + } + } + } + assert app.lambda_handler(event, lambda_context) # type: ignore + r = dynamodb_persistence_layer.collection.query(PartitionKey(f'SEAT#ORG#{org_id}')) + assert len(r['items']) diff --git a/enrollments-events/tests/seeds.jsonl b/enrollments-events/tests/seeds.jsonl index 3420734..5f5494d 100644 --- a/enrollments-events/tests/seeds.jsonl +++ b/enrollments-events/tests/seeds.jsonl @@ -47,4 +47,7 @@ {"id": "00237409-9384-4692-9be5-b4443a41e1c4", "sk": "admins#1234", "email": "sergio@somosbeta.com.br", "name": "Sérgio R Siqueira"} // file: tests/events/test_reenroll_if_failed.py::test_reenroll_custom_dedup_window -{"id": "SUBSCRIPTION", "sk": "ORG#123"} \ No newline at end of file +{"id": "SUBSCRIPTION", "sk": "ORG#123"} + +// file: tests/events/test_restore_seat_on_scheduled_canceled.py +{"id": "f1ecaa69-8054-4cdc-ba13-a6680e18df21", "sk": "ENROLLMENT#19c0aa75-473e-4d4c-822d-2d42d46d2167", "status": "SCHEDULED"} \ No newline at end of file diff --git a/orders-events/app/events/start_fulfillment.py b/orders-events/app/events/start_fulfillment.py index 8f91af7..dfe1b40 100644 --- a/orders-events/app/events/start_fulfillment.py +++ b/orders-events/app/events/start_fulfillment.py @@ -352,6 +352,7 @@ def _enroll_later(enrollment: Enrollment, context: Context) -> None: 'user': user.model_dump(), 'course': course.model_dump(), 'org_name': org.name, + 'enrollment_id': enrollment.id, 'created_by': { 'id': created_by['user_id'], 'name': created_by['name'],