diff --git a/enrollment-management/app/events/stopgap/del_vacancies.py b/enrollment-management/app/events/stopgap/del_vacancies.py index fb9ac7e..deaa5eb 100644 --- a/enrollment-management/app/events/stopgap/del_vacancies.py +++ b/enrollment-management/app/events/stopgap/del_vacancies.py @@ -32,7 +32,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: + KeyPair( pk=order_id, sk=SortKey( - 'metadata#tenant', + sk='metadata#tenant', path_spec='tenant_id', remove_prefix='metadata#', ), @@ -61,7 +61,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: for pair in result['items']: batch.delete_item( Key={ - 'id': {'S': f'vacancies#{pair["id"]}'}, + 'id': {'S': ComposeKey(pair['id'], prefix='vacancies')}, 'sk': {'S': pair['sk']}, } ) diff --git a/enrollment-management/template.yaml b/enrollment-management/template.yaml index 0e338c4..610019b 100644 --- a/enrollment-management/template.yaml +++ b/enrollment-management/template.yaml @@ -20,7 +20,7 @@ Globals: Architectures: - x86_64 Layers: - - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:72 + - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:75 Environment: Variables: TZ: America/Sao_Paulo diff --git a/enrollment-management/uv.lock b/enrollment-management/uv.lock index 7526fe8..10b9ce1 100644 --- a/enrollment-management/uv.lock +++ b/enrollment-management/uv.lock @@ -522,7 +522,7 @@ wheels = [ [[package]] name = "layercake" -version = "0.6.2" +version = "0.6.5" source = { directory = "../layercake" } dependencies = [ { name = "arnparse" }, diff --git a/http-api/app/app.py b/http-api/app/app.py index 91e18bc..0fd3199 100644 --- a/http-api/app/app.py +++ b/http-api/app/app.py @@ -1,20 +1,36 @@ +import json import os +from datetime import date +from functools import partial from typing import Any from aws_lambda_powertools import Logger, Tracer from aws_lambda_powertools.event_handler.api_gateway import ( APIGatewayHttpResolver, CORSConfig, - Response, - content_types, ) from aws_lambda_powertools.event_handler.exceptions import ServiceError from aws_lambda_powertools.logging import correlation_paths from aws_lambda_powertools.utilities.typing import LambdaContext +from api_gateway import JSONResponse from middlewares import AuthenticationMiddleware from routes import courses, enrollments, lookup, orders, orgs, settings, users, webhooks + +class JSONEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, date): + return o.isoformat() + + return super().default(o) + + +def foo(obj): + print(obj) + return json.dumps(obj, separators=(',', ':'), cls=JSONEncoder) + + tracer = Tracer() logger = Logger(__name__) cors = CORSConfig( @@ -27,6 +43,7 @@ app = APIGatewayHttpResolver( enable_validation=True, cors=cors, debug='AWS_SAM_LOCAL' in os.environ, + serializer=partial(json.dumps, separators=(',', ':'), cls=JSONEncoder), ) app.use(middlewares=[AuthenticationMiddleware()]) app.include_router(courses.router, prefix='/courses') @@ -47,12 +64,11 @@ app.include_router(lookup.router, prefix='/lookup') @app.exception_handler(ServiceError) def exc_error(exc: ServiceError): - return Response( + return JSONResponse( body={ - 'msg': exc.msg, - 'err': type(exc).__name__, + 'type': type(exc).__name__, + 'message': str(exc), }, - content_type=content_types.APPLICATION_JSON, status_code=exc.status_code, ) diff --git a/http-api/app/routes/enrollments/enroll.py b/http-api/app/routes/enrollments/enroll.py index 8b0605a..b3b27fa 100644 --- a/http-api/app/routes/enrollments/enroll.py +++ b/http-api/app/routes/enrollments/enroll.py @@ -51,18 +51,9 @@ def enroll_(payload: Payload): context = {'tenant': router.context['tenant']} with processor(payload.items, handler, context): - processor.process() + processed_messages = processor.process() - print(processor.exceptions) - - return JSONResponse( - HTTPStatus.OK, - { - 'successes': processor.successes, - 'failures': processor.failures, - 'exceptions': [str(exc) for exc in processor.exceptions], - }, - ) + return JSONResponse(HTTPStatus.OK, processed_messages) def handler(record: Item, context: dict): diff --git a/http-api/app/rules/enrollment.py b/http-api/app/rules/enrollment.py index ed6655e..4a9ca34 100644 --- a/http-api/app/rules/enrollment.py +++ b/http-api/app/rules/enrollment.py @@ -63,6 +63,7 @@ def enroll( user = enrollment.user course = enrollment.course tenant_id = tenant['id'] + lock_hash = md5_hash('%s%s' % (user.id, course.id)) with persistence_layer.transact_writer() as transact: transact.put( @@ -117,19 +118,24 @@ def enroll( }, ) + class DeduplicationConflictError(Exception): + def __init__(self, *args): + super().__init__('Enrollment already exists') + # Prevents the user from enrolling in the same course again until # the deduplication window expires or is removed + transact.condition( + key=KeyPair('lock', lock_hash), + cond_expr='attribute_not_exists(sk)', + exc_cls=DeduplicationConflictError, + ) + if deduplication_window: - lock_hash = md5_hash('%s%s' % (user.id, course.id)) offset_days = deduplication_window['offset_days'] ttl_expiration = ttl( start_dt=now_ + timedelta(days=course.access_period - offset_days) ) - class DeduplicationConflictError(Exception): - def __init__(self, *args): - super().__init__('Enrollment already exists') - transact.put( item={ 'id': 'lock', @@ -138,8 +144,6 @@ def enroll( 'create_date': now_, 'ttl': ttl_expiration, }, - cond_expr='attribute_not_exists(sk)', - exc_cls=DeduplicationConflictError, ) transact.put( item={ diff --git a/http-api/template.yaml b/http-api/template.yaml index 3f0b7cf..2dedba9 100644 --- a/http-api/template.yaml +++ b/http-api/template.yaml @@ -23,7 +23,7 @@ Globals: Architectures: - x86_64 Layers: - - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:72 + - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:75 Environment: Variables: TZ: America/Sao_Paulo diff --git a/http-api/tests/routes/test_enrollments.py b/http-api/tests/routes/test_enrollments.py index 5db04c7..5f8ee18 100644 --- a/http-api/tests/routes/test_enrollments.py +++ b/http-api/tests/routes/test_enrollments.py @@ -26,6 +26,7 @@ def test_enroll( headers={'X-Tenant': 'cJtK9SsnJhKPyxESe7g3DG'}, body={ 'items': [ + # existing enrollment, must fail { 'user': { 'id': '5OxmMjL-ujoR5IMGegQz', @@ -37,9 +38,6 @@ def test_enroll( 'id': '6d69a34a-cefd-40aa-a89b-dceb694c3e61', 'name': 'pytest', }, - 'deduplication_window': { - 'offset_days': 60, - }, }, { 'user': { @@ -52,6 +50,9 @@ def test_enroll( 'id': '6d69a34a-cefd-40aa-a89b-dceb694c3e61', 'name': 'pytest', }, + 'deduplication_window': { + 'offset_days': 60, + }, }, ], }, @@ -59,8 +60,13 @@ def test_enroll( lambda_context, ) - # assert r['statusCode'] == HTTPStatus.OK - print(json.loads(r['body'])) + assert r['statusCode'] == HTTPStatus.OK + + fail, _ = json.loads(r['body']) + assert fail['cause'] == { + 'type': 'DeduplicationConflictError', + 'message': 'Enrollment already exists', + } def test_vacancies( diff --git a/http-api/uv.lock b/http-api/uv.lock index 34bb53a..b312853 100644 --- a/http-api/uv.lock +++ b/http-api/uv.lock @@ -560,7 +560,7 @@ wheels = [ [[package]] name = "layercake" -version = "0.6.2" +version = "0.6.5" source = { directory = "../layercake" } dependencies = [ { name = "arnparse" }, diff --git a/user-management/app/ses_utils.py b/user-management/app/ses_utils.py new file mode 100644 index 0000000..04a6580 --- /dev/null +++ b/user-management/app/ses_utils.py @@ -0,0 +1,20 @@ +from typing import Any, Iterator + +from aws_lambda_powertools.utilities.data_classes.ses_event import SESMailHeader + + +def get_header_value( + headers: Iterator[SESMailHeader], + header_name: str, + *, + default: Any = None, + raise_on_missing: bool = True, +) -> str: + for header in headers: + if header.name.lower() == header_name: + return header.value + + if raise_on_missing: + raise ValueError(f'{header_name} not found.') + + return default diff --git a/user-management/tests/seeds.jsonl b/user-management/tests/seeds.jsonl new file mode 100644 index 0000000..b704442 --- /dev/null +++ b/user-management/tests/seeds.jsonl @@ -0,0 +1,4 @@ +{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "0"}, "name": {"S": "EDUSEG"}} +{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "admins#5OxmMjL-ujoR5IMGegQz"}, "name": {"S": "Sérgio R Siqueira"}, "email": {"S": "sergio@somosbeta.com.br"}} +{"id": {"S": "cnpj"}, "sk": {"S": "15608435000190"}, "user_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}} +{"id": {"S": "email"}, "sk": {"S": "org+15608435000190@users.noreply.saladeaula.digital"}, "user_id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}} \ No newline at end of file