fix duplicate user

This commit is contained in:
2026-01-28 10:43:46 -03:00
parent 428006cfac
commit fc14d425f2
14 changed files with 151 additions and 141 deletions

View File

@@ -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)',

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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, \

View File

@@ -4,6 +4,7 @@ import events.ask_to_sign as app
def test_ask_to_sign(
dynamodb_seeds,
lambda_context: LambdaContext,
):
event = {

View File

@@ -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'])

View File

@@ -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

View File

@@ -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'},
},
}
}

View File

@@ -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"}
{"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"}