Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1443,6 +1443,20 @@ describe('GROUP 5: Error States', () => {
})
})

test('Expired merchant charge names the cashier action, not a provider outage', async () => {
// A static POS sticker never expires — what timed out is the charge the
// cashier rang up. Blaming the rail sends the user away; naming the
// cashier action gets them paid.
mockMantecaApi.initiateQrPayment.mockRejectedValue(new Error('PAYMENT_DESTINATION_EXPIRED'))

renderQrPay({ qrCode: 'mercadopago://pay?id=123', type: 'MERCADO_PAGO', t: '1' })

await waitFor(() => {
expect(screen.getByText(/enter the amount again/i)).toBeInTheDocument()
})
expect(screen.queryByText(/currently experiencing issues/i)).not.toBeInTheDocument()
})

test('Below-minimum Pix charge shows the Pix minimum-amount error', async () => {
mockMantecaApi.initiateQrPayment.mockRejectedValue(new Error('PIX_MIN_AMOUNT'))

Expand Down
10 changes: 10 additions & 0 deletions src/app/(mobile-ui)/qr-pay/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ const MIN_QR_PAYMENT_AMOUNT = '0.1'
// change the outcome, so fail fast instead of burning the 3-attempt budget.
const NON_RETRYABLE_QR_PAY_ERRORS = [
'PAYMENT_DESTINATION_DECODING_ERROR',
// The cashier's charge timed out on the till. Only the cashier can clear it,
// so retrying the same destination just burns the attempt budget.
'PAYMENT_DESTINATION_EXPIRED',
'PIX_MIN_AMOUNT',
'PIX_RECURRING_NOT_SUPPORTED',
// Missing auth header (AJV 400) — retrying sends the same headerless request,
Expand Down Expand Up @@ -653,6 +656,13 @@ export default function QRPayPage() {
setErrorInitiatingPayment(qrType === EQrType.PIX ? t('errors.pixDecode') : t('errors.genericDecode'))
posthog.capture(ANALYTICS_EVENTS.QR_DECODING_ERROR_SHOWN, { qr_type: qrType })
setWaitingForMerchantAmount(false)
} else if (error.message.includes('PAYMENT_DESTINATION_EXPIRED')) {
// The QR is fine — a static POS sticker never expires. The charge
// the cashier rang up on the till did. Name the cashier action
// instead of blaming the rail.
setErrorInitiatingPayment(t('errors.merchantChargeExpired'))
posthog.capture(ANALYTICS_EVENTS.QR_MERCHANT_CHARGE_EXPIRED_SHOWN, { qr_type: qrType })
setWaitingForMerchantAmount(false)
} else if (error.message.includes('PIX_MIN_AMOUNT')) {
// Deterministic rejection — the merchant-encoded amount is below
// the rail minimum, so there's no merchant amount to wait for.
Expand Down
1 change: 1 addition & 0 deletions src/constants/analytics.consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export const ANALYTICS_EVENTS = {
QR_SCANNED: 'qr_scanned',
QR_NOTIFY_ME_CLICKED: 'qr_notify_me_clicked',
QR_DECODING_ERROR_SHOWN: 'qr_decoding_error_shown',
QR_MERCHANT_CHARGE_EXPIRED_SHOWN: 'qr_merchant_charge_expired_shown',

// ── Home ──
BALANCE_VISIBILITY_TOGGLED: 'balance_visibility_toggled',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/app/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1255,6 +1255,7 @@
"pixDecode": "We could not decode this Pix QR code. Please ask the merchant to generate a new one.",
"genericDecode": "We could not decode this particular QR code. Please ask the Merchant if they can generate a Mercado Pago QR",
"providerIssues": "We are currently experiencing issues with {method} payments. We are working to fix it as soon as possible",
"merchantChargeExpired": "The cashier's charge has timed out. Ask them to enter the amount again, then scan once more.",
"initiateUnexpected": "Could not initiate payment due to unexpected error. Please contact support",
"fetchDetails": "Could not fetch qr payment details",
"cardAuthNeeded": "One-time card authorization needed. You'll be asked to confirm once.",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/app/messages/es-419.json
Original file line number Diff line number Diff line change
Expand Up @@ -1255,6 +1255,7 @@
"pixDecode": "No pudimos decodificar este código QR de Pix. Pídele al comercio que genere uno nuevo.",
"genericDecode": "No pudimos decodificar este código QR. Pregúntale al comercio si puede generar un QR de Mercado Pago",
"providerIssues": "Estamos teniendo problemas con los pagos de {method}. Estamos trabajando para solucionarlo lo antes posible",
"merchantChargeExpired": "El cobro del cajero se venció. Pídele que ingrese el monto de nuevo y escanea otra vez.",
"initiateUnexpected": "No se pudo iniciar el pago por un error inesperado. Contacta a soporte",
"fetchDetails": "No se pudieron obtener los detalles del pago QR",
"cardAuthNeeded": "Se necesita una autorización única de la tarjeta. Se te pedirá confirmar una vez.",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/app/messages/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -1255,6 +1255,7 @@
"pixDecode": "Não conseguimos decodificar este código QR do Pix. Peça ao lojista para gerar um novo.",
"genericDecode": "Não conseguimos decodificar este código QR. Pergunte ao lojista se ele pode gerar um QR do Mercado Pago",
"providerIssues": "Estamos com problemas nos pagamentos de {method}. Estamos trabalhando para resolver o quanto antes",
"merchantChargeExpired": "A cobrança do caixa expirou. Peça para inserir o valor de novo e escaneie mais uma vez.",
"initiateUnexpected": "Não foi possível iniciar o pagamento por um erro inesperado. Fale com o suporte",
"fetchDetails": "Não foi possível obter os detalhes do pagamento QR",
"cardAuthNeeded": "É necessária uma autorização única do cartão. Você vai confirmar uma vez.",
Expand Down
7 changes: 4 additions & 3 deletions src/utils/sentry.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ const SKIP_REPORTING: Array<{ pattern: string | RegExp; statuses: number[] }> =
// runs three — and bury the backend signal that can actually be acted on.
{ pattern: /\/fx\/rate(?:\?|$)/, statuses: [400, 404, 429, 503] },
// qr-payment/init: 400 = open QR awaiting merchant amount; 422 = a QR the
// provider can't decode (bad/expired/unsupported) — both are user-input
// outcomes shown to the user, not server bugs. (BE peanut-api-ts #1041.)
{ pattern: /qr-payment\/init/, statuses: [400, 422] },
// provider can't decode (bad/expired/unsupported); 409 = the charge the
// cashier rang up on the till timed out — all three are user-input outcomes
// shown to the user, not server bugs. (BE peanut-api-ts #1041, #1484.)
{ pattern: /qr-payment\/init/, statuses: [400, 409, 422] },
Comment thread
abalinda marked this conversation as resolved.
Outdated
// Rain card secrets endpoints are intentionally rate-limited (5/min) — a
// 429 here is an expected outcome surfaced to the user, not a server bug.
{ pattern: /\/rain\/cards\/[^/]+\/details/, statuses: [429] },
Expand Down
Loading