Flow: Credit Card Payment with 3DS
Summary
This document describes the end-to-end user journey for paying an order with a credit card, including the optional 3D Secure (3DS) authentication step. It covers the interaction between the participant frontend (app-console), the backend API (app-api), the PagBank payment provider, and the database.
Motivation
Credit card payments are the primary revenue channel. Adding 3DS authentication reduces fraud and chargebacks while maintaining a smooth checkout experience when 3DS is not required. The flow is designed to be backward-compatible: events without is3DSActive use the original payment path unchanged.
End-to-end flow
Sequence diagram
Step-by-step breakdown
1. Form submission (frontend)
File: apps/app-console/src/components/cart/CartPaymentCreditCardForm.tsx
The CartPaymentCreditCardForm component collects:
- Card number, holder name (first + last required), CPF, expiry date, CVV
- Installment count (1–10, minimum R$5 per installment)
- Billing address (auto-filled via CEP lookup using
cep-promise)
Validation uses Zod via react-hook-form with zodResolver.
2. 3DS authentication (conditional)
Only runs when event.is3DSActive === true.
- Get session:
POST /order/{id}/3ds-session→ returnssession+env. - Initialize SDK:
window.PagSeguro.setUp({ session, env }). - Build request: Customer data, card details (plain number, not encrypted), amount in cents, billing address.
- Authenticate:
window.PagSeguro.authenticate3DS(request)— may open an iframe challenge. - Handle result: See PagBank 3DS Integration for status details.
3. Card encryption
Always runs (both with and without 3DS).
const card = window.PagSeguro.encryptCard({
publicKey,
holder: data.cardHolder,
number: data.cardNumber,
expMonth: month,
expYear: `20${year}`,
securityCode: data.cardCvv,
});
The publicKey comes from NEXT_PUBLIC_PAGSEGURO_PUBLIC_KEY environment variable.
4. Payment request
Endpoint: POST /order/{id}/pay/credit-card
File: apps/app-api/src/order/handleOrderPayCreditCardPost.ts
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
street | string | Yes | Billing street |
number | string | Yes | Billing number |
complement | string | No | Billing complement |
neighborhood | string | Yes | Billing neighborhood |
city | string | Yes | Billing city |
state | string | Yes | Billing state (2 chars) |
postalCode | string | Yes | Billing postal code |
installments | string | Yes | Number of installments |
holderName | string | Yes | Cardholder name |
holderTaxId | string | Yes | Cardholder CPF |
cardEncrypted | string | Yes | PagSeguro encrypted card |
authenticationId | string | No | 3DS authentication ID |
5. Backend validation
6. Interest calculation
The handler calculates the final value including interest:
- Without event custom rates:
(installments - 1) * 3%per installment (fromCREDIT_CARD_INSTALLMENTS_INTEREST_PERCENTAGE). - With event custom rates (
event.installments): Uses per-installment rates defined on the event. Falls back to the default 3% for undefined installments. - 1 installment: No interest applied.
7. PagBank charge creation
The adapter at packages/pagbank/src/creditCard/adapters/creditCardOrderToPagbankOrder.ts transforms the internal request into PagBank's API format. If authenticationId is present, it adds:
"authentication_method": { "type": "THREEDS", "id": "<id>" }
8. Post-payment processing
After a successful charge:
- Order update: Saves
baasOrderId,baasChargeId,status,paidAt,installments,interest, andvalue(with interest). - Lock acquisition:
acquireLockprevents duplicate ticket generation. - Order paid:
orderPaid()creates tickets, updates the cart, and triggers notifications. - Lock release:
releaseLockwith a 4-second TTL.
Where it is used
| File | Usage |
|---|---|
apps/app-console/src/components/cart/CartPaymentCreditCardForm.tsx | Main form component that orchestrates the entire flow |
apps/app-console/src/components/cart/CartPaymentSection.tsx | Parent component that renders the credit card form |
apps/app-api/src/order/orderRoutes.ts | Registers both /order/{id}/3ds-session and /order/{id}/pay/credit-card |
apps/app-console/src/schema/default/default.ts | Orval-generated fetchers (postOrderId3dsSession, postOrderIdPayCreditCard) |
Corner cases
| Case | Behavior |
|---|---|
event.is3DSActive is false or undefined | 3DS skipped entirely, direct card encryption + payment |
3DS returns AUTH_NOT_SUPPORTED | Payment proceeds without authenticationId |
3DS returns CHANGE_PAYMENT_METHOD | Payment halted, toast shown, no mutation called |
| 3DS throws error | Payment halted, error logged, toast shown |
| Cart expires between 3DS and payment | 400 "Carrinho expirado" |
PagBank returns CANCELED status | 400 "Pedido não pago" |
orderPaid fails (lock missing, cart not found) | 400 "Order not paid" — charge was created but tickets not generated |
| Single-word cardholder name | Zod rejects at form level (requires first + last name) |
Tests
Automated tests are located at:
apps/app-api/__tests__/handleOrder3dsSessionPost.test.ts— 3DS session endpointapps/app-api/__tests__/handleOrderPayCreditCardPost.test.ts— Credit card payment (with and without 3DS)
Test patterns: setupApp() with createTestApp(), vi.mock for config/pagbank/redis/order, parseResponse(), createAuthCookie().
References
- Domain docs:
- PagBank 3DS Integration — SDK setup, status enum, sandbox testing
- Related docs:
- API Handlers — Handler conventions
- Testing — Test patterns
- Architecture Overview — System architecture
- Source files:
apps/app-api/src/order/handleOrder3dsSessionPost.tsapps/app-api/src/order/handleOrderPayCreditCardPost.tsapps/app-console/src/components/cart/CartPaymentCreditCardForm.tsxpackages/pagbank/src/threeds/pagbank3dsSessionPost.tspackages/pagbank/src/creditCard/adapters/creditCardOrderToPagbankOrder.tspackages/enum/src/pagbankThreeDSStatusEnum.ts