diff --git a/app/api/operator.py b/app/api/operator.py index 68d4c14..6492c85 100644 --- a/app/api/operator.py +++ b/app/api/operator.py @@ -303,6 +303,16 @@ def tiny_test(user=Depends(operator)): 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} +@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') def tiny_callback(code: str = Query('', max_length=4096), state: str = Query('', max_length=128), error: str = Query('', max_length=128)): diff --git a/app/mercadopago_probe.py b/app/mercadopago_probe.py index 32f3c41..d929cf8 100644 --- a/app/mercadopago_probe.py +++ b/app/mercadopago_probe.py @@ -3,7 +3,7 @@ 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 +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 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 @@ -20,9 +20,35 @@ from .core.secrets import load as load_secret_files 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): 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') args = parser.parse_args(argv) load_secret_files() @@ -30,37 +56,8 @@ def main(argv=None): 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]}') + import json + print(json.dumps(check(token, args.bin, args.valor), indent=2, ensure_ascii=False)) return 0 diff --git a/web/kanban.js b/web/kanban.js index a5f4448..3517abe 100644 --- a/web/kanban.js +++ b/web/kanban.js @@ -674,8 +674,15 @@ function renderIntegrations(){ t.connected&&!t.problem?'ok':'warn',tinyDetail(t),actions)); } 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', - 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 kg=v=>String(v).replace('.',',')+' kg'; cards.push(fr.provider==='jadlog'