Mejoras de sesion: estabilizacion, IA, Nexus Yonke y marketplace B2B
- 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
This commit is contained in:
123
scripts/credit_score_report.py
Normal file
123
scripts/credit_score_report.py
Normal file
@@ -0,0 +1,123 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user