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
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:
@@ -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)):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user