feat: check the Mercado Pago account from the Kanban
All checks were successful
Build and deploy / Validate source (push) Successful in 4s
Build and deploy / Integration suite on a real stack (push) Successful in 2m18s
Build and deploy / Secret scan and release gate (push) Successful in 5s
Build and deploy / Publish images (push) Successful in 44s

"Verificar conta" on the Mercado Pago card of the Integrations tab runs the
read-only account check (whether the token is a test user's, the card
methods, how the test card is classified) without a container console.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cauê Faleiros
2026-09-29 14:18:45 -03:00
parent 2495edf188
commit b5bb03cbb4
3 changed files with 48 additions and 34 deletions

View File

@@ -303,6 +303,16 @@ def tiny_test(user=Depends(operator)):
audit('tiny_tested', operator=user, ok=all(v == 'ok' for v in results.values())) audit('tiny_tested', operator=user, ok=all(v == 'ok' for v in results.values()))
return {'ok': all(v == 'ok' for v in results.values()), 'results': results} return {'ok': all(v == 'ok' for v in results.values()), 'results': results}
@router.post('/api/operator/mercadopago/check')
def mercadopago_check(user=Depends(operator)):
"""The Mercado Pago account behind the configured token; reads only."""
if payment.name != 'mercadopago':
raise HTTPException(409, 'Mercado Pago não está configurado')
from ..mercadopago_probe import check
audit('mercadopago_check', operator=user)
return check(payment.access_token)
@router.get('/api/operator/tiny/callback') @router.get('/api/operator/tiny/callback')
def tiny_callback(code: str = Query('', max_length=4096), state: str = Query('', max_length=128), def tiny_callback(code: str = Query('', max_length=4096), state: str = Query('', max_length=128),
error: str = Query('', max_length=128)): error: str = Query('', max_length=128)):

View File

@@ -3,7 +3,7 @@
python -m app.mercadopago_probe [--bin 503143] [--valor 50] python -m app.mercadopago_probe [--bin 503143] [--valor 50]
Run from the api container's console (Portainer > Containers > api > Run from the api container's console (Portainer > Containers > api >
Console). It creates nothing and charges nothing: it asks Mercado Pago which Console), or with "Verificar conta" on the Kanban's Integrations tab. It creates nothing and charges nothing: it asks Mercado Pago which
account the token belongs to, which card methods the account accepts, and how 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 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 is what payment errors such as 10111 (issuer) and 10113 (method excluded by a
@@ -20,9 +20,35 @@ from .core.secrets import load as load_secret_files
API = 'https://api.mercadopago.com' API = 'https://api.mercadopago.com'
def check(token, bin_='548083', amount='50', transport=None):
"""The account, its card methods and how a card's first digits are read."""
http = httpx.Client(base_url=API, timeout=15, transport=transport,
headers={'Authorization': f'Bearer {token}'})
result = {'token': 'test' if token.startswith('TEST-') else 'production'}
me = http.get('/users/me')
if me.status_code == 200:
user = me.json()
tags = user.get('tags') or []
result['account'] = {'id': user.get('id'), 'nickname': user.get('nickname'),
'site': user.get('site_id'), 'test_user': 'test_user' in tags, 'tags': tags}
else:
result['account'] = {'error': f'HTTP {me.status_code}'}
methods = http.get('/v1/payment_methods')
result['cards'] = ([{'id': m.get('id'), 'type': m.get('payment_type_id'), 'status': m.get('status')}
for m in methods.json() if m.get('payment_type_id') in ('credit_card', 'debit_card')]
if methods.status_code == 200 else {'error': f'HTTP {methods.status_code}'})
options = http.get('/v1/payment_methods/installments', params={'bin': bin_, 'amount': amount})
result['bin'] = ([{'method': o.get('payment_method_id'), 'type': o.get('payment_type_id'),
'issuer': (o.get('issuer') or {}).get('name'),
'installments': [c.get('installments') for c in o.get('payer_costs') or []]}
for o in options.json()]
if options.status_code == 200 else {'error': f'HTTP {options.status_code} {options.text[:200]}'})
return result
def main(argv=None): def main(argv=None):
parser = argparse.ArgumentParser(prog='python -m app.mercadopago_probe') 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('--bin', default='548083', help="the card's first six digits")
parser.add_argument('--valor', default='50', help='amount in reais for the instalment lookup') parser.add_argument('--valor', default='50', help='amount in reais for the instalment lookup')
args = parser.parse_args(argv) args = parser.parse_args(argv)
load_secret_files() load_secret_files()
@@ -30,37 +56,8 @@ def main(argv=None):
if not token: if not token:
print('MP_ACCESS_TOKEN is not set in this container.') print('MP_ACCESS_TOKEN is not set in this container.')
return 1 return 1
http = httpx.Client(base_url=API, timeout=15, headers={'Authorization': f'Bearer {token}'}) import json
print(json.dumps(check(token, args.bin, args.valor), indent=2, ensure_ascii=False))
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 return 0

View File

@@ -674,8 +674,15 @@ function renderIntegrations(){
t.connected&&!t.problem?'ok':'warn',tinyDetail(t),actions)); t.connected&&!t.problem?'ok':'warn',tinyDetail(t),actions));
} }
const mp=board.providers?.payment; const mp=board.providers?.payment;
const mpActions=mp==='mercadopago'?[button('Verificar conta',async()=>{
const r=await api('/mercadopago/check',{});
const conta=r.account.error?'conta: '+r.account.error:'conta '+r.account.nickname+' ('+r.account.id+') · '+
(r.account.test_user?'usuário de teste':'não é usuário de teste');
const bin=Array.isArray(r.bin)?(r.bin.length?r.bin.map(o=>o.method+' '+o.type+' · '+o.issuer+' · até '+Math.max(...o.installments)+'x').join('; '):'cartão de teste não reconhecido'):'cartão: '+r.bin.error;
say('Mercado Pago: credencial '+(r.token==='test'?'de teste':'de produção')+' · '+conta+' · '+bin,false,60000);
},'btn ghost')]:[];
cards.push(integration('Mercado Pago',mp==='mercadopago'?'Ativo':'Não configurado',mp==='mercadopago'?'ok':'off', cards.push(integration('Mercado Pago',mp==='mercadopago'?'Ativo':'Não configurado',mp==='mercadopago'?'ok':'off',
mp==='mercadopago'?'PIX e cartão ativos.':'Aguardando credenciais.')); mp==='mercadopago'?'PIX e cartão ativos.':'Aguardando credenciais.',mpActions));
const fr=board.providers?.freight||{}; const fr=board.providers?.freight||{};
const kg=v=>String(v).replace('.',',')+' kg'; const kg=v=>String(v).replace('.',',')+' kg';
cards.push(fr.provider==='jadlog' cards.push(fr.provider==='jadlog'