From 593ddf82c8d342b006d2d9b5fa38e65187703a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Tue, 29 Sep 2026 13:47:39 -0300 Subject: [PATCH] feat: add a read-only Mercado Pago account probe Card payments with the test credentials are refused with 10111 and 10113, which depend on the account behind the token. python -m app.mercadopago_probe shows that account (and whether it is a test user), the card methods it accepts, and how Mercado Pago classifies a card's first digits: type, issuer and instalments. It creates and charges nothing. Co-Authored-By: Claude Opus 5.5 --- app/mercadopago_probe.py | 68 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 app/mercadopago_probe.py diff --git a/app/mercadopago_probe.py b/app/mercadopago_probe.py new file mode 100644 index 0000000..32f3c41 --- /dev/null +++ b/app/mercadopago_probe.py @@ -0,0 +1,68 @@ +"""Read-only look at the Mercado Pago account behind MP_ACCESS_TOKEN. + + python -m app.mercadopago_probe [--bin 503143] [--valor 50] + +Run from the api container's console (Portainer > Containers > api > +Console). It creates nothing and charges nothing: it asks Mercado Pago which +account the token belongs to, which card methods the account accepts, and how +it classifies a card number's first digits (type, issuer, instalments). That +is what payment errors such as 10111 (issuer) and 10113 (method excluded by a +rule) depend on. +""" +import argparse +import os +import sys + +import httpx + +from .core.secrets import load as load_secret_files + +API = 'https://api.mercadopago.com' + + +def main(argv=None): + parser = argparse.ArgumentParser(prog='python -m app.mercadopago_probe') + parser.add_argument('--bin', default='503143', help="the card's first six digits (default: the Mastercard test card)") + parser.add_argument('--valor', default='50', help='amount in reais for the instalment lookup') + args = parser.parse_args(argv) + load_secret_files() + token = os.environ.get('MP_ACCESS_TOKEN', '') + if not token: + print('MP_ACCESS_TOKEN is not set in this container.') + return 1 + http = httpx.Client(base_url=API, timeout=15, headers={'Authorization': f'Bearer {token}'}) + + print(f"Token: {'TEST' if token.startswith('TEST-') else 'produção'}") + me = http.get('/users/me') + if me.status_code == 200: + user = me.json() + tags = user.get('tags') or [] + print(f"Conta: id {user.get('id')} · {user.get('nickname')} · site {user.get('site_id')} · " + f"usuário de teste: {'sim' if 'test_user' in tags else 'não'} · tags {tags}") + else: + print(f'Conta: HTTP {me.status_code} {me.text[:200]}') + + methods = http.get('/v1/payment_methods') + if methods.status_code == 200: + cards = [m for m in methods.json() if m.get('payment_type_id') in ('credit_card', 'debit_card')] + print('Cartões aceitos pela conta:') + for m in cards: + print(f" {m.get('id'):<12} {m.get('payment_type_id'):<12} {m.get('status')}") + else: + print(f'Meios de pagamento: HTTP {methods.status_code} {methods.text[:200]}') + + quote = http.get('/v1/payment_methods/installments', params={'bin': args.bin, 'amount': args.valor}) + if quote.status_code == 200 and quote.json(): + print(f'BIN {args.bin}:') + for option in quote.json(): + issuer = option.get('issuer') or {} + counts = [c.get('installments') for c in option.get('payer_costs') or []] + print(f" meio {option.get('payment_method_id')} · tipo {option.get('payment_type_id')} · " + f"emissor {issuer.get('id')} ({issuer.get('name')}) · parcelas {counts}") + else: + print(f'BIN {args.bin}: HTTP {quote.status_code} {quote.text[:300]}') + return 0 + + +if __name__ == '__main__': + sys.exit(main())