add tests to canceled enrollment or scheduled with seat

This commit is contained in:
2026-01-26 13:00:36 -03:00
parent 3e080a2d21
commit 2d191c5fc8
14 changed files with 176 additions and 13 deletions

View File

@@ -343,6 +343,7 @@ def _enroll_later(enrollment: Enrollment, context: Context):
'user': user.model_dump(), 'user': user.model_dump(),
'course': course.model_dump(), 'course': course.model_dump(),
'org_name': org.name, 'org_name': org.name,
'enrollment_id': enrollment.id,
'created_by': { 'created_by': {
'id': created_by.id, 'id': created_by.id,
'name': created_by.name, 'name': created_by.name,

View File

@@ -162,9 +162,9 @@ const statuses: Record<string, { icon: LucideIcon; color?: string }> = {
const labels: Record<string, string> = { const labels: Record<string, string> = {
PENDING: 'Aguardando', PENDING: 'Aguardando',
EXECUTED: 'Executado', EXECUTED: 'Executada',
SCHEDULED: 'Agendado', SCHEDULED: 'Agendada',
ROLLBACK: 'Revogado' ROLLBACK: 'Revogada'
} }
function Status({ status: s }: { status: string }) { function Status({ status: s }: { status: string }) {

View File

@@ -38,7 +38,7 @@ class Course(BaseModel):
class Enrollment(BaseModel): class Enrollment(BaseModel):
id: UUID4 | str = Field(default_factory=uuid4) id: UUID4 | str
user: User user: User
course: Course course: Course
progress: int = Field(default=0, ge=0, le=100) 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}) 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}) DeduplicationWindow = TypedDict('DeduplicationWindow', {'offset_days': int})

View File

@@ -1,3 +1,5 @@
from uuid import uuid4
from aws_lambda_powertools import Logger from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.data_classes import ( from aws_lambda_powertools.utilities.data_classes import (
EventBridgeEvent, EventBridgeEvent,
@@ -84,6 +86,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
def _handler(course: Course, context: dict) -> Enrollment: def _handler(course: Course, context: dict) -> Enrollment:
enrollment = Enrollment( enrollment = Enrollment(
id=uuid4(),
user=context['user'], user=context['user'],
course=course, course=course,
) )

View File

@@ -1,4 +1,5 @@
from datetime import datetime from datetime import datetime
from uuid import uuid4
from aws_lambda_powertools import Logger from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.data_classes import ( 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') offset_days = old_image.get('dedup_window_offset_days')
billing_day = old_image.get('subscription_billing_day') billing_day = old_image.get('subscription_billing_day')
created_by = old_image.get('created_by') created_by = old_image.get('created_by')
seat: Seat | None = old_image.get('seat') seat: Seat = old_image.get('seat', {})
enrollment = Enrollment( enrollment = Enrollment(
id=old_image['enrollment_id'],
course=old_image['course'], course=old_image['course'],
user=old_image['user'], user=old_image['user'],
) )

View File

@@ -37,7 +37,6 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
rollback_at = :now, \ rollback_at = :now, \
reason = :reason', reason = :reason',
cond_expr='attribute_exists(sk) AND #status = :executed', cond_expr='attribute_exists(sk) AND #status = :executed',
table_name=ORDER_TABLE,
expr_attr_names={ expr_attr_names={
'#status': 'status', '#status': 'status',
}, },
@@ -47,6 +46,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
':reason': 'CANCELLATION', ':reason': 'CANCELLATION',
':now': now_, ':now': now_,
}, },
table_name=ORDER_TABLE,
) )
transact.put( transact.put(
item={ item={

View File

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

View File

@@ -211,11 +211,14 @@ Resources:
keys: keys:
id: id:
- prefix: SCHEDULED#ORG# - prefix: SCHEDULED#ORG#
old_image:
enrollment_id:
- exists: true
EventReenrollIfFailedFunction: EventReenrollOnFailedFunction:
Type: AWS::Serverless::Function Type: AWS::Serverless::Function
Properties: Properties:
Handler: events.reenroll_if_failed.lambda_handler Handler: events.reenroll_on_failed.lambda_handler
LoggingConfig: LoggingConfig:
LogGroup: !Ref EventLog LogGroup: !Ref EventLog
Policies: Policies:
@@ -240,10 +243,10 @@ Resources:
old_image: old_image:
status: [IN_PROGRESS] status: [IN_PROGRESS]
EventRestoreSeatFunction: EventRestoreSeatOnCanceledFunction:
Type: AWS::Serverless::Function Type: AWS::Serverless::Function
Properties: Properties:
Handler: events.restore_seat.lambda_handler Handler: events.restore_seat_on_canceled.lambda_handler
LoggingConfig: LoggingConfig:
LogGroup: !Ref EventLog LogGroup: !Ref EventLog
Policies: Policies:
@@ -265,6 +268,32 @@ Resources:
order_id: order_id:
- exists: true - 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 # DEPRECATED
EventAllocateSlotsFunction: EventAllocateSlotsFunction:
Type: AWS::Serverless::Function Type: AWS::Serverless::Function

View File

@@ -1,7 +1,7 @@
from aws_lambda_powertools.utilities.typing import LambdaContext from aws_lambda_powertools.utilities.typing import LambdaContext
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair 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( def test_reenroll_custom_dedup_window(

View File

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

View File

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

View File

@@ -47,4 +47,7 @@
{"id": "00237409-9384-4692-9be5-b4443a41e1c4", "sk": "admins#1234", "email": "sergio@somosbeta.com.br", "name": "Sérgio R Siqueira"} {"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 // file: tests/events/test_reenroll_if_failed.py::test_reenroll_custom_dedup_window
{"id": "SUBSCRIPTION", "sk": "ORG#123"} {"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"}

View File

@@ -352,6 +352,7 @@ def _enroll_later(enrollment: Enrollment, context: Context) -> None:
'user': user.model_dump(), 'user': user.model_dump(),
'course': course.model_dump(), 'course': course.model_dump(),
'org_name': org.name, 'org_name': org.name,
'enrollment_id': enrollment.id,
'created_by': { 'created_by': {
'id': created_by['user_id'], 'id': created_by['user_id'],
'name': created_by['name'], 'name': created_by['name'],