From fc14d425f2a55084ece465f63bbd796a3148982c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Rafael=20Siqueira?= Date: Wed, 28 Jan 2026 10:43:46 -0300 Subject: [PATCH] fix duplicate user --- .../app/routes/enrollments/enroll.py | 37 +++++++--- .../app/routes/orgs/enrollments/scheduled.py | 37 ++++++++-- .../_.$orgid.enrollments.seats/route.tsx | 4 +- enrollments-events/app/enrollment.py | 63 ++++++---------- enrollments-events/app/events/enroll.py | 16 +--- .../app/events/enroll_scheduled.py | 74 +++++++++++-------- .../app/events/reenroll_on_failed.py | 11 +-- .../restore_seat_on_scheduled_canceled.py | 1 - .../tests/events/test_ask_to_sign.py | 1 + .../tests/events/test_enroll.py | 20 ++--- .../tests/events/test_reenroll_on_failed.py | 4 +- .../events/test_restore_seat_on_canceled.py | 4 +- enrollments-events/tests/seeds.jsonl | 6 +- orders-events/app/events/start_fulfillment.py | 14 +--- 14 files changed, 151 insertions(+), 141 deletions(-) diff --git a/api.saladeaula.digital/app/routes/enrollments/enroll.py b/api.saladeaula.digital/app/routes/enrollments/enroll.py index fad2e25..da60bea 100644 --- a/api.saladeaula.digital/app/routes/enrollments/enroll.py +++ b/api.saladeaula.digital/app/routes/enrollments/enroll.py @@ -220,15 +220,25 @@ def enroll_now(enrollment: Enrollment, context: Context): exc_cls=OrderNotFoundError, table_name=ORDER_TABLE, ) - transact.put( - item={ - 'id': seat.order_id, - 'sk': f'ENROLLMENT#{enrollment.id}', - 'course': course.model_dump(), - 'user': user.model_dump(), - 'status': 'EXECUTED', - 'executed_at': now_, - 'created_at': now_, + transact.update( + key=KeyPair( + pk=str(seat.order_id), + sk=f'ENROLLMENT#{enrollment.id}', + ), + update_expr='SET course = :course, \ + #user = :user, \ + #status = :executed, \ + executed_at = :now, \ + created_at = if_not_exists(created_at, :now)', + expr_attr_names={ + '#user': 'user', + '#status': 'status', + }, + expr_attr_values={ + ':course': course.model_dump(), + ':user': user.model_dump(), + ':executed': 'EXECUTED', + ':now': now_, }, table_name=ORDER_TABLE, ) @@ -240,6 +250,15 @@ def enroll_now(enrollment: Enrollment, context: Context): cond_expr='attribute_exists(sk)', exc_cls=SeatNotFoundError, ) + # Enrollment should know where it comes from + transact.put( + item={ + 'id': enrollment.id, + 'sk': f'LINKED_ENTITY#PARENT#ORDER#{seat.order_id}', + 'created_at': now_, + }, + cond_expr='attribute_not_exists(sk)', + ) transact.put( item={ diff --git a/api.saladeaula.digital/app/routes/orgs/enrollments/scheduled.py b/api.saladeaula.digital/app/routes/orgs/enrollments/scheduled.py index 82f0425..eec7ffd 100644 --- a/api.saladeaula.digital/app/routes/orgs/enrollments/scheduled.py +++ b/api.saladeaula.digital/app/routes/orgs/enrollments/scheduled.py @@ -5,14 +5,23 @@ from aws_lambda_powertools import Logger from aws_lambda_powertools.event_handler.api_gateway import Router from aws_lambda_powertools.event_handler.exceptions import NotFoundError from aws_lambda_powertools.event_handler.openapi.params import Body, Query +from layercake.dateutils import now from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, PartitionKey from pydantic import FutureDatetime from api_gateway import JSONResponse from boto3clients import dynamodb_client from config import ENROLLMENT_TABLE +from middlewares.authentication_middleware import User as Authenticated -from ...enrollments.enroll import Context, Enrollment, Org, Subscription, enroll_now +from ...enrollments.enroll import ( + Context, + Enrollment, + Org, + Seat, + Subscription, + enroll_now, +) logger = Logger(__name__) router = Router() @@ -65,6 +74,7 @@ def proceed( scheduled_for: Annotated[FutureDatetime, Body(embed=True)], lock_hash: Annotated[str, Body(embed=True)], ): + now_ = now() pk = f'SCHEDULED#ORG#{org_id}' sk = f'{scheduled_for.isoformat()}#{lock_hash}' @@ -72,10 +82,13 @@ def proceed( KeyPair(pk, sk), exc_cls=ScheduledNotFoundError, ) - billing_day = scheduled.get('subscription_billing_day') + org = Org(id=org_id, name=scheduled['org_name']) + created_by: Authenticated = router.context['user'] + seat: Seat | None = scheduled.get('seat') + billing_day: int | None = scheduled.get('subscription_billing_day') ctx: Context = { - 'created_by': router.context['user'], - 'org': Org(id=org_id, name=scheduled['org_name']), + 'created_by': created_by, + 'org': org, } if billing_day: @@ -86,12 +99,26 @@ def proceed( Enrollment( user=scheduled['user'], course=scheduled['course'], - seat=scheduled.get('seat'), + seat=seat, ), ctx, ) with dyn.transact_writer() as transact: + transact.put( + item={ + 'id': pk, + 'sk': f'{sk}#EXECUTED', + 'enrollment_id': enrollment.id, + 'user': scheduled['user'], + 'course': scheduled['course'], + 'created_by': { + 'id': created_by.id, + 'name': created_by.name, + }, + 'created_at': now_, + } + ) transact.delete(key=KeyPair(pk, sk)) transact.delete(key=KeyPair('LOCK#SCHEDULED', lock_hash)) except Exception: diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.seats/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.seats/route.tsx index 4c5dcdf..8e16319 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.seats/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.seats/route.tsx @@ -212,7 +212,7 @@ export default function Route({ loaderData: { seats } }: Route.ComponentProps) { return null } - const { course, ...rest } = getValues(`enrollments.${index}`) + const { course, scheduled_for } = getValues(`enrollments.${index}`) if (!course?.id) { Array.from({ length: times }, (_, i) => { @@ -239,7 +239,7 @@ export default function Route({ loaderData: { seats } }: Route.ComponentProps) { id: seat.enrollment_id, seat: { order_id: seat.order_id }, course, - ...rest + scheduled_for }) } else { // @ts-ignore diff --git a/enrollments-events/app/enrollment.py b/enrollments-events/app/enrollment.py index 81c8dab..e857cc3 100644 --- a/enrollments-events/app/enrollment.py +++ b/enrollments-events/app/enrollment.py @@ -1,9 +1,5 @@ -from abc import ABC -from dataclasses import dataclass from datetime import datetime, timedelta -from enum import Enum -from typing import Any, Literal, TypedDict -from uuid import uuid4 +from typing import Literal, TypedDict from layercake.dateutils import now, ttl from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair @@ -27,7 +23,6 @@ class User(BaseModel): id: UUID4 | str name: NameStr email: EmailStr - email_verified: bool = False cpf: CpfStr | None = None @@ -44,18 +39,6 @@ class Enrollment(BaseModel): progress: int = Field(default=0, ge=0, le=100) status: Literal['PENDING'] = 'PENDING' - def model_dump( - self, - exclude=None, - *args, - **kwargs, - ) -> dict[str, Any]: - return super().model_dump( - exclude={'user': {'email_verified'}}, - *args, - **kwargs, - ) - Org = TypedDict('Org', {'org_id': str, 'name': str}) @@ -75,18 +58,6 @@ Subscription = TypedDict( ) -class Kind(str, Enum): - ORDER = 'ORDER' - ENROLLMENT = 'ENROLLMENT' - - -@dataclass(frozen=True) -class LinkedEntity(ABC): - id: str - kind: Kind - table_name: str | None = None - - class DeduplicationConflictError(Exception): def __init__(self, *args): super().__init__('Enrollment already exists') @@ -121,7 +92,7 @@ def enroll( created_by: CreatedBy | None = None, scheduled_at: datetime | None = None, seat: Seat | None = None, - linked_entities: frozenset[LinkedEntity] = frozenset(), + parent_entity: str | None = None, deduplication_window: DeduplicationWindow | None = None, persistence_layer: DynamoDBPersistenceLayer, ) -> bool: @@ -156,43 +127,51 @@ def enroll( ) if seat: + order_id = seat['order_id'] transact.condition( - key=KeyPair(str(seat['order_id']), '0'), + key=KeyPair(order_id, '0'), cond_expr='attribute_exists(sk)', exc_cls=OrderNotFoundError, table_name=ORDER_TABLE, ) transact.put( item={ - 'id': seat['order_id'], + 'id': order_id, 'sk': f'ENROLLMENT#{enrollment.id}', 'course': course.model_dump(), - 'user': user.model_dump(), + 'user': user.model_dump(exclude={'cpf'}), 'status': 'EXECUTED', 'executed_at': now_, 'created_at': now_, }, table_name=ORDER_TABLE, ) - - # Relationships between this enrollment and its related entities - for entity in linked_entities: - # Parent knows the child + # Enrollment should know where it comes from transact.put( item={ - 'id': entity.id, - 'sk': f'LINKED_ENTITIES#CHILD#ENROLLMENT#{enrollment.id}', + 'id': enrollment.id, + 'sk': f'LINKED_ENTITY#PARENT#ORDER#{order_id}', + 'created_at': now_, + }, + cond_expr='attribute_not_exists(sk)', + ) + + if parent_entity: + # Parent knows the child + transact.put( + item={ + 'id': parent_entity, + 'sk': f'LINKED_ENTITY#CHILD#ENROLLMENT#{enrollment.id}', 'created_at': now_, }, cond_expr='attribute_not_exists(sk)', - table_name=entity.table_name, ) # Child knows the parent transact.put( item={ 'id': enrollment.id, - 'sk': f'LINKED_ENTITIES#PARENT#{entity.kind.value}#{entity.id}', + 'sk': f'LINKED_ENTITY#PARENT#ENROLLMENT#{parent_entity}', 'created_at': now_, }, cond_expr='attribute_not_exists(sk)', diff --git a/enrollments-events/app/events/enroll.py b/enrollments-events/app/events/enroll.py index 6057ca3..74825f2 100644 --- a/enrollments-events/app/events/enroll.py +++ b/enrollments-events/app/events/enroll.py @@ -21,8 +21,6 @@ from config import COURSE_TABLE, ENROLLMENT_TABLE, ORDER_TABLE from enrollment import ( Course, Enrollment, - Kind, - LinkedEntity, User, enroll, ) @@ -91,19 +89,7 @@ def _handler(course: Course, context: dict) -> Enrollment: course=course, ) - enroll( - enrollment, - persistence_layer=enrollment_layer, - linked_entities=frozenset( - { - LinkedEntity( - id=context['order_id'], - kind=Kind.ORDER, - table_name=ORDER_TABLE, - ), - } - ), - ) + enroll(enrollment, persistence_layer=enrollment_layer) return enrollment diff --git a/enrollments-events/app/events/enroll_scheduled.py b/enrollments-events/app/events/enroll_scheduled.py index a565fc3..ee81768 100644 --- a/enrollments-events/app/events/enroll_scheduled.py +++ b/enrollments-events/app/events/enroll_scheduled.py @@ -14,8 +14,6 @@ from boto3clients import dynamodb_client from config import ENROLLMENT_TABLE, ORDER_TABLE from enrollment import ( Enrollment, - Kind, - LinkedEntity, Seat, Subscription, enroll, @@ -54,21 +52,6 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: ) try: - # The enrollment must know its source - linked_entities = ( - frozenset( - { - LinkedEntity( - id=seat['order_id'], - kind=Kind.ORDER, - table_name=ORDER_TABLE, - ), - }, - ) - if seat - else frozenset() - ) - enroll( enrollment, org={ @@ -82,7 +65,6 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: scheduled_at=datetime.fromisoformat(old_image['scheduled_at']), # Transfer the deduplication window if it exists deduplication_window={'offset_days': offset_days} if offset_days else None, - linked_entities=linked_entities, persistence_layer=dyn, ) @@ -105,17 +87,49 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: ), ) except Exception as exc: - dyn.put_item( - item={ - 'id': old_image['id'], - 'sk': f'{sk}#FAILED', - 'cause': { - 'type': type(exc).__name__, - 'message': str(exc), - }, - 'snapshot': old_image, - 'created_at': now_, - } - ) + with dyn.transact_writer() as transact: + transact.put( + item={ + 'id': old_image['id'], + 'sk': f'{sk}#FAILED', + 'cause': { + 'type': type(exc).__name__, + 'message': str(exc), + }, + 'snapshot': old_image, + 'created_at': now_, + } + ) + + if seat: + order_id = seat['order_id'] + transact.update( + key=KeyPair( + pk=order_id, + sk=f'ENROLLMENT#{enrollment.id}', + ), + 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': 'DEDUPLICATION', + ':now': now_, + }, + table_name=ORDER_TABLE, + ) + transact.put( + item={ + 'id': f'SEAT#ORG#{org_id}', + 'sk': f'ORDER#{order_id}#ENROLLMENT#{uuid4()}', + 'course': enrollment.course.model_dump(), + 'created_at': now_, + } + ) return True diff --git a/enrollments-events/app/events/reenroll_on_failed.py b/enrollments-events/app/events/reenroll_on_failed.py index f2fdb46..bc893a8 100644 --- a/enrollments-events/app/events/reenroll_on_failed.py +++ b/enrollments-events/app/events/reenroll_on_failed.py @@ -13,8 +13,6 @@ from config import ENROLLMENT_TABLE from enrollment import ( Course, Enrollment, - Kind, - LinkedEntity, SubscriptionFrozenError, User, enroll, @@ -64,14 +62,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: # Reuse the deduplication window if it exists deduplication_window={'offset_days': offset_days} if offset_days else None, # The enrollment must know its source - linked_entities=frozenset( - { - LinkedEntity( - id=new_image['id'], - kind=Kind.ENROLLMENT, - ), - }, - ), + parent_entity=new_image['id'], persistence_layer=dyn, ) except SubscriptionFrozenError: diff --git a/enrollments-events/app/events/restore_seat_on_scheduled_canceled.py b/enrollments-events/app/events/restore_seat_on_scheduled_canceled.py index d780e2e..904f171 100644 --- a/enrollments-events/app/events/restore_seat_on_scheduled_canceled.py +++ b/enrollments-events/app/events/restore_seat_on_scheduled_canceled.py @@ -30,7 +30,6 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: 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, \ diff --git a/enrollments-events/tests/events/test_ask_to_sign.py b/enrollments-events/tests/events/test_ask_to_sign.py index 6b5cdae..ef5edba 100644 --- a/enrollments-events/tests/events/test_ask_to_sign.py +++ b/enrollments-events/tests/events/test_ask_to_sign.py @@ -4,6 +4,7 @@ import events.ask_to_sign as app def test_ask_to_sign( + dynamodb_seeds, lambda_context: LambdaContext, ): event = { diff --git a/enrollments-events/tests/events/test_enroll.py b/enrollments-events/tests/events/test_enroll.py index a1ef4eb..fa38bda 100644 --- a/enrollments-events/tests/events/test_enroll.py +++ b/enrollments-events/tests/events/test_enroll.py @@ -21,16 +21,16 @@ def test_enroll( assert app.lambda_handler(event, lambda_context) # type: ignore # Parent knows the child - r = dynamodb_persistence_layer.collection.query( - KeyPair(order_id, 'LINKED_ENTITIES#CHILD') - ) - *_, enrollment_id = r['items'][0]['sk'].split('#') + # r = dynamodb_persistence_layer.collection.query( + # KeyPair(order_id, 'LINKED_ENTITY#CHILD') + # ) + # *_, enrollment_id = r['items'][0]['sk'].split('#') # Child knows the parent - enrollment = dynamodb_persistence_layer.collection.get_item( - KeyPair(enrollment_id, f'LINKED_ENTITIES#PARENT#ORDER#{order_id}'), - ) - assert enrollment + # enrollment = dynamodb_persistence_layer.collection.get_item( + # KeyPair(enrollment_id, f'LINKED_ENTITY#PARENT#ORDER#{order_id}'), + # ) + # assert enrollment - r = dynamodb_persistence_layer.collection.query(PartitionKey(enrollment['id'])) - assert not any(x['sk'] == 'METADATA#DEDUPLICATION_WINDOW' for x in r['items']) + # r = dynamodb_persistence_layer.collection.query(PartitionKey(enrollment['id'])) + # assert not any(x['sk'] == 'METADATA#DEDUPLICATION_WINDOW' for x in r['items']) diff --git a/enrollments-events/tests/events/test_reenroll_on_failed.py b/enrollments-events/tests/events/test_reenroll_on_failed.py index 4937566..284a4d7 100644 --- a/enrollments-events/tests/events/test_reenroll_on_failed.py +++ b/enrollments-events/tests/events/test_reenroll_on_failed.py @@ -34,7 +34,7 @@ def test_reenroll_custom_dedup_window( r = dynamodb_persistence_layer.collection.query( KeyPair( pk=enrollment_id, - sk='LINKED_ENTITIES#CHILD', + sk='LINKED_ENTITY#CHILD', ) ) *_, child_id = r['items'][0]['sk'].split('#') @@ -43,7 +43,7 @@ def test_reenroll_custom_dedup_window( child = dynamodb_persistence_layer.collection.get_item( KeyPair( pk=child_id, - sk=f'LINKED_ENTITIES#PARENT#ENROLLMENT#{enrollment_id}', + sk=f'LINKED_ENTITY#PARENT#ENROLLMENT#{enrollment_id}', ) ) assert child diff --git a/enrollments-events/tests/events/test_restore_seat_on_canceled.py b/enrollments-events/tests/events/test_restore_seat_on_canceled.py index cc5be37..c2e80ff 100644 --- a/enrollments-events/tests/events/test_restore_seat_on_canceled.py +++ b/enrollments-events/tests/events/test_restore_seat_on_canceled.py @@ -12,8 +12,8 @@ def test_restore_seat_on_canceled( event = { 'detail': { 'old_image': { - 'id': '', - 'seat': {'order_id': ''}, + 'id': 'a1ba618d-b14b-412d-a0ee-6e3ccac4794d', + 'seat': {'order_id': 'bebf2e39-b23e-47c2-9001-6c1f32ad2abb'}, }, } } diff --git a/enrollments-events/tests/seeds.jsonl b/enrollments-events/tests/seeds.jsonl index 5f5494d..c0862fc 100644 --- a/enrollments-events/tests/seeds.jsonl +++ b/enrollments-events/tests/seeds.jsonl @@ -50,4 +50,8 @@ {"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 +{"id": "f1ecaa69-8054-4cdc-ba13-a6680e18df21", "sk": "ENROLLMENT#19c0aa75-473e-4d4c-822d-2d42d46d2167", "status": "SCHEDULED"} + +// file: tests/events/test_restore_seat_on_canceled.py +{"id": "a1ba618d-b14b-412d-a0ee-6e3ccac4794d", "sk": "0", "status": "CANCELED", "org_id": "75fed8ec-f1ec-46c7-859a-5ccaaaa71fa5", "course": {"id": "2867e8c8-00bf-4147-a474-ed9fe3f84a8a", "name": "pytest", "access_period": "360"}} +{"id": "bebf2e39-b23e-47c2-9001-6c1f32ad2abb", "sk": "ENROLLMENT#a1ba618d-b14b-412d-a0ee-6e3ccac4794d", "status": "EXECUTED"} \ 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 bc3b48f..edd1837 100644 --- a/orders-events/app/events/start_fulfillment.py +++ b/orders-events/app/events/start_fulfillment.py @@ -270,21 +270,11 @@ def _enroll_now(enrollment: Enrollment, context: Context) -> None: 'seat': {'order_id': order_id}, } ) - # Relationships between this enrollment and its related entities - transact.put( - item={ - 'id': order_id, - 'sk': f'LINKED_ENTITIES#CHILD#ENROLLMENT#{enrollment.id}', - 'created_at': now_, - }, - cond_expr='attribute_not_exists(sk)', - table_name=ORDER_TABLE, - ) - # Child knows the parent + # Enrollment should know where it comes from transact.put( item={ 'id': enrollment.id, - 'sk': f'LINKED_ENTITIES#PARENT#ORDER#{order_id}', + 'sk': f'LINKED_ENTITY#PARENT#ORDER#{order_id}', 'created_at': now_, }, cond_expr='attribute_not_exists(sk)',