diff --git a/api.saladeaula.digital/app/app.py b/api.saladeaula.digital/app/app.py index dc5cc74..dee4d8e 100644 --- a/api.saladeaula.digital/app/app.py +++ b/api.saladeaula.digital/app/app.py @@ -49,6 +49,7 @@ app.include_router(users.orgs, prefix='/users') app.include_router(users.password, prefix='/users') app.include_router(orders.router, prefix='/orders') app.include_router(orders.checkout, prefix='/orders') +app.include_router(orders.payment_retries, prefix='/orders') app.include_router(orgs.add, prefix='/orgs') app.include_router(orgs.address, prefix='/orgs') app.include_router(orgs.admins, prefix='/orgs') diff --git a/api.saladeaula.digital/app/exceptions.py b/api.saladeaula.digital/app/exceptions.py index f139555..4c22f80 100644 --- a/api.saladeaula.digital/app/exceptions.py +++ b/api.saladeaula.digital/app/exceptions.py @@ -14,6 +14,9 @@ class ConflictError(ServiceError): class OrderNotFoundError(NotFoundError): ... +class OrderConflictError(NotFoundError): ... + + class UserNotFoundError(NotFoundError): ... diff --git a/api.saladeaula.digital/app/routes/courses/__init__.py b/api.saladeaula.digital/app/routes/courses/__init__.py index 789ec9a..93e3f6b 100644 --- a/api.saladeaula.digital/app/routes/courses/__init__.py +++ b/api.saladeaula.digital/app/routes/courses/__init__.py @@ -112,6 +112,7 @@ def sample(course_id: str, s3_uri: Annotated[str, Body(embed=True)]): # Send template URI and data to Paperforge API to generate a PDF r = requests.post( PAPERFORGE_API, + timeout=(1, 3), json={ 'template_uri': s3_uri, 'args': { diff --git a/api.saladeaula.digital/app/routes/orders/__init__.py b/api.saladeaula.digital/app/routes/orders/__init__.py index 505996a..e3bcbde 100644 --- a/api.saladeaula.digital/app/routes/orders/__init__.py +++ b/api.saladeaula.digital/app/routes/orders/__init__.py @@ -11,8 +11,9 @@ from config import ORDER_TABLE from exceptions import OrderNotFoundError from .checkout import router as checkout +from .payment_retries import router as payment_retries -__all__ = ['checkout'] +__all__ = ['checkout', 'payment_retries'] router = Router() dyn = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client) diff --git a/api.saladeaula.digital/app/routes/orders/payment_retries.py b/api.saladeaula.digital/app/routes/orders/payment_retries.py new file mode 100644 index 0000000..76b1237 --- /dev/null +++ b/api.saladeaula.digital/app/routes/orders/payment_retries.py @@ -0,0 +1,66 @@ +from http import HTTPStatus +from typing import Annotated + +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, ttl +from layercake.dynamodb import ( + DynamoDBPersistenceLayer, + KeyPair, +) +from layercake.extra_types import CreditCard +from pydantic import UUID4 + +from api_gateway import JSONResponse +from boto3clients import dynamodb_client +from config import ORDER_TABLE +from exceptions import OrderConflictError + +router = Router() +dyn = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client) + + +class InvoiceNotFoundError(NotFoundError): ... + + +@router.post('//payment-retries') +def payment_retries( + order_id: str, + credit_card: Annotated[CreditCard, Body(embed=True)], + invoice_id: Annotated[UUID4 | str, Body(embed=True)], + installments: Annotated[int, Body(embed=True)], +): + now_ = now() + + with dyn.transact_writer() as transact: + transact.condition( + key=KeyPair(order_id, '0'), + cond_expr='attribute_exists(sk) AND installments = :installments', + expr_attr_values={ + ':installments': installments, + }, + exc_cls=OrderConflictError, + ) + transact.condition( + key=KeyPair(order_id, 'INVOICE'), + cond_expr='attribute_exists(sk) AND invoice_id = :invoice_id', + expr_attr_values={ + ':invoice_id': invoice_id, + }, + exc_cls=InvoiceNotFoundError, + ) + transact.put( + item={ + 'id': order_id, + 'sk': 'TRANSACTION', + 'invoice_id': invoice_id, + 'credit_card': credit_card.model_dump(), + 'installments': installments, + 'ttl': ttl(start_dt=now_, minutes=5), + 'created_at': now_, + }, + cond_expr='attribute_not_exists(sk)', + ) + + return JSONResponse(status_code=HTTPStatus.CREATED) diff --git a/api.saladeaula.digital/tests/routes/orders/test_payment_retries.py b/api.saladeaula.digital/tests/routes/orders/test_payment_retries.py new file mode 100644 index 0000000..46f9a15 --- /dev/null +++ b/api.saladeaula.digital/tests/routes/orders/test_payment_retries.py @@ -0,0 +1,38 @@ +from http import HTTPMethod, HTTPStatus + +from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair + +from ...conftest import HttpApiProxy, LambdaContext + + +def test_payment_retries( + app, + seeds, + http_api_proxy: HttpApiProxy, + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + lambda_context: LambdaContext, +): + r = app.lambda_handler( + http_api_proxy( + raw_path='/orders/4b23f6f5-5377-476b-b1de-79427c0295f6/payment-retries', + method=HTTPMethod.POST, + body={ + 'invoice_id': '123', + 'credit_card': { + 'holder_name': 'Sergio R Siqueira', + 'number': '4111111111111111', + 'exp_month': '01', + 'exp_year': '2026', + 'cvv': '123', + }, + 'installments': 3, + }, + ), + lambda_context, + ) + assert r['statusCode'] == HTTPStatus.CREATED + + r = dynamodb_persistence_layer.collection.get_item( + KeyPair('4b23f6f5-5377-476b-b1de-79427c0295f6', 'TRANSACTION') + ) + assert r['credit_card']['number'] == '4111111111111111' diff --git a/api.saladeaula.digital/tests/seeds.jsonl b/api.saladeaula.digital/tests/seeds.jsonl index 8d6d9e1..68669f4 100644 --- a/api.saladeaula.digital/tests/seeds.jsonl +++ b/api.saladeaula.digital/tests/seeds.jsonl @@ -26,6 +26,11 @@ {"id": "orgmembers#f6000f79-6e5c-49a0-952f-3bda330ef278", "sk": "15bacf02-1535-4bee-9022-19d106fd7518"} +// Seeds for Order +// file: tests/routes/orders/test_payment_retries.py +{"id": "4b23f6f5-5377-476b-b1de-79427c0295f6", "sk": "0", "installments": 3} +{"id": "4b23f6f5-5377-476b-b1de-79427c0295f6", "sk": "INVOICE", "invoice_id": "123"} + // Indicies // CNPJs {"id": "cnpj", "sk": "04978826000180", "org_id": "2a8963fc-4694-4fe2-953a-316d1b10f1f5"}