add test mode to checkout
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
1187
orders-events/uv.lock
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user