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:
104
scripts/backfill_meli_listings.py
Normal file
104
scripts/backfill_meli_listings.py
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Backfill marketplace_listings.inventory_id for a tenant (MercadoLibre).
|
||||
|
||||
For every listing already imported from MercadoLibre, fetch the ML item and
|
||||
match it against local inventory by:
|
||||
1. seller_custom_field / seller_sku
|
||||
2. SELLER_SKU / PART_NUMBER / SELLER_PART_NUMBER attributes
|
||||
(item level and per variation)
|
||||
3. inventory.part_number, then inventory_sku_aliases.sku
|
||||
(exact match first, then normalized: spaces/dashes/dots stripped)
|
||||
4. the ML item id itself, as a last resort (some tenants store it as alias)
|
||||
|
||||
Dry-run by default: it reports what it *would* map without writing anything.
|
||||
Pass --apply to persist inventory_id + inventory_match_source + matched_at.
|
||||
|
||||
Usage (from /home/Autopartes):
|
||||
python3 scripts/backfill_meli_listings.py --tenant 31 # dry-run
|
||||
python3 scripts/backfill_meli_listings.py --tenant 31 --apply
|
||||
python3 scripts/backfill_meli_listings.py --tenant 31 --apply --limit 10
|
||||
python3 scripts/backfill_meli_listings.py --tenant 31 --all # re-check mapped too
|
||||
python3 scripts/backfill_meli_listings.py --tenant 31 --json
|
||||
|
||||
Requires MASTER_DB_URL + TENANT_DB_URL_TEMPLATE in the environment; if missing
|
||||
they are loaded from /etc/nexus/pos.env.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'pos'))
|
||||
|
||||
|
||||
def _load_env():
|
||||
env_file = '/etc/nexus/pos.env'
|
||||
if os.environ.get('MASTER_DB_URL') or not os.path.exists(env_file):
|
||||
return
|
||||
for line in open(env_file):
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#') or '=' not in line:
|
||||
continue
|
||||
key, value = line.split('=', 1)
|
||||
os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument('--tenant', type=int, required=True, help='tenant id (e.g. 31 = Rached)')
|
||||
ap.add_argument('--apply', action='store_true',
|
||||
help='write the matches (default: dry-run, read-only)')
|
||||
ap.add_argument('--limit', type=int, default=None, help='max listings to process')
|
||||
ap.add_argument('--all', action='store_true',
|
||||
help='also re-check listings that already have an inventory_id')
|
||||
ap.add_argument('--json', action='store_true', help='print the full summary as JSON')
|
||||
args = ap.parse_args()
|
||||
|
||||
_load_env()
|
||||
|
||||
from tenant_db import get_tenant_conn
|
||||
from services import marketplace_external_service as meli
|
||||
|
||||
conn = get_tenant_conn(args.tenant)
|
||||
try:
|
||||
summary = meli.backfill_listing_inventory(
|
||||
conn,
|
||||
limit=args.limit,
|
||||
only_unmapped=not args.all,
|
||||
dry_run=not args.apply,
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(summary, indent=2, ensure_ascii=False, default=str))
|
||||
return 0
|
||||
|
||||
mode = 'DRY-RUN (no writes)' if summary['dry_run'] else 'APPLIED'
|
||||
print(f"[tenant {args.tenant}] backfill inventory_id — {mode}")
|
||||
print(f" scanned: {summary['scanned']}")
|
||||
print(f" matched: {summary['matched']}")
|
||||
print(f" unmatched: {summary['unmatched']}")
|
||||
print(f" errors: {summary['errors']}")
|
||||
|
||||
unmatched = [d for d in summary['details'] if d.get('status') == 'unmatched']
|
||||
if unmatched:
|
||||
print("\n Unmatched listings (ML item has no local part number/SKU):")
|
||||
for d in unmatched:
|
||||
cands = d.get('candidates') or []
|
||||
print(f" listing {d['listing_id']} ({d['external_item_id']}) "
|
||||
f"{(d.get('title') or '')[:45]!r} candidates={cands}")
|
||||
if summary['errors']:
|
||||
print("\n Errors:")
|
||||
for d in summary['details']:
|
||||
if d.get('status') == 'error':
|
||||
print(f" listing {d['listing_id']}: {d.get('error')}")
|
||||
if summary['dry_run'] and summary['matched']:
|
||||
print("\n Re-run with --apply to persist these matches.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user