From 466936acf40fddda42391a87d3ac7704d7e53267 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9rgio=20Rafael=20Siqueira?=
Date: Thu, 15 Jan 2026 19:58:57 -0300
Subject: [PATCH] add retries to order
---
.../app/routes/orders/payment_retries.py | 8 +
.../routes/orders/test_payment_retries.py | 8 +-
api.saladeaula.digital/tests/seeds.jsonl | 1 +
.../_.$orgid.enrollments.buy/payment.tsx | 409 +++++++++---------
.../_.$orgid.enrollments.buy/review.tsx | 54 +--
.../routes/_.$orgid.enrollments.buy/store.tsx | 6 +-
.../_.$orgid.payments.$id._index/route.tsx | 185 ++++++--
.../app/events/payments/create_invoice.py | 2 +-
.../events/payments/test_create_invoice.py | 2 +-
9 files changed, 402 insertions(+), 273 deletions(-)
diff --git a/api.saladeaula.digital/app/routes/orders/payment_retries.py b/api.saladeaula.digital/app/routes/orders/payment_retries.py
index 76b1237..76a2546 100644
--- a/api.saladeaula.digital/app/routes/orders/payment_retries.py
+++ b/api.saladeaula.digital/app/routes/orders/payment_retries.py
@@ -62,5 +62,13 @@ def payment_retries(
},
cond_expr='attribute_not_exists(sk)',
)
+ transact.update(
+ key=KeyPair(order_id, 'TRANSACTION#STATS'),
+ update_expr='SET updated_at = :now REMOVE last_attempt_succeeded',
+ expr_attr_values={
+ ':now': now(),
+ },
+ cond_expr='attribute_exists(sk)',
+ )
return JSONResponse(status_code=HTTPStatus.CREATED)
diff --git a/api.saladeaula.digital/tests/routes/orders/test_payment_retries.py b/api.saladeaula.digital/tests/routes/orders/test_payment_retries.py
index 46f9a15..3560151 100644
--- a/api.saladeaula.digital/tests/routes/orders/test_payment_retries.py
+++ b/api.saladeaula.digital/tests/routes/orders/test_payment_retries.py
@@ -32,7 +32,13 @@ def test_payment_retries(
)
assert r['statusCode'] == HTTPStatus.CREATED
- r = dynamodb_persistence_layer.collection.get_item(
+ r = dynamodb_persistence_layer.collection.get_items(
KeyPair('4b23f6f5-5377-476b-b1de-79427c0295f6', 'TRANSACTION')
+ + KeyPair(
+ '4b23f6f5-5377-476b-b1de-79427c0295f6',
+ 'TRANSACTION#STATS',
+ rename_key='stats',
+ )
)
assert r['credit_card']['number'] == '4111111111111111'
+ assert 'last_attempt_succeeded' not in r['stats']
diff --git a/api.saladeaula.digital/tests/seeds.jsonl b/api.saladeaula.digital/tests/seeds.jsonl
index 68669f4..5f6e5de 100644
--- a/api.saladeaula.digital/tests/seeds.jsonl
+++ b/api.saladeaula.digital/tests/seeds.jsonl
@@ -30,6 +30,7 @@
// file: tests/routes/orders/test_payment_retries.py
{"id": "4b23f6f5-5377-476b-b1de-79427c0295f6", "sk": "0", "installments": 3}
{"id": "4b23f6f5-5377-476b-b1de-79427c0295f6", "sk": "INVOICE", "invoice_id": "123"}
+{"id": "4b23f6f5-5377-476b-b1de-79427c0295f6", "sk": "TRANSACTION#STATS", "last_attempt_succeeded": false}
// Indicies
// CNPJs
diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/payment.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/payment.tsx
index 51585e1..25d5882 100644
--- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/payment.tsx
+++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/payment.tsx
@@ -1,23 +1,14 @@
-import { useForm, Controller, useWatch, type Control } from 'react-hook-form'
-import { PatternFormat } from 'react-number-format'
-import { zodResolver } from '@hookform/resolvers/zod'
import { ErrorMessage } from '@hookform/error-message'
-import { z } from 'zod'
-import { ArrowRightIcon, CircleQuestionMarkIcon } from 'lucide-react'
+import { zodResolver } from '@hookform/resolvers/zod'
import valid from 'card-validator'
+import { ArrowRightIcon, CircleQuestionMarkIcon } from 'lucide-react'
+import { Controller, useForm, useWatch, type Control } from 'react-hook-form'
+import { PatternFormat } from 'react-number-format'
+import { z } from 'zod'
+import { Currency } from '@repo/ui/components/currency'
import { Button } from '@repo/ui/components/ui/button'
-import { Kbd } from '@repo/ui/components/ui/kbd'
-import { Label } from '@repo/ui/components/ui/label'
-import { RadioGroup, RadioGroupItem } from '@repo/ui/components/ui/radio-group'
-import { Separator } from '@repo/ui/components/ui/separator'
-import { Input } from '@repo/ui/components/ui/input'
import { Card, CardContent } from '@repo/ui/components/ui/card'
-import {
- HoverCard,
- HoverCardContent,
- HoverCardTrigger
-} from '@repo/ui/components/ui/hover-card'
import {
Field,
FieldError,
@@ -25,18 +16,27 @@ import {
FieldLabel,
FieldSet
} from '@repo/ui/components/ui/field'
+import {
+ HoverCard,
+ HoverCardContent,
+ HoverCardTrigger
+} from '@repo/ui/components/ui/hover-card'
+import { Input } from '@repo/ui/components/ui/input'
+import { Kbd } from '@repo/ui/components/ui/kbd'
+import { Label } from '@repo/ui/components/ui/label'
import {
NativeSelect,
NativeSelectOption
} from '@repo/ui/components/ui/native-select'
-import { Currency } from '@repo/ui/components/currency'
+import { RadioGroup, RadioGroupItem } from '@repo/ui/components/ui/radio-group'
+import { Separator } from '@repo/ui/components/ui/separator'
import { useWizard } from '@/components/wizard'
import { isName } from '../_.$orgid.users.add/data'
-import { applyDiscount } from './discount'
+
import { useWizardStore } from './store'
-const creditCard = z.object({
+export const creditCardSchema = z.object({
holder_name: z
.string()
.trim()
@@ -71,7 +71,7 @@ const formSchema = z.discriminatedUnion(
z.object({
payment_method: z.literal('CREDIT_CARD'),
- credit_card: creditCard,
+ credit_card: creditCardSchema,
installments: z.coerce.number().int().min(1).max(12)
})
],
@@ -80,7 +80,7 @@ const formSchema = z.discriminatedUnion(
type Schema = z.input
-export type CreditCard = z.infer
+export type CreditCard = z.infer
export function Payment({}) {
const wizard = useWizard()
@@ -95,6 +95,7 @@ export function Payment({}) {
resolver: zodResolver(formSchema)
})
const paymentMethod = useWatch({ control, name: 'payment_method' })
+ const total = subtotal + discount
const onSubmit = async ({ payment_method, ...data }: Schema) => {
if (payment_method === 'CREDIT_CARD') {
@@ -156,191 +157,9 @@ export function Payment({}) {
/>
{paymentMethod === 'CREDIT_CARD' ? (
-
- ) : null}
-
-
-
-
-
-
-
-
-
- )
-}
-
-export function CreditCard({
- total,
- control
-}: {
- total: number
- control: Control
-}) {
- const currentYear = new Date().getFullYear()
- const years = Array.from({ length: 10 }, (_, i) => currentYear + i)
-
- return (
-
-
-
-
-
-
-
+
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export function CreditCard({ control }: { control: Control }) {
+ const currentYear = new Date().getFullYear()
+ const years = Array.from({ length: 10 }, (_, i) => currentYear + i)
+
+ return (
+
+
+ {/* Credir card number */}
+ (
+
+ Número do cartão
+ {
+ onChange(value)
+ }}
+ {...field}
+ />
+
+ {fieldState.invalid && }
+
+ )}
+ />
+ {/* Holder name */}
+ (
+
+ Nome do titular
+
+ {fieldState.invalid && }
+
+ )}
+ />
+
+
+ (
+
+ Mês
+
+
+ Selecione
+
+
+ {Array.from({ length: 12 }, (_, i) => {
+ const v = String(i + 1).padStart(2, '0')
+ return (
+
+ {v}
+
+ )
+ })}
+
+
+ )}
+ />
+
+ (
+
+ Ano
+
+
+ Selecione
+
+
+ {years.map((year) => (
+
+ {year}
+
+ ))}
+
+
+ )}
+ />
+
+ (
+
+
+ CVC
+
+
+
+
+
+
+ O CVC é o código de segurança do cartão de
+ crédito.
+
+
+
+ Ele fica no verso do cartão e geralmente possui{' '}
+ 3 dígitos (ou 4 dígitos na frente,
+ no caso do American Express).
+
+
+
+
+
+
+ )}
+ />
+
+
+
)
}
diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/review.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/review.tsx
index 9e4214b..ebdabf3 100644
--- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/review.tsx
+++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/review.tsx
@@ -1,16 +1,34 @@
+import { formatCEP } from '@brazilian-utils/brazilian-utils'
+import { zodResolver } from '@hookform/resolvers/zod'
import { useRequest, useToggle } from 'ahooks'
-import { PatternFormat } from 'react-number-format'
+import valid from 'card-validator'
import { ExternalLinkIcon, PencilIcon, SearchIcon } from 'lucide-react'
import { Controller, useForm } from 'react-hook-form'
-import { zodResolver } from '@hookform/resolvers/zod'
-import { formatCEP } from '@brazilian-utils/brazilian-utils'
+import { PatternFormat } from 'react-number-format'
import { z } from 'zod'
-import valid from 'card-validator'
-import { Currency } from '@repo/ui/components/currency'
-import { Kbd } from '@repo/ui/components/ui/kbd'
import { Abbr } from '@repo/ui/components/abbr'
+import { Currency } from '@repo/ui/components/currency'
import { Button } from '@repo/ui/components/ui/button'
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger
+} from '@repo/ui/components/ui/dialog'
+import {
+ Item,
+ ItemActions,
+ ItemContent,
+ ItemDescription,
+ ItemGroup,
+ ItemTitle
+} from '@repo/ui/components/ui/item'
+import { Kbd } from '@repo/ui/components/ui/kbd'
import { Separator } from '@repo/ui/components/ui/separator'
import { Spinner } from '@repo/ui/components/ui/spinner'
import {
@@ -22,24 +40,6 @@ import {
TableHeader,
TableRow
} from '@repo/ui/components/ui/table'
-import {
- Item,
- ItemActions,
- ItemContent,
- ItemDescription,
- ItemGroup,
- ItemTitle
-} from '@repo/ui/components/ui/item'
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger
-} from '@repo/ui/components/ui/dialog'
import { paymentMethods } from '@repo/ui/routes/orders/data'
import { useWizard } from '@/components/wizard'
@@ -59,8 +59,6 @@ import {
InputGroupInput
} from '@repo/ui/components/ui/input-group'
-import { useWizardStore } from './store'
-import { useParams } from 'react-router'
import {
Empty,
EmptyContent,
@@ -68,6 +66,8 @@ import {
EmptyHeader,
EmptyTitle
} from '@repo/ui/components/ui/empty'
+import { useParams } from 'react-router'
+import { useWizardStore } from './store'
type ReviewProps = {
onSubmit: () => void | Promise
@@ -445,7 +445,7 @@ export function AddressDialog({ children }) {
tabIndex={-1}
className="text-black dark:text-white"
>
- Cancel
+ Cancelar
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 90aac54..cbb46eb 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
@@ -1,10 +1,12 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
-import { applyDiscount, type Coupon } from './discount'
-import { calcInterest, type CreditCard } from './payment'
+
import type { PaymentMethod } from '@repo/ui/routes/orders/data'
+
import type { Enrollment } from '../_.$orgid.enrollments.add/data'
import type { Item } from './bulk'
+import { applyDiscount, type Coupon } from './discount'
+import { calcInterest, type CreditCard } from './payment'
import type { Address } from './review'
export type WizardState = {
diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.payments.$id._index/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.payments.$id._index/route.tsx
index e12a281..f3b09c9 100644
--- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.payments.$id._index/route.tsx
+++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.payments.$id._index/route.tsx
@@ -1,13 +1,17 @@
import type { Route } from './+types/route'
import { formatCEP } from '@brazilian-utils/brazilian-utils'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { useRequest } from 'ahooks'
import {
AlertCircleIcon,
ArrowLeftRightIcon,
HelpCircleIcon
} from 'lucide-react'
import { useEffect } from 'react'
+import { useForm } from 'react-hook-form'
import { Link } from 'react-router'
+import { z } from 'zod'
import { Currency } from '@repo/ui/components/currency'
import {
@@ -52,7 +56,23 @@ import {
type Order as Order_
} from '@repo/ui/routes/orders/data'
-import type { CreditCard as CreditCard_ } from '../_.$orgid.enrollments.buy/payment'
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger
+} from '@repo/ui/components/ui/dialog'
+import { Separator } from '@repo/ui/components/ui/separator'
+import { Spinner } from '@repo/ui/components/ui/spinner'
+import {
+ CreditCard,
+ creditCardSchema,
+ type CreditCard as CreditCardProps
+} from '../_.$orgid.enrollments.buy/payment'
import type { Address } from '../_.$orgid.enrollments.buy/review'
import { useWizardStore } from '../_.$orgid.enrollments.buy/store'
@@ -65,12 +85,12 @@ export function meta() {
}
const PaymentMethodComponent = {
- PIX: Pix,
- BANK_SLIP: BankSlip,
- CREDIT_CARD: CreditCard
+ PIX: PixPaymentMethod,
+ BANK_SLIP: BankSlipPaymentMethod,
+ CREDIT_CARD: CreditCardPaymentMethod
}
-type Item_ = {
+type Item = {
id: string
name: string
unit_price: number
@@ -82,18 +102,24 @@ type User = {
name: string
}
+type Invoice = {
+ invoice_id: string
+ secure_url: string
+}
+
type Order = Order_ & {
- items: Item_[]
+ items: Item[]
interest_amount: number
due_date: string
created_at: string
subtotal: number
discount: number
address: Address
- credit_card?: CreditCard_
+ credit_card?: CreditCardProps
coupon?: string
installments?: number
created_by?: User
+ invoice: Invoice
}
export async function loader({ context, request, params }: Route.LoaderArgs) {
@@ -115,11 +141,15 @@ export default function Route({ loaderData: { order } }: Route.ComponentProps) {
payment_method,
interest_amount,
discount,
+ invoice,
subtotal,
items = []
} = order
- const Component = PaymentMethodComponent[payment_method]
+ const Component =
+ (PaymentMethodComponent as Record>)[
+ payment_method
+ ] ?? UnknownPaymentMethod
useEffect(() => {
reset()
@@ -168,7 +198,9 @@ export default function Route({ loaderData: { order } }: Route.ComponentProps) {
Forma de pagamento
- {Component && }
+ {Component && (
+
+ )}
@@ -271,23 +303,50 @@ function Status({ status: s }: { status: string }) {
}
type PaymentMethodProps = {
+ id: string
status: string
total: number
+ invoice_id: string
installments: number
}
-type CreditCardProps = PaymentMethodProps & {
+type BankSlipPaymentMethodProps = PaymentMethodProps & {}
+
+function BankSlipPaymentMethod({ status }: BankSlipPaymentMethodProps) {
+ return (
+
+ - Boleto bancário
+ -
+
+
+
+ )
+}
+
+type PixPaymentMethodrops = PaymentMethodProps & {}
+
+function PixPaymentMethod({}: PixPaymentMethodrops) {
+ return <>Pix>
+}
+
+function UnknownPaymentMethod() {
+ return <>Deposito bancário>
+}
+
+type CreditCardPaymentMethodProps = PaymentMethodProps & {
stats: { last_attempt_succeeded: boolean }
credit_card: { last4: string; brand: string }
}
-function CreditCard({
+function CreditCardPaymentMethod({
+ id,
status,
total,
credit_card,
installments,
+ invoice_id,
stats
-}: CreditCardProps) {
+}: CreditCardPaymentMethodProps) {
return (
<>
@@ -295,12 +354,12 @@ function CreditCard({
{credit_card.brand} (Crédito) **** {credit_card.last4}
-
- {!stats.last_attempt_succeeded ? (
+ {stats.last_attempt_succeeded === false ? (
- Transação negada
+ Pagamento não aprovado
) : (
@@ -312,32 +371,94 @@ function CreditCard({
{installments}x {total / Number(installments)}
- {!stats.last_attempt_succeeded ? (
+ {stats.last_attempt_succeeded === false ? (
-
+
+
+
) : null}
>
)
}
-type BankSlipProps = PaymentMethodProps & {}
+const formSchema = z.object({
+ credit_card: creditCardSchema
+})
+
+type Schema = z.input
+
+function CreditCardPaymentDialog({
+ children,
+ id,
+ installments,
+ invoice_id,
+ total
+}) {
+ const { runAsync } = useRequest(
+ async ({ credit_card }) => {
+ return await fetch(`/~/api/orders/${id}/payment-retries`, {
+ method: 'POST',
+ headers: new Headers({ 'Content-Type': 'application/json' }),
+ body: JSON.stringify({ credit_card, installments, invoice_id })
+ })
+ },
+ { manual: true }
+ )
+ const { control, handleSubmit, formState } = useForm({
+ resolver: zodResolver(formSchema)
+ })
+
+ const onSubmit = async ({ credit_card }: Schema) => {
+ const r = await runAsync({ credit_card })
+ console.log(r.ok)
+ console.log(await r.json())
+ }
-function BankSlip({ status }: BankSlipProps) {
return (
-
- - Boleto bancário
- -
-
-
-
+
)
}
-
-type PixProps = PaymentMethodProps & {}
-
-function Pix({}: PixProps) {
- return <>Pix>
-}
diff --git a/orders-events/app/events/payments/create_invoice.py b/orders-events/app/events/payments/create_invoice.py
index a14ae4d..9d6b585 100644
--- a/orders-events/app/events/payments/create_invoice.py
+++ b/orders-events/app/events/payments/create_invoice.py
@@ -67,7 +67,7 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool:
'id': order_id,
'sk': 'INVOICE',
'payment_method': payment_method,
- 'secure_id': invoice['secure_id'],
+ 'invoice_id': invoice['secure_id'],
'secure_url': invoice['secure_url'],
'created_at': now_,
# Uncomment this when adding for multiple payment providers
diff --git a/orders-events/tests/events/payments/test_create_invoice.py b/orders-events/tests/events/payments/test_create_invoice.py
index bc2dcc0..de8c7f3 100644
--- a/orders-events/tests/events/payments/test_create_invoice.py
+++ b/orders-events/tests/events/payments/test_create_invoice.py
@@ -58,7 +58,7 @@ def test_create_bank_slip_invoice(
invoice['secure_url']
== 'https://checkout.iugu.com/invoices/16f7aa3d-2e0b-41e9-987b-1dc95b957456-d7a2'
)
- assert invoice['secure_id'] == '16f7aa3d-2e0b-41e9-987b-1dc95b957456-d7a2'
+ assert invoice['invoice_id'] == '16f7aa3d-2e0b-41e9-987b-1dc95b957456-d7a2'
assert (
invoice['bank_slip']['bank_slip_url']
== 'https://boletos.iugu.com/v1/public/invoice/16f7aa3d-2e0b-41e9-987b-1dc95b957456-d7a2/bank_slip'