add fix
This commit is contained in:
@@ -11,6 +11,7 @@ openapi:
|
||||
uv run -m cli.openapi && \
|
||||
uv run python -m http.server 80 -d swagger
|
||||
|
||||
.PHONY: seeds
|
||||
seeds:
|
||||
uv run -m cli.seeds
|
||||
|
||||
|
||||
102
http-api/enrollment.py
Normal file
102
http-api/enrollment.py
Normal file
@@ -0,0 +1,102 @@
|
||||
from typing import TypedDict
|
||||
from uuid import uuid4
|
||||
|
||||
from layercake.dateutils import now
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, TransactItems
|
||||
|
||||
from settings import ORDER_TABLE
|
||||
|
||||
|
||||
class Author(TypedDict):
|
||||
id: str
|
||||
name: str
|
||||
|
||||
|
||||
class Course(TypedDict):
|
||||
id: str
|
||||
name: str
|
||||
time_in_days: int
|
||||
|
||||
|
||||
def set_status_as_canceled(
|
||||
id: str,
|
||||
*,
|
||||
lock_hash: str,
|
||||
author: Author,
|
||||
course: Course | None = None,
|
||||
vacancy_key: KeyPair | None = None,
|
||||
persistence_layer: DynamoDBPersistenceLayer,
|
||||
):
|
||||
"""Cancel the enrollment if there's a `cancel_policy`
|
||||
and put its vacancy back if `vacancy_key` is provided."""
|
||||
now_ = now()
|
||||
transact = TransactItems(persistence_layer.table_name)
|
||||
transact.update(
|
||||
key=KeyPair(id, '0'),
|
||||
update_expr='SET #status = :canceled, update_date = :update',
|
||||
expr_attr_names={
|
||||
'#status': 'status',
|
||||
},
|
||||
expr_attr_values={
|
||||
':canceled': 'CANCELED',
|
||||
':update': now_,
|
||||
},
|
||||
)
|
||||
transact.put(
|
||||
item={
|
||||
'id': id,
|
||||
'sk': 'canceled_date',
|
||||
'author': author,
|
||||
'create_date': now_,
|
||||
},
|
||||
)
|
||||
transact.delete(
|
||||
key=KeyPair(id, 'cancel_policy'),
|
||||
cond_expr='attribute_exists(sk)',
|
||||
)
|
||||
# Remove schedules lifecycle events, referencies and locks
|
||||
transact.delete(key=KeyPair(id, 'schedules#archive_it'))
|
||||
transact.delete(key=KeyPair(id, 'schedules#no_activity'))
|
||||
transact.delete(key=KeyPair(id, 'schedules#access_period_ends'))
|
||||
transact.delete(key=KeyPair(id, 'schedules#does_not_access'))
|
||||
transact.delete(key=KeyPair(id, 'parent_vacancy'))
|
||||
transact.delete(key=KeyPair(id, 'lock'))
|
||||
transact.delete(key=KeyPair('lock', lock_hash))
|
||||
|
||||
if vacancy_key and course:
|
||||
vacancy_pk, vacancy_sk = vacancy_key.values()
|
||||
org_id = vacancy_pk.removeprefix('vacancies#')
|
||||
order_id, enrollment_id = vacancy_sk.split('#')
|
||||
|
||||
transact.condition(
|
||||
key=KeyPair(order_id, '0'),
|
||||
cond_expr='attribute_exists(id)',
|
||||
table_name=ORDER_TABLE,
|
||||
)
|
||||
# Put the vacancy back and assign a new ID
|
||||
transact.put(
|
||||
item={
|
||||
'id': f'vacancies#{org_id}',
|
||||
'sk': f'{order_id}#{uuid4()}',
|
||||
'course': course,
|
||||
'create_date': now_,
|
||||
},
|
||||
cond_expr='attribute_not_exists(sk)',
|
||||
)
|
||||
# Set the status of `generated_items` to `ROLLBACK` to know
|
||||
# which vacancy is available for reuse
|
||||
transact.update(
|
||||
key=KeyPair(order_id, f'generated_items#{enrollment_id}'),
|
||||
update_expr='SET #status = :status, update_date = :update',
|
||||
expr_attr_names={
|
||||
'#status': 'status',
|
||||
},
|
||||
expr_attr_values={
|
||||
':status': 'ROLLBACK',
|
||||
':update': now_,
|
||||
},
|
||||
cond_expr='attribute_exists(sk)',
|
||||
table_name=ORDER_TABLE,
|
||||
)
|
||||
|
||||
return persistence_layer.transact_write_items(transact)
|
||||
40
http-api/org.py
Normal file
40
http-api/org.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from layercake.dateutils import now
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, TransactItems
|
||||
|
||||
|
||||
def update_policies(
|
||||
id: str,
|
||||
/,
|
||||
payment_policy: dict = {},
|
||||
billing_policy: dict = {},
|
||||
*,
|
||||
persistence_layer: DynamoDBPersistenceLayer,
|
||||
):
|
||||
now_ = now()
|
||||
transact = TransactItems(persistence_layer.table_name)
|
||||
|
||||
if payment_policy:
|
||||
transact.put(
|
||||
item={
|
||||
'id': id,
|
||||
'sk': 'payment_policy',
|
||||
'create_date': now_,
|
||||
}
|
||||
| payment_policy
|
||||
)
|
||||
else:
|
||||
transact.delete(key=KeyPair(id, 'payment_policy'))
|
||||
|
||||
if billing_policy:
|
||||
transact.put(
|
||||
item={
|
||||
'id': id,
|
||||
'sk': 'billing_policy',
|
||||
'create_date': now_,
|
||||
}
|
||||
| billing_policy
|
||||
)
|
||||
else:
|
||||
transact.delete(key=KeyPair(id, 'billing_policy'))
|
||||
|
||||
return persistence_layer.transact_write_items(transact)
|
||||
@@ -1,11 +1,11 @@
|
||||
import json
|
||||
from typing import TypedDict
|
||||
|
||||
from aws_lambda_powertools.event_handler.api_gateway import Router
|
||||
from elasticsearch import Elasticsearch
|
||||
from layercake.dynamodb import (
|
||||
DynamoDBCollection,
|
||||
DynamoDBPersistenceLayer,
|
||||
KeyPair,
|
||||
SortKey,
|
||||
TransactKey,
|
||||
)
|
||||
@@ -13,6 +13,7 @@ from pydantic import UUID4, BaseModel
|
||||
|
||||
import elastic
|
||||
from boto3clients import dynamodb_client
|
||||
from enrollment import set_status_as_canceled
|
||||
from middlewares.audit_log_middleware import AuditLogMiddleware
|
||||
from middlewares.authorizer_middleware import User
|
||||
from settings import ELASTIC_CONN, ENROLLMENT_TABLE, USER_TABLE
|
||||
@@ -20,8 +21,8 @@ from settings import ELASTIC_CONN, ENROLLMENT_TABLE, USER_TABLE
|
||||
router = Router()
|
||||
elastic_client = Elasticsearch(**ELASTIC_CONN)
|
||||
enrollment_layer = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
|
||||
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
|
||||
enrollment_collect = DynamoDBCollection(enrollment_layer)
|
||||
user_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
|
||||
user_collect = DynamoDBCollection(user_layer)
|
||||
|
||||
|
||||
@@ -50,7 +51,7 @@ def get_enrollment(id: str):
|
||||
+ SortKey('canceled_date')
|
||||
+ SortKey('archived_date')
|
||||
+ SortKey('cancel_policy')
|
||||
+ SortKey('parent_vacancy')
|
||||
+ SortKey('parent_vacancy', path_spec='vacancy')
|
||||
+ SortKey('lock', path_spec='hash')
|
||||
+ SortKey('author')
|
||||
+ SortKey('tenant')
|
||||
@@ -58,14 +59,11 @@ def get_enrollment(id: str):
|
||||
)
|
||||
|
||||
|
||||
class Course(TypedDict):
|
||||
id: str
|
||||
name: str
|
||||
|
||||
|
||||
class Cancel(BaseModel):
|
||||
id: UUID4 | str
|
||||
course: Course
|
||||
lock_hash: str
|
||||
course: dict = {}
|
||||
vacancy: dict = {}
|
||||
|
||||
|
||||
@router.patch(
|
||||
@@ -78,6 +76,16 @@ class Cancel(BaseModel):
|
||||
)
|
||||
def cancel(id: str, payload: Cancel):
|
||||
user: User = router.context['user']
|
||||
|
||||
set_status_as_canceled(
|
||||
id,
|
||||
lock_hash=payload.lock_hash,
|
||||
author=user.model_dump(), # type: ignore
|
||||
course=payload.course, # type: ignore
|
||||
vacancy_key=KeyPair.parse_obj(payload.vacancy),
|
||||
persistence_layer=enrollment_layer,
|
||||
)
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
|
||||
69
http-api/routes/orgs/__init__.py
Normal file
69
http-api/routes/orgs/__init__.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from http import HTTPStatus
|
||||
|
||||
from aws_lambda_powertools.event_handler import Response, content_types
|
||||
from aws_lambda_powertools.event_handler.api_gateway import Router
|
||||
from aws_lambda_powertools.event_handler.exceptions import (
|
||||
BadRequestError,
|
||||
)
|
||||
from layercake.dynamodb import (
|
||||
DynamoDBCollection,
|
||||
DynamoDBPersistenceLayer,
|
||||
SortKey,
|
||||
TransactKey,
|
||||
)
|
||||
from pydantic.main import BaseModel
|
||||
from typing_extensions import Literal
|
||||
|
||||
from boto3clients import dynamodb_client
|
||||
from org import update_policies
|
||||
from settings import USER_TABLE
|
||||
|
||||
router = Router()
|
||||
org_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client)
|
||||
org_collect = DynamoDBCollection(org_layer, exception_cls=BadRequestError)
|
||||
|
||||
|
||||
@router.get(
|
||||
'/<id>/policies',
|
||||
compress=True,
|
||||
tags=['Organization'],
|
||||
summary='Get organization policies',
|
||||
)
|
||||
def get_policies(id: str):
|
||||
return org_collect.get_items(
|
||||
TransactKey(id) + SortKey('billing_policy') + SortKey('payment_policy'),
|
||||
flatten_top=False,
|
||||
)
|
||||
|
||||
|
||||
class BillingPolicy(BaseModel):
|
||||
billing_day: int
|
||||
payment_method: Literal['PIX', 'BANK_SLIP', 'MANUAL']
|
||||
|
||||
|
||||
class PaymentPolicy(BaseModel):
|
||||
due_days: int
|
||||
|
||||
|
||||
class Policies(BaseModel):
|
||||
billing_policy: BillingPolicy | None = None
|
||||
payment_policy: PaymentPolicy | None = None
|
||||
|
||||
|
||||
@router.put('/<id>/policies', compress=True, tags=['Organization'])
|
||||
def put_policies(id: str, payload: Policies):
|
||||
payment_policy = payload.payment_policy
|
||||
billing_policy = payload.billing_policy
|
||||
|
||||
update_policies(
|
||||
id,
|
||||
payment_policy=payment_policy.model_dump() if payment_policy else {},
|
||||
billing_policy=billing_policy.model_dump() if billing_policy else {},
|
||||
persistence_layer=org_layer,
|
||||
)
|
||||
|
||||
return Response(
|
||||
body=payload,
|
||||
content_type=content_types.APPLICATION_JSON,
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
@@ -2,6 +2,7 @@ import json
|
||||
from http import HTTPStatus
|
||||
from typing import Annotated
|
||||
|
||||
from aws_lambda_powertools.event_handler import content_types
|
||||
from aws_lambda_powertools.event_handler.api_gateway import (
|
||||
Response,
|
||||
Router,
|
||||
@@ -19,7 +20,7 @@ from layercake.dynamodb import (
|
||||
PartitionKey,
|
||||
PrefixKey,
|
||||
)
|
||||
from pydantic import UUID4, BaseModel, StringConstraints
|
||||
from pydantic import UUID4, BaseModel, EmailStr, StringConstraints
|
||||
|
||||
import cognito
|
||||
import elastic
|
||||
@@ -27,6 +28,7 @@ from boto3clients import dynamodb_client, idp_client
|
||||
from middlewares import AuditLogMiddleware
|
||||
from models import User
|
||||
from settings import ELASTIC_CONN, USER_POOOL_ID, USER_TABLE
|
||||
from user import add_email, del_email
|
||||
|
||||
|
||||
class BadRequestError(MissingError, PowertoolsBadRequestError): ...
|
||||
@@ -63,13 +65,13 @@ def post_user(payload: User):
|
||||
return Response(status_code=HTTPStatus.CREATED)
|
||||
|
||||
|
||||
class NewPassword(BaseModel):
|
||||
class Password(BaseModel):
|
||||
cognito_sub: UUID4
|
||||
new_password: Annotated[str, StringConstraints(min_length=6)]
|
||||
|
||||
|
||||
@router.patch('/<id>', compress=True, tags=['User'])
|
||||
def patch_newpassword(id: str, payload: NewPassword):
|
||||
@router.post('/<id>/password', compress=True, tags=['User'], include_in_schema=False)
|
||||
def new_password(id: str, payload: Password):
|
||||
return Response(status_code=HTTPStatus.OK)
|
||||
|
||||
|
||||
@@ -100,6 +102,38 @@ def get_emails(id: str):
|
||||
)
|
||||
|
||||
|
||||
class Email(BaseModel):
|
||||
email: EmailStr
|
||||
|
||||
|
||||
@router.post(
|
||||
'/<id>/emails',
|
||||
compress=True,
|
||||
tags=['User'],
|
||||
summary='Add user email',
|
||||
middlewares=[AuditLogMiddleware('EMAIL_ADD', user_collect, ('email',))],
|
||||
)
|
||||
def post_email(id: str, payload: Email):
|
||||
assert add_email(id, payload.email, persistence_layer=user_layer)
|
||||
return Response(
|
||||
body=payload,
|
||||
content_type=content_types.APPLICATION_JSON,
|
||||
status_code=HTTPStatus.CREATED,
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
'/<id>/emails',
|
||||
compress=True,
|
||||
tags=['User'],
|
||||
summary='Delete user email',
|
||||
middlewares=[AuditLogMiddleware('EMAIL_DEL', user_collect, ('email',))],
|
||||
)
|
||||
def delete_email(id: str, payload: Email):
|
||||
assert del_email(id, payload.email, persistence_layer=user_layer)
|
||||
return payload
|
||||
|
||||
|
||||
@router.get(
|
||||
'/<id>/logs',
|
||||
compress=True,
|
||||
@@ -109,8 +143,8 @@ def get_emails(id: str):
|
||||
def get_logs(id: str):
|
||||
return user_collect.query(
|
||||
# Post-migration: uncomment to enable PartitionKey with a composite key (id with `logs` prefix).
|
||||
# PartitionKey(ComposeKey(id, prefix='logs')),
|
||||
PartitionKey(ComposeKey(id, prefix='log', delimiter=':')),
|
||||
# PartitionKey(ComposeKey(id, 'logs')),
|
||||
PartitionKey(ComposeKey(id, 'log', delimiter=':')),
|
||||
start_key=router.current_event.get_query_string_value('start_key', None),
|
||||
)
|
||||
|
||||
|
||||
@@ -88,7 +88,11 @@
|
||||
{"id": {"S": "YarwKYcw2sxjYWJTu2wnUy"}, "sk": {"S": "lock"}, "create_date": {"S": "2024-11-04T16:27:37.042051-03:00"}, "hash": {"S": "f8f7996aa99d50eb85266be5a9fca1db"}, "ttl": {"N": "1759692457"}, "ttl_date": {"S": "2025-10-05T16:27:37.042051-03:00"}}
|
||||
{"id": {"S": "YarwKYcw2sxjYWJTu2wnUy"}, "sk": {"S": "assignees#cJtK9SsnJhKPyxESe7g3DG"}, "create_date": {"S": "2024-11-04T16:27:37.042051-03:00"}, "name": {"S": "Beta Educa\u00e7\u00e3o"}, "scope": {"S": "ORG"}}
|
||||
{"id": {"S": "YarwKYcw2sxjYWJTu2wnUy"}, "sk": {"S": "mentions#QV4sXY3DvSTUMGJ4QqsrwJ"}, "scope": {"S": "ORDER"}, "create_date": {"S": "2024-11-04T16:27:37.042051-03:00"}}
|
||||
{"id": {"S": "YarwKYcw2sxjYWJTu2wnUy"}, "sk": {"S": "parent_draft"}, "draft": {"M": {"id": {"S": "vacancies#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "QV4sXY3DvSTUMGJ4QqsrwJ#YarwKYcw2sxjYWJTu2wnUy"}}}, "create_date": {"S": "2024-11-04T16:27:37.042051-03:00"}}
|
||||
{"id": {"S": "YarwKYcw2sxjYWJTu2wnUy"}, "sk": {"S": "parent_vacancy"}, "vacancy": {"M": {"id": {"S": "vacancy#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "QV4sXY3DvSTUMGJ4QqsrwJ#YarwKYcw2sxjYWJTu2wnUy"}}}, "create_date": {"S": "2024-11-04T16:27:37.042051-03:00"}}
|
||||
{"id": {"S": "766k6jLry3x2vwvUZP24s4"}, "sk": {"S": "0"}, "course": {"M": {"id": {"S": "a6775b71-d68a-4263-8ab4-acb3a4f8a8b9"}, "name": {"S": "NR-18 PEMT PTA"}, "time_in_days": {"N": "365"}}}, "create_date": {"S": "2024-11-13T09:32:37.858091-03:00"}, "progress": {"N": "0"}, "score": {"NULL": true}, "status": {"S": "CANCELED"}, "update_date": {"S": "2024-11-13T09:34:25.057030-03:00"}, "user": {"M": {"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "cpf": {"S": "07879819908"}, "email": {"S": "sergio@somosbeta.com.br"}, "name": {"S": "S\u00e9rgio Rafael de Siqueira"}}}}
|
||||
{"id": {"S": "766k6jLry3x2vwvUZP24s4"}, "sk": {"S": "canceled_date"}, "author": {"M": {"id": {"S": "9a41e867-55e1-4573-bd27-7b5d1d1bcfde"}, "name": {"S": "Tiago Maciel"}}}, "create_date": {"S": "2024-11-13T09:34:25.057030-03:00"}}
|
||||
{"sk": {"S": "3CNrFB9dy2RLit2pdeUWy4#GNo8r7EMr9mF73tPH3Tvnc"}, "course": {"M": {"name": {"S": "NR-10 B\u00e1sico"}, "time_in_days": {"N": "180"}, "id": {"S": "YDkh4BtTSWFCJVTmDkhqb3"}}}, "id": {"S": "vacancies#8TVSi5oACLxTiT8ycKPmaQ"}, "create_date": {"S": "2024-08-28T23:52:59.141966-03:00"}}
|
||||
{"id": {"S": "7bzQhaPEB9uR8jRkzkFe2h"},"sk": {"S": "0"},"course": {"M": {"id": {"S": "a6775b71-d68a-4263-8ab4-acb3a4f8a8b9"},"name": {"S": "NR-18 PEMT PTA"},"time_in_days": {"N": "365"}}},"create_date": {"S": "2025-04-11T15:07:56.871203-03:00"},"konviva:id": {"N": "238697"},"progress": {"N": "0"},"score": {"NULL": true},"status": {"S": "PENDING"},"update_date": {"S": "2025-04-11T15:08:00.387831-03:00"},"user": {"M": {"id": {"S": "5OxmMjL-ujoR5IMGegQz"},"cpf": {"S": "07879819908"},"email": {"S": "sergio@somosbeta.com.br"},"name": {"S": "Sérgio Rafael Siqueira"}}}}
|
||||
{"id": {"S": "7bzQhaPEB9uR8jRkzkFe2h"},"sk": {"S": "lock"},"create_date": {"S": "2025-04-11T15:07:56.871203-03:00"},"hash": {"S": "f8f7996aa99d50eb85266be5a9fca1db"},"ttl": {"N": "1773338876"},"ttl_date": {"S": "2026-03-12T15:07:56.871203-03:00"}}
|
||||
{"id": {"S": "7bzQhaPEB9uR8jRkzkFe2h"},"sk": {"S": "parent_vacancy"},"vacancy": {"M": {"id": {"S": "vacancies#cJtK9SsnJhKPyxESe7g3DG"},"sk": {"S": "QV4sXY3DvSTUMGJ4QqsrwJ#7bzQhaPEB9uR8jRkzkFe2h"}}}}
|
||||
{"id": {"S": "7bzQhaPEB9uR8jRkzkFe2h"},"sk": {"S": "cancel_policy"},"create_date": {"S": "2025-04-11T15:07:56.871203-03:00"}}
|
||||
|
||||
@@ -39,3 +39,5 @@
|
||||
{"sk": {"S": "lock"}, "id": {"S": "bTRW6w69aYYKjKy3FZeVjt"}, "lock_type": {"S": "CNPJ"}, "create_date": {"S": "2024-02-06T10:28:30.194688-03:00"}}
|
||||
{"sk": {"S": "nfse"}, "nfse": {"S": "10184"}, "id": {"S": "bTRW6w69aYYKjKy3FZeVjt"}, "create_date": {"S": "2024-02-06T10:29:43.839357-03:00"}}
|
||||
{"sk": {"S": "user"}, "user_id": {"S": "mESNbpk4pMTASxtnstd37B"}, "id": {"S": "bTRW6w69aYYKjKy3FZeVjt"}, "create_date": {"S": "2024-02-06T10:28:06.906367-03:00"}}
|
||||
{"id": {"S": "QV4sXY3DvSTUMGJ4QqsrwJ"},"sk": {"S": "0"},"assignee": {"M": {"name": {"S": "Sérgio Rafael de Siqueira"}}},"cnpj": {"S": "15608435000190"},"create_date": {"S": "2024-11-02T19:39:57.174669-03:00"},"due_date": {"S": "2024-11-03T19:39:09.897000-03:00"},"email": {"S": "sergio@somosbeta.com.br"},"name": {"S": "Beta Educação"},"payment_date": {"S": "2024-11-02T19:40:12.143000-03:00"},"payment_method": {"S": "MANUAL"},"status": {"S": "PAID"},"total": {"N": "10"},"update_date": {"S": "2024-11-02T19:40:12.831120-03:00"}}
|
||||
{"id": {"S": "QV4sXY3DvSTUMGJ4QqsrwJ"},"sk": {"S": "generated_items#7bzQhaPEB9uR8jRkzkFe2h"},"create_date": {"S": "2025-04-11T15:08:00.256341-03:00"},"status": {"S": "SUCCESS"}}
|
||||
|
||||
@@ -23,7 +23,7 @@ Globals:
|
||||
Architectures:
|
||||
- x86_64
|
||||
Layers:
|
||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:42
|
||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:46
|
||||
Environment:
|
||||
Variables:
|
||||
TZ: America/Sao_Paulo
|
||||
|
||||
@@ -9,13 +9,14 @@ import pytest
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer
|
||||
|
||||
PYTEST_TABLE_NAME = 'pytest'
|
||||
PK = os.getenv('DYNAMODB_PARTITION_KEY', 'pk')
|
||||
SK = os.getenv('DYNAMODB_SORT_KEY', 'sk')
|
||||
PK = os.getenv('DYNAMODB_PARTITION_KEY')
|
||||
SK = os.getenv('DYNAMODB_SORT_KEY')
|
||||
|
||||
|
||||
patch = pytest.MonkeyPatch()
|
||||
patch.setenv('USER_TABLE', PYTEST_TABLE_NAME)
|
||||
patch.setenv('COURSE_TABLE', PYTEST_TABLE_NAME)
|
||||
patch.setenv('ENROLLMENT_TABLE', PYTEST_TABLE_NAME)
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
52
http-api/tests/routes/test_enrollments.py
Normal file
52
http-api/tests/routes/test_enrollments.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from http import HTTPMethod, HTTPStatus
|
||||
|
||||
from layercake.dynamodb import (
|
||||
ComposeKey,
|
||||
DynamoDBCollection,
|
||||
DynamoDBPersistenceLayer,
|
||||
KeyPair,
|
||||
PartitionKey,
|
||||
)
|
||||
|
||||
from ..conftest import HttpApiProxy, LambdaContext
|
||||
|
||||
|
||||
def test_cancel_enrollment(
|
||||
mock_app,
|
||||
dynamodb_seeds,
|
||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||
http_api_proxy: HttpApiProxy,
|
||||
lambda_context: LambdaContext,
|
||||
):
|
||||
r = mock_app.lambda_handler(
|
||||
http_api_proxy(
|
||||
raw_path='/enrollments/43ea4475-c369-4f90-b576-135b7df5106b/cancel',
|
||||
method=HTTPMethod.PATCH,
|
||||
headers={'X-Tenant': '*'},
|
||||
body={
|
||||
'id': '43ea4475-c369-4f90-b576-135b7df5106b',
|
||||
'lock_hash': 'f8f7996aa99d50eb85266be5a9fca1db',
|
||||
'course': {
|
||||
'id': '123',
|
||||
'name': 'NR-10',
|
||||
'time_in_days': 720,
|
||||
},
|
||||
'vacancy': {
|
||||
'sk': 'QV4sXY3DvSTUMGJ4QqsrwJ#43ea4475-c369-4f90-b576-135b7df5106b',
|
||||
'id': 'vacancies#cJtK9SsnJhKPyxESe7g3DG',
|
||||
},
|
||||
},
|
||||
),
|
||||
lambda_context,
|
||||
)
|
||||
|
||||
assert r['statusCode'] == HTTPStatus.OK
|
||||
|
||||
collect = DynamoDBCollection(dynamodb_persistence_layer)
|
||||
enrollment = collect.get_item(KeyPair('43ea4475-c369-4f90-b576-135b7df5106b', '0'))
|
||||
assert enrollment['status'] == 'CANCELED'
|
||||
|
||||
vacancies = collect.query(
|
||||
PartitionKey(ComposeKey('cJtK9SsnJhKPyxESe7g3DG', 'vacancies'))
|
||||
)
|
||||
assert len(vacancies['items']) == 1
|
||||
55
http-api/tests/routes/test_orgs.py
Normal file
55
http-api/tests/routes/test_orgs.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import json
|
||||
from http import HTTPMethod, HTTPStatus
|
||||
|
||||
from layercake.dynamodb import (
|
||||
DynamoDBCollection,
|
||||
DynamoDBPersistenceLayer,
|
||||
KeyPair,
|
||||
)
|
||||
|
||||
from ..conftest import HttpApiProxy, LambdaContext
|
||||
|
||||
|
||||
def test_get_policies(
|
||||
mock_app,
|
||||
dynamodb_seeds,
|
||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||
http_api_proxy: HttpApiProxy,
|
||||
lambda_context: LambdaContext,
|
||||
):
|
||||
r = mock_app.lambda_handler(
|
||||
http_api_proxy(
|
||||
raw_path='/orgs/cJtK9SsnJhKPyxESe7g3DG/policies',
|
||||
method=HTTPMethod.GET,
|
||||
headers={'X-Tenant': '*'},
|
||||
),
|
||||
lambda_context,
|
||||
)
|
||||
assert r['statusCode'] == HTTPStatus.OK
|
||||
assert json.loads(r['body']) == {
|
||||
'billing_policy': {'billing_day': 1, 'payment_method': 'PIX'},
|
||||
'payment_policy': {'due_days': 90},
|
||||
}
|
||||
|
||||
|
||||
def test_put_org(
|
||||
mock_app,
|
||||
dynamodb_seeds,
|
||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||
http_api_proxy: HttpApiProxy,
|
||||
lambda_context: LambdaContext,
|
||||
):
|
||||
r = mock_app.lambda_handler(
|
||||
http_api_proxy(
|
||||
raw_path='/orgs/cJtK9SsnJhKPyxESe7g3DG/policies',
|
||||
method=HTTPMethod.PUT,
|
||||
headers={'X-Tenant': '*'},
|
||||
body={},
|
||||
),
|
||||
lambda_context,
|
||||
)
|
||||
assert r['statusCode'] == HTTPStatus.OK
|
||||
|
||||
collect = DynamoDBCollection(dynamodb_persistence_layer)
|
||||
course = collect.get_item(KeyPair('cJtK9SsnJhKPyxESe7g3DG', '0'))
|
||||
assert course['name'] == 'EDUSEG'
|
||||
@@ -1,6 +1,8 @@
|
||||
import json
|
||||
from http import HTTPMethod, HTTPStatus
|
||||
|
||||
from layercake.dynamodb import DynamoDBCollection, DynamoDBPersistenceLayer, KeyPair
|
||||
|
||||
from ..conftest import HttpApiProxy, LambdaContext
|
||||
|
||||
|
||||
@@ -105,3 +107,61 @@ def test_post_user(
|
||||
)
|
||||
|
||||
assert r['statusCode'] == HTTPStatus.CREATED
|
||||
|
||||
|
||||
def test_post_email(
|
||||
mock_app,
|
||||
dynamodb_client,
|
||||
dynamodb_seeds,
|
||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||
http_api_proxy: HttpApiProxy,
|
||||
lambda_context: LambdaContext,
|
||||
):
|
||||
r = mock_app.lambda_handler(
|
||||
http_api_proxy(
|
||||
raw_path='/users/5OxmMjL-ujoR5IMGegQz/emails',
|
||||
method=HTTPMethod.POST,
|
||||
body={
|
||||
'email': 'sergio+pytest@somosbeta.com.br',
|
||||
},
|
||||
),
|
||||
lambda_context,
|
||||
)
|
||||
|
||||
assert r['statusCode'] == HTTPStatus.CREATED
|
||||
|
||||
collect = DynamoDBCollection(dynamodb_persistence_layer)
|
||||
user = collect.get_item(KeyPair('5OxmMjL-ujoR5IMGegQz', '0'))
|
||||
assert user['emails'] == {
|
||||
'sergio@somosbeta.com.br',
|
||||
'osergiosiqueira@gmail.com',
|
||||
'sergio+pytest@somosbeta.com.br',
|
||||
}
|
||||
|
||||
|
||||
def test_delete_email(
|
||||
mock_app,
|
||||
dynamodb_client,
|
||||
dynamodb_seeds,
|
||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||
http_api_proxy: HttpApiProxy,
|
||||
lambda_context: LambdaContext,
|
||||
):
|
||||
r = mock_app.lambda_handler(
|
||||
http_api_proxy(
|
||||
raw_path='/users/5OxmMjL-ujoR5IMGegQz/emails',
|
||||
method=HTTPMethod.DELETE,
|
||||
body={
|
||||
'email': 'osergiosiqueira@gmail.com',
|
||||
},
|
||||
),
|
||||
lambda_context,
|
||||
)
|
||||
|
||||
assert r['statusCode'] == HTTPStatus.OK
|
||||
|
||||
collect = DynamoDBCollection(dynamodb_persistence_layer)
|
||||
user = collect.get_item(KeyPair('5OxmMjL-ujoR5IMGegQz', '0'))
|
||||
assert user['emails'] == {
|
||||
'sergio@somosbeta.com.br',
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{"id": {"S": "apikey"}, "sk": {"S": "MzI1MDQ0NTctZjEzMy00YzAwLTkzNmItNmFhNzEyY2E5ZjQw"}, "tenant": {"M": {"id": {"S": "*"}, "name": {"S": "default"}}}, "user": {"M": {"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "name": {"S": "Sérgio R Siqueira"}, "email": {"S": "sergio@somosbeta.com.br"}}}}
|
||||
{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "0"}, "update_date": {"S": "2024-02-08T16:42:33.776409-03:00"}, "create_date": {"S": "2019-03-25T00:00:00-03:00"}, "email_verified": {"BOOL": true}, "cognito:sub": {"S": "58efed8d-d276-41a8-8502-4ab8b5a6415e"}, "cpf": {"S": "07879819908"}, "email": {"S": "sergio@somosbeta.com.br"}, "name": {"S": "S\u00e9rgio Rafael de Siqueira"}, "last_login": {"S": "2024-02-08T20:53:45.818126-03:00"}, "tenant:org_id": {"L": [{"S": "cJtK9SsnJhKPyxESe7g3DG"}]}}
|
||||
{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "0"}, "update_date": {"S": "2024-02-08T16:42:33.776409-03:00"}, "create_date": {"S": "2019-03-25T00:00:00-03:00"}, "email_verified": {"BOOL": true}, "cognito:sub": {"S": "58efed8d-d276-41a8-8502-4ab8b5a6415e"}, "cpf": {"S": "07879819908"}, "email": {"S": "sergio@somosbeta.com.br"}, "name": {"S": "S\u00e9rgio Rafael de Siqueira"}, "last_login": {"S": "2024-02-08T20:53:45.818126-03:00"}, "emails": {"SS": ["sergio@somosbeta.com.br","osergiosiqueira@gmail.com"]}, "tenant:org_id": {"L": [{"S": "cJtK9SsnJhKPyxESe7g3DG"}]}}
|
||||
{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "cognito"}, "create_date": {"S": "2025-03-03T17:12:26.443507-03:00"}, "sub": {"S": "58efed8d-d276-41a8-8502-4ab8b5a6415e"}}
|
||||
{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "emails#sergio@somosbeta.com.br"}, "email_verified": {"BOOL": true}, "update_date": {"S": "2024-02-08T16:42:33.776409-03:00"}, "create_date": {"S": "2019-03-25T00:00:00-03:00"}, "email_primary": {"BOOL": true}, "mx_record_exists": {"BOOL": true}, "update_date": {"S": "2023-11-09T12:13:04.308986-03:00"}}
|
||||
{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "emails#osergiosiqueira@gmail.com"}, "email_verified": {"BOOL": true}, "update_date": {"S": "2024-02-08T16:42:33.776409-03:00"}, "create_date": {"S": "2019-03-25T00:00:00-03:00"}, "email_primary": {"BOOL": false}, "mx_record_exists": {"BOOL": true}, "update_date": {"S": "2023-11-09T12:13:04.308986-03:00"}}
|
||||
{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "acls#*"}, "create_date": {"S": "2022-06-13T15:00:24.309410-03:00"}, "roles": {"L": [{"S": "ADMIN"}]}}
|
||||
{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "acls#cJtK9SsnJhKPyxESe7g3DG"}, "create_date": {"S": "2025-03-14T10:06:34.628078-03:00"}, "roles": {"L": [{"S": "ADMIN"}]}}
|
||||
{"id": {"S": "5OxmMjL-ujoR5IMGegQz"}, "sk": {"S": "orgs#cJtK9SsnJhKPyxESe7g3DG"}, "cnpj": {"S": "15608435000190"}, "create_date": {"S": "2025-03-13T16:36:50.073156-03:00"}, "name": {"S": "Beta Educação"}}
|
||||
@@ -11,5 +12,11 @@
|
||||
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "payment_policy"}, "due_days": {"N": "90"}}
|
||||
{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "billing_policy"}, "billing_day": {"N": "1"}, "payment_method": {"S": "PIX"}}
|
||||
{"id": {"S": "90d7f0d2-d9a4-4467-a31c-f9a7955964cf"}, "sk": {"S": "0"}, "access_period": {"N": "720"}, "create_date": {"S": "2024-12-30T00:00:33.088916-03:00"},"konviva__class_id": {"N": "266"},"name": {"S": "Reciclagem em NR-18 Básico"},"tenant__org_id": {"SS": ["cJtK9SsnJhKPyxESe7g3DG"]}}
|
||||
{"id": {"S": "43ea4475-c369-4f90-b576-135b7df5106b"}, "sk": {"S": "0"}, "status": {"S": "PENDING"}}
|
||||
{"id": {"S": "43ea4475-c369-4f90-b576-135b7df5106b"}, "sk": {"S": "0"}, "course": {"M": {"id": {"S": "a6775b71-d68a-4263-8ab4-acb3a4f8a8b9"}, "name": {"S": "NR-18 PEMT PTA"}, "time_in_days": {"N": "365"}}}, "status": {"S": "PENDING"}}
|
||||
{"id": {"S": "43ea4475-c369-4f90-b576-135b7df5106b"}, "sk": {"S": "cancel_policy"}}
|
||||
{"id": {"S": "43ea4475-c369-4f90-b576-135b7df5106b"}, "sk": {"S": "parent_vacancy"}, "vacancy": {"M": {"id": {"S": "vacancies#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "QV4sXY3DvSTUMGJ4QqsrwJ#YarwKYcw2sxjYWJTu2wnUy"}}}, "create_date": {"S": "2024-11-04T16:27:37.042051-03:00"}}
|
||||
{"id": {"S": "43ea4475-c369-4f90-b576-135b7df5106b"}, "sk": {"S": "lock"}, "create_date": {"S": "2024-11-04T16:27:37.042051-03:00"}, "hash": {"S": "f8f7996aa99d50eb85266be5a9fca1db"}, "ttl": {"N": "1759692457"}, "ttl_date": {"S": "2025-10-05T16:27:37.042051-03:00"}}
|
||||
{"id": {"S": "QV4sXY3DvSTUMGJ4QqsrwJ"}, "sk": {"S": "0"}}
|
||||
{"id": {"S": "QV4sXY3DvSTUMGJ4QqsrwJ"}, "sk": {"S": "generated_items#43ea4475-c369-4f90-b576-135b7df5106b"}}
|
||||
{"id": {"S": "email"}, "sk": {"S": "sergio@somosbeta.com.br"}}
|
||||
{"id": {"S": "cpf"}, "sk": {"S": "07879819908"}}
|
||||
|
||||
85
http-api/user.py
Normal file
85
http-api/user.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from aws_lambda_powertools.event_handler.exceptions import (
|
||||
BadRequestError,
|
||||
)
|
||||
from botocore.exceptions import ClientError
|
||||
from layercake.dateutils import now
|
||||
from layercake.dynamodb import (
|
||||
ComposeKey,
|
||||
DynamoDBPersistenceLayer,
|
||||
KeyPair,
|
||||
TransactItems,
|
||||
)
|
||||
|
||||
|
||||
def add_email(
|
||||
id: str,
|
||||
email: str,
|
||||
/,
|
||||
*,
|
||||
persistence_layer: DynamoDBPersistenceLayer,
|
||||
):
|
||||
now_ = now()
|
||||
transact = TransactItems(persistence_layer.table_name)
|
||||
transact.update(
|
||||
key=KeyPair(id, '0'),
|
||||
update_expr='ADD emails :email',
|
||||
expr_attr_values={
|
||||
':email': {email},
|
||||
},
|
||||
)
|
||||
transact.put(
|
||||
item={
|
||||
'id': id,
|
||||
'sk': f'emails#{email}',
|
||||
'email_primary': False,
|
||||
'email_verified': False,
|
||||
'create_date': now_,
|
||||
},
|
||||
cond_expr='attribute_not_exists(sk)',
|
||||
)
|
||||
transact.put(
|
||||
item={
|
||||
'id': 'email',
|
||||
'sk': email,
|
||||
'user_id': id,
|
||||
'create_date': now_,
|
||||
},
|
||||
cond_expr='attribute_not_exists(sk)',
|
||||
)
|
||||
|
||||
try:
|
||||
return persistence_layer.transact_write_items(transact)
|
||||
except ClientError:
|
||||
raise BadRequestError('Email already exists.')
|
||||
|
||||
|
||||
def del_email(
|
||||
id: str,
|
||||
email: str,
|
||||
/,
|
||||
*,
|
||||
persistence_layer: DynamoDBPersistenceLayer,
|
||||
) -> bool:
|
||||
"""Delete any email except the primary email."""
|
||||
|
||||
transact = TransactItems(persistence_layer.table_name)
|
||||
transact.delete(
|
||||
key=KeyPair('email', email),
|
||||
)
|
||||
transact.delete(
|
||||
key=KeyPair(id, ComposeKey(email, 'emails')),
|
||||
cond_expr='email_primary <> :primary',
|
||||
expr_attr_values={':primary': True},
|
||||
)
|
||||
transact.update(
|
||||
key=KeyPair(id, '0'),
|
||||
update_expr='DELETE emails :email',
|
||||
expr_attr_values={
|
||||
':email': {email},
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
return persistence_layer.transact_write_items(transact)
|
||||
except ClientError:
|
||||
raise BadRequestError('Cannot remove the primary email.')
|
||||
2
http-api/uv.lock
generated
2
http-api/uv.lock
generated
@@ -521,7 +521,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "layercake"
|
||||
version = "0.2.5"
|
||||
version = "0.2.9"
|
||||
source = { directory = "../layercake" }
|
||||
dependencies = [
|
||||
{ name = "arnparse" },
|
||||
|
||||
@@ -14,7 +14,7 @@ from boto3.dynamodb.types import TypeDeserializer, TypeSerializer
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
from .dateutils import now, timestamp
|
||||
from .funcs import omit
|
||||
from .funcs import omit, pick
|
||||
|
||||
TZ = os.getenv('TZ', 'UTC')
|
||||
PK = os.getenv('DYNAMODB_PARTITION_KEY', 'pk')
|
||||
@@ -75,8 +75,9 @@ else:
|
||||
def __new__(
|
||||
cls,
|
||||
keyparts: str | tuple[str, ...],
|
||||
*,
|
||||
/,
|
||||
prefix: str | None = None,
|
||||
*,
|
||||
delimiter: str = '#',
|
||||
) -> str:
|
||||
if isinstance(keyparts, str):
|
||||
@@ -90,8 +91,9 @@ else:
|
||||
def __init__(
|
||||
self,
|
||||
keyparts: str | tuple[str, ...],
|
||||
*,
|
||||
/,
|
||||
prefix: str | None = None,
|
||||
*,
|
||||
delimiter: str = '#',
|
||||
) -> None:
|
||||
# __init__ is used to store the parameters for later reference.
|
||||
@@ -258,7 +260,7 @@ class KeyPair(Key):
|
||||
|
||||
match obj:
|
||||
case dict():
|
||||
pair = obj.values()
|
||||
pair = pick((PK, SK), obj).values() # Gotta keep them in order
|
||||
case _:
|
||||
pair = obj
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "layercake"
|
||||
version = "0.2.5"
|
||||
version = "0.2.9"
|
||||
description = "Packages shared dependencies to optimize deployment and ensure consistency across functions."
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
|
||||
@@ -76,6 +76,7 @@ def test_keypair():
|
||||
assert KeyPair('123', 'abc').expr_attr_values() == {':pk': '123', ':sk': 'abc'}
|
||||
|
||||
assert KeyPair.parse_obj({'id': '123', 'sk': 'abc'}) == {'id': '123', 'sk': 'abc'}
|
||||
assert KeyPair.parse_obj({'sk': 'abc', 'id': '123'}) == {'id': '123', 'sk': 'abc'}
|
||||
assert KeyPair.parse_obj(['123', 'abc']) == {'id': '123', 'sk': 'abc'}
|
||||
assert KeyPair.parse_obj([]) is None
|
||||
|
||||
@@ -103,7 +104,7 @@ def test_transact_write_items(
|
||||
transact.put(
|
||||
item=KeyPair(
|
||||
'5OxmMjL-ujoR5IMGegQz',
|
||||
ComposeKey('sergio@somosbeta.com.br', prefix='emails'),
|
||||
ComposeKey('sergio@somosbeta.com.br', 'emails'),
|
||||
),
|
||||
cond_expr='attribute_not_exists(sk)',
|
||||
)
|
||||
|
||||
2
layercake/uv.lock
generated
2
layercake/uv.lock
generated
@@ -600,7 +600,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "layercake"
|
||||
version = "0.2.5"
|
||||
version = "0.2.8"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "arnparse" },
|
||||
|
||||
Reference in New Issue
Block a user