diff --git a/app/mercadopago.py b/app/mercadopago.py index 9edc10a..c0b722c 100644 --- a/app/mercadopago.py +++ b/app/mercadopago.py @@ -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', ''))) diff --git a/deploy/portainer.env.example b/deploy/portainer.env.example index b1d0453..3694ecb 100644 --- a/deploy/portainer.env.example +++ b/deploy/portainer.env.example @@ -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:///api/payments/webhook +MP_PUBLIC_KEY= FREIGHT_ADAPTER=TBD TINY_ADAPTER=TBD # Tiny API v3 application (Configurações > Geral > Aplicativos in Tiny). The diff --git a/docker-compose.yml b/docker-compose.yml index ce4b609..fcca18c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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:///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 diff --git a/tests/test_mercadopago.py b/tests/test_mercadopago.py index 277fdda..9cef52b 100644 --- a/tests/test_mercadopago.py +++ b/tests/test_mercadopago.py @@ -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},