fix subscription

This commit is contained in:
2025-12-08 15:57:13 -03:00
parent a8bb1799bc
commit 0600ad7da1
10 changed files with 391 additions and 12 deletions

View File

@@ -0,0 +1,35 @@
from aws_lambda_powertools.event_handler.api_gateway import (
APIGatewayHttpResolver,
Response,
)
from aws_lambda_powertools.event_handler.middlewares import (
BaseMiddlewareHandler,
NextMiddleware,
)
from pydantic import UUID4, BaseModel, EmailStr, Field
class User(BaseModel):
id: str | UUID4 = Field(alias='sub')
name: str
email: EmailStr
email_verified: bool
class AuthenticationMiddleware(BaseMiddlewareHandler):
"""This middleware extracts user authentication details from
the jwt_claim authorizer context and makes them available
in the application context.
"""
def handler(
self,
app: APIGatewayHttpResolver,
next_middleware: NextMiddleware,
) -> Response:
jwt_claim = app.current_event.request_context.authorizer.jwt_claim
if jwt_claim:
app.append_context(user=User.model_validate(jwt_claim))
return next_middleware(app)

View File

@@ -0,0 +1,149 @@
from http import HTTPStatus
from typing import Annotated
from uuid import uuid4
from aws_lambda_powertools.event_handler.api_gateway import Router
from aws_lambda_powertools.event_handler.exceptions import NotFoundError
from aws_lambda_powertools.event_handler.openapi.params import Body
from layercake.dateutils import now
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from layercake.extra_types import CnpjStr, NameStr
from pydantic import UUID4, BaseModel, EmailStr
from api_gateway import JSONResponse
from boto3clients import dynamodb_client
from config import INTERNAL_EMAIL_DOMAIN, USER_TABLE
from exceptions import ConflictError
router = Router()
dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
class CNPJConflictError(ConflictError): ...
class EmailConflictError(ConflictError): ...
class UserNotFoundError(NotFoundError): ...
class EmailNotFoundError(NotFoundError): ...
class User(BaseModel):
id: str | UUID4
name: NameStr
email: EmailStr
@router.post('/')
def add(
name: Annotated[str, Body(embed=True)],
cnpj: Annotated[CnpjStr, Body(embed=True)],
user: Annotated[User, Body(embed=True)],
):
now_ = now()
org_id = str(uuid4())
email = f'org+{cnpj}@{INTERNAL_EMAIL_DOMAIN}'
with dyn.transact_writer() as transact:
transact.put(
item={
# Post-migration (users): rename `cnpj` to `CNPJ`
'id': 'cnpj',
'sk': cnpj,
'org_id': org_id,
'created_at': now_,
},
cond_expr='attribute_not_exists(sk)',
exc_cls=CNPJConflictError,
)
transact.put(
item={
# Post-migration (users): rename `email` to `EMAIL`
'id': 'email',
'sk': email,
'user_id': org_id,
'created_at': now_,
},
cond_expr='attribute_not_exists(sk)',
exc_cls=EmailConflictError,
)
transact.put(
item={
'id': org_id,
'sk': '0',
'name': name,
'email': email,
'cnpj': cnpj,
'created_at': now_,
}
)
transact.put(
item={
'id': org_id,
# Post-migration: rename `emails` to `EMAIL`
'sk': f'emails#{email}',
'email_primary': True,
'email_verified': True,
'mx_record_exists': True,
'created_at': now_,
}
)
transact.put(
item={
'id': org_id,
# Post-migration (users): rename `admins#` to `ADMIN#`
'sk': f'admins#{user.id}',
'name': user.name,
'email': user.email,
'created_at': now_,
}
)
transact.put(
item={
'id': user.id,
# Post-migration (users): rename `orgs#` to `ORG#`
'sk': f'orgs#{org_id}',
'name': name,
'cnpj': cnpj,
'created_at': now_,
}
)
transact.put(
item={
'id': user.id,
'sk': f'SCOPE#{org_id}',
'scope': {'apps:admin'},
'created_at': now_,
}
)
transact.put(
item={
# Post-migration (users): rename `orgmembers#` to `MEMBER#ORG#`
'id': f'orgmembers#{org_id}',
'sk': user.id,
'created_at': now_,
}
)
transact.condition(
key=KeyPair(str(user.id), '0'),
cond_expr='attribute_exists(sk)',
exc_cls=UserNotFoundError,
)
transact.condition(
# Post-migration (users): rename `email` to `EMAIL`
key=KeyPair('email', user.email),
cond_expr='attribute_exists(sk)',
exc_cls=EmailNotFoundError,
)
return JSONResponse(
status_code=HTTPStatus.CREATED,
body={
'id': org_id,
'name': name,
'email': email,
},
)

View File

@@ -0,0 +1,55 @@
import json
from http import HTTPMethod, HTTPStatus
from ...conftest import HttpApiProxy, LambdaContext
def test_enroll(
app,
seeds,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
r = app.lambda_handler(
http_api_proxy(
raw_path='/enrollments',
method=HTTPMethod.POST,
body={
'org_id': '2a8963fc-4694-4fe2-953a-316d1b10f1f5',
'enrollments': [
{
'user': {
'id': '15bacf02-1535-4bee-9022-19d106fd7518',
'name': 'Sérgio R Siqueira',
'email': 'sergio@somosbeta.com.br',
'cpf': '07879819908',
},
'course': {
'id': 'c27d1b4f-575c-4b6b-82a1-9b91ff369e0b',
'name': 'NR-10',
'access_period': '360',
'unit_price': '100.30',
},
'scheduled_for': '2028-01-01',
},
{
'user': {
'id': '15bacf02-1535-4bee-9022-19d106fd7518',
'name': 'Sérgio R Siqueira',
'email': 'sergio@somosbeta.com.br',
'cpf': '07879819908',
},
'course': {
'id': '9b1bd8e1-b6da-4f68-9a83-c8d5b8f3b628',
'name': 'CIPA',
'access_period': '360',
'unit_price': '99',
},
},
],
},
),
lambda_context,
)
print(r)

View File

@@ -0,0 +1,76 @@
import json
from http import HTTPMethod, HTTPStatus
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from ...conftest import HttpApiProxy, LambdaContext
def test_get_scormset(
app,
seeds,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
r = app.lambda_handler(
http_api_proxy(
raw_path='/enrollments/9c166c5e-890f-4e77-9855-769c29aaeb2e/scorm',
method=HTTPMethod.GET,
),
lambda_context,
)
assert r['statusCode'] == HTTPStatus.OK
body = json.loads(r['body'])
print(body)
def test_post_scormset(
app,
seeds,
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
http_api_proxy: HttpApiProxy,
lambda_context: LambdaContext,
):
scormbody = {
'suspend_data': '{"v":2,"d":[123,34,112,114,111,103,114,101,115,115,34,58,256,108,263,115,111,110,265,267,34,48,266,256,112,266,49,53,44,34,105,278,276,287,99,281,284,286,275,277,275,290,58,49,125,300,284,49,289,291,285,287,295,256,297,299,302,304,298,125,284,50,313,299,301,34,317,275,293,123,320,51,287,324,320,52,328,278,320,53,332,267,320,54,336,325,315,34,55,340,320,56,345,342,57,348,302,308,306,337,342,49,303,323,333,356,322,256,329,300,365,125],"cpv":"_lnxccXW"}',
'launch_data': '',
'comments': '',
'comments_from_lms': '',
'core': {
'student_id': '',
'student_name': '',
'lesson_location': '',
'credit': '',
'lesson_status': 'incomplete',
'entry': '',
'lesson_mode': 'normal',
'exit': 'suspend',
'session_time': '00:00:00',
'score': {'raw': '', 'min': '', 'max': '100'},
'total_time': '00:00:00',
},
'objectives': {},
'student_data': {
'mastery_score': '',
'max_time_allowed': '',
'time_limit_action': '',
},
'student_preference': {'audio': '', 'language': '', 'speed': '', 'text': ''},
'interactions': {},
}
r = app.lambda_handler(
http_api_proxy(
raw_path='/enrollments/578ec87f-94c7-4840-8780-bb4839cc7e64/scorm',
method=HTTPMethod.POST,
body=scormbody,
),
lambda_context,
)
assert r['statusCode'] == HTTPStatus.NO_CONTENT
r = dynamodb_persistence_layer.collection.get_item(
KeyPair('578ec87f-94c7-4840-8780-bb4839cc7e64', 'SCORM_COMMIT#LAST')
)
assert r['cmi']['suspend_data'] == scormbody['suspend_data']