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 '')
|
payment_id = str((query or {}).get('data.id') or (data.get('data') or {}).get('id') or '')
|
||||||
if not payment_id.isdigit():
|
if not payment_id.isdigit():
|
||||||
return None
|
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', '')))
|
return event_from_payment(payment, notification_id=str(data.get('id', '')))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ POSTGRES_VOLUME=TBD
|
|||||||
OPERATOR_EMAIL=TBD
|
OPERATOR_EMAIL=TBD
|
||||||
|
|
||||||
PAYMENT_ADAPTER=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
|
FREIGHT_ADAPTER=TBD
|
||||||
TINY_ADAPTER=TBD
|
TINY_ADAPTER=TBD
|
||||||
# Tiny API v3 application (Configurações > Geral > Aplicativos in Tiny). The
|
# Tiny API v3 application (Configurações > Geral > Aplicativos in Tiny). The
|
||||||
|
|||||||
@@ -19,10 +19,20 @@ x-app-environment: &app-environment
|
|||||||
# this value is configured.
|
# this value is configured.
|
||||||
OPERATOR_EMAIL: ${OPERATOR_EMAIL:-}
|
OPERATOR_EMAIL: ${OPERATOR_EMAIL:-}
|
||||||
OPERATOR_PASSWORD: ${OPERATOR_PASSWORD:?set OPERATOR_PASSWORD}
|
OPERATOR_PASSWORD: ${OPERATOR_PASSWORD:?set OPERATOR_PASSWORD}
|
||||||
PAYMENT_ADAPTER: fake
|
# fake until Mercado Pago is configured; mercadopago requires MP_ACCESS_TOKEN
|
||||||
# Optional: without it the webhook verifies nothing and therefore accepts
|
# and MP_WEBHOOK_SECRET or the API and worker refuse to start. Test
|
||||||
# nothing, which is the correct state until a provider is connected. Set it
|
# credentials (TEST-...) until the sandbox flows have passed. The card form
|
||||||
# when the provider is configured, never to a value anyone could guess.
|
# 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:-}
|
PAYMENT_WEBHOOK_SECRET: ${PAYMENT_WEBHOOK_SECRET:-}
|
||||||
FREIGHT_ADAPTER: fake
|
FREIGHT_ADAPTER: fake
|
||||||
# Order creation in Tiny stays off until it has been tested against the
|
# 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)
|
self.requests.append(request)
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
payment_id = request.url.path.rsplit('/', 1)[-1]
|
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])
|
return httpx.Response(200, json=self.payments[payment_id])
|
||||||
body = json.loads(request.content)
|
body = json.loads(request.content)
|
||||||
payment = {'id': 555, 'status': 'pending', 'status_detail': 'pending_waiting_transfer',
|
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.assertEqual(self.requests[-1].headers['authorization'], 'Bearer TEST-token')
|
||||||
self.assertIsNone(self.mp.parse(json.dumps({'type': 'merchant_order', 'data': {'id': '1'}}).encode()))
|
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):
|
def test_amounts_outside_brl_centavos_are_not_trusted(self):
|
||||||
for payment in ({'currency_id': 'USD', 'transaction_amount': 10},
|
for payment in ({'currency_id': 'USD', 'transaction_amount': 10},
|
||||||
{'currency_id': 'BRL', 'transaction_amount': 10.001},
|
{'currency_id': 'BRL', 'transaction_amount': 10.001},
|
||||||
|
|||||||
Reference in New Issue
Block a user