fix: show why a card payment failed instead of leaving the form spinning
All checks were successful
Build and deploy / Validate source (push) Successful in 7s
Build and deploy / Integration suite on a real stack (push) Successful in 3m0s
Build and deploy / Secret scan and release gate (push) Successful in 6s
Build and deploy / Publish images (push) Successful in 56s
All checks were successful
Build and deploy / Validate source (push) Successful in 7s
Build and deploy / Integration suite on a real stack (push) Successful in 3m0s
Build and deploy / Secret scan and release gate (push) Successful in 6s
Build and deploy / Publish images (push) Successful in 56s
When creating the card payment failed, the form's onSubmit rejected with no message and Mercado Pago's button kept spinning. The page now shows the reason above the form. A payment Mercado Pago refuses is logged with its status, message and cause codes (payment_intent_refused) and answered 422 with that reason; other failures log their type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ and an unverified delivery is recorded and refused rather than retried.
|
||||
"""
|
||||
from uuid import uuid4
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from psycopg.types.json import Jsonb
|
||||
|
||||
@@ -60,6 +61,17 @@ def event_provider():
|
||||
return payment.name
|
||||
|
||||
|
||||
def provider_reason(response):
|
||||
"""A short, loggable reason from a refused provider call."""
|
||||
try:
|
||||
data = response.json()
|
||||
except ValueError:
|
||||
return f'HTTP {response.status_code}'
|
||||
causes = '; '.join(f"{c.get('code')}: {c.get('description')}" for c in data.get('cause') or []
|
||||
if isinstance(c, dict))
|
||||
return (causes or data.get('message') or data.get('error') or f'HTTP {response.status_code}')[:300]
|
||||
|
||||
|
||||
@router.post('/api/payments/intent')
|
||||
def intent(body: PaymentIntent, session_id=Depends(owner)):
|
||||
"""Start paying an approved quote: a PIX code, or a card token from the
|
||||
@@ -105,8 +117,17 @@ def intent(body: PaymentIntent, session_id=Depends(owner)):
|
||||
quote['approved']['customer'], method)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(422, str(exc))
|
||||
except Exception:
|
||||
audit('payment_intent_failed', quote=str(body.quote_id))
|
||||
except httpx.HTTPStatusError as exc:
|
||||
# Mercado Pago's own reason (status, message and cause codes) goes to
|
||||
# the log; it never contains card data, only what was refused.
|
||||
reason = provider_reason(exc.response)
|
||||
audit('payment_intent_refused', quote=str(body.quote_id), method=body.method.type,
|
||||
status=exc.response.status_code, reason=reason)
|
||||
if exc.response.status_code < 500:
|
||||
raise HTTPException(422, f'O Mercado Pago recusou o pagamento: {reason}')
|
||||
raise HTTPException(502, 'Payment provider unavailable; try again')
|
||||
except Exception as exc:
|
||||
audit('payment_intent_failed', quote=str(body.quote_id), error=type(exc).__name__)
|
||||
raise HTTPException(502, 'Payment provider unavailable; try again')
|
||||
c.execute('''INSERT INTO dtf_local.payment_intents(id,quote_id,provider,provider_payment_id,method,
|
||||
status,amount_cents,response) VALUES(%s,%s,%s,%s,%s,%s,%s,%s)
|
||||
|
||||
Reference in New Issue
Block a user