Skip to main content

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.

  1. Get session: POST /order/{id}/3ds-session → returns session + env.
  2. Initialize SDK: window.PagSeguro.setUp({ session, env }).
  3. Build request: Customer data, card details (plain number, not encrypted), amount in cents, billing address.
  4. Authenticate: window.PagSeguro.authenticate3DS(request) — may open an iframe challenge.
  5. 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:

FieldTypeRequiredDescription
streetstringYesBilling street
numberstringYesBilling number
complementstringNoBilling complement
neighborhoodstringYesBilling neighborhood
citystringYesBilling city
statestringYesBilling state (2 chars)
postalCodestringYesBilling postal code
installmentsstringYesNumber of installments
holderNamestringYesCardholder name
holderTaxIdstringYesCardholder CPF
cardEncryptedstringYesPagSeguro encrypted card
authenticationIdstringNo3DS 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 (from CREDIT_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:

  1. Order update: Saves baasOrderId, baasChargeId, status, paidAt, installments, interest, and value (with interest).
  2. Lock acquisition: acquireLock prevents duplicate ticket generation.
  3. Order paid: orderPaid() creates tickets, updates the cart, and triggers notifications.
  4. Lock release: releaseLock with a 4-second TTL.

Where it is used

FileUsage
apps/app-console/src/components/cart/CartPaymentCreditCardForm.tsxMain form component that orchestrates the entire flow
apps/app-console/src/components/cart/CartPaymentSection.tsxParent component that renders the credit card form
apps/app-api/src/order/orderRoutes.tsRegisters both /order/{id}/3ds-session and /order/{id}/pay/credit-card
apps/app-console/src/schema/default/default.tsOrval-generated fetchers (postOrderId3dsSession, postOrderIdPayCreditCard)

Corner cases

CaseBehavior
event.is3DSActive is false or undefined3DS skipped entirely, direct card encryption + payment
3DS returns AUTH_NOT_SUPPORTEDPayment proceeds without authenticationId
3DS returns CHANGE_PAYMENT_METHODPayment halted, toast shown, no mutation called
3DS throws errorPayment halted, error logged, toast shown
Cart expires between 3DS and payment400 "Carrinho expirado"
PagBank returns CANCELED status400 "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 nameZod rejects at form level (requires first + last name)

Tests

Automated tests are located at:

  • apps/app-api/__tests__/handleOrder3dsSessionPost.test.ts — 3DS session endpoint
  • apps/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:
  • Related docs:
  • Source files:
    • apps/app-api/src/order/handleOrder3dsSessionPost.ts
    • apps/app-api/src/order/handleOrderPayCreditCardPost.ts
    • apps/app-console/src/components/cart/CartPaymentCreditCardForm.tsx
    • packages/pagbank/src/threeds/pagbank3dsSessionPost.ts
    • packages/pagbank/src/creditCard/adapters/creditCardOrderToPagbankOrder.ts
    • packages/enum/src/pagbankThreeDSStatusEnum.ts