update orders
This commit is contained in:
@@ -68,7 +68,6 @@ def health():
|
||||
@app.exception_handler(ServiceError)
|
||||
def exc_error(exc: ServiceError):
|
||||
logger.exception(exc)
|
||||
|
||||
return JSONResponse(
|
||||
body={
|
||||
'type': type(exc).__name__,
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from functools import reduce
|
||||
from http import HTTPStatus
|
||||
from typing import Any, Literal
|
||||
from uuid import uuid4
|
||||
|
||||
from aws_lambda_powertools.event_handler.api_gateway import Router
|
||||
from layercake.dateutils import now
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer
|
||||
from layercake.extra_types import CnpjStr, CpfStr, NameStr
|
||||
from aws_lambda_powertools.event_handler.exceptions import (
|
||||
NotFoundError,
|
||||
)
|
||||
from layercake.dateutils import now, ttl
|
||||
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
||||
from layercake.extra_types import CnpjStr, CpfStr, CreditCard, NameStr
|
||||
from pydantic import (
|
||||
UUID4,
|
||||
BaseModel,
|
||||
@@ -22,12 +26,14 @@ from api_gateway import JSONResponse
|
||||
from boto3clients import dynamodb_client
|
||||
from config import ORDER_TABLE
|
||||
from routes.enrollments.enroll import Enrollment
|
||||
from routes.orgs.address import address
|
||||
|
||||
router = Router()
|
||||
dyn = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
|
||||
|
||||
|
||||
class CouponNotFoundError(NotFoundError): ...
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
id: UUID4 | str
|
||||
name: NameStr
|
||||
@@ -71,13 +77,15 @@ class Checkout(BaseModel):
|
||||
address: Address
|
||||
payment_method: Literal['PIX', 'CREDIT_CARD', 'BANK_SLIP', 'MANUAL']
|
||||
items: tuple[Item, ...]
|
||||
enrollments: tuple[Enrollment, ...] | None = None
|
||||
enrollments: tuple[Enrollment, ...] = tuple()
|
||||
coupon: Coupon | None = None
|
||||
org_id: UUID4 | str | None = None
|
||||
user_id: UUID4 | str | None = None
|
||||
cnpj: CnpjStr | None = None
|
||||
cpf: CpfStr | None = None
|
||||
created_by: User | None = None
|
||||
credit_card: CreditCard | None = None
|
||||
installments: int | None = Field(None, ge=1, le=12)
|
||||
|
||||
@model_validator(mode='after')
|
||||
def verify_fields(self):
|
||||
@@ -99,7 +107,14 @@ class Checkout(BaseModel):
|
||||
def model_dump(self, **kwargs) -> dict[str, Any]:
|
||||
return super().model_dump(
|
||||
exclude_none=True,
|
||||
exclude={'items', 'address', 'created_by'},
|
||||
exclude={
|
||||
'items',
|
||||
'address',
|
||||
'created_by',
|
||||
'coupon',
|
||||
'credit_card',
|
||||
'enrollments',
|
||||
},
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -107,28 +122,43 @@ class Checkout(BaseModel):
|
||||
@router.post('/')
|
||||
def checkout(payload: Checkout):
|
||||
now_ = now()
|
||||
order_id = str(payload.id)
|
||||
order_id = payload.id
|
||||
address = payload.address
|
||||
credit_card = payload.credit_card
|
||||
items = payload.items
|
||||
enrollments = payload.enrollments
|
||||
coupon = payload.coupon
|
||||
subtotal = _sum_items(items)
|
||||
discount = (
|
||||
_apply_discount(subtotal, coupon.amount, coupon.type) * -1
|
||||
if coupon
|
||||
else Decimal('0')
|
||||
)
|
||||
total = subtotal + discount if subtotal > Decimal('0') else Decimal('0')
|
||||
|
||||
with dyn.transact_writer() as transact:
|
||||
transact.put(
|
||||
item={
|
||||
'id': order_id,
|
||||
'sk': '0',
|
||||
'total': '',
|
||||
'discount': '',
|
||||
'status': 'PENDING',
|
||||
'subtotal': subtotal,
|
||||
'total': total,
|
||||
'discount': discount,
|
||||
# Post-migration (orders): rename `create_date` to `created_at`
|
||||
'create_date': now_,
|
||||
'due_date': '',
|
||||
'created_at': now_,
|
||||
}
|
||||
| ({'coupon': coupon.code} if coupon else {})
|
||||
| ({'installments': payload.installments} if payload.installments else {})
|
||||
| payload.model_dump()
|
||||
)
|
||||
|
||||
transact.put(
|
||||
item={
|
||||
'id': order_id,
|
||||
'sk': 'ITEMS',
|
||||
'items': [],
|
||||
'items': [item.model_dump() for item in items],
|
||||
'created_at': now_,
|
||||
}
|
||||
)
|
||||
@@ -141,14 +171,78 @@ def checkout(payload: Checkout):
|
||||
| address.model_dump()
|
||||
)
|
||||
|
||||
if credit_card:
|
||||
transact.put(
|
||||
item={
|
||||
'id': order_id,
|
||||
'sk': 'CREDIT_CARD',
|
||||
'ttl': ttl(start_dt=now_, minutes=5),
|
||||
'created_at': now_,
|
||||
}
|
||||
| credit_card.model_dump(),
|
||||
)
|
||||
|
||||
if coupon:
|
||||
transact.put(
|
||||
item={
|
||||
'id': order_id,
|
||||
'sk': 'COUPON',
|
||||
'sk': 'METADATA#COUPON',
|
||||
'created_at': now_,
|
||||
}
|
||||
| coupon.model_dump()
|
||||
)
|
||||
transact.condition(
|
||||
key=KeyPair('COUPON', coupon.code),
|
||||
cond_expr='attribute_exists(sk) \
|
||||
AND discount_type = :type \
|
||||
AND discount_amount = :amount',
|
||||
expr_attr_values={
|
||||
':type': coupon.type,
|
||||
':amount': coupon.amount,
|
||||
},
|
||||
exc_cls=CouponNotFoundError,
|
||||
)
|
||||
|
||||
for enrollment in enrollments:
|
||||
transact.put(
|
||||
item={
|
||||
'id': order_id,
|
||||
'sk': f'ENROLLMENT#{enrollment.id}',
|
||||
'status': 'UNPROCESSED',
|
||||
'created_at': now_,
|
||||
}
|
||||
| enrollment.model_dump(exclude={'id'})
|
||||
)
|
||||
|
||||
return JSONResponse(body={'id': order_id}, status_code=HTTPStatus.CREATED)
|
||||
|
||||
|
||||
def _sum_items(items: tuple[Item, ...]):
|
||||
def sum(total: Decimal, item: Item) -> Decimal:
|
||||
return total + item.unit_price * item.quantity
|
||||
|
||||
return reduce(sum, items, Decimal(0))
|
||||
|
||||
|
||||
def _calc_interest(total, installments: int) -> Decimal:
|
||||
rate2to6 = 0.055
|
||||
rate7to12 = 0.0608
|
||||
rate = rate7to12 if installments >= 7 else rate2to6
|
||||
return total * Decimal((1 - 0.0382) / (1 - rate))
|
||||
|
||||
|
||||
def _apply_discount(
|
||||
subtotal: Decimal,
|
||||
discount_amount: Decimal,
|
||||
discount_type: Literal['FIXED', 'PERCENT'],
|
||||
) -> Decimal:
|
||||
if subtotal <= Decimal('0'):
|
||||
return Decimal('0')
|
||||
|
||||
amount = (
|
||||
(subtotal * discount_amount) / Decimal('100')
|
||||
if discount_type == 'PERCENT'
|
||||
else discount_amount
|
||||
)
|
||||
|
||||
return min(amount, subtotal)
|
||||
|
||||
@@ -26,7 +26,7 @@ Globals:
|
||||
Architectures:
|
||||
- x86_64
|
||||
Layers:
|
||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
|
||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:104
|
||||
Environment:
|
||||
Variables:
|
||||
TZ: America/Sao_Paulo
|
||||
|
||||
@@ -7,7 +7,7 @@ from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey
|
||||
from ...conftest import HttpApiProxy, LambdaContext
|
||||
|
||||
|
||||
def test_checkout(
|
||||
def test_checkout_coupon(
|
||||
app,
|
||||
seeds,
|
||||
http_api_proxy: HttpApiProxy,
|
||||
@@ -23,7 +23,15 @@ def test_checkout(
|
||||
'cnpj': '00000000000191',
|
||||
'name': 'Branco do Brasil',
|
||||
'email': 'bb@users.noreply.saladeaula.digital',
|
||||
'payment_method': 'BANK_SLIP',
|
||||
'payment_method': 'CREDIT_CARD',
|
||||
'installments': 12,
|
||||
'credit_card': {
|
||||
'holder_name': 'Sergio R Siqueira',
|
||||
'number': '4111111111111111',
|
||||
'exp_month': '01',
|
||||
'exp_year': '2026',
|
||||
'cvv': '123',
|
||||
},
|
||||
'created_by': {
|
||||
'id': '15bacf02-1535-4bee-9022-19d106fd7518',
|
||||
'name': 'Sérgio R Siqueira',
|
||||
@@ -33,17 +41,59 @@ def test_checkout(
|
||||
'postcode': '81280350',
|
||||
'neighborhood': 'Cidade Industrial',
|
||||
'address1': 'Rua Monsenhor Ivo Zanlorenzi',
|
||||
'address2': 'nº 5190, ap 1802',
|
||||
'address2': '5190, ap 1802',
|
||||
'state': 'PR',
|
||||
},
|
||||
'items': [
|
||||
{
|
||||
'id': 'e1c44881-2fe3-484e-ada2-12b6bf5b9398',
|
||||
'name': 'NR-35 Segurança nos Trabalhos em Altura',
|
||||
'quantity': 2,
|
||||
'unit_price': 119,
|
||||
}
|
||||
'name': 'CIPA Grau de Risco 1',
|
||||
'id': '3c27ea9c-9464-46a1-9717-8c1441793186',
|
||||
'quantity': 1,
|
||||
'unit_price': 99,
|
||||
},
|
||||
{
|
||||
'name': 'CIPA Grau de Risco 2',
|
||||
'id': '99bb3b60-4ded-4a8e-937c-ba2d78ec6454',
|
||||
'quantity': 1,
|
||||
'unit_price': 99,
|
||||
},
|
||||
],
|
||||
'enrollments': [
|
||||
{
|
||||
'user': {
|
||||
'name': 'Sérgio Rafael de Siqueira',
|
||||
'cpf': '07879819908',
|
||||
'id': '5OxmMjL-ujoR5IMGegQz',
|
||||
'email': 'sergio@somosbeta.com.br',
|
||||
},
|
||||
'course': {
|
||||
'name': 'CIPA Grau de Risco 1',
|
||||
'id': '3c27ea9c-9464-46a1-9717-8c1441793186',
|
||||
'access_period': 365,
|
||||
},
|
||||
'id': '2f026d38-1edc-44ea-abf3-60c10bc58909',
|
||||
},
|
||||
{
|
||||
'course': {
|
||||
'name': 'CIPA Grau de Risco 2',
|
||||
'id': '99bb3b60-4ded-4a8e-937c-ba2d78ec6454',
|
||||
'access_period': 365,
|
||||
},
|
||||
'scheduled_for': '2026-01-20',
|
||||
'id': '1f0931ad-7dd4-4ca1-bce2-a2e89efa5b56',
|
||||
'user': {
|
||||
'name': 'Maitê L Siqueira',
|
||||
'cpf': '02186829991',
|
||||
'id': '87606a7f-de56-4198-a91d-b6967499d382',
|
||||
'email': 'osergiosiqueira+maite@gmail.com',
|
||||
},
|
||||
},
|
||||
],
|
||||
'coupon': {
|
||||
'code': '10OFF',
|
||||
'type': 'PERCENT',
|
||||
'amount': 10,
|
||||
},
|
||||
},
|
||||
),
|
||||
lambda_context,
|
||||
@@ -56,42 +106,44 @@ def test_checkout(
|
||||
pprint(r['items'])
|
||||
|
||||
|
||||
# def test_checkout_from_user(
|
||||
# app,
|
||||
# seeds,
|
||||
# http_api_proxy: HttpApiProxy,
|
||||
# dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||
# lambda_context: LambdaContext,
|
||||
# ):
|
||||
# r = app.lambda_handler(
|
||||
# http_api_proxy(
|
||||
# raw_path='/orders',
|
||||
# method=HTTPMethod.POST,
|
||||
# body={
|
||||
# 'user_id': '15bacf02-1535-4bee-9022-19d106fd7518',
|
||||
# 'cpf': '07879819908',
|
||||
# 'name': 'Sérgio R Siqueira',
|
||||
# 'email': 'sergio@somosbeta.com.br',
|
||||
# 'payment_method': 'MANUAL',
|
||||
# 'address': {
|
||||
# 'city': 'Curitiba',
|
||||
# 'postcode': '81280350',
|
||||
# 'neighborhood': 'Cidade Industrial',
|
||||
# 'address1': 'Rua Monsenhor Ivo Zanlorenzi',
|
||||
# 'address2': 'nº 5190, ap 1802',
|
||||
# 'state': 'PR',
|
||||
# },
|
||||
# 'items': [
|
||||
# {
|
||||
# 'id': 'e1c44881-2fe3-484e-ada2-12b6bf5b9398',
|
||||
# 'name': 'NR-35 Segurança nos Trabalhos em Altura',
|
||||
# 'quantity': 2,
|
||||
# 'unit_price': 119,
|
||||
# }
|
||||
# ],
|
||||
# },
|
||||
# ),
|
||||
# lambda_context,
|
||||
# )
|
||||
# print(r)
|
||||
# assert r['statusCode'] == HTTPStatus.CREATED
|
||||
def test_checkout_from_user(
|
||||
app,
|
||||
seeds,
|
||||
http_api_proxy: HttpApiProxy,
|
||||
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||
lambda_context: LambdaContext,
|
||||
):
|
||||
r = app.lambda_handler(
|
||||
http_api_proxy(
|
||||
raw_path='/orders',
|
||||
method=HTTPMethod.POST,
|
||||
body={
|
||||
'user_id': '15bacf02-1535-4bee-9022-19d106fd7518',
|
||||
'cpf': '07879819908',
|
||||
'name': 'Sérgio R Siqueira',
|
||||
'email': 'sergio@somosbeta.com.br',
|
||||
'payment_method': 'MANUAL',
|
||||
'address': {
|
||||
'sk': 'METADATA#ADDRESS',
|
||||
'address1': 'Rua Monsenhor Ivo Zanlorenzi',
|
||||
'address2': '5190, ap 1802',
|
||||
'postcode': '81280350',
|
||||
'city': 'Curitiba',
|
||||
'neighborhood': 'Cidade Industrial',
|
||||
'state': 'PR',
|
||||
},
|
||||
'items': [
|
||||
{
|
||||
'id': 'e1c44881-2fe3-484e-ada2-12b6bf5b9398',
|
||||
'name': 'NR-35 Segurança nos Trabalhos em Altura',
|
||||
'quantity': 2,
|
||||
'unit_price': 119,
|
||||
'access_period': 20,
|
||||
}
|
||||
],
|
||||
},
|
||||
),
|
||||
lambda_context,
|
||||
)
|
||||
print(r)
|
||||
assert r['statusCode'] == HTTPStatus.CREATED
|
||||
|
||||
2
api.saladeaula.digital/uv.lock
generated
2
api.saladeaula.digital/uv.lock
generated
@@ -667,7 +667,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "layercake"
|
||||
version = "0.11.4"
|
||||
version = "0.12.0"
|
||||
source = { directory = "../layercake" }
|
||||
dependencies = [
|
||||
{ name = "arnparse" },
|
||||
|
||||
Reference in New Issue
Block a user