From 3c1751f4bf1678a754100d71ea2036e249055f5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Rafael=20Siqueira?= Date: Fri, 5 Dec 2025 20:43:49 -0300 Subject: [PATCH] add org --- api.saladeaula.digital/app/app.py | 1 + api.saladeaula.digital/app/exceptions.py | 10 +++ .../app/routes/orgs/__init__.py | 85 +++++++++++++++++-- .../tests/routes/test_orgs.py | 38 ++++++++- 4 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 api.saladeaula.digital/app/exceptions.py diff --git a/api.saladeaula.digital/app/app.py b/api.saladeaula.digital/app/app.py index d6d3b01..3bebd66 100644 --- a/api.saladeaula.digital/app/app.py +++ b/api.saladeaula.digital/app/app.py @@ -45,6 +45,7 @@ app.include_router(users.emails, prefix='/users') app.include_router(users.orgs, prefix='/users') app.include_router(users.password, prefix='/users') app.include_router(orders.router, prefix='/orders') +app.include_router(orgs.router, prefix='/orgs') app.include_router(orgs.admins, prefix='/orgs') app.include_router(orgs.custom_pricing, prefix='/orgs') app.include_router(orgs.scheduled, prefix='/orgs') diff --git a/api.saladeaula.digital/app/exceptions.py b/api.saladeaula.digital/app/exceptions.py new file mode 100644 index 0000000..2a0fd75 --- /dev/null +++ b/api.saladeaula.digital/app/exceptions.py @@ -0,0 +1,10 @@ +from http import HTTPStatus + +from aws_lambda_powertools.event_handler.exceptions import ( + ServiceError, +) + + +class ConflictError(ServiceError): + def __init__(self, msg: str | dict): + super().__init__(HTTPStatus.CONFLICT, msg) diff --git a/api.saladeaula.digital/app/routes/orgs/__init__.py b/api.saladeaula.digital/app/routes/orgs/__init__.py index b225d5f..87bee13 100644 --- a/api.saladeaula.digital/app/routes/orgs/__init__.py +++ b/api.saladeaula.digital/app/routes/orgs/__init__.py @@ -1,13 +1,16 @@ +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 +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 @@ -24,7 +27,13 @@ dyn = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) __all__ = ['admins', 'custom_pricing', 'scheduled', 'users'] -class OrgConflictError(ConflictError): ... +class CNPJConflictError(ConflictError): ... + + +class EmailConflictError(ConflictError): ... + + +class EmailNotFoundError(NotFoundError): ... class User(BaseModel): @@ -33,7 +42,7 @@ class User(BaseModel): email: EmailStr -@router.post('/s') +@router.post('/') def add_org( name: Annotated[str, Body(embed=True)], cnpj: Annotated[CnpjStr, Body(embed=True)], @@ -41,6 +50,7 @@ def add_org( ): now_ = now() org_id = str(uuid4()) + email = f'org+{cnpj}@{INTERNAL_EMAIL_DOMAIN}' with dyn.transact_writer() as transact: transact.put( @@ -52,15 +62,80 @@ def add_org( 'created_at': now_, }, cond_expr='attribute_not_exists(sk)', - exc_cls=OrgConflictError, + 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': f'org+{cnpj}@{INTERNAL_EMAIL_DOMAIN}', + '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={ + # Post-migration (users): rename `orgmembers#` to `MEMBER#ORG#` + 'id': f'orgmembers#{org_id}', + 'sk': user.id, + 'created_at': now_, + } + ) + 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, + }, + ) diff --git a/api.saladeaula.digital/tests/routes/test_orgs.py b/api.saladeaula.digital/tests/routes/test_orgs.py index b107ae9..17b7916 100644 --- a/api.saladeaula.digital/tests/routes/test_orgs.py +++ b/api.saladeaula.digital/tests/routes/test_orgs.py @@ -1,11 +1,46 @@ import json from http import HTTPMethod, HTTPStatus -from layercake.dynamodb import DynamoDBPersistenceLayer +from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, PartitionKey from ..conftest import HttpApiProxy, LambdaContext +def test_add_org( + app, + seeds, + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + http_api_proxy: HttpApiProxy, + lambda_context: LambdaContext, +): + user_id = '213a6682-2c59-4404-9189-12eec0a846d4' + r = app.lambda_handler( + http_api_proxy( + raw_path='/orgs', + method=HTTPMethod.POST, + body={ + 'name': 'Banco Central do Brasil', + 'cnpj': '00038166000105', + 'user': { + 'id': user_id, + 'name': 'Sérgio R Siqueira', + 'email': 'sergio@somosbeta.com.br', + }, + }, + ), + lambda_context, + ) + body = json.loads(r['body']) + assert r['statusCode'] == HTTPStatus.CREATED + + org = dynamodb_persistence_layer.collection.query(PartitionKey(body['id'])) + assert len(org['items']) == 3 + + user = dynamodb_persistence_layer.collection.query(KeyPair(user_id, 'orgs#')) + # One item was added from seeds + assert len(user['items']) == 2 + + def test_get_admins( app, seeds, @@ -28,7 +63,6 @@ def test_get_admins( def test_revoke( app, seeds, - dynamodb_persistence_layer: DynamoDBPersistenceLayer, http_api_proxy: HttpApiProxy, lambda_context: LambdaContext, ):