diff --git a/api.saladeaula.digital/template.yaml b/api.saladeaula.digital/template.yaml index aa4e7eb..0ca3741 100644 --- a/api.saladeaula.digital/template.yaml +++ b/api.saladeaula.digital/template.yaml @@ -2,12 +2,18 @@ AWSTemplateFormatVersion: "2010-09-09" Transform: "AWS::Serverless-2016-10-31" Parameters: + UsersTable: + Type: String + Default: betaeducacao-prod-users_d2o3r5gmm4it7j CourseTable: Type: String Default: saladeaula_courses EnrollmentTable: Type: String Default: betaeducacao-prod-enrollments + OrderTable: + Type: String + Default: betaeducacao-prod-orders BucketName: Type: String Default: saladeaula.digital @@ -66,10 +72,14 @@ Resources: LoggingConfig: LogGroup: !Ref HttpLog Policies: + - DynamoDBCrudPolicy: + TableName: !Ref UserTable - DynamoDBCrudPolicy: TableName: !Ref CourseTable - DynamoDBCrudPolicy: TableName: !Ref EnrollmentTable + - DynamoDBCrudPolicy: + TableName: !Ref OrderTable - S3CrudPolicy: BucketName: !Ref BucketName Events: diff --git a/enrollments-events/app/config.py b/enrollments-events/app/config.py index 6a5cd24..c782ed9 100644 --- a/enrollments-events/app/config.py +++ b/enrollments-events/app/config.py @@ -1,5 +1,7 @@ import os +DEDUP_WINDOW_OFFSET_DAYS = 90 + USER_TABLE: str = os.getenv('USER_TABLE') # type: ignore ORDER_TABLE: str = os.getenv('ORDER_TABLE') # type: ignore ENROLLMENT_TABLE: str = os.getenv('ENROLLMENT_TABLE') # type: ignore diff --git a/enrollments-events/app/enrollment.py b/enrollments-events/app/enrollment.py index c0c6478..67f879e 100644 --- a/enrollments-events/app/enrollment.py +++ b/enrollments-events/app/enrollment.py @@ -5,9 +5,10 @@ from enum import Enum from typing import NotRequired, TypedDict from layercake.dateutils import now, ttl -from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair +from layercake.dynamodb import DynamoDBPersistenceLayer from layercake.strutils import md5_hash +from config import DEDUP_WINDOW_OFFSET_DAYS from schemas import Enrollment Org = TypedDict( @@ -124,33 +125,38 @@ def enroll( # Prevents the user from enrolling in the same course again until # the deduplication window expires or is removed. + offset_days = ( + int(deduplication_window['offset_days']) + if deduplication_window + else DEDUP_WINDOW_OFFSET_DAYS + ) + ttl_ = ttl( + start_dt=now_, + days=course.access_period - offset_days, + ) + transact.put( + item={ + 'id': 'LOCK', + 'sk': lock_hash, + 'enrollment_id': enrollment.id, + 'created_at': now_, + 'ttl': ttl_, + }, + cond_expr='attribute_not_exists(sk)', + exc_cls=DeduplicationConflictError, + ) + transact.put( + item={ + 'id': enrollment.id, + 'sk': 'LOCK', + 'hash': lock_hash, + 'created_at': now_, + 'ttl': ttl_, + }, + ) + + # The deduplication window can be recalculated based on user settings. if deduplication_window: - offset_days = int(deduplication_window['offset_days']) - ttl_ = ttl( - start_dt=now_, - days=course.access_period - offset_days, - ) - transact.put( - item={ - 'id': 'LOCK', - 'sk': lock_hash, - 'enrollment_id': enrollment.id, - 'created_at': now_, - 'ttl': ttl_, - }, - cond_expr='attribute_not_exists(sk)', - exc_cls=DeduplicationConflictError, - ) - transact.put( - item={ - 'id': enrollment.id, - 'sk': 'LOCK', - 'hash': lock_hash, - 'created_at': now_, - 'ttl': ttl_, - }, - ) - # Deduplication window can be recalculated if needed transact.put( item={ 'id': enrollment.id, @@ -159,11 +165,5 @@ def enroll( 'created_at': now_, }, ) - else: - transact.condition( - key=KeyPair('LOCK', lock_hash), - cond_expr='attribute_not_exists(sk)', - exc_cls=DeduplicationConflictError, - ) return True diff --git a/enrollments-events/app/events/enroll.py b/enrollments-events/app/events/enroll.py index bc60608..be76219 100644 --- a/enrollments-events/app/events/enroll.py +++ b/enrollments-events/app/events/enroll.py @@ -92,7 +92,6 @@ def _handler(record: Course, context: dict) -> Enrollment: enroll( enrollment, persistence_layer=enrollment_layer, - deduplication_window={'offset_days': 90}, linked_entities=frozenset( { LinkedEntity( diff --git a/enrollments-events/app/events/issue_cert.py b/enrollments-events/app/events/issue_cert.py index 1010376..55458b9 100644 --- a/enrollments-events/app/events/issue_cert.py +++ b/enrollments-events/app/events/issue_cert.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta from typing import NotRequired, TypedDict import requests -from aws_lambda_powertools import Logger +from aws_lambda_powertools import Logger, Tracer from aws_lambda_powertools.utilities.data_classes import ( EventBridgeEvent, event_source, @@ -21,12 +21,14 @@ from config import ( PAPERFORGE_API, ) +tracer = Tracer() logger = Logger(__name__) dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) -@event_source(data_class=EventBridgeEvent) @logger.inject_lambda_context +@tracer.capture_lambda_handler +@event_source(data_class=EventBridgeEvent) def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: new_image = event.detail['new_image'] now_ = now() diff --git a/enrollments-events/app/events/reenroll_if_failed.py b/enrollments-events/app/events/reenroll_if_failed.py index 6c2f756..8d4d21f 100644 --- a/enrollments-events/app/events/reenroll_if_failed.py +++ b/enrollments-events/app/events/reenroll_if_failed.py @@ -23,18 +23,25 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: new_image = event.detail['new_image'] metadata = dyn.collection.get_items( TransactKey(new_image['id']) - + SortKey('METADATA#SUBSCRIPTION_COVERED', rename_key='subscription') + + SortKey( + 'METADATA#SUBSCRIPTION_COVERED', + rename_key='subscription', + ) + SortKey( 'METADATA#DEDUPLICATION_WINDOW', path_spec='offset_days', rename_key='dedup_window_offset_days', ) - + SortKey('ORG', rename_key='org'), + + SortKey( + 'ORG', + rename_key='org', + ), flatten_top=False, ) user = User.model_validate(new_image['user']) course = Course.model_validate(new_image['course']) subscription = metadata['subscription'] if 'subscription' in metadata else None + offset_days = metadata.get('dedup_window_offset_days', None) enrollment = Enrollment( id=uuid4(), course=course, @@ -45,9 +52,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: enrollment, org=metadata.get('org', None), subscription=subscription, - deduplication_window={ - 'offset_days': metadata['dedup_window_offset_days'], - }, + deduplication_window={'offset_days': offset_days} if offset_days else None, linked_entities=frozenset( { LinkedEntity( diff --git a/enrollments-events/app/events/reporting/append_cert.py b/enrollments-events/app/events/reporting/append_cert.py index 60e8cf5..c52af27 100644 --- a/enrollments-events/app/events/reporting/append_cert.py +++ b/enrollments-events/app/events/reporting/append_cert.py @@ -31,12 +31,12 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool | No ) target_month = expires_at.strftime('%Y-%m') - now_ = now() - pk = f'CERT#REPORTING#ORG#{org_id}' + now_ = now(tz) + pk = f'CERT_REPORTING#ORG#{org_id}' try: if now_ > expires_at: - raise InvalidDateError() + raise InvalidDateError('Invalid date') # The reporting month is the month before the certificate expires report_month = (expires_at.replace(day=1) - timedelta(days=1)).replace(day=1) @@ -52,7 +52,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool | No 'created_at': now_, }, cond_expr='attribute_not_exists(sk)', - exc_cls=ReportingConflictError, + exc_cls=ReportExistsError, ) transact.put( item={ @@ -64,7 +64,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool | No }, ) except Exception as exc: - logger.exception(exc) + logger.info(exc) try: dyn.put_item( @@ -89,4 +89,6 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool | No class InvalidDateError(Exception): ... -class ReportingConflictError(Exception): ... +class ReportExistsError(Exception): + def __init__(self, *args: object) -> None: + super().__init__('Report already exists') diff --git a/enrollments-events/app/events/reporting/send_report_email.py b/enrollments-events/app/events/reporting/send_report_email.py index d458f35..bda20aa 100644 --- a/enrollments-events/app/events/reporting/send_report_email.py +++ b/enrollments-events/app/events/reporting/send_report_email.py @@ -49,7 +49,7 @@ user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) @logger.inject_lambda_context def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: old_image = event.detail['old_image'] - # Key pattern `CERT#REPORTING#ORG#{org_id}` + # Key pattern `CERT_REPORTING#ORG#{org_id}` *_, org_id = old_image['id'].split('#') # Key pattern `MONTH#{month}#SCHEDULE#SEND_REPORT_EMAIL` _, month, *_ = old_image['sk'].split('#') diff --git a/enrollments-events/template.yaml b/enrollments-events/template.yaml index 90f41c8..f622f9f 100644 --- a/enrollments-events/template.yaml +++ b/enrollments-events/template.yaml @@ -22,7 +22,6 @@ Globals: Function: CodeUri: app/ Runtime: python3.13 - Tracing: Active Architectures: - x86_64 Layers: @@ -312,7 +311,8 @@ Resources: Type: AWS::Serverless::Function Properties: Handler: events.issue_cert.lambda_handler - # Timeout: 30 + Tracing: Active + Timeout: 30 LoggingConfig: LogGroup: !Ref EventLog Policies: @@ -391,6 +391,6 @@ Resources: detail: keys: id: - - prefix: CERT#REPORTING#ORG + - prefix: CERT_REPORTING#ORG sk: - suffix: SCHEDULE#SEND_REPORT_EMAIL diff --git a/enrollments-events/tests/events/reporting/test_append_cert.py b/enrollments-events/tests/events/reporting/test_append_cert.py index 36c31f4..2b7146d 100644 --- a/enrollments-events/tests/events/reporting/test_append_cert.py +++ b/enrollments-events/tests/events/reporting/test_append_cert.py @@ -45,7 +45,7 @@ def test_append_cert( ) r = dynamodb_persistence_layer.collection.get_items( - TransactKey('CERT#REPORTING#ORG#1e2eaf0e-e319-49eb-ab33-1ddec156dc94') + TransactKey('CERT_REPORTING#ORG#1e2eaf0e-e319-49eb-ab33-1ddec156dc94') + SortKey( sk=report_sk, rename_key='report_email', @@ -61,3 +61,31 @@ def test_append_cert( assert 'course' in r['enrollment'] assert 'ttl' in r['report_email'] + + +def test_report_exists( + seeds, + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + lambda_context: LambdaContext, +): + event = { + 'detail': { + 'new_image': { + 'id': 'e45019d8-be7a-4a82-9b37-12a01f0127bb', + 'sk': '0', + 'course': { + 'id': '431', + 'name': 'How to Sing Better', + }, + 'cert_expires_at': '2025-07-02T00:00:00-03:06', + 'user': { + 'id': '1234', + 'name': 'Tobias Summit', + }, + 'org_id': '00237409-9384-4692-9be5-b4443a41e1c4', + 'created_at': '2025-01-01T00:00:00-03:06', + 'completed_at': '2025-01-10T00:00:00-03:06', + } + } + } + assert app.lambda_handler(event, lambda_context) # type: ignore diff --git a/enrollments-events/tests/events/reporting/test_send_report_email.py b/enrollments-events/tests/events/reporting/test_send_report_email.py index ff58067..3306be5 100644 --- a/enrollments-events/tests/events/reporting/test_send_report_email.py +++ b/enrollments-events/tests/events/reporting/test_send_report_email.py @@ -12,7 +12,7 @@ def test_send_report_email( dynamodb_persistence_layer: DynamoDBPersistenceLayer, lambda_context: LambdaContext, ): - pk = 'CERT#REPORTING#ORG#00237409-9384-4692-9be5-b4443a41e1c4' + pk = 'CERT_REPORTING#ORG#00237409-9384-4692-9be5-b4443a41e1c4' event = { 'detail': { 'old_image': { diff --git a/enrollments-events/tests/events/test_enroll.py b/enrollments-events/tests/events/test_enroll.py index ed0f0a7..575c400 100644 --- a/enrollments-events/tests/events/test_enroll.py +++ b/enrollments-events/tests/events/test_enroll.py @@ -1,6 +1,6 @@ import app.events.enroll as app from aws_lambda_powertools.utilities.typing import LambdaContext -from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair +from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, PartitionKey def test_enroll( @@ -30,3 +30,6 @@ def test_enroll( KeyPair(enrollment_id, f'LINKED_ENTITIES#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']) diff --git a/enrollments-events/tests/events/test_reenroll_if_failed.py b/enrollments-events/tests/events/test_reenroll_if_failed.py index a42cdfb..e1b0dc5 100644 --- a/enrollments-events/tests/events/test_reenroll_if_failed.py +++ b/enrollments-events/tests/events/test_reenroll_if_failed.py @@ -3,7 +3,7 @@ from aws_lambda_powertools.utilities.typing import LambdaContext from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair -def test_reenroll( +def test_reenroll_custom_dedup_window( seeds, dynamodb_persistence_layer: DynamoDBPersistenceLayer, lambda_context: LambdaContext, @@ -46,3 +46,8 @@ def test_reenroll( ) ) assert child + + dedup_window = dynamodb_persistence_layer.collection.get_item( + KeyPair(child_id, 'METADATA#DEDUPLICATION_WINDOW') + ) + assert dedup_window diff --git a/enrollments-events/tests/seeds.jsonl b/enrollments-events/tests/seeds.jsonl index 2dc2aca..b5af7a1 100644 --- a/enrollments-events/tests/seeds.jsonl +++ b/enrollments-events/tests/seeds.jsonl @@ -38,8 +38,8 @@ {"id": "294e9864-8284-4287-b153-927b15d90900", "sk": "tenant", "org_id": "123", "name": "EDUSEG", "create_date": "2025-09-12T17:11:00.556907-03:00"} // Certificate reporting -{"id": "CERT#REPORTING#ORG#00237409-9384-4692-9be5-b4443a41e1c4", "sk": "MONTH#2025-06", "status": "PENDING"} -{"id": "CERT#REPORTING#ORG#00237409-9384-4692-9be5-b4443a41e1c4", "sk": "MONTH#2025-07#ENROLLMENT#ba4d48e6-3671-4060-988a-d6cf97dd0ea4", "completed_at": "2025-01-10T00:00:00-03:06", "enrolled_at": "2025-01-01T00:00:00-03:06", "expires_at": "2026-02-10T20:14:42.880991", "course": {"name": "How to Sing Better", "id": "431"}, "created_at": "2025-10-11T23:39:12.194344-03:00", "user": {"name": "Tobias Summit", "id": "1234"}, "enrollment_id": "e45019d8-be7a-4a82-9b37-12a01f0127bb"} +{"id": "CERT_REPORTING#ORG#00237409-9384-4692-9be5-b4443a41e1c4", "sk": "MONTH#2025-06", "status": "PENDING"} +{"id": "CERT_REPORTING#ORG#00237409-9384-4692-9be5-b4443a41e1c4", "sk": "MONTH#2025-07#ENROLLMENT#ba4d48e6-3671-4060-988a-d6cf97dd0ea4", "completed_at": "2025-01-10T00:00:00-03:06", "enrolled_at": "2025-01-01T00:00:00-03:06", "expires_at": "2026-02-10T20:14:42.880991", "course": {"name": "How to Sing Better", "id": "431"}, "created_at": "2025-10-11T23:39:12.194344-03:00", "user": {"name": "Tobias Summit", "id": "1234"}, "enrollment_id": "e45019d8-be7a-4a82-9b37-12a01f0127bb"} // Org {"id": "1e2eaf0e-e319-49eb-ab33-1ddec156dc94", "sk": "0", "name": "pytest"} diff --git a/streams-events/template.yaml b/streams-events/template.yaml index cb42ad1..890eb14 100644 --- a/streams-events/template.yaml +++ b/streams-events/template.yaml @@ -8,7 +8,7 @@ Globals: Architectures: - x86_64 Layers: - - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:96 + - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:99 Environment: Variables: LOG_LEVEL: DEBUG