diff --git a/http-api/app/app.py b/http-api/app/app.py index 85d5aab..ef95e2e 100644 --- a/http-api/app/app.py +++ b/http-api/app/app.py @@ -63,6 +63,7 @@ app.include_router(users.logs, prefix='/users') app.include_router(users.emails, prefix='/users') app.include_router(users.orgs, prefix='/users') app.include_router(orgs.policies, prefix='/orgs') +app.include_router(orgs.address, prefix='/orgs') app.include_router(webhooks.router, prefix='/webhooks') app.include_router(settings.router, prefix='/settings') app.include_router(lookup.router, prefix='/lookup') diff --git a/http-api/app/middlewares/audit_log_middleware.py b/http-api/app/middlewares/audit_log_middleware.py index 60ddc0e..a208a9d 100644 --- a/http-api/app/middlewares/audit_log_middleware.py +++ b/http-api/app/middlewares/audit_log_middleware.py @@ -83,7 +83,7 @@ class AuditLogMiddleware(BaseMiddlewareHandler): self.collection.put_item( key=KeyPair( # Post-migration: remove `delimiter` and update prefix - # from `log` to `logs` in ComposeKey. + # from `log` to `logs#user` in ComposeKey. pk=ComposeKey(user.id, prefix='log', delimiter=':'), sk=now_.isoformat(), ), diff --git a/http-api/app/routes/orgs/__init__.py b/http-api/app/routes/orgs/__init__.py index 33546e4..176b4e3 100644 --- a/http-api/app/routes/orgs/__init__.py +++ b/http-api/app/routes/orgs/__init__.py @@ -1,3 +1,4 @@ +from .address import router as address from .policies import router as policies -__all__ = ['policies'] +__all__ = ['policies', 'address'] diff --git a/http-api/app/routes/orgs/address.py b/http-api/app/routes/orgs/address.py new file mode 100644 index 0000000..134172c --- /dev/null +++ b/http-api/app/routes/orgs/address.py @@ -0,0 +1,51 @@ +from http import HTTPStatus + +from aws_lambda_powertools.event_handler.api_gateway import Router +from layercake.dynamodb import ( + DynamoDBPersistenceLayer, + KeyPair, +) +from pydantic import BaseModel + +from api_gateway import JSONResponse +from boto3clients import dynamodb_client +from config import USER_TABLE + +router = Router() +org_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) + + +@router.get( + '//address', + compress=True, + tags=['Organization'], + summary='Get organization address', +) +def get_address(id: str): + return org_layer.collection.get_item( + KeyPair(id, 'metadata#address'), + ) + + +class Address(BaseModel): + postcode: str + address1: str + address2: str | None = None + neighborhood: str + city: str + state: str + + +@router.post('//address', compress=True, tags=['Organization']) +def post_address(id: str, payload: Address): + address = payload.model_dump() + org_layer.collection.put_item( + key=KeyPair(id, 'metadata#address'), + cond_expr='attribute_exists(sk)', + **address, + ) + + return JSONResponse( + body=payload, + status_code=HTTPStatus.OK, + ) diff --git a/http-api/app/routes/orgs/policies.py b/http-api/app/routes/orgs/policies.py index 3515e8c..439b348 100644 --- a/http-api/app/routes/orgs/policies.py +++ b/http-api/app/routes/orgs/policies.py @@ -1,26 +1,21 @@ from http import HTTPStatus from typing import Literal -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 pydantic import BaseModel +from api_gateway import JSONResponse from boto3clients import dynamodb_client from config import USER_TABLE from rules.org import update_policies router = Router() org_layer = DynamoDBPersistenceLayer(USER_TABLE, dynamodb_client) -org_collect = DynamoDBCollection(org_layer, exc_cls=BadRequestError) @router.get( @@ -30,7 +25,7 @@ org_collect = DynamoDBCollection(org_layer, exc_cls=BadRequestError) summary='Get organization policies', ) def get_policies(id: str): - return org_collect.get_items( + return org_layer.collection.get_items( TransactKey(id) + SortKey('metadata#billing_policy', remove_prefix='metadata#') + SortKey('metadata#payment_policy', remove_prefix='metadata#'), @@ -40,7 +35,7 @@ def get_policies(id: str): class BillingPolicy(BaseModel): billing_day: int - payment_method: Literal['PIX', 'BANK_SLIP', 'MANUAL'] + payment_method: Literal['BANK_SLIP', 'MANUAL'] class PaymentPolicy(BaseModel): @@ -64,8 +59,7 @@ def put_policies(id: str, payload: Policies): persistence_layer=org_layer, ) - return Response( + return JSONResponse( body=payload, - content_type=content_types.APPLICATION_JSON, status_code=HTTPStatus.OK, ) diff --git a/http-api/app/routes/users/emails.py b/http-api/app/routes/users/emails.py index c4fc312..5079a97 100644 --- a/http-api/app/routes/users/emails.py +++ b/http-api/app/routes/users/emails.py @@ -35,9 +35,11 @@ user_collect = DynamoDBCollection(user_layer, exc_cls=BadRequestError) summary='Get user emails', ) def get_emails(id: str): + start_key = router.current_event.get_query_string_value('start_key', None) + return user_collect.query( KeyPair(id, PrefixKey('emails')), - start_key=router.current_event.get_query_string_value('start_key', None), + start_key=start_key, ) @@ -54,6 +56,7 @@ class Email(BaseModel): ) def post_email(id: str, payload: Email): add_email(id, payload.email, persistence_layer=user_layer) + return JSONResponse( body=payload, status_code=HTTPStatus.CREATED, @@ -90,7 +93,11 @@ def patch_email(id: str, payload: EmailAsPrimary): email_verified=payload.email_verified, persistence_layer=user_layer, ) - return JSONResponse(body=payload, status_code=HTTPStatus.OK) + + return JSONResponse( + body=payload, + status_code=HTTPStatus.OK, + ) @router.delete( @@ -101,5 +108,9 @@ def patch_email(id: str, payload: EmailAsPrimary): middlewares=[AuditLogMiddleware('EMAIL_DEL', user_collect, ('email',))], ) def delete_email(id: str, payload: Email): - del_email(id, payload.email, persistence_layer=user_layer) + del_email( + id, + payload.email, + persistence_layer=user_layer, + ) return payload diff --git a/http-api/app/rules/org.py b/http-api/app/rules/org.py index 38dff5b..0330967 100644 --- a/http-api/app/rules/org.py +++ b/http-api/app/rules/org.py @@ -5,36 +5,38 @@ from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair def update_policies( id: str, /, - payment_policy: dict = {}, - billing_policy: dict = {}, + payment_policy: dict | None = None, + billing_policy: dict | None = None, *, persistence_layer: DynamoDBPersistenceLayer, ): now_ = now() + payment_sk = 'metadata#payment_policy' + billing_sk = 'metadata#billing_policy' with persistence_layer.transact_writer() as transact: if payment_policy: transact.put( item={ 'id': id, - 'sk': 'metadata#payment_policy', - 'create_date': now_, + 'sk': payment_sk, + 'created_at': now_, } | payment_policy ) else: - transact.delete(key=KeyPair(id, 'metadata#payment_policy')) + transact.delete(key=KeyPair(id, payment_sk)) if billing_policy: transact.put( item={ 'id': id, - 'sk': 'metadata#billing_policy', - 'create_date': now_, + 'sk': billing_sk, + 'created_at': now_, } | billing_policy ) else: - transact.delete(key=KeyPair(id, 'metadata#billing_policy')) + transact.delete(key=KeyPair(id, billing_sk)) return True diff --git a/http-api/app/rules/user.py b/http-api/app/rules/user.py index a598751..398b37a 100644 --- a/http-api/app/rules/user.py +++ b/http-api/app/rules/user.py @@ -7,7 +7,6 @@ from aws_lambda_powertools.event_handler.exceptions import ( ) from layercake.dateutils import now, ttl from layercake.dynamodb import ( - ComposeKey, DynamoDBPersistenceLayer, KeyPair, SortKey, @@ -35,14 +34,14 @@ def update_user( with persistence_layer.transact_writer() as transact: transact.update( key=KeyPair(user.id, '0'), - update_expr='SET #name = :name, cpf = :cpf, update_date = :update_date', + update_expr='SET #name = :name, cpf = :cpf, updated_at = :updated_at', expr_attr_names={ '#name': 'name', }, expr_attr_values={ ':name': user.name, ':cpf': user.cpf, - ':update_date': now_, + ':updated_at': now_, }, cond_expr='attribute_exists(sk)', ) @@ -56,7 +55,7 @@ def update_user( item={ 'id': user.id, 'sk': 'rate_limit#user_update', - 'create_date': now_, + 'created_at': now_, 'ttl': ttl(start_dt=now_ + timedelta(hours=24)), }, exc_cls=RateLimitError, @@ -73,7 +72,7 @@ def update_user( 'id': 'cpf', 'sk': user.cpf, 'user_id': user.id, - 'create_date': now_, + 'created_at': now_, }, cond_expr='attribute_not_exists(sk)', exc_cls=CPFConflictError, @@ -107,7 +106,7 @@ def add_email( transact.put( item={ 'id': id, - 'sk': ComposeKey(email, prefix='emails'), + 'sk': f'emails#{email}', 'email_primary': False, 'email_verified': False, 'create_date': now_, @@ -145,7 +144,7 @@ def del_email( with persistence_layer.transact_writer() as transact: transact.delete(key=KeyPair('email', email)) transact.delete( - key=KeyPair(id, ComposeKey(email, prefix='emails')), + key=KeyPair(id, f'emails#{email}'), cond_expr='email_primary <> :email_primary', expr_attr_values={ ':email_primary': True, @@ -177,7 +176,7 @@ def set_email_as_primary( with persistence_layer.transact_writer() as transact: # Set the old email as non-primary transact.update( - key=KeyPair(id, ComposeKey(old_email, prefix='emails')), + key=KeyPair(id, f'emails#{old_email}'), update_expr=expr, expr_attr_values={ ':email_primary': False, @@ -186,7 +185,7 @@ def set_email_as_primary( ) # Set the new email as primary transact.update( - key=KeyPair(id, ComposeKey(new_email, 'emails')), + key=KeyPair(id, f'emails#{new_email}'), update_expr=expr, expr_attr_values={ ':email_primary': True, @@ -214,19 +213,22 @@ def del_org_member( org_id: str, persistence_layer: DynamoDBPersistenceLayer, ) -> bool: + now_ = now() with persistence_layer.transact_writer() as transact: # Remove the user's relationship with the organization and their privileges - transact.delete(key=KeyPair(id, ComposeKey(org_id, prefix='acls'))) - transact.delete(key=KeyPair(id, ComposeKey(org_id, prefix='orgs'))) + transact.delete(key=KeyPair(id, f'acls#{org_id}')) + transact.delete(key=KeyPair(id, f'orgs#{org_id}')) transact.update( key=KeyPair(id, '0'), - update_expr='DELETE #tenant :org_id', - expr_attr_names={'#tenant': 'tenant__org_id'}, - expr_attr_values={':org_id': {org_id}}, + update_expr='DELETE tenant_id :tenant_id SET updated_at = :updated_at', + expr_attr_values={ + ':tenant_id': {org_id}, + ':updated_at': now_, + }, ) # Remove the user from the organization's admins and members list - transact.delete(key=KeyPair(org_id, ComposeKey(id, prefix='admins'))) - transact.delete(key=KeyPair(ComposeKey(org_id, prefix='orgmembers'), id)) + transact.delete(key=KeyPair(org_id, f'admins#{id}')) + transact.delete(key=KeyPair(f'orgmembers#{org_id}', id)) return True diff --git a/http-api/tests/routes/sample.html b/http-api/tests/routes/sample.html new file mode 100644 index 0000000..fbb90d7 --- /dev/null +++ b/http-api/tests/routes/sample.html @@ -0,0 +1,251 @@ + + + + + NR-10 Complementar (SEP) + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +

Certificamos que

+

{{ name }}

+

+ Portador(a) do CPF {{ cpf }} , concluiu o curso + de NR-10 Complementar (SEP) com aproveitamento + de + {{ progress }}% +

+

Realizado entre {{ started_date }} e {{ finished_date }}

+

Florianópolis, SC, {{ today }}

+ +
+
+
+

Tiago Maciel do Santos

+

CEO/Diretor

+
+
+ +
+ +
+
+ +
+
+

Conteúdo programático ministrado

+
    +
  • Organização do sistema elétrico de potência
  • +
  • Organização do trabalho
  • +
  • Aspectos comportamentais
  • +
  • Condições impeditivas para serviços
  • +
  • Riscos típicos no SEP e sua prevenção
  • +
  • Técnicas de análise de riscos no SEP
  • +
  • Procedimentos de trabalho (análise e discussão)
  • +
  • Técnicas de análise de riscos no SEP
  • +
  • Equipamentos e ferramentas de trabalho
  • +
  • Sistemas de proteção coletiva
  • +
  • Equipamentos de proteção individual
  • +
  • Posturas e vestuários de trabalhos
  • +
  • + Segurança com veículos e transporte de pessoas, + materiais e equipamentos +
  • +
  • Sinalização e isolamento de áreas de trabalho
  • +
  • + Liberação de instalação para serviço, operação e uso +
  • +
  • + Treinamento em técnicas de remoção, atendimento e + transporte de acidentados +
  • +
  • Acidentes típicos
  • +
  • Responsabilidades
  • +
+
+ +
+
+

Carga horária

+

40 horas

+
+ +
+

Instrutor e responsável técnico

+
+

Francis Ricardo Baretta

+

CPF 039.539.409-02

+

Eng. de Segurança no Trabalho Eng. Eletricista

+

CREA/SC 126693-0

+
+
+
+
+ + diff --git a/http-api/tests/routes/test_orgs.py b/http-api/tests/routes/test_orgs.py index 879e975..fa482d8 100644 --- a/http-api/tests/routes/test_orgs.py +++ b/http-api/tests/routes/test_orgs.py @@ -1,7 +1,7 @@ import json from http import HTTPMethod, HTTPStatus -from layercake.dynamodb import DynamoDBCollection, DynamoDBPersistenceLayer, KeyPair +from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair from ..conftest import HttpApiProxy, LambdaContext @@ -46,6 +46,66 @@ def test_put_policies( ) assert r['statusCode'] == HTTPStatus.OK - collect = DynamoDBCollection(dynamodb_persistence_layer) - course = collect.get_item(KeyPair('cJtK9SsnJhKPyxESe7g3DG', '0')) + course = dynamodb_persistence_layer.collection.get_item( + KeyPair('cJtK9SsnJhKPyxESe7g3DG', '0') + ) assert course['name'] == 'EDUSEG' + + +def test_get_address( + 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/address', + method=HTTPMethod.GET, + body={'payment_policy': None}, + ), + lambda_context, + ) + address = { + 'id': 'cJtK9SsnJhKPyxESe7g3DG', + 'sk': 'metadata#address', + 'postcode': '88101001', + 'address1': 'Av. Presidente Kennedy, 815', + 'address2': 'Sala 1', + 'neighborhood': 'Campinas', + 'city': 'São José', + 'state': 'SC', + } + assert r['statusCode'] == HTTPStatus.OK + assert json.loads(r['body']) == address + + +def test_post_address( + 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/address', + method=HTTPMethod.POST, + body={ + 'postcode': '81280350', + 'address1': 'Rua Monsenhor Ivo Zanlorenzi, 5190', + 'address2': 'ap 1802', + 'neighborhood': 'Cidade Industrial', + 'city': 'Curitiba', + 'state': 'PR', + }, + ), + lambda_context, + ) + assert r['statusCode'] == HTTPStatus.OK + + data = dynamodb_persistence_layer.collection.get_item( + KeyPair('cJtK9SsnJhKPyxESe7g3DG', 'metadata#address') + ) + assert data['address1'] == 'Rua Monsenhor Ivo Zanlorenzi, 5190' diff --git a/http-api/tests/routes/test_users.py b/http-api/tests/routes/test_users.py index 5d11df6..371d0f9 100644 --- a/http-api/tests/routes/test_users.py +++ b/http-api/tests/routes/test_users.py @@ -2,7 +2,6 @@ import json from http import HTTPMethod, HTTPStatus from layercake.dynamodb import ( - DynamoDBCollection, DynamoDBPersistenceLayer, KeyPair, SortKey, @@ -32,13 +31,12 @@ def test_update_user_cpf( ) assert r['statusCode'] == HTTPStatus.OK - collect = DynamoDBCollection(dynamodb_persistence_layer) - user = collect.get_items( + user = dynamodb_persistence_layer.collection.get_items( TransactKey('5OxmMjL-ujoR5IMGegQz') + SortKey('0') - + SortKey('last_profile_edit') + + SortKey('rate_limit#user_update') ) - assert 'last_profile_edit' in user + assert 'rate_limit#user_update' in user def test_update_user_name( @@ -216,8 +214,9 @@ def test_post_email( assert r['statusCode'] == HTTPStatus.CREATED - collect = DynamoDBCollection(dynamodb_persistence_layer) - user = collect.get_item(KeyPair('5OxmMjL-ujoR5IMGegQz', '0')) + user = dynamodb_persistence_layer.collection.get_item( + KeyPair('5OxmMjL-ujoR5IMGegQz', '0') + ) assert user['emails'] == { 'sergio@somosbeta.com.br', 'osergiosiqueira@gmail.com', @@ -274,8 +273,9 @@ def test_delete_email( assert r['statusCode'] == HTTPStatus.OK - collect = DynamoDBCollection(dynamodb_persistence_layer) - user = collect.get_item(KeyPair('5OxmMjL-ujoR5IMGegQz', '0')) + user = dynamodb_persistence_layer.collection.get_item( + KeyPair('5OxmMjL-ujoR5IMGegQz', '0') + ) assert user['emails'] == { 'sergio@somosbeta.com.br', } diff --git a/http-api/tests/seeds.jsonl b/http-api/tests/seeds.jsonl index 80e3991..c2c61b9 100644 --- a/http-api/tests/seeds.jsonl +++ b/http-api/tests/seeds.jsonl @@ -11,6 +11,7 @@ {"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "0"}, "name": {"S": "EDUSEG"}, "cnpj": {"S": "15608435000190"}, "email": {"S": "org+15608435000190@users.noreply.betaeducacao.com.br"}} {"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "metadata#payment_policy"}, "due_days": {"N": "90"}} {"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "metadata#billing_policy"}, "billing_day": {"N": "1"}, "payment_method": {"S": "PIX"}} +{"id": {"S": "cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "metadata#address"}, "address1": {"S": "Av. Presidente Kennedy, 815"}, "address2": {"S": "Sala 1"}, "city": {"S": "São José"}, "state": {"S": "SC"}, "postcode": {"S": "88101001"}, "neighborhood": {"S": "Campinas"}} {"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"}, "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"}}