- 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
18 KiB
Plan Maestro de Mejoras — Nexus Autoparts
Fecha: 11 Oct 2026 · Base: auditoría completa del sistema (6 investigaciones con evidencia en archivo:línea, SQL y curl) Énfasis: MercadoLibre (eje comercial) + apertura del mercado de partes usadas (yonkes/deshuesaderos)
0. Resumen ejecutivo
El sistema es un esqueleto potente con el ciclo comercial roto en los últimos metros. Lo que funciona bien es la base (motor de ventas con locks y partida doble, aislamiento multi-tenant, compatibilidad vehicular, OAuth de MeLi con refresh persistido, WA CRM recién instalado). Lo que está roto es justo lo que genera ingresos:
| # | Bloqueante | Impacto | Evidencia |
|---|---|---|---|
| 1 | Backups caídos desde el 1-oct | Sin respaldo fresco de ningún tenant (riesgo existencial) | scripts/backup_selective.sh:299-303 — ((deleted++)) devuelve 1 y mata el script bajo set -euo pipefail |
| 2 | Alta de tenants falla | No se puede onboardear ningún cliente nuevo | tenant_manager.py:201-211 aplica v1.2_subdomain.sql (migración de master) al tenant → rollback |
| 3 | CFDI 100% roto | Ninguna factura se timbra | invoicing_bp.py:65 — SELECT … codigo_postal FROM branches (la columna es cp) |
| 4 | Órdenes MeLi nunca se convierten a venta | 31/31 órdenes de Rached sin vender en el POS | marketplace_external_service.py:1619 — inventory_record_sale(..., reference=) no existe en la firma |
| 5 | Publicar en MeLi desde el POS está muerto | Validar/Publicar no hacen nada (ReferenceError) | inventory.js:2069,2135 llaman _collectMeliCustomData(), que no existe |
| 6 | Sync de stock MeLi falla 52/52 | Envía price:0 a ML cada 15 min (0 éxitos desde junio) |
tasks.py:135-145 — listings con inventory_id NULL |
| 7 | Webhook MeLi nunca procesa | Sin notificaciones en tiempo real, público y sin firma | marketplace_external_bp.py:775-790 — consulta tenant_config en la BD maestra (no existe ahí) |
| 8 | Catálogo público con 500s | El visitante nuevo ve error en buscador, categorías y detalle | dashboard/server.py:977,1281,1994 contra tablas TecDoc retiradas |
| 9 | Plan demo → fallback free |
Estrada (157,243 productos) bloqueada en 100 productos | billing.py:25-28 + tenants.plan='demo' |
| 10 | /api/admin/tenants* sin autenticación |
Cualquiera en internet lista y modifica módulos de tenants | dashboard/server.py:4660,4674,4691 |
Y el hallazgo estratégico: el negocio de partes usadas es hoy imposible de operar en el sistema (no existe condición, ni pieza única, ni vehículo donador, ni multi-foto) — y MeLi publica todo como "condition":"new" hardcodeado. Abrir ese mercado requiere un cambio de modelo de datos, no cosmético.
1. MercadoLibre — cerrar el ciclo antes de soñar en grande
1.1 Fase A — Desbloquear lo que ya está construido (1 semana)
| Tarea | Detalle | Esfuerzo |
|---|---|---|
| A1 | Implementar _collectMeliCustomData(), _pollMeliAsync(), _renderPublishResult() en inventory.js (los inputs de título/precio/stock y atributos ya se generan pero nunca se envían) y mandar custom_data al backend (marketplace_external_service.py:669-673) |
M |
| A2 | Corregir convert_order_to_sale: usar record_sale(conn, inventory_id, branch_id, quantity, sale_id=…) — hoy TypeError → rollback → 31 órdenes sin convertir (service.py:1619, inventory_engine.py:199) |
S |
| A3 | import_existing_listings: poblar inventory_id por seller_custom_field/part_number/alias y saltar el push cuando price<=0; el ON CONFLICT no aplica con NULL (service.py:1785) |
S |
| A4 | Webhook: resolver tenant por meli_user_id consultando cada tenant_config de tenant (no la maestra), validar X-Signature con validate_webhook_signature (ya existe, nunca se llama) y procesar el resource correcto (no un sync completo) |
M |
| A5 | Normalizar status de preguntas (UPPER) — hoy 4 preguntas reales no se pueden responder por comparar 'unanswered' vs UNANSWERED (marketplace_external.js:516) |
S |
| A6 | Pasar tenant_conn a _get_meli_service en sync_meli_stock_price_task (tasks.py:112) para no perder el refresh token rotado |
S |
| A7 | Pausar/reactivar automáticamente por stock (0 → pausar, >0 → reactivar) en la cola meli_sync_queue + drenarla por timer |
M |
| A8 | Sincronizar órdenes en todos los estados (cancelled, payment_required…) y corregir buyer_nickname (ML ya no manda from.nickname) |
S |
| A9 | Corregir el estimador de comisiones por artículo (busca #meliFee-<id> en un DOM que nunca lo crea, inventory.js:2295) |
S |
| A10 | Definir/otorgar el permiso marketplace.manage (no existe en ningún tenant) y proteger los GET del módulo |
S |
Resultado: el ciclo MeLi→POS (publicar → vender → convertir → facturar) cierra por primera vez.
1.2 Fase B — Publicar en volumen (2-3 semanas) ← el verdadero cuello de botella
| Tarea | Detalle | Esfuerzo |
|---|---|---|
| B1 | Pipeline masivo de imágenes — el bloqueo real: 0 imágenes en 6,851 SKUs de Rached, 0 en 14,568 de Capi, 1 en 157,243 de Estrada. ML exige imagen. Usar los servicios de scraping existentes (services/image_scraper.py, image_service.py) + una cola por lotes con revisión |
L |
| B2 | Selección y publicación por lotes real: hoy la UI solo selecciona una página de 50 y un solo category_id para todo el lote. Agregar: seleccionar todo el resultado filtrado, categorización asistida por IA (nombre+marca→categoría ML con domain_discovery), y confirmación por grupos |
M |
| B3 | Rate-limit/backoff en el worker de publicación (Celery) con reintentos y progreso visible (barra + reporte de fallidos por motivo) | M |
| B4 | Ciclo export→edición→reimportación para fitment e imágenes (hoy el export no incluye compat ni image_url) | M |
1.3 Fase C — Vender más (3-4 semanas)
| Oportunidad | Hoy | Qué agregar |
|---|---|---|
| Variaciones | Nada (0 referencias a variations) |
Agrupar SKUs en una publicación con variations/attribute_combinations (mismo producto, varias medidas) |
| Catálogo de MeLi | Solo category_id |
Detectar categorías que exigen catálogo (catalog_listing, catalog_product_id), avisar en la UI y usar price_to_win para competir por la oferta ganadora |
| Promociones | Nada | Oferta del día, descuentos por volumen, sale_price por publicación |
| Envíos | Solo me2/custom, sin etiquetas | Flex, generación de etiqueta, mark_ready_to_ship (existe y nunca se usa), tracking |
| Preguntas | Manual y roto | Auto-respuesta con ai_chat.py + plantillas por categoría + SLA visible |
| Reputación | Nada | Panel con /users/{id}/reputation: nivel, reclamos, cancelaciones, tiempo de despacho |
| Rentabilidad por publicación | Solo el estimado al publicar (no se guarda) | Persistir comisión + envío + costo en marketplace_listings y panel de margen real por SKU |
| Facturación de ventas MeLi | Ningún enganche a CFDI | Encolar CFDI automático al convertir orden (con RFC del comprador si lo pide) |
| Cancelaciones/reintegros/claims | Solo estado local | Llamar a la API de ML (cancelar, devoluciones, mediaciones) con SLA |
| Multi-cuenta | 1 cuenta MeLi por tenant | Varias cuentas MeLi por tenant (varias marcas) con selección por publicación |
| Panel comercial | Nada | Ventas MeLi vs POS, conversión, SKUs sin stock con publicación activa, top ventas, alertas de publicaciones caídas |
1.4 Fase D — Robustez operativa
- Onboarding asistido de credenciales MeLi (hoy es manual y el fallo típico —ACCA— es un
client_secretregenerado que nadie detecta hasta que el cliente llama; ya mejoramos el mensaje, falta un chequeo proactivo diario que avise antes). - Versionar el timer de sync en el repo (hoy vive solo en
/etc/systemd/system). - Pruebas automatizadas del módulo (hoy: cero tests de MeLi).
2. Abrir el mercado: negocios de SOLO partes usadas (yonkes/deshuesaderos)
2.1 Por qué hoy no se puede (evidencia)
| Requisito del yonke | Estado | Dónde se rompe |
|---|---|---|
| Pieza única (1-de-1, sin código de parte) | ❌ | part_number es NOT NULL + UNIQUE global (v4.0_multi_branch.sql:214); dos espejos iguales de donadores distintos se fusionan en el import |
| Saber de qué auto salió (donador) | ❌ | No hay donor_vin/donor_mileage; la compat existente es "sirve para", no "salió de" |
| Condición nuevo/usado/A/B/C | ❌ | 0 columnas de condición en todo el esquema; MeLi publica "condition":"new" hardcodeado |
| Fotos múltiples (ángulos + daños) | ❌ | Un solo image_url varchar; cada subida reemplaza la anterior (inventory_bp.py:995) |
| Garantía por días (15/30 días) | ⚠️ | Solo warranty_months (mínimo 1 mes) |
| Buscar "todo lo que tengo de un Civic 2015" | ❌ | La navegación por vehículo exige part_number para poblar compat |
| Consignación / piezas de terceros | ❌ | No existe modelo de propiedad; todo el stock se asume propio |
| Valuación con costo 0 | ⚠️ | Vender funciona, pero los reportes de valoración/ABC dan $0 (inventory_bp.py:1879) |
2.2 Plan de producto: "Nexus Yonke" (5-6 semanas)
M1 — Modelo de datos (1 semana)
inventory:condition(nuevo|usado|reconstruido),condition_grade(A/B/C/D),is_unique_piece,serial_number,donor_vin,donor_make/model/year,donor_mileage_km,ownership(propio|consigna),acquisition_cost.- Relajar unicidad: índice único parcial
WHERE part_number IS NOT NULL+ auto-SKU para piezas únicas (USADO-<VIN-corto>-<secuencia>), manteniendo el flujo de partes nuevas intacto. - Nueva tabla
inventory_images (inventory_id, url, order, is_primary)con migración delimage_urlactual. warranties:warranty_days+ política por defecto de usados entenant_config.- Nuevas tablas
consignments/consignment_items(tercero, pieza, precio pactado, comisión, estado: en_consigna/vendida/devuelta/liquidada).
M2 — Alta rápida en mostrador (1-2 semanas)
- Flujo "Registrar pieza usada": foto múltiple (cámara del tablet) + VIN del donador (el decodificador NHTSA ya existe:
/pos/api/catalog/vin) + pieza + precio + condición. Objetivo: < 30 segundos por pieza. - Ingreso de donador completo: registrar el auto (VIN, km, color) y generar N piezas colgadas de ese donador con un solo flujo (hoy: pieza por pieza).
- Etiqueta propia por pieza única (código de barras ya implementado) para control físico en el patio.
M3 — Búsqueda por donador (1 semana)
- Filtro/búsqueda por vehículo donador (marca/modelo/año/color/km) sin requerir
part_number; indexar esos campos en Meilisearch. - Buscador "¿qué tengo de este auto?" + "¿qué tengo compatible con este auto?" (compat comprador, ya existe) como dos pestañas distintas.
M4 — MeLi para usados (1 semana)
conditiondinámico en el payload (usado/remanufacturado) + grado en el título/atributos.- Mapeo de categorías ML para usados (ML tiene categorías de autopartes usadas con reglas propias).
- Fotos múltiples al anuncio (ML acepta hasta 10; hoy se manda 1).
M5 — Consignación y garantía (1-2 semanas)
- Liquidación a consignantes (reporte por periodo, comisión pactada, estado de cuenta del consignante).
- Garantía de usados por días con política por defecto y comprobante impreso.
- Reportes del segmento: rotación por donador, piezas sin foto, días en patio, margen por pieza.
M6 — Plan y onboarding comercial (3-4 días)
- Plan "Yonke" (precio menor): sin catálogo TecDoc, límites amplios, módulos ajustados, onboarding sin importación de catálogo nuevo (alta por escaneo/foto).
- Landing y material para el segmento (hoy
grep yonke|usadasen docs/pitch → 0 resultados). - Marketplace B2B entre yonkes (
is_seller, hoy nunca activado en ningún tenant) como diferenciador: intercambio de piezas usadas entre deshuesaderos de la red.
3. Núcleo comercial y fiscal
CFDI (crítico):
- Corregir
codigo_postal→cp(invoicing_bp.py:65) — desbloquea ingreso, egreso, PDF y factura global. - Complementos de pago (REP) para PPD:
build_pago_payloadexiste y nunca se invoca; hoy toda venta a crédito incumple SAT. - Subir CSD por sucursal (hoy
has_csd:false→ ni timbrando funcionaría). - Devoluciones: encolar CFDI egreso automático + reembolso trazado en caja.
- Moneda/tipo de cambio en facturación (
invoicing_bp.py:83no leecurrency).
Contabilidad:
- Pólizas para abonos de clientes, cortes de caja, devoluciones y apartados (4 funciones de
accounting_engineexisten y nunca se llaman). record_return_entry(no existe).- Dejar de perder asientos en silencio: los hooks
try/except: passdeben loggear (ya se perdió 1 de 18 ventas sin aviso). - Exportables para el contador: pólizas, balanza, estado de resultados, DIOT (hoy solo antigüedad de saldos).
- CxP real con tabla de pagos a proveedores (
paid = 0 # TODO). - Caja: pagos mixtos no suman al efectivo esperado; apartados duplican el efectivo en el corte.
4. Catálogo, búsqueda y datos
- Arreglar el catálogo público (visitante nuevo ve 500):
/api/catalog/search,/api/search,/api/categories, modo OEM y detalle de parte. Modolocalpor defecto + degradación elegante (el POS ya tiene el guard_oem_blocked, el dashboard no). - Conectar Meilisearch de verdad: los índices
nexus_inventory_*ya están poblados y actualizados a diario, ningún código los consulta; la búsqueda local hace seq-scan de 157k filas (175 ms). - Calidad de fitment: el fitment QWEN generó 20.5M filas ruidosas (Corolla 2012 → 411,685 "compatibles" ≈ 40% del catálogo). Política de confianza por fuente + validación + priorizar la compat de proveedor (limpia, 29,760 MYEs). Decidir el destino del catálogo maestro (los 15 GB de JSON TecDoc siguen en disco).
- Imágenes masivas (bloquea catálogo público y MeLi) — mismo pipeline de B1.
- Import/export round-trip con compat, imágenes y min/max/reorder.
- Import con vehículo roto:
bulk-importdevuelve 500 si el CSV trae make/model/year (consulta tablas de vehículos en la conexión del tenant) — justo el flujo de "para qué carro sirve". - Multi-sucursal real: todo el stock vive en la sucursal 1 y
inventory.branch_ides NULL; transferencias/conteos/reorden implementados y sin uso (reorden además filtra por la columna equivocada yreorder_pointes NULL al 100%). - Etiquetas: código de barras generado pero sin endpoint de impresión de etiquetas (solo tickets).
5. SaaS, onboarding y escala
| Tema | Estado | Acción |
|---|---|---|
| Alta de tenants | ❌ Rota (v1.2/v1.6 de master en el runner de tenants) | Marcar como master-only/mover al runner master |
| Template de tenant | Congelado en 32 tablas (le faltan ~56) | Refrescar template en cada alta o eliminarlo del flujo |
| Planes | demo/standard no existen en PLANS → fallback a límites de free |
Agregar planes reales o mapear; aplicar la expiración de demos |
| Fuga de ingresos | Upgrade a enterprise gratis desde el POS + demos vencidas sin efecto + subscriptions desincronizada |
Quitar el autoservicio, sincronizar tenants.plan↔subscriptions.plan, job diario de expiración |
| Seguridad multi-tenant | /api/admin/tenants* del dashboard sin auth |
Proteger + quitar ?tenant= de la superficie pública |
| Módulos | Dos nomenclaturas (module_* vs *_enabled) con defaults contradictorios |
Unificar y sembrar en el template |
| Drift de esquema | v3.5_whatsapp_state_machine marcada SKIP → 6 tenants sin wa_customer_links/wa_learning_sessions → el chatbot WhatsApp falla en esos tenants |
Aplicar la migración real |
| Bases huérfanas | 6 DBs tenant_* sin fila en tenants (~84 MB) |
Limpiar |
| Reset de demo | No limpia clientes ni conversaciones (filtra PII entre prospectos) | Completar el truncado |
| Escala a 100 tenants | Pool 2-20 conexiones × 8 workers × 100 tenants > límite de Postgres; todo con superusuario | PgBouncer + roles por tenant + métricas por tenant |
| Secretos | POS_JWT_SECRET/API keys en unit files legibles por grupo |
EnvironmentFile root-only + rotación |
6. Roadmap propuesto (6 sprints)
| Sprint | Foco | Entregable verificable |
|---|---|---|
| S1 — Estabilizar (1 sem) | Backups (fix ((deleted++)) + verificar drill), alta de tenants, auth de /api/admin/tenants*, plan demo, expiración de demos |
Respaldo fresco de 9 bases verificado con restore; un tenant nuevo creado de punta a punta |
| S2 — MeLi cierra el ciclo (1 sem) | Fase A completa (10 tareas) | Publicar 1 SKU desde el POS → vender en ML → orden convertida a venta → CFDI encolado |
| S3 — Volumen MeLi (2 sem) | Imágenes masivas + lotes + rate-limit + export/import con compat | 200+ SKUs publicados sin intervención manual, con progreso y reporte de fallos |
| S4 — Yonke MVP (2-3 sem) | M1+M2+M3 (modelo, alta rápida por donador, búsqueda por donador) | Un deshuesadero de prueba carga 50 piezas usadas con foto y VIN, y las encuentra por vehículo |
| S5 — Fiscal y contabilidad (2 sem) | CFDI (cp, REP, CSD, egresos), pólizas faltantes, caja (mixtos/apartados), exportables contador | Factura timbrada real; estado de resultados cuadra; DIOT exportable |
| S6 — Vender más + abrir mercado (2-3 sem) | MeLi Fase C (variaciones, catálogo, reputación, rentabilidad, preguntas IA) + Yonke M4-M6 (ML usados, consignación, plan Yonke) | Panel de rentabilidad por publicación; primera pieza usada publicada en ML como used |
7. Métricas de éxito
- Confiabilidad: 30 días consecutivos con respaldo verificado; 0 tenants con esquema incompleto; 0 endpoints 500 en el barrido automatizado.
- MercadoLibre: conversión de órdenes → venta en el POS (hoy 0%, meta 100%); publicaciones sincronizadas sin error (hoy 0/52, meta 100%); ≥200 SKUs publicados en volumen; % de órdenes facturadas automáticamente.
- Yonke: tiempo de alta de una pieza usada < 30 s; 100% de piezas usadas con foto y VIN de donador; búsqueda por donador con resultados < 500 ms; 1 cliente del segmento en producción.
- Comercial: tenants de pago con plan correcto aplicado (hoy 3 con plan desincronizado), fin de la fuga por autoservicio, ≥1 cliente nuevo onboardeado sin intervención manual.