diff --git a/api.saladeaula.digital/app/app.py b/api.saladeaula.digital/app/app.py
index ca62fa4..5a3948a 100644
--- a/api.saladeaula.digital/app/app.py
+++ b/api.saladeaula.digital/app/app.py
@@ -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__,
diff --git a/api.saladeaula.digital/app/routes/orders/checkout.py b/api.saladeaula.digital/app/routes/orders/checkout.py
index 92dfc58..6e5f3a5 100644
--- a/api.saladeaula.digital/app/routes/orders/checkout.py
+++ b/api.saladeaula.digital/app/routes/orders/checkout.py
@@ -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)
diff --git a/api.saladeaula.digital/template.yaml b/api.saladeaula.digital/template.yaml
index 959b181..2aa74a2 100644
--- a/api.saladeaula.digital/template.yaml
+++ b/api.saladeaula.digital/template.yaml
@@ -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
diff --git a/api.saladeaula.digital/tests/routes/orders/test_checkout.py b/api.saladeaula.digital/tests/routes/orders/test_checkout.py
index 7fdfb3e..819f713 100644
--- a/api.saladeaula.digital/tests/routes/orders/test_checkout.py
+++ b/api.saladeaula.digital/tests/routes/orders/test_checkout.py
@@ -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
diff --git a/api.saladeaula.digital/uv.lock b/api.saladeaula.digital/uv.lock
index 7c08be1..0d7f83a 100644
--- a/api.saladeaula.digital/uv.lock
+++ b/api.saladeaula.digital/uv.lock
@@ -667,7 +667,7 @@ wheels = [
[[package]]
name = "layercake"
-version = "0.11.4"
+version = "0.12.0"
source = { directory = "../layercake" }
dependencies = [
{ name = "arnparse" },
diff --git a/apps/admin.saladeaula.digital/app/conf.ts b/apps/admin.saladeaula.digital/app/conf.ts
index 2597a6d..03cbf50 100644
--- a/apps/admin.saladeaula.digital/app/conf.ts
+++ b/apps/admin.saladeaula.digital/app/conf.ts
@@ -1 +1,2 @@
export const TZ = 'America/Sao_Paulo'
+export const INTERNAL_EMAIL_DOMAIN = 'users.noreply.saladeaula.digital'
diff --git a/apps/admin.saladeaula.digital/app/middleware/workspace.ts b/apps/admin.saladeaula.digital/app/middleware/workspace.ts
index a044f8c..ebfc795 100644
--- a/apps/admin.saladeaula.digital/app/middleware/workspace.ts
+++ b/apps/admin.saladeaula.digital/app/middleware/workspace.ts
@@ -14,6 +14,7 @@ export type Workspace = {
id: string
name: string
cnpj: string
+ email: string
}
export type WorkspaceContextProps = {
diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/route.tsx
index 9bb615f..dffe854 100644
--- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/route.tsx
+++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/route.tsx
@@ -1,7 +1,7 @@
import type { Route } from './+types/route'
-import { useFetcher } from 'react-router'
-import { Link } from 'react-router'
+import { useEffect, useState } from 'react'
+import { useFetcher, Link } from 'react-router'
import { useMount } from 'ahooks'
import { BookSearchIcon, CircleCheckBigIcon, WalletIcon } from 'lucide-react'
@@ -22,10 +22,14 @@ import {
} from '@repo/ui/components/ui/breadcrumb'
import { Switch } from '@repo/ui/components/ui/switch'
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 { 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 { Wizard, WizardStep } from '@/components/wizard'
import type { Course } from '../_.$orgid.enrollments.add/data'
@@ -33,10 +37,7 @@ import { Bulk } from './bulk'
import { Payment } from './payment'
import { Assigned } from './assigned'
import { Review } from './review'
-
import { useWizardStore } from './store'
-import { useEffect, useState } from 'react'
-import { useWorksapce } from '@/components/workspace-switcher'
export function meta({}: Route.MetaArgs) {
return [{ title: 'Comprar matrículas' }]
@@ -55,10 +56,36 @@ export async function loader({ context }: Route.LoaderArgs) {
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 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({
@@ -71,7 +98,11 @@ export default function Route({
useWizardStore()
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',
encType: 'application/json'
})
@@ -88,6 +119,10 @@ export default function Route({
}
}, [address])
+ useEffect(() => {
+ console.log(fetcher.data)
+ }, [fetcher.data])
+
if (!mounted) {
return
}
diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/store.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/store.tsx
index 3729cc8..90aac54 100644
--- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/store.tsx
+++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/store.tsx
@@ -59,7 +59,7 @@ export const useWizardStore = create()(
const subtotal = items.reduce(
(acc, { course, quantity }) =>
acc +
- (course?.unit_price || 0) *
+ (course.unit_price || 0) *
(Number.isFinite(quantity) && quantity > 0 ? quantity : 1),
0
)
diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/data.ts b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/data.ts
index 65b58ac..25f1d0e 100644
--- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/data.ts
+++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/data.ts
@@ -1,3 +1,4 @@
+import { INTERNAL_EMAIL_DOMAIN } from '@/conf'
import { isValidCPF } from '@brazilian-utils/brazilian-utils'
import {
adjectives,
@@ -17,7 +18,7 @@ function randomEmail() {
separator: '-'
})
- return `${randomName}@users.noreply.saladeaula.digital`
+ return `${randomName}@${INTERNAL_EMAIL_DOMAIN}`
}
export const formSchema = z
diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid/route.tsx
index ac9aad4..90418b0 100644
--- a/apps/admin.saladeaula.digital/app/routes/_.$orgid/route.tsx
+++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid/route.tsx
@@ -2,7 +2,7 @@ import type { Route } from './+types/route'
import * as cookie from 'cookie'
import { Outlet, type ShouldRevalidateFunctionArgs } from 'react-router'
-import { use, useEffect } from 'react'
+import { useEffect } from 'react'
import {
SidebarInset,
diff --git a/courses-events/template.yaml b/courses-events/template.yaml
index 8b34927..f9dff61 100644
--- a/courses-events/template.yaml
+++ b/courses-events/template.yaml
@@ -14,7 +14,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
diff --git a/enrollments-events/template.yaml b/enrollments-events/template.yaml
index d5c39e9..513fcd1 100644
--- a/enrollments-events/template.yaml
+++ b/enrollments-events/template.yaml
@@ -25,7 +25,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
diff --git a/id.saladeaula.digital/template.yaml b/id.saladeaula.digital/template.yaml
index 981003b..1059180 100644
--- a/id.saladeaula.digital/template.yaml
+++ b/id.saladeaula.digital/template.yaml
@@ -14,7 +14,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
diff --git a/konviva-events/template.yaml b/konviva-events/template.yaml
index b45531f..c298f5b 100644
--- a/konviva-events/template.yaml
+++ b/konviva-events/template.yaml
@@ -20,7 +20,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
diff --git a/layercake/layercake/extra_types.py b/layercake/layercake/extra_types.py
index 6803e01..d42aacd 100644
--- a/layercake/layercake/extra_types.py
+++ b/layercake/layercake/extra_types.py
@@ -60,53 +60,25 @@ else:
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):
- 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
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
def brand(self) -> str:
@@ -118,12 +90,12 @@ class CreditCard(BaseModel):
@property
def first_name(self) -> str:
- first_name, _ = self.name.split(' ', 1)
+ first_name, _ = self.holder_name.split(' ', 1)
return first_name
@property
def last_name(self) -> str:
- _, last_name = self.name.split(' ', 1)
+ _, last_name = self.holder_name.split(' ', 1)
return last_name
diff --git a/layercake/pyproject.toml b/layercake/pyproject.toml
index 9bc953f..ba4a7c9 100644
--- a/layercake/pyproject.toml
+++ b/layercake/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "layercake"
-version = "0.11.4"
+version = "0.12.0"
description = "Packages shared dependencies to optimize deployment and ensure consistency across functions."
readme = "README.md"
authors = [
diff --git a/layercake/uv.lock b/layercake/uv.lock
index c4371af..433e4ef 100644
--- a/layercake/uv.lock
+++ b/layercake/uv.lock
@@ -824,7 +824,7 @@ wheels = [
[[package]]
name = "layercake"
-version = "0.11.2"
+version = "0.12.0"
source = { editable = "." }
dependencies = [
{ name = "arnparse" },
diff --git a/orders-events/template.yaml b/orders-events/template.yaml
index 2930f57..2d66168 100644
--- a/orders-events/template.yaml
+++ b/orders-events/template.yaml
@@ -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
diff --git a/orders-events/tests/events/payments/__init__.py b/orders-events/tests/events/payments/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/orders-events/tests/events/payments/test_create_invoice.py b/orders-events/tests/events/payments/test_create_invoice.py
new file mode 100644
index 0000000..1de3510
--- /dev/null
+++ b/orders-events/tests/events/payments/test_create_invoice.py
@@ -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
diff --git a/streams-events/template.yaml b/streams-events/template.yaml
index b3c72af..8a9679c 100644
--- a/streams-events/template.yaml
+++ b/streams-events/template.yaml
@@ -8,7 +8,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:
LOG_LEVEL: DEBUG
diff --git a/users-events/template.yaml b/users-events/template.yaml
index 1b8859d..03db8e2 100644
--- a/users-events/template.yaml
+++ b/users-events/template.yaml
@@ -17,7 +17,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