fix enrollment

This commit is contained in:
2025-05-30 20:27:00 -03:00
parent 47b3381108
commit 957f9c4a72
11 changed files with 76 additions and 35 deletions

View File

@@ -32,7 +32,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
+ KeyPair( + KeyPair(
pk=order_id, pk=order_id,
sk=SortKey( sk=SortKey(
'metadata#tenant', sk='metadata#tenant',
path_spec='tenant_id', path_spec='tenant_id',
remove_prefix='metadata#', remove_prefix='metadata#',
), ),
@@ -61,7 +61,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
for pair in result['items']: for pair in result['items']:
batch.delete_item( batch.delete_item(
Key={ Key={
'id': {'S': f'vacancies#{pair["id"]}'}, 'id': {'S': ComposeKey(pair['id'], prefix='vacancies')},
'sk': {'S': pair['sk']}, 'sk': {'S': pair['sk']},
} }
) )

View File

@@ -20,7 +20,7 @@ Globals:
Architectures: Architectures:
- x86_64 - x86_64
Layers: Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:72 - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:75
Environment: Environment:
Variables: Variables:
TZ: America/Sao_Paulo TZ: America/Sao_Paulo

View File

@@ -522,7 +522,7 @@ wheels = [
[[package]] [[package]]
name = "layercake" name = "layercake"
version = "0.6.2" version = "0.6.5"
source = { directory = "../layercake" } source = { directory = "../layercake" }
dependencies = [ dependencies = [
{ name = "arnparse" }, { name = "arnparse" },

View File

@@ -1,20 +1,36 @@
import json
import os import os
from datetime import date
from functools import partial
from typing import Any from typing import Any
from aws_lambda_powertools import Logger, Tracer from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.event_handler.api_gateway import ( from aws_lambda_powertools.event_handler.api_gateway import (
APIGatewayHttpResolver, APIGatewayHttpResolver,
CORSConfig, CORSConfig,
Response,
content_types,
) )
from aws_lambda_powertools.event_handler.exceptions import ServiceError from aws_lambda_powertools.event_handler.exceptions import ServiceError
from aws_lambda_powertools.logging import correlation_paths from aws_lambda_powertools.logging import correlation_paths
from aws_lambda_powertools.utilities.typing import LambdaContext from aws_lambda_powertools.utilities.typing import LambdaContext
from api_gateway import JSONResponse
from middlewares import AuthenticationMiddleware from middlewares import AuthenticationMiddleware
from routes import courses, enrollments, lookup, orders, orgs, settings, users, webhooks 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() tracer = Tracer()
logger = Logger(__name__) logger = Logger(__name__)
cors = CORSConfig( cors = CORSConfig(
@@ -27,6 +43,7 @@ app = APIGatewayHttpResolver(
enable_validation=True, enable_validation=True,
cors=cors, cors=cors,
debug='AWS_SAM_LOCAL' in os.environ, debug='AWS_SAM_LOCAL' in os.environ,
serializer=partial(json.dumps, separators=(',', ':'), cls=JSONEncoder),
) )
app.use(middlewares=[AuthenticationMiddleware()]) app.use(middlewares=[AuthenticationMiddleware()])
app.include_router(courses.router, prefix='/courses') app.include_router(courses.router, prefix='/courses')
@@ -47,12 +64,11 @@ app.include_router(lookup.router, prefix='/lookup')
@app.exception_handler(ServiceError) @app.exception_handler(ServiceError)
def exc_error(exc: ServiceError): def exc_error(exc: ServiceError):
return Response( return JSONResponse(
body={ body={
'msg': exc.msg, 'type': type(exc).__name__,
'err': type(exc).__name__, 'message': str(exc),
}, },
content_type=content_types.APPLICATION_JSON,
status_code=exc.status_code, status_code=exc.status_code,
) )

View File

@@ -51,18 +51,9 @@ def enroll_(payload: Payload):
context = {'tenant': router.context['tenant']} context = {'tenant': router.context['tenant']}
with processor(payload.items, handler, context): with processor(payload.items, handler, context):
processor.process() processed_messages = processor.process()
print(processor.exceptions) return JSONResponse(HTTPStatus.OK, processed_messages)
return JSONResponse(
HTTPStatus.OK,
{
'successes': processor.successes,
'failures': processor.failures,
'exceptions': [str(exc) for exc in processor.exceptions],
},
)
def handler(record: Item, context: dict): def handler(record: Item, context: dict):

View File

@@ -63,6 +63,7 @@ def enroll(
user = enrollment.user user = enrollment.user
course = enrollment.course course = enrollment.course
tenant_id = tenant['id'] tenant_id = tenant['id']
lock_hash = md5_hash('%s%s' % (user.id, course.id))
with persistence_layer.transact_writer() as transact: with persistence_layer.transact_writer() as transact:
transact.put( 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 # Prevents the user from enrolling in the same course again until
# the deduplication window expires or is removed # 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: if deduplication_window:
lock_hash = md5_hash('%s%s' % (user.id, course.id))
offset_days = deduplication_window['offset_days'] offset_days = deduplication_window['offset_days']
ttl_expiration = ttl( ttl_expiration = ttl(
start_dt=now_ + timedelta(days=course.access_period - offset_days) start_dt=now_ + timedelta(days=course.access_period - offset_days)
) )
class DeduplicationConflictError(Exception):
def __init__(self, *args):
super().__init__('Enrollment already exists')
transact.put( transact.put(
item={ item={
'id': 'lock', 'id': 'lock',
@@ -138,8 +144,6 @@ def enroll(
'create_date': now_, 'create_date': now_,
'ttl': ttl_expiration, 'ttl': ttl_expiration,
}, },
cond_expr='attribute_not_exists(sk)',
exc_cls=DeduplicationConflictError,
) )
transact.put( transact.put(
item={ item={

View File

@@ -23,7 +23,7 @@ Globals:
Architectures: Architectures:
- x86_64 - x86_64
Layers: Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:72 - !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:75
Environment: Environment:
Variables: Variables:
TZ: America/Sao_Paulo TZ: America/Sao_Paulo

View File

@@ -26,6 +26,7 @@ def test_enroll(
headers={'X-Tenant': 'cJtK9SsnJhKPyxESe7g3DG'}, headers={'X-Tenant': 'cJtK9SsnJhKPyxESe7g3DG'},
body={ body={
'items': [ 'items': [
# existing enrollment, must fail
{ {
'user': { 'user': {
'id': '5OxmMjL-ujoR5IMGegQz', 'id': '5OxmMjL-ujoR5IMGegQz',
@@ -37,9 +38,6 @@ def test_enroll(
'id': '6d69a34a-cefd-40aa-a89b-dceb694c3e61', 'id': '6d69a34a-cefd-40aa-a89b-dceb694c3e61',
'name': 'pytest', 'name': 'pytest',
}, },
'deduplication_window': {
'offset_days': 60,
},
}, },
{ {
'user': { 'user': {
@@ -52,6 +50,9 @@ def test_enroll(
'id': '6d69a34a-cefd-40aa-a89b-dceb694c3e61', 'id': '6d69a34a-cefd-40aa-a89b-dceb694c3e61',
'name': 'pytest', 'name': 'pytest',
}, },
'deduplication_window': {
'offset_days': 60,
},
}, },
], ],
}, },
@@ -59,8 +60,13 @@ def test_enroll(
lambda_context, lambda_context,
) )
# assert r['statusCode'] == HTTPStatus.OK assert r['statusCode'] == HTTPStatus.OK
print(json.loads(r['body']))
fail, _ = json.loads(r['body'])
assert fail['cause'] == {
'type': 'DeduplicationConflictError',
'message': 'Enrollment already exists',
}
def test_vacancies( def test_vacancies(

2
http-api/uv.lock generated
View File

@@ -560,7 +560,7 @@ wheels = [
[[package]] [[package]]
name = "layercake" name = "layercake"
version = "0.6.2" version = "0.6.5"
source = { directory = "../layercake" } source = { directory = "../layercake" }
dependencies = [ dependencies = [
{ name = "arnparse" }, { name = "arnparse" },

View File

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

View File

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