feat: let production select Mercado Pago and acknowledge simulated notifications
The production compose hard-coded the fake payment adapter; it now takes PAYMENT_ADAPTER and the MP_* settings from the stack's environment, so the sandbox can run with test credentials. A signed notification about a payment Mercado Pago does not have, such as the panel's "Simular notificação", is acknowledged instead of answering 500 and being retried; any other lookup failure still raises. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -134,7 +134,15 @@ class MercadoPagoPayment:
|
||||
payment_id = str((query or {}).get('data.id') or (data.get('data') or {}).get('id') or '')
|
||||
if not payment_id.isdigit():
|
||||
return None
|
||||
payment = self.lookup(payment_id)
|
||||
try:
|
||||
payment = self.lookup(payment_id)
|
||||
except httpx.HTTPStatusError as error:
|
||||
# Signed by Mercado Pago but about no payment of ours, such as the
|
||||
# panel's "Simular notificação". Anything else is raised so that a
|
||||
# real notification is retried.
|
||||
if error.response.status_code == 404:
|
||||
return None
|
||||
raise
|
||||
return event_from_payment(payment, notification_id=str(data.get('id', '')))
|
||||
|
||||
|
||||
|
||||
@@ -27,6 +27,10 @@ POSTGRES_VOLUME=TBD
|
||||
OPERATOR_EMAIL=TBD
|
||||
|
||||
PAYMENT_ADAPTER=TBD
|
||||
# With PAYMENT_ADAPTER=mercadopago. MP_ACCESS_TOKEN and MP_WEBHOOK_SECRET are
|
||||
# secrets: enter them in Portainer only, never in this file.
|
||||
MP_NOTIFICATION_URL=https://<SITE_DOMAIN>/api/payments/webhook
|
||||
MP_PUBLIC_KEY=
|
||||
FREIGHT_ADAPTER=TBD
|
||||
TINY_ADAPTER=TBD
|
||||
# Tiny API v3 application (Configurações > Geral > Aplicativos in Tiny). The
|
||||
|
||||
@@ -19,10 +19,20 @@ x-app-environment: &app-environment
|
||||
# this value is configured.
|
||||
OPERATOR_EMAIL: ${OPERATOR_EMAIL:-}
|
||||
OPERATOR_PASSWORD: ${OPERATOR_PASSWORD:?set OPERATOR_PASSWORD}
|
||||
PAYMENT_ADAPTER: fake
|
||||
# Optional: without it the webhook verifies nothing and therefore accepts
|
||||
# nothing, which is the correct state until a provider is connected. Set it
|
||||
# when the provider is configured, never to a value anyone could guess.
|
||||
# fake until Mercado Pago is configured; mercadopago requires MP_ACCESS_TOKEN
|
||||
# and MP_WEBHOOK_SECRET or the API and worker refuse to start. Test
|
||||
# credentials (TEST-...) until the sandbox flows have passed. The card form
|
||||
# appears only with MP_PUBLIC_KEY, and then needs PAYMENT_CSP_SOURCES too.
|
||||
PAYMENT_ADAPTER: ${PAYMENT_ADAPTER:-fake}
|
||||
MP_ACCESS_TOKEN: ${MP_ACCESS_TOKEN:-}
|
||||
# The "assinatura secreta" from the webhook settings in Mercado Pago.
|
||||
MP_WEBHOOK_SECRET: ${MP_WEBHOOK_SECRET:-}
|
||||
# https://<SITE_DOMAIN>/api/payments/webhook, sent with every payment.
|
||||
MP_NOTIFICATION_URL: ${MP_NOTIFICATION_URL:-}
|
||||
MP_PUBLIC_KEY: ${MP_PUBLIC_KEY:-}
|
||||
# The fake adapter's secret. Optional: without it the webhook verifies
|
||||
# nothing and therefore accepts nothing, which is the correct state until a
|
||||
# provider is connected. Never set it to a value anyone could guess.
|
||||
PAYMENT_WEBHOOK_SECRET: ${PAYMENT_WEBHOOK_SECRET:-}
|
||||
FREIGHT_ADAPTER: fake
|
||||
# Order creation in Tiny stays off until it has been tested against the
|
||||
|
||||
@@ -36,6 +36,10 @@ class MercadoPagoTests(unittest.TestCase):
|
||||
self.requests.append(request)
|
||||
if request.method == 'GET':
|
||||
payment_id = request.url.path.rsplit('/', 1)[-1]
|
||||
if payment_id == '500':
|
||||
return httpx.Response(500, json={'message': 'internal_error'})
|
||||
if payment_id not in self.payments:
|
||||
return httpx.Response(404, json={'message': 'Payment not found'})
|
||||
return httpx.Response(200, json=self.payments[payment_id])
|
||||
body = json.loads(request.content)
|
||||
payment = {'id': 555, 'status': 'pending', 'status_detail': 'pending_waiting_transfer',
|
||||
@@ -78,6 +82,15 @@ class MercadoPagoTests(unittest.TestCase):
|
||||
self.assertEqual(self.requests[-1].headers['authorization'], 'Bearer TEST-token')
|
||||
self.assertIsNone(self.mp.parse(json.dumps({'type': 'merchant_order', 'data': {'id': '1'}}).encode()))
|
||||
|
||||
def test_notification_for_an_unknown_payment_is_acknowledged(self):
|
||||
# The panel's "Simular notificação" sends a payment id that does not
|
||||
# exist. Raising would answer 500 and Mercado Pago would retry for ever.
|
||||
body = json.dumps({'id': 43, 'type': 'payment', 'data': {'id': '123456'}}).encode()
|
||||
self.assertIsNone(self.mp.parse(body, {'data.id': '123456', 'type': 'payment'}))
|
||||
# Any other failure still raises, so a real notification is retried.
|
||||
with self.assertRaises(httpx.HTTPStatusError):
|
||||
self.mp.parse(json.dumps({'type': 'payment', 'data': {'id': '500'}}).encode(), {'data.id': '500'})
|
||||
|
||||
def test_amounts_outside_brl_centavos_are_not_trusted(self):
|
||||
for payment in ({'currency_id': 'USD', 'transaction_amount': 10},
|
||||
{'currency_id': 'BRL', 'transaction_amount': 10.001},
|
||||
|
||||
Reference in New Issue
Block a user