- F0 blindaje: Redis/Celery, pg_hba sin trust LAN, secretos a env 600, backups, webhook auth, jobs muertos - F1 caja: bugs POS, reembolsos al corte, apartados, CFDI, topes de descuento en cotizacion/apartado - F2/F7 IA de catalogo: autocategorizacion, dedup de SKUs con embeddings, OCR de facturas, descripciones, claves SAT, limpieza de fitment - F3 IA en el flujo: vision Yonke, preguntas MeLi, venta cruzada, anomalias, sinonimos, busqueda semantica (rerank/embeddings) - F4/F8 escala y cartera: /metrics+Prometheus+Grafana, pool, score de credito, churn, metricas multi-tenant - Nexus Yonke (M1-M6): modelo de datos 1-de-1, alta por donador, vitrina publica, MeLi usado, consignacion, garantia por dias, reportes, etiquetas - Marketplace B2B entre yonkes (catalogo + pedidos cross-tenant) - Seguridad: auth en /api/admin/* (35 endpoints), alertas por tenant - Migraciones v4.27/v4.28; timers expire-demos y rollup-metrics
124 lines
4.3 KiB
Python
124 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Credit risk score report (A9).
|
|
|
|
Prints rule-based credit scores for a tenant's customers using
|
|
``services.credit_scoring.score_customer``. Read-only: never writes to the DB.
|
|
|
|
Usage (as root/postgres, from /home/Autopartes):
|
|
python3 scripts/credit_score_report.py --tenant 31
|
|
python3 scripts/credit_score_report.py --tenant 45 --only-credit
|
|
python3 scripts/credit_score_report.py --all --tier risk
|
|
python3 scripts/credit_score_report.py --tenant 31 --json
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'pos'))
|
|
|
|
from tenant_db import get_tenant_conn, get_master_conn # noqa: E402
|
|
from services.credit_scoring import score_customer # noqa: E402
|
|
|
|
|
|
def list_tenants(only_id=None):
|
|
conn = get_master_conn()
|
|
try:
|
|
cur = conn.cursor()
|
|
if only_id:
|
|
cur.execute(
|
|
"SELECT id, name, db_name FROM tenants "
|
|
"WHERE is_active = true AND id = %s",
|
|
(only_id,),
|
|
)
|
|
else:
|
|
cur.execute(
|
|
"SELECT id, name, db_name FROM tenants WHERE is_active = true ORDER BY id"
|
|
)
|
|
rows = cur.fetchall()
|
|
cur.close()
|
|
return rows
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def customer_ids(conn, only_credit=False, limit=None):
|
|
cur = conn.cursor()
|
|
where = "WHERE c.is_active = true"
|
|
if only_credit:
|
|
where += " AND (c.credit_limit > 0 OR c.credit_balance > 0)"
|
|
sql = f"SELECT c.id FROM customers c {where} ORDER BY c.id"
|
|
if limit:
|
|
sql += " LIMIT %s"
|
|
cur.execute(sql, (limit,))
|
|
else:
|
|
cur.execute(sql)
|
|
ids = [r[0] for r in cur.fetchall()]
|
|
cur.close()
|
|
return ids
|
|
|
|
|
|
def report_tenant(tenant_id, tenant_name, only_credit, limit, tier_filter, as_json):
|
|
conn = get_tenant_conn(tenant_id)
|
|
try:
|
|
ids = customer_ids(conn, only_credit=only_credit, limit=limit)
|
|
rows = []
|
|
for cid in ids:
|
|
try:
|
|
rows.append(score_customer(conn, cid))
|
|
except Exception as exc:
|
|
rows.append({'customer_id': cid, 'error': str(exc)})
|
|
finally:
|
|
conn.close()
|
|
|
|
if tier_filter:
|
|
rows = [r for r in rows if r.get('tier') == tier_filter]
|
|
|
|
if as_json:
|
|
print(json.dumps({'tenant_id': tenant_id, 'tenant': tenant_name, 'scores': rows},
|
|
ensure_ascii=False, indent=2, default=str))
|
|
return
|
|
|
|
print(f"\n=== Tenant {tenant_id} — {tenant_name} ({len(rows)} clientes) ===")
|
|
print(f"{'ID':>6} {'score':>5} {'tier':<6} {'lim_sug':>12} {'datos':>5} señal principal")
|
|
for r in sorted(rows, key=lambda x: x.get('score', 0)):
|
|
if 'error' in r:
|
|
print(f"{r['customer_id']:>6} ERROR: {r['error']}")
|
|
continue
|
|
reasons = r.get('reasons') or []
|
|
headline = next(
|
|
(x for x in reasons if 'Mora' in x or 'atraso' in x or 'excedido' in x),
|
|
reasons[-1] if reasons else '',
|
|
)
|
|
print(f"{r['customer_id']:>6} {r['score']:>5} {r['tier']:<6} "
|
|
f"{r['recommended_limit']:>12,.2f} {r['data_points']:>5} {headline}")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Credit risk score report (A9)")
|
|
group = parser.add_mutually_exclusive_group(required=True)
|
|
group.add_argument('--tenant', type=int, help='Tenant id')
|
|
group.add_argument('--all', action='store_true', help='All active tenants')
|
|
parser.add_argument('--only-credit', action='store_true',
|
|
help='Only customers with a credit line or balance')
|
|
parser.add_argument('--tier', choices=['good', 'medium', 'risk'],
|
|
help='Only show customers in this tier')
|
|
parser.add_argument('--limit', type=int, help='Max customers per tenant')
|
|
parser.add_argument('--json', action='store_true', help='Emit JSON')
|
|
args = parser.parse_args()
|
|
|
|
tenants = list_tenants(None if args.all else args.tenant)
|
|
if not tenants:
|
|
print("No tenants found", file=sys.stderr)
|
|
return 1
|
|
|
|
for tenant_id, name, _db_name in tenants:
|
|
report_tenant(tenant_id, name, args.only_credit, args.limit,
|
|
args.tier, args.json)
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|