fix enrollment
This commit is contained in:
@@ -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']},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
2
enrollment-management/uv.lock
generated
2
enrollment-management/uv.lock
generated
@@ -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" },
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
2
http-api/uv.lock
generated
@@ -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" },
|
||||||
|
|||||||
20
user-management/app/ses_utils.py
Normal file
20
user-management/app/ses_utils.py
Normal 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
|
||||||
4
user-management/tests/seeds.jsonl
Normal file
4
user-management/tests/seeds.jsonl
Normal 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"}}
|
||||||
Reference in New Issue
Block a user