add test mode to checkout

This commit is contained in:
2026-01-20 10:41:32 -03:00
parent fde5c31ffc
commit 49e0c3333b
13 changed files with 847 additions and 493 deletions

View File

@@ -4,7 +4,7 @@ from decimal import Decimal
from enum import Enum
from functools import reduce
from http import HTTPStatus
from typing import Any, Literal
from typing import Any, Literal, NotRequired, TypedDict, cast
from uuid import uuid4
from aws_lambda_powertools import Logger
@@ -13,7 +13,7 @@ from aws_lambda_powertools.event_handler.exceptions import (
NotFoundError,
)
from layercake.dateutils import now, ttl
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, SortKey, TransactKey
from layercake.extra_types import CnpjStr, CpfStr, CreditCard, NameStr
from pydantic import (
UUID4,
@@ -136,8 +136,8 @@ class Checkout(BaseModel):
@router.post('/')
def checkout(payload: Checkout):
now_ = now()
settings = _get_settings(str(payload.org_id or payload.user_id))
order_id = payload.id
org_id = payload.org_id
address = payload.address
credit_card = payload.credit_card
created_by = payload.created_by
@@ -148,7 +148,7 @@ def checkout(payload: Checkout):
installments = payload.installments
subtotal = _sum_items(items)
due_date = (
_calc_due_date(now_, _get_due_days(org_id) if org_id else DUE_DAYS)
_calc_due_date(now_, settings['due_days'])
if payment_method == 'BANK_SLIP'
else now_ + timedelta(hours=1)
)
@@ -267,6 +267,16 @@ def checkout(payload: Checkout):
| enrollment.model_dump(exclude={'id'})
)
if 'iugu_api_token' in settings:
transact.put(
item={
'id': order_id,
'sk': 'METADATA#TEST_MODE',
'iugu_api_token': settings['iugu_api_token'],
'created_at': now_,
}
)
return JSONResponse(
body={'id': order_id},
status_code=HTTPStatus.CREATED,
@@ -319,18 +329,32 @@ def _calc_due_date(
return due_date
def _get_due_days(
org_id: str | UUID4,
default: int = DUE_DAYS,
) -> int:
return int(
dyn.collection.get_item(
KeyPair(
pk=str(org_id),
sk=SortKey('METADATA#BILLING', path_spec='due_days'),
table_name=USER_TABLE,
Settings = TypedDict(
'Settings',
{
'due_days': int,
'iugu_api_token': NotRequired[str],
},
)
def _get_settings(id: str) -> Settings:
r = dyn.collection.get_items(
TransactKey(id, table_name=USER_TABLE)
+ SortKey(
sk='METADATA#BILLING',
path_spec='due_days',
rename_key='due_days',
)
+ SortKey(
'METADATA#TEST_MODE',
path_spec='iugu_api_token',
rename_key='iugu_api_token',
),
raise_on_error=False,
default=default,
)
flatten_top=False,
)
if 'due_days' not in r:
r['due_days'] = DUE_DAYS
return cast(Settings, r)

View File

@@ -82,7 +82,7 @@ def test_checkout_coupon(
'id': '99bb3b60-4ded-4a8e-937c-ba2d78ec6454',
'access_period': 365,
},
'scheduled_for': '2026-01-20',
'scheduled_for': '2040-01-20',
'id': '1f0931ad-7dd4-4ca1-bce2-a2e89efa5b56',
'user': {
'name': 'Maitê L Siqueira',

View File

@@ -22,6 +22,8 @@
// Seeds for Org
{"id": "f6000f79-6e5c-49a0-952f-3bda330ef278", "sk": "0", "name": "Banco do Brasil", "cnpj": "00000000000191"}
{"id": "f6000f79-6e5c-49a0-952f-3bda330ef278", "sk": "admins#15bacf02-1535-4bee-9022-19d106fd7518", "name": "Chester Bennington", "email": "chester@linkinpark.com"}
{"id": "f6000f79-6e5c-49a0-952f-3bda330ef278", "sk": "METADATA#BILLING", "due_days": "23"}
{"id": "f6000f79-6e5c-49a0-952f-3bda330ef278", "sk": "METADATA#TEST_MODE", "iugu_api_token": "123"}
{"id": "orgmembers#f6000f79-6e5c-49a0-952f-3bda330ef278", "sk": "15bacf02-1535-4bee-9022-19d106fd7518"}
// Seeds for Org

View File

@@ -148,12 +148,17 @@ type Order = Order_ & {
}
export async function loader({ context, request, params }: Route.LoaderArgs) {
const order = (await req({
const r = await req({
url: `/orders/${params.id}`,
context,
request
}).then((r) => r.json())) as Order
})
if (!r.ok) {
throw new Response(null, { status: r.status })
}
const order = (await r.json()) as Order
return { order }
}

View File

@@ -42,11 +42,11 @@ export const columns: ColumnDef<Org>[] = [
return (
<div className="flex gap-2.5 items-center">
<div className="relative">
<div className="relative hidden lg:block">
{subscription_covered ? (
<BadgeCheckIcon className="fill-blue-500 stroke-white absolute size-4 dark:size-3.5 -top-0 -right-0 z-2" />
) : null}
<Avatar className="size-10 hidden lg:block">
<Avatar className="size-10">
<AvatarFallback className="border">
{initials(name)}
</AvatarFallback>

View File

@@ -47,6 +47,12 @@ def _status_attr(status: str) -> StatusAttr | None:
return None
def _friendly_status(s: str) -> str:
if 'status' == 'EXTERNALLY_PAID':
return 'PAID'
return s
@app.post('/<order_id>/postback')
@tracer.capture_method
def postback(order_id: str):
@@ -73,7 +79,7 @@ def postback(order_id: str):
'#status_attr': status_attr.value,
},
expr_attr_values={
':status': status,
':status': _friendly_status(status),
':now': now_,
},
exc_cls=OrderNotFoundError,

View File

@@ -7,8 +7,7 @@ ENROLLMENT_TABLE: str = os.getenv('ENROLLMENT_TABLE') # type: ignore
IUGU_ACCOUNT_ID: str = 'AF01CF1B3451459F92666F10589278EE'
IUGU_API_TOKEN: str = os.getenv('IUGU_API_TOKEN') # type: ignore
# IUGU_TEST_MODE: bool = os.getenv('AWS_LAMBDA_FUNCTION_NAME') is None
IUGU_TEST_MODE: bool = True
IUGU_TEST_MODE: bool = os.getenv('AWS_LAMBDA_FUNCTION_NAME') is None
IUGU_POSTBACK_URL = 'https://zjg09ppxq8.execute-api.sa-east-1.amazonaws.com'
HTTP_CONNECT_TIMEOUT = int(os.environ.get('HTTP_CONNECT_TIMEOUT', 1))

View File

@@ -8,6 +8,7 @@ from layercake.dateutils import now
from layercake.dynamodb import (
DynamoDBPersistenceLayer,
KeyPair,
SortKey,
)
from layercake.extra_types import CreditCard
@@ -17,13 +18,6 @@ from iugu import Credentials, Iugu
logger = Logger(__name__)
dyn = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
iugu = Iugu(
Credentials(
IUGU_ACCOUNT_ID,
IUGU_API_TOKEN,
test_mode=IUGU_TEST_MODE,
)
)
@event_source(data_class=EventBridgeEvent)
@@ -36,6 +30,26 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
credit_card = CreditCard(**new_image['credit_card'])
now_ = now()
api_token = dyn.collection.get_item(
KeyPair(
pk=order_id,
sk=SortKey(
'METADATA#TEST_MODE',
path_spec='iugu_api_token',
),
),
default=None,
raise_on_error=False,
)
test_mode = (api_token is not None) or IUGU_TEST_MODE
credentials = Credentials(
IUGU_ACCOUNT_ID,
# Note: `api_token` can be set from the database
api_token or IUGU_API_TOKEN,
test_mode=test_mode,
)
iugu = Iugu(credentials)
token = iugu.payment_token(credit_card)
charge = iugu.charge(
invoice_id=invoice_id,

View File

@@ -24,13 +24,6 @@ from iugu import Credentials, Iugu, Order
logger = Logger(__name__)
dyn = DynamoDBPersistenceLayer(ORDER_TABLE, dynamodb_client)
iugu = Iugu(
Credentials(
IUGU_ACCOUNT_ID,
IUGU_API_TOKEN,
test_mode=IUGU_TEST_MODE,
)
)
@event_source(data_class=EventBridgeEvent)
@@ -43,14 +36,29 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
TransactKey(order_id)
+ SortKey('ADDRESS', rename_key='address')
+ SortKey('ITEMS', path_spec='items', rename_key='items')
+ SortKey('CREDIT_CARD#PAYMENT_INTENT', rename_key='credit_card'),
+ SortKey('CREDIT_CARD#PAYMENT_INTENT', rename_key='credit_card')
+ SortKey(
'METADATA#TEST_MODE',
rename_key='iugu_api_token',
path_spec='iugu_api_token',
),
flatten_top=False,
)
api_token = r.get('iugu_api_token')
test_mode = (api_token is not None) or IUGU_TEST_MODE
credentials = Credentials(
IUGU_ACCOUNT_ID,
# Note: `api_token` can be set from the database
api_token or IUGU_API_TOKEN,
test_mode=test_mode,
)
payment_method = new_image['payment_method']
is_pix = payment_method == 'PIX'
is_bank_slip = payment_method == 'BANK_SLIP'
iugu = Iugu(credentials)
invoice = iugu.create_invoice(
order=Order(
address=r.get('address', {}),
@@ -95,6 +103,15 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
cond_expr='attribute_not_exists(sk)',
)
if test_mode:
transact.put(
item={
'id': order_id,
'sk': 'SCHEDULE#SELF_DESTRUCTION',
'ttl': ttl(start_dt=now_, days=14),
'created_at': now_,
}
)
except Exception:
pass

View File

@@ -237,6 +237,7 @@ class Iugu:
)
r.raise_for_status()
except requests.HTTPError as err:
logger.info('Response', err.response)
logger.exception(err)
raise
else:

View File

@@ -39,8 +39,7 @@ Globals:
ENROLLMENT_TABLE: !Ref EnrollmentTable
COURSE_TABLE: !Ref CourseTable
BUCKET_NAME: !Ref BucketName
# IUGU_API_TOKEN: '{{resolve:ssm:/saladeaula/iugu_api_token}}'
IUGU_API_TOKEN: 419BEF0AD0B4EC180AEF80281BBF3A1CBBCC0EC45C8AE200D8A53ACC994DE639
IUGU_API_TOKEN: '{{resolve:ssm:/saladeaula/iugu_api_token}}'
Resources:
EventLog:
@@ -102,6 +101,7 @@ Resources:
Type: AWS::Serverless::Function
Properties:
Handler: events.payments.charge_credit_card.lambda_handler
Timeout: 12
LoggingConfig:
LogGroup: !Ref EventLog
Policies:

View File

@@ -22,6 +22,7 @@
{"id": "121c1140-779d-4664-8d99-4a006a22f547", "sk": "ADDRESS", "city": "São José", "neighborhood": "Campinas", "address2": "", "postcode": "88101001", "state": "SC", "address1": "Avenida Presidente Kennedy", "created_at": "2026-01-07T19:07:49.272967-03:00"}
{"id": "121c1140-779d-4664-8d99-4a006a22f547", "sk": "ITEMS", "items": [{"name": "CIPA Grau de Risco 2", "id": "99bb3b60-4ded-4a8e-937c-ba2d78ec6454", "quantity": 3, "unit_price": 99}], "created_at": "2026-01-07T19:07:49.272967-03:00"}
{"id": "121c1140-779d-4664-8d99-4a006a22f547", "sk": "CREDIT_CARD#PAYMENT_INTENT", "credit_card": {"number": "4111111111111111","cvv": "123","created_at": "2026-01-13T02:33:57.088176-03:00","exp_month": "03","exp_year": "2027","ttl": 1768282737,"holder_name": "Sergio R Siqueira"}}
{"id": "121c1140-779d-4664-8d99-4a006a22f547", "sk": "METADATA#TEST_MODE", "iugu_api_token": "123"}
// User data
{"id": "5OxmMjL-ujoR5IMGegQz", "sk": "0", "name": "Sérgio R Siqueira"}

1187
orders-events/uv.lock generated

File diff suppressed because it is too large Load Diff