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 ( - - - -
- {/* 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). -

-
-
-
- -
- )} - /> -
+ + + )} /> -
-
-
-
+ + + ) : 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
    • -
    • - -
    • -
    + + {children} + + + + Novo cartão de crédito + + Use um novo cartão para concluir o pagamento. Nenhuma cobrança foi + realizada no cartão anterior. + + + +
    + + + + + + + + + + + + +
    +
    ) } - -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'