update orders
This commit is contained in:
@@ -68,7 +68,6 @@ def health():
|
|||||||
@app.exception_handler(ServiceError)
|
@app.exception_handler(ServiceError)
|
||||||
def exc_error(exc: ServiceError):
|
def exc_error(exc: ServiceError):
|
||||||
logger.exception(exc)
|
logger.exception(exc)
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
body={
|
body={
|
||||||
'type': type(exc).__name__,
|
'type': type(exc).__name__,
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import re
|
import re
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from functools import reduce
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from aws_lambda_powertools.event_handler.api_gateway import Router
|
from aws_lambda_powertools.event_handler.api_gateway import Router
|
||||||
from layercake.dateutils import now
|
from aws_lambda_powertools.event_handler.exceptions import (
|
||||||
from layercake.dynamodb import DynamoDBPersistenceLayer
|
NotFoundError,
|
||||||
from layercake.extra_types import CnpjStr, CpfStr, NameStr
|
)
|
||||||
|
from layercake.dateutils import now, ttl
|
||||||
|
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
|
||||||
|
from layercake.extra_types import CnpjStr, CpfStr, CreditCard, NameStr
|
||||||
from pydantic import (
|
from pydantic import (
|
||||||
UUID4,
|
UUID4,
|
||||||
BaseModel,
|
BaseModel,
|
||||||
@@ -22,12 +26,14 @@ from api_gateway import JSONResponse
|
|||||||
from boto3clients import dynamodb_client
|
from boto3clients import dynamodb_client
|
||||||
from config import ORDER_TABLE
|
from config import ORDER_TABLE
|
||||||
from routes.enrollments.enroll import Enrollment
|
from routes.enrollments.enroll import Enrollment
|
||||||
from routes.orgs.address import address
|
|
||||||
|
|
||||||
router = Router()
|
router = Router()
|
||||||
dyn = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
|
dyn = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
|
||||||
|
|
||||||
|
|
||||||
|
class CouponNotFoundError(NotFoundError): ...
|
||||||
|
|
||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
id: UUID4 | str
|
id: UUID4 | str
|
||||||
name: NameStr
|
name: NameStr
|
||||||
@@ -71,13 +77,15 @@ class Checkout(BaseModel):
|
|||||||
address: Address
|
address: Address
|
||||||
payment_method: Literal['PIX', 'CREDIT_CARD', 'BANK_SLIP', 'MANUAL']
|
payment_method: Literal['PIX', 'CREDIT_CARD', 'BANK_SLIP', 'MANUAL']
|
||||||
items: tuple[Item, ...]
|
items: tuple[Item, ...]
|
||||||
enrollments: tuple[Enrollment, ...] | None = None
|
enrollments: tuple[Enrollment, ...] = tuple()
|
||||||
coupon: Coupon | None = None
|
coupon: Coupon | None = None
|
||||||
org_id: UUID4 | str | None = None
|
org_id: UUID4 | str | None = None
|
||||||
user_id: UUID4 | str | None = None
|
user_id: UUID4 | str | None = None
|
||||||
cnpj: CnpjStr | None = None
|
cnpj: CnpjStr | None = None
|
||||||
cpf: CpfStr | None = None
|
cpf: CpfStr | None = None
|
||||||
created_by: User | 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')
|
@model_validator(mode='after')
|
||||||
def verify_fields(self):
|
def verify_fields(self):
|
||||||
@@ -99,7 +107,14 @@ class Checkout(BaseModel):
|
|||||||
def model_dump(self, **kwargs) -> dict[str, Any]:
|
def model_dump(self, **kwargs) -> dict[str, Any]:
|
||||||
return super().model_dump(
|
return super().model_dump(
|
||||||
exclude_none=True,
|
exclude_none=True,
|
||||||
exclude={'items', 'address', 'created_by'},
|
exclude={
|
||||||
|
'items',
|
||||||
|
'address',
|
||||||
|
'created_by',
|
||||||
|
'coupon',
|
||||||
|
'credit_card',
|
||||||
|
'enrollments',
|
||||||
|
},
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -107,28 +122,43 @@ class Checkout(BaseModel):
|
|||||||
@router.post('/')
|
@router.post('/')
|
||||||
def checkout(payload: Checkout):
|
def checkout(payload: Checkout):
|
||||||
now_ = now()
|
now_ = now()
|
||||||
order_id = str(payload.id)
|
order_id = payload.id
|
||||||
address = payload.address
|
address = payload.address
|
||||||
|
credit_card = payload.credit_card
|
||||||
|
items = payload.items
|
||||||
|
enrollments = payload.enrollments
|
||||||
coupon = payload.coupon
|
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:
|
with dyn.transact_writer() as transact:
|
||||||
transact.put(
|
transact.put(
|
||||||
item={
|
item={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
'sk': '0',
|
'sk': '0',
|
||||||
'total': '',
|
'status': 'PENDING',
|
||||||
'discount': '',
|
'subtotal': subtotal,
|
||||||
|
'total': total,
|
||||||
|
'discount': discount,
|
||||||
|
# Post-migration (orders): rename `create_date` to `created_at`
|
||||||
|
'create_date': now_,
|
||||||
'due_date': '',
|
'due_date': '',
|
||||||
'created_at': now_,
|
|
||||||
}
|
}
|
||||||
| ({'coupon': coupon.code} if coupon else {})
|
| ({'coupon': coupon.code} if coupon else {})
|
||||||
|
| ({'installments': payload.installments} if payload.installments else {})
|
||||||
| payload.model_dump()
|
| payload.model_dump()
|
||||||
)
|
)
|
||||||
|
|
||||||
transact.put(
|
transact.put(
|
||||||
item={
|
item={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
'sk': 'ITEMS',
|
'sk': 'ITEMS',
|
||||||
'items': [],
|
'items': [item.model_dump() for item in items],
|
||||||
'created_at': now_,
|
'created_at': now_,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -141,14 +171,78 @@ def checkout(payload: Checkout):
|
|||||||
| address.model_dump()
|
| 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:
|
if coupon:
|
||||||
transact.put(
|
transact.put(
|
||||||
item={
|
item={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
'sk': 'COUPON',
|
'sk': 'METADATA#COUPON',
|
||||||
'created_at': now_,
|
'created_at': now_,
|
||||||
}
|
}
|
||||||
| coupon.model_dump()
|
| 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)
|
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:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
|
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:104
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
TZ: America/Sao_Paulo
|
TZ: America/Sao_Paulo
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from layercake.dynamodb import DynamoDBPersistenceLayer, PartitionKey
|
|||||||
from ...conftest import HttpApiProxy, LambdaContext
|
from ...conftest import HttpApiProxy, LambdaContext
|
||||||
|
|
||||||
|
|
||||||
def test_checkout(
|
def test_checkout_coupon(
|
||||||
app,
|
app,
|
||||||
seeds,
|
seeds,
|
||||||
http_api_proxy: HttpApiProxy,
|
http_api_proxy: HttpApiProxy,
|
||||||
@@ -23,7 +23,15 @@ def test_checkout(
|
|||||||
'cnpj': '00000000000191',
|
'cnpj': '00000000000191',
|
||||||
'name': 'Branco do Brasil',
|
'name': 'Branco do Brasil',
|
||||||
'email': 'bb@users.noreply.saladeaula.digital',
|
'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': {
|
'created_by': {
|
||||||
'id': '15bacf02-1535-4bee-9022-19d106fd7518',
|
'id': '15bacf02-1535-4bee-9022-19d106fd7518',
|
||||||
'name': 'Sérgio R Siqueira',
|
'name': 'Sérgio R Siqueira',
|
||||||
@@ -33,17 +41,59 @@ def test_checkout(
|
|||||||
'postcode': '81280350',
|
'postcode': '81280350',
|
||||||
'neighborhood': 'Cidade Industrial',
|
'neighborhood': 'Cidade Industrial',
|
||||||
'address1': 'Rua Monsenhor Ivo Zanlorenzi',
|
'address1': 'Rua Monsenhor Ivo Zanlorenzi',
|
||||||
'address2': 'nº 5190, ap 1802',
|
'address2': '5190, ap 1802',
|
||||||
'state': 'PR',
|
'state': 'PR',
|
||||||
},
|
},
|
||||||
'items': [
|
'items': [
|
||||||
{
|
{
|
||||||
'id': 'e1c44881-2fe3-484e-ada2-12b6bf5b9398',
|
'name': 'CIPA Grau de Risco 1',
|
||||||
'name': 'NR-35 Segurança nos Trabalhos em Altura',
|
'id': '3c27ea9c-9464-46a1-9717-8c1441793186',
|
||||||
'quantity': 2,
|
'quantity': 1,
|
||||||
'unit_price': 119,
|
'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,
|
lambda_context,
|
||||||
@@ -56,42 +106,44 @@ def test_checkout(
|
|||||||
pprint(r['items'])
|
pprint(r['items'])
|
||||||
|
|
||||||
|
|
||||||
# def test_checkout_from_user(
|
def test_checkout_from_user(
|
||||||
# app,
|
app,
|
||||||
# seeds,
|
seeds,
|
||||||
# http_api_proxy: HttpApiProxy,
|
http_api_proxy: HttpApiProxy,
|
||||||
# dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||||
# lambda_context: LambdaContext,
|
lambda_context: LambdaContext,
|
||||||
# ):
|
):
|
||||||
# r = app.lambda_handler(
|
r = app.lambda_handler(
|
||||||
# http_api_proxy(
|
http_api_proxy(
|
||||||
# raw_path='/orders',
|
raw_path='/orders',
|
||||||
# method=HTTPMethod.POST,
|
method=HTTPMethod.POST,
|
||||||
# body={
|
body={
|
||||||
# 'user_id': '15bacf02-1535-4bee-9022-19d106fd7518',
|
'user_id': '15bacf02-1535-4bee-9022-19d106fd7518',
|
||||||
# 'cpf': '07879819908',
|
'cpf': '07879819908',
|
||||||
# 'name': 'Sérgio R Siqueira',
|
'name': 'Sérgio R Siqueira',
|
||||||
# 'email': 'sergio@somosbeta.com.br',
|
'email': 'sergio@somosbeta.com.br',
|
||||||
# 'payment_method': 'MANUAL',
|
'payment_method': 'MANUAL',
|
||||||
# 'address': {
|
'address': {
|
||||||
# 'city': 'Curitiba',
|
'sk': 'METADATA#ADDRESS',
|
||||||
# 'postcode': '81280350',
|
'address1': 'Rua Monsenhor Ivo Zanlorenzi',
|
||||||
# 'neighborhood': 'Cidade Industrial',
|
'address2': '5190, ap 1802',
|
||||||
# 'address1': 'Rua Monsenhor Ivo Zanlorenzi',
|
'postcode': '81280350',
|
||||||
# 'address2': 'nº 5190, ap 1802',
|
'city': 'Curitiba',
|
||||||
# 'state': 'PR',
|
'neighborhood': 'Cidade Industrial',
|
||||||
# },
|
'state': 'PR',
|
||||||
# 'items': [
|
},
|
||||||
# {
|
'items': [
|
||||||
# 'id': 'e1c44881-2fe3-484e-ada2-12b6bf5b9398',
|
{
|
||||||
# 'name': 'NR-35 Segurança nos Trabalhos em Altura',
|
'id': 'e1c44881-2fe3-484e-ada2-12b6bf5b9398',
|
||||||
# 'quantity': 2,
|
'name': 'NR-35 Segurança nos Trabalhos em Altura',
|
||||||
# 'unit_price': 119,
|
'quantity': 2,
|
||||||
# }
|
'unit_price': 119,
|
||||||
# ],
|
'access_period': 20,
|
||||||
# },
|
}
|
||||||
# ),
|
],
|
||||||
# lambda_context,
|
},
|
||||||
# )
|
),
|
||||||
# print(r)
|
lambda_context,
|
||||||
# assert r['statusCode'] == HTTPStatus.CREATED
|
)
|
||||||
|
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]]
|
[[package]]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.11.4"
|
version = "0.12.0"
|
||||||
source = { directory = "../layercake" }
|
source = { directory = "../layercake" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "arnparse" },
|
{ name = "arnparse" },
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export const TZ = 'America/Sao_Paulo'
|
export const TZ = 'America/Sao_Paulo'
|
||||||
|
export const INTERNAL_EMAIL_DOMAIN = 'users.noreply.saladeaula.digital'
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type Workspace = {
|
|||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
cnpj: string
|
cnpj: string
|
||||||
|
email: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkspaceContextProps = {
|
export type WorkspaceContextProps = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Route } from './+types/route'
|
import type { Route } from './+types/route'
|
||||||
|
|
||||||
import { useFetcher } from 'react-router'
|
import { useEffect, useState } from 'react'
|
||||||
import { Link } from 'react-router'
|
import { useFetcher, Link } from 'react-router'
|
||||||
import { useMount } from 'ahooks'
|
import { useMount } from 'ahooks'
|
||||||
import { BookSearchIcon, CircleCheckBigIcon, WalletIcon } from 'lucide-react'
|
import { BookSearchIcon, CircleCheckBigIcon, WalletIcon } from 'lucide-react'
|
||||||
|
|
||||||
@@ -22,10 +22,14 @@ import {
|
|||||||
} from '@repo/ui/components/ui/breadcrumb'
|
} from '@repo/ui/components/ui/breadcrumb'
|
||||||
import { Switch } from '@repo/ui/components/ui/switch'
|
import { Switch } from '@repo/ui/components/ui/switch'
|
||||||
import { createSearch } from '@repo/util/meili'
|
import { createSearch } from '@repo/util/meili'
|
||||||
import { cloudflareContext } from '@repo/auth/context'
|
import { cloudflareContext, userContext } from '@repo/auth/context'
|
||||||
import { Label } from '@repo/ui/components/ui/label'
|
import { Label } from '@repo/ui/components/ui/label'
|
||||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||||
|
import { request as req, HttpMethod } from '@repo/util/request'
|
||||||
|
|
||||||
|
import { INTERNAL_EMAIL_DOMAIN } from '@/conf'
|
||||||
|
import { workspaceContext } from '@/middleware/workspace'
|
||||||
|
import { useWorksapce } from '@/components/workspace-switcher'
|
||||||
import { Step, StepItem, StepSeparator } from '@/components/step'
|
import { Step, StepItem, StepSeparator } from '@/components/step'
|
||||||
import { Wizard, WizardStep } from '@/components/wizard'
|
import { Wizard, WizardStep } from '@/components/wizard'
|
||||||
import type { Course } from '../_.$orgid.enrollments.add/data'
|
import type { Course } from '../_.$orgid.enrollments.add/data'
|
||||||
@@ -33,10 +37,7 @@ import { Bulk } from './bulk'
|
|||||||
import { Payment } from './payment'
|
import { Payment } from './payment'
|
||||||
import { Assigned } from './assigned'
|
import { Assigned } from './assigned'
|
||||||
import { Review } from './review'
|
import { Review } from './review'
|
||||||
|
|
||||||
import { useWizardStore } from './store'
|
import { useWizardStore } from './store'
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { useWorksapce } from '@/components/workspace-switcher'
|
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
export function meta({}: Route.MetaArgs) {
|
||||||
return [{ title: 'Comprar matrículas' }]
|
return [{ title: 'Comprar matrículas' }]
|
||||||
@@ -55,10 +56,36 @@ export async function loader({ context }: Route.LoaderArgs) {
|
|||||||
return { courses }
|
return { courses }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function action({ request }: Route.ActionArgs) {
|
export async function action({ params, request, context }: Route.ActionArgs) {
|
||||||
const body = (await request.json()) as object
|
const body = (await request.json()) as object
|
||||||
|
const user = context.get(userContext)!
|
||||||
|
const { activeWorkspace } = context.get(workspaceContext)
|
||||||
|
const { id: org_id, name, cnpj } = activeWorkspace
|
||||||
|
|
||||||
console.log(body)
|
const r = await req({
|
||||||
|
url: '/orders',
|
||||||
|
headers: new Headers({ 'Content-Type': 'application/json' }),
|
||||||
|
method: HttpMethod.POST,
|
||||||
|
body: JSON.stringify({
|
||||||
|
org_id,
|
||||||
|
name,
|
||||||
|
cnpj,
|
||||||
|
email: `org+${cnpj}@${INTERNAL_EMAIL_DOMAIN}`,
|
||||||
|
created_by: { id: user.sub, name: user.name },
|
||||||
|
...body
|
||||||
|
}),
|
||||||
|
request,
|
||||||
|
context
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!r.ok) {
|
||||||
|
const error = await r.json().catch(() => ({}))
|
||||||
|
return { ok: false, error }
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(await r.json())
|
||||||
|
|
||||||
|
return { ok: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Route({
|
export default function Route({
|
||||||
@@ -71,7 +98,11 @@ export default function Route({
|
|||||||
useWizardStore()
|
useWizardStore()
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
await fetcher.submit(JSON.stringify(state), {
|
const items = state.items.map(({ course, quantity }) => ({
|
||||||
|
...course,
|
||||||
|
quantity
|
||||||
|
}))
|
||||||
|
await fetcher.submit(JSON.stringify({ ...state, items }), {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
encType: 'application/json'
|
encType: 'application/json'
|
||||||
})
|
})
|
||||||
@@ -88,6 +119,10 @@ export default function Route({
|
|||||||
}
|
}
|
||||||
}, [address])
|
}, [address])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(fetcher.data)
|
||||||
|
}, [fetcher.data])
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return <Skeleton />
|
return <Skeleton />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export const useWizardStore = create<WizardStore>()(
|
|||||||
const subtotal = items.reduce(
|
const subtotal = items.reduce(
|
||||||
(acc, { course, quantity }) =>
|
(acc, { course, quantity }) =>
|
||||||
acc +
|
acc +
|
||||||
(course?.unit_price || 0) *
|
(course.unit_price || 0) *
|
||||||
(Number.isFinite(quantity) && quantity > 0 ? quantity : 1),
|
(Number.isFinite(quantity) && quantity > 0 ? quantity : 1),
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { INTERNAL_EMAIL_DOMAIN } from '@/conf'
|
||||||
import { isValidCPF } from '@brazilian-utils/brazilian-utils'
|
import { isValidCPF } from '@brazilian-utils/brazilian-utils'
|
||||||
import {
|
import {
|
||||||
adjectives,
|
adjectives,
|
||||||
@@ -17,7 +18,7 @@ function randomEmail() {
|
|||||||
separator: '-'
|
separator: '-'
|
||||||
})
|
})
|
||||||
|
|
||||||
return `${randomName}@users.noreply.saladeaula.digital`
|
return `${randomName}@${INTERNAL_EMAIL_DOMAIN}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formSchema = z
|
export const formSchema = z
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { Route } from './+types/route'
|
|||||||
|
|
||||||
import * as cookie from 'cookie'
|
import * as cookie from 'cookie'
|
||||||
import { Outlet, type ShouldRevalidateFunctionArgs } from 'react-router'
|
import { Outlet, type ShouldRevalidateFunctionArgs } from 'react-router'
|
||||||
import { use, useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SidebarInset,
|
SidebarInset,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Globals:
|
|||||||
Architectures:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
|
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:104
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
TZ: America/Sao_Paulo
|
TZ: America/Sao_Paulo
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ Globals:
|
|||||||
Architectures:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
|
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:104
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
TZ: America/Sao_Paulo
|
TZ: America/Sao_Paulo
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Globals:
|
|||||||
Architectures:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
|
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:104
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
TZ: America/Sao_Paulo
|
TZ: America/Sao_Paulo
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Globals:
|
|||||||
Architectures:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
|
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:104
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
TZ: America/Sao_Paulo
|
TZ: America/Sao_Paulo
|
||||||
|
|||||||
@@ -60,53 +60,25 @@ else:
|
|||||||
return field_schema
|
return field_schema
|
||||||
|
|
||||||
|
|
||||||
class PaymentCardValidation:
|
|
||||||
"""
|
|
||||||
>>> class CreditCard(BaseModel):
|
|
||||||
... exp: PaymentCardValidation
|
|
||||||
|
|
||||||
|
|
||||||
>>> CreditCard(exp='20/23')
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
pydantic_core._pydantic_core.ValidationError: 1 validation error for CreditCard
|
|
||||||
...
|
|
||||||
|
|
||||||
>>> CreditCard(exp='12/23')
|
|
||||||
CreditCard(exp=datetime.date(2023, 12, 1))
|
|
||||||
|
|
||||||
>>> CreditCard(exp='12/2024')
|
|
||||||
CreditCard(exp=datetime.date(2024, 12, 1))
|
|
||||||
|
|
||||||
>>> CreditCard(exp='2024-12-02')
|
|
||||||
CreditCard(exp=datetime.date(2024, 12, 2))
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def __get_pydantic_core_schema__(
|
|
||||||
cls, _source: type[Any], _handler: GetCoreSchemaHandler
|
|
||||||
) -> CoreSchema:
|
|
||||||
return core_schema.no_info_after_validator_function(
|
|
||||||
cls._validate, core_schema.str_schema()
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _validate(cls, __input_value: str) -> date:
|
|
||||||
if '/' in __input_value:
|
|
||||||
month, year = __input_value.split('/')
|
|
||||||
return date(int(f'20{year[-2:]}'), int(month), 1)
|
|
||||||
|
|
||||||
try:
|
|
||||||
return date.fromisoformat(__input_value)
|
|
||||||
except Exception:
|
|
||||||
raise ValueError('Invalid card expiration date.')
|
|
||||||
|
|
||||||
|
|
||||||
class CreditCard(BaseModel):
|
class CreditCard(BaseModel):
|
||||||
name: NameStr
|
"""
|
||||||
|
>>> cc = CreditCard(
|
||||||
|
... holder_name='Mike Shinoda',
|
||||||
|
... number='4111111111111111',
|
||||||
|
... cvv='123',
|
||||||
|
... exp_month='01',
|
||||||
|
... exp_year='2026'
|
||||||
|
... )
|
||||||
|
>>> str(cc.number.brand)
|
||||||
|
'Visa'
|
||||||
|
>>> cc
|
||||||
|
CreditCard(holder_name='Mike Shinoda', number='4111111111111111', cvv='123', exp_month='01', exp_year='2026')
|
||||||
|
"""
|
||||||
|
holder_name: NameStr
|
||||||
number: PaymentCardNumber
|
number: PaymentCardNumber
|
||||||
cvv: str = Field(..., min_length=3)
|
cvv: str = Field(..., min_length=3)
|
||||||
exp: PaymentCardValidation
|
exp_month: str = Field(..., pattern=r'^\d{2}$')
|
||||||
|
exp_year: str = Field(..., pattern=r'^\d{4}$')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def brand(self) -> str:
|
def brand(self) -> str:
|
||||||
@@ -118,12 +90,12 @@ class CreditCard(BaseModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def first_name(self) -> str:
|
def first_name(self) -> str:
|
||||||
first_name, _ = self.name.split(' ', 1)
|
first_name, _ = self.holder_name.split(' ', 1)
|
||||||
return first_name
|
return first_name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def last_name(self) -> str:
|
def last_name(self) -> str:
|
||||||
_, last_name = self.name.split(' ', 1)
|
_, last_name = self.holder_name.split(' ', 1)
|
||||||
return last_name
|
return last_name
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.11.4"
|
version = "0.12.0"
|
||||||
description = "Packages shared dependencies to optimize deployment and ensure consistency across functions."
|
description = "Packages shared dependencies to optimize deployment and ensure consistency across functions."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [
|
authors = [
|
||||||
|
|||||||
2
layercake/uv.lock
generated
2
layercake/uv.lock
generated
@@ -824,7 +824,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "layercake"
|
name = "layercake"
|
||||||
version = "0.11.2"
|
version = "0.12.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "arnparse" },
|
{ name = "arnparse" },
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Globals:
|
|||||||
Architectures:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
|
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:104
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
TZ: America/Sao_Paulo
|
TZ: America/Sao_Paulo
|
||||||
|
|||||||
0
orders-events/tests/events/payments/__init__.py
Normal file
0
orders-events/tests/events/payments/__init__.py
Normal file
21
orders-events/tests/events/payments/test_create_invoice.py
Normal file
21
orders-events/tests/events/payments/test_create_invoice.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from aws_lambda_powertools.utilities.typing.lambda_context import LambdaContext
|
||||||
|
from layercake.dynamodb import DynamoDBPersistenceLayer
|
||||||
|
|
||||||
|
import events.payments.create_invoice as app
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_invoice(
|
||||||
|
dynamodb_seeds,
|
||||||
|
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||||
|
lambda_context: LambdaContext,
|
||||||
|
):
|
||||||
|
event = {
|
||||||
|
'detail': {
|
||||||
|
'new_image': {
|
||||||
|
'id': '',
|
||||||
|
'sk': '0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert app.lambda_handler(event, lambda_context) # type: ignore
|
||||||
@@ -8,7 +8,7 @@ Globals:
|
|||||||
Architectures:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
|
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:104
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
LOG_LEVEL: DEBUG
|
LOG_LEVEL: DEBUG
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Globals:
|
|||||||
Architectures:
|
Architectures:
|
||||||
- x86_64
|
- x86_64
|
||||||
Layers:
|
Layers:
|
||||||
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
|
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:104
|
||||||
Environment:
|
Environment:
|
||||||
Variables:
|
Variables:
|
||||||
TZ: America/Sao_Paulo
|
TZ: America/Sao_Paulo
|
||||||
|
|||||||
Reference in New Issue
Block a user