Mejoras de sesion: estabilizacion, IA, Nexus Yonke y marketplace B2B
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled

- 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:
2026-10-12 00:57:31 +00:00
parent f2d4558b80
commit a386ca5b4a
165 changed files with 31313 additions and 2203 deletions

View File

@@ -320,6 +320,30 @@ python3 scripts/check_facturapi_tenants.py
---
## Mejoras A5 / A6 / A10 (2026-10-11)
Capacidades que no dependen de gran volumen de ventas históricas.
| # | Feature | Archivos | Qué hace |
|---|---------|----------|----------|
| **A10** | Venta cruzada "esto + esto" | `services/cross_sell.py`, `blueprints/pos_bp.py` (`POST /pos/api/recommendations`), `static/js/pos.js`, `templates/pos.html` | Sugiere complementos del carrito usando `part_kits.KIT_SUGGESTIONS` + co-ocurrencia real en `sale_items` (mismo `sale_id`). Prioriza piezas con stock; panel en el POS al agregar un artículo. Permiso `pos.sell`. |
| **A5** | Anomalías de negocio | `services/anomaly_detector.py`, `blueprints/pos_bp.py` (`GET /pos/api/reports/anomalies`), `scripts/run_anomaly_scan.py` | z-score robusto (MAD) por empleado/SKU sobre `sale_items` + `sales` + `audit_log`: descuentos atípicos, ventas bajo costo, cancelaciones repetidas y ventas fuera de horario (TZ `America/Mexico_City`). Maneja *cold start* (`min_samples`, default 5) y nunca marca `unit_cost = 0` como bajo costo. Permiso `reports.view`. |
| **A6** | Sinónimos + cross-refs en búsqueda | `services/meili_search.py` (`build_synonyms`, `apply_synonyms_to_all`), `scripts/index_meilisearch.py` (`synonyms` mode, `aliases`), `services/catalog_service.py` (`_search_inventory_by_alias`) | Sinónimos ES↔EN (curados + `services.translations.PART_TRANSLATIONS`, 653 términos) aplicados a `nexus_inventory_*` y `nexus_supplier_catalog` sin reindexar. Los SKU alias (`inventory_sku_aliases`) e interchange se indexan en `aliases` y además se consultan por SQL, así la búsqueda por número cruzado funciona de inmediato. |
Comandos de operación (A6):
```bash
# Aplicar solo settings/sinónimos (no reindexa documentos)
python3 scripts/index_meilisearch.py synonyms
# Reindexar un tenant (incluye aliases) o todos
python3 scripts/index_meilisearch.py tenant tenant_capi_autopartes
python3 scripts/index_meilisearch.py all
# Escaneo de anomalías (exit 1 si hay hallazgos de severidad alta)
python3 scripts/run_anomaly_scan.py all 90
```
---
## Mejoras Pendientes (Roadmap Actualizado)
### 🔴 Crítico — Deuda Técnica

47
docs/YONKE_COMERCIAL.md Normal file
View File

@@ -0,0 +1,47 @@
# Nexus Yonke — Documento Comercial (segmento partes usadas)
> Complemento del `docs/YONKE_ONBOARDING.md` (técnico). Aquí va el **posicionamiento, precios, segmento y copy** para vender el módulo a yonkes/deshuesaderos.
## 1. A quién le vendemos
Yonkes, deshuesaderos y refaccionarias que venden **piezas usadas** (1‑de‑1, con vehículo donador y fotos). Hoy operan con un cuaderno, WhatsApp y MercadoLibre; sin sistema que entienda que **cada pieza es única**.
## 2. Propuesta de valor (diferenciadores)
1. **Pieza única 1‑de‑1** con SKU automático (`USADO‑<VIN>‑<n>`): dos espejos iguales de donadores distintos **no se fusionan**.
2. **Alta en <30 s** desde mostrador: **foto múltiple + VIN del donador** (decodificado solo) → pieza lista.
3. **Búsqueda por donador** ("¿qué tengo de este Civic 2015?") además de "¿qué le queda a este auto?".
4. **Visión por IA**: sugiere el **grado A/B/C/D** desde la foto y **identifica la pieza**.
5. **Publicación en MercadoLibre como USADO** (grado, donador, hasta 10 fotos) desde el mismo sistema.
6. **Consignación**: piezas de terceros con comisión y **estado de cuenta del consignante**.
7. **Vitrina pública** de usados con SEO (Google encuentra tus piezas).
8. **Marketplace B2B entre yonkes** (roadmap): intercambio de usadas entre deshuesaderos de la red.
## 3. Precio
- Plan **`yonke`** — **MXN 299/mes**: sin límite de piezas, hasta 10 empleados, 1 sucursal, **sin catálogo TecDoc** (no lo necesita).
- Upsell: multi‑sucursal, WhatsApp automatizado, marketplace B2B.
## 4. Onboarding guiado (checklist)
1. Crear el tenant del negocio (plan `yonke`), activar el módulo **Yonke**.
2. Otorgar `yonke.view`/`yonke.manage` a los roles (el owner ya los tiene).
3. **Configurar la política de garantía de usados** (`yonke_default_warranty_days`, p.ej. 30).
4. Cargar las **primeras 20 piezas** en `/pos/yonke` (VIN → Decodificar → fotos → guardar).
5. Registrar el primer **donador completo** (auto + sus piezas de una pasada).
6. Opcional: publicar 3 piezas en **MercadoLibre** y activar la **vitrina pública**.
## 5. Copy para landing (borrador)
**Título:** *Tu yonke, con sistema. Piezas usadas, ordenadas como nunca.*
**Bajada:** *Cada pieza es única. Nexus Yonke lo entiende: registra la pieza en 30 segundos desde una foto y el VIN del donador, la encuentra por vehículo, le pone su grado con IA y la publica en MercadoLibre como usada. Consignación, etiquetas y vitrina pública, incluidos.*
**Bullets:**
- "Alta por foto + VIN en menos de 30 segundos."
- "Tu inventario en el patio, buscable por donador."
- "Publica en MercadoLibre como USADO con grado y fotos."
- "Consigna piezas de terceros y liquida con estado de cuenta."
- "Vitrina pública indexada en Google."
**CTA:** *Prueba Nexus Yonke — MXN 299/mes, sin límite de piezas.*
## 6. Roadmap comercial del segmento
- **Fase 1 (listo):** alta, búsqueda por donador, MeLi usado, consignación, plan.
- **Fase 2:** vitrina pública + garantías/devolución + etiquetas + reportes (en curso).
- **Fase 3:** **marketplace B2B entre yonkes** (`is_seller`), analítica del segmento, importación masiva.

51
docs/YONKE_ONBOARDING.md Normal file
View File

@@ -0,0 +1,51 @@
# Nexus Yonke — Onboarding (negocios de partes usadas)
> Módulo para refaccionarias/yonkes/deshuesaderos que venden **piezas usadas** (piezas únicas 1-de-1,
> con vehículo donador y varias fotos). No requiere catálogo TecDoc.
## Qué se habilitó
**Modelo de datos (migración `v4.27_yonke_m1`, aplicada a todos los tenants):**
- `inventory`: `condition` (nuevo/usado/reconstruido), `condition_grade` (A/B/C/D), `is_unique_piece`,
`serial_number`, `donor_vin`, `donor_make`, `donor_model`, `donor_year`, `donor_mileage_km`,
`ownership` (propio/consigna), `acquisition_cost`.
- `part_number` acepta `NULL` → dos piezas idénticas de donadores distintos **no se fusionan**.
- `inventory_images` (fotos múltiples por pieza).
- `warranties.warranty_days` (garantía por días).
- `consignments` / `consignment_items` (piezas de terceros a consignación).
**Módulo `/pos/yonke`** (pestañas):
1. **Piezas usadas** — listado con filtros (texto, condición, grado, propiedad) y paginación.
2. **Nueva pieza** — alta rápida: nombre, precio, **VIN del donador + botón "Decodificar"** (NHTSA)
que autocompleta marca/modelo/año, km, propiedad, cobertura en días y **varias fotos**. Genera SKU
automático `USADO-<últimos 6 del VIN>-<n>` si no se captura número de parte.
3. **Por donador** — "¿Qué tengo de este auto?" (agrupado por VIN) y piezas de un donador concreto.
4. **Consignaciones** — alta de consignante, piezas, montos y **liquidación** (reporte por periodo).
**Publicación en MercadoLibre (M4):** las piezas usadas se publican con `condition="used"`, el grado
en el título, datos del donador como atributos y **hasta 10 fotos**.
**Permisos:** `yonke.view` (ver) y `yonke.manage` (crear/editar). El rol `owner`/`admin` los tiene por
defecto; otros roles se otorgan desde Configuración → Permisos (grupo "Yonke").
## Cómo dar de alta un cliente de este segmento
1. Alta normal del tenant (el flujo de tenants ya funciona; ver S1).
2. En el panel admin, activar el módulo `yonke_enabled` del tenant (opcional).
3. Otorgar `yonke.view`/`yonke.manage` a los roles que correspondan (por defecto el owner ya los tiene).
4. En `/pos/yonke` → **Nueva pieza**: capturar la primera pieza (VIN → Decodificar → fotos → guardar).
5. Repetir por pieza; usar **Por donador** para ubicar todo lo que se tiene de un auto.
## Plan comercial
El plan **`yonke`** (MXN 299/mes) está disponible en `services/billing.py`: sin límite de productos,
hasta 10 empleados, 1 sucursal y sin catálogo TecDoc.
## Pendientes conocidos
- La garantía por días se registra vía `POST /pos/api/warranties` con `warranty_days`; **aún no se
auto-registra al vender** (el POS no registra garantías automáticamente hoy). Implementar el hook al
vender una pieza con cobertura.
- Las categorías de ML para usados pueden exigir reglas propias; el mapeo de categorías por segmento
queda como mejora.
- `donor_vin` no se publica en ML (no hay atributo universal); se puede añadir a la descripción.

View File

@@ -0,0 +1,106 @@
# Auditoría #2 — Mejoras y IA no conversacional (post F0–F4)
> **Fecha:** 11-oct-2026 · **Base:** 5 investigaciones nuevas con evidencia en `archivo:línea`, SQL real y estado del host.
> **Contexto:** segunda pasada **sobre el estado ya mejorado** (F0–F4 hechos). Busca la capa que falta, incluidas **regresiones** y **puntos ciegos** de la primera pasada.
---
## 0. Resumen ejecutivo
La primera pasada dejó una base sólida, pero esta segunda encontró **regresiones y puntos ciegos**:
**Corregido YA en esta pasada:**
1. **Redis caído otra vez** (18:31 UTC): `redis.conf` hacía `bind 127.0.0.1 172.17.0.1`; si `docker0` no está arriba, **Redis muere en bucle** ("Cannot assign requested address") y con él **todo Celery** (fitment, publicación MeLi en lote, webhook). → Arreglado (`bind 127.0.0.1 ::1`) + `vm.overcommit_memory=1` + `reset-failed`; Celery vivo.
2. **30 de 35 endpoints `/api/admin/*` SIN auth** (solo `tenants*`/`users*` la tenían): `DELETE /api/admin/parts`, `POST /api/admin/import/<type>`, `upload-image`, `export/<type>`… expuestos. → Guard global `before_request` que exige token **ADMIN** en **toda** la superficie; verificado 401.
**Lo demás que falta** (priorizado):
- **MeLi sigue "inerte"**: 52 publicaciones de Rached con `inventory_id=NULL` (0/52), 31 órdenes pagadas sin convertir, `module_marketplace=false`, sync congelado desde el 11-jun, 0 imágenes. No es la API: es el **mapeo SKU→inventario + imágenes**.
- **Template de tenant congelado en 32 tablas** (un tenant real tiene ~91): funciona solo porque el alta aplica v1.1→v4.27 sobre un template *desactualizado* → frágil.
- **Flags de módulo inconsistentes** (`*_enabled` en dashboard vs `module_*` en POS): activar MercadoLibre/WhatsApp en el panel **no** se refleja en el POS.
- **Yonke construido y vacío**: sin flag de módulo, sin link de navegación, sin plan asignable, 0 piezas.
- **Lealtad sin cerrar el ciclo** (acumula pero no se puede canjear con descuento real).
- **Backups**: sin offsite, sin retención (7.2 GB), **sin alerta de backup**; y `expire_demos.py` no está cableado a ningún timer.
- **Reset de demo no limpia PII** (clientes/conversaciones quedan).
- **La IA de embeddings está construida pero SIN usar**: `semantic_search.embed()/find_similar()` **no tienen un solo caller** → es la palanca más barata para media docena de mejoras.
---
## 1. Regresiones / puntos ciegos operativos (con evidencia)
| # | Hallazgo | Evidencia | Estado |
|---|---|---|---|
| 1 | **Redis muere por `bind` a docker0** | `redis-server.log:847` `Cannot assign requested address 172.17.0.1`; `redis.conf:88` | ✅ arreglado |
| 2 | **32 endpoints `/api/admin/*` sin auth** | `dashboard/server.py` (35 rutas, 3 con ADMIN) | ✅ arreglado (guard global) |
| 3 | **Admin de catálogo roto (500)** | `/api/admin/stats`,`/groups` → 500 (tablas `parts`/`part_categories` retiradas, `server.py:2716-2743`) | pendiente |
| 4 | **Backups Oct 2–10 inexistentes** + sin offsite + sin retención + **sin alerta de backup** | `backups/`; `BACKUP_S3_BUCKET` sin set | parcial (timer ya corre) |
| 5 | **`expire_demos.py` sin timer** | grep `systemd/`=0 | pendiente |
| 6 | **Template congelado (32 tablas) + drift** (`sale_payments` ausente) | `tenant_template` 32 vs tenant real 91 | pendiente |
| 7 | **Reset de demo deja PII** (no trunca `customers`,`whatsapp_messages`,`wa_*`,`audit_log`) | `manager/services/tenant_service.py:188-240` | pendiente |
| 8 | **14 DB `tenant_*` huérfanas** (6 con PII de prueba) vs 8 registradas | `pg_database` | pendiente |
| 9 | **Flags de módulo inconsistentes** | dashboard escribe `meli_enabled`, POS lee `module_meli` (`config_bp.py:1269`), manager usa `module_*` | pendiente |
| 10 | **Manager sin nginx/TLS/2FA; auditoría vacía; bug de comparación de versiones** (`"v4.9">="v4.10"`) | `manager/services/migration_service.py:82` | pendiente |
| 11 | **Sin TLS en el host, sin firewall; `5432`/`5003` en 0.0.0.0** | `ss -tlnp`; `iptables -S` ACCEPT | pendiente (decisión) |
| 12 | **`nexus` comparte rol en TODAS las DB; sin roles por tenant** | `has_database_privilege`=t | pendiente |
| 13 | **nginx del repo difiere del vivo** (falta bloque wildcard de tenants; sobra `nexus_quart`/`login2`) | `nginx/nexus-pos.conf` vs `/etc/nginx/...` | pendiente |
| 14 | **Sin analytics/tracking** en la cara pública (cero GA/GTM/pixel) | grep = 0 | pendiente |
| 15 | **`i18n.js` muerto** (fuerza `es`, cero `t()`), `*.min.js` desactualizados | `i18n.js:322` | pendiente |
| 16 | **Aislamiento multi-tenant por proceso**: 8 workers × N tenants × pool; sin PgBouncer; scans cross-tenant O(n) | `tenant_db.py`, `marketplace_external_bp.py:113` | pendiente |
| 17 | **Token del webhook de Alertmanager en claro en el repo** | `docker/alertmanager/alertmanager.yml` | pendiente |
---
## 2. MeLi / Yonke / WhatsApp — estado real y brechas
**MercadoLibre (Rached):** 52 listings, **`inventory_id` 0/52**, 31 órdenes `paid` sin convertir (`nexus_sale_id` NULL), `sync_errors` 52/52 (jun-11), `last_sync_at` 2026-06-11, 0 imágenes, `module_marketplace=false`. Bloqueo operativo = **mapear SKU→inventario + subir imágenes + encender el módulo**.
Ausente comercialmente (0 refs en código): **descripción de anuncio**, **facturación automática al convertir** (usa `_create_sale_manual` que salta el hook de CFDI/contabilidad), **persistir comisión/rentabilidad**, variaciones, `catalog_listing`/`price_to_win`, promociones, reputación, **SLA de preguntas** (4 sin responder, una de febrero), multi-cuenta, backoff de rate-limit (429).
**Yonke:** código completo (M1–M6), **0 datos**. Falta: flag `module_yonke` (no existe), link en navegación, plan asignable desde admin, tenant piloto, exposición pública de usados, categorías ML de usados, botón "publicar usado" en la UI.
**WhatsApp:** 8 bridges vivos + WA CRM (`:3010`) pero `module_whatsapp=false` en todos, WA CRM sin cablear (secret placeholder), **0 mensajes**; y el flujo WA **no escribe actividades en el CRM** (aunque sí factura pedidos).
---
## 3. IA no conversacional — la siguiente ola (con prontitud de datos)
### 3.1 La palanca infrautilizada
`pos/services/semantic_search.py` tiene `embed()` (4096 dims), `find_similar()`, `cosine_similarity()`, `rerank()` — **el rerank está integrado y el resto NO tiene un solo caller**. Es la base más barata para: matching, dedup, alternativas, limpieza de fitment, búsqueda semántica de bodega y respuestas de compatibilidad.
### 3.2 Priorizado por impacto/esfuerzo
| Prio | Oportunidad | Punto de integración | Datos (real) | Enfoque | Esfuerzo |
|---|---|---|---|---|---|
| **1** | **Ligar inventario→catálogo a escala** (habilita casi todo) | `sku_matcher.py:82,138`; endpoint `/inventory/match-catalog` | 355k items sin ligar; 17,246 catálogo; 406,393 xrefs | exact+interchange (ya) **+ embeddings** en el fallback de nombres (hoy `difflib`) | M |
| **2** | **Enriquecer catálogo: descripciones e imágenes** | `catalog_import_service.py:14`; `image_scraper.py:55` | 95% de `inventory.description` vacío; 0/17,246 `supplier_catalog` con imagen/desc; hay imágenes en los Excel de `data/` | LLM para descripción ES; cargar imágenes de proveedor + visión para validar | S–M |
| **3** | **"Alternativas / piezas similares" (feature muerta)** | `catalog_service._get_alternatives:1859`; `dashboard/server.py:1915` | `supplier_catalog_interchange` (406k) + catálogo 17k | reconstruir sobre interchange + `find_similar()` (hoy consulta tablas TecDoc retiradas → vacío) | M |
| **4** | **Anuncios MeLi: título + descripción + categoría** | `marketplace_external_service.build_item_payload:237`; `meli_service.search_categories:280` | nombre/marca/compat/donor | LLM (categoría vía `domain_discovery`) + descripción | S–M |
| **5** | **Claves SAT de productos** (`clave_prod_serv`/`clave_unidad` 100% vacías) | `cfdi_builder.py:178`; `cfdi_facturapi_builder.py:83` | nombre/categoría/marca de 170k | reglas + LLM → CFDI correcto | S |
| **6** | **Persistir comisión/rentabilidad por listing** | `marketplace_listings` (nuevas columnas); `_calculate_meli_net_price:387` (ya calcula, no guarda) | listings + costo | guardar fee/net/costo → margen por publicación | S |
| **7** | **Facturación automática al convertir orden MeLi** | `convert_order_to_sale`/`_create_sale_manual:1759` (salta `pos_engine` CFDI) | órdenes MeLi | enrutar por `process_sale` o encolar CFDI | S |
| **8** | **Extracción de pedidos desde WhatsApp (texto/voz/foto)** | `whatsapp_bp.py:376` (hoy menú rígido) | `whisper_local` (voz), `part_vision`/`invoice_ocr` (foto), catálogo | LLM/NER → cotización borrador | M–L |
| **9** | **Limpieza de fitment ruidoso** | `qwen_fitment.py:113,298`; `inventory_vehicle_compat.py:322` | 41.5M filas (estrada ~131/pieza; `qwen` 99.5%) | apagar `qwen` por defecto en bulk-import + scorear ruido con embeddings/rerank | M |
| **10** | **Búsqueda semántica de bodega** (150,778 items, multi-vendedor) | `peer_service`, `catalog_service.get_bodega_availability:2562` | 150k con nombre/SKU | embeddings + rerank (offline) | M |
| **11** | **Métricas de negocio por tenant + anomalías multi-tenant + salud de cartera** | master rollup diario; `anomaly_detector.py` (intra-tenant) | por definir | rollup + z-score entre tenants | M |
| **12** | **Dedup/normalización con embeddings** | `sku_matcher.match_name:82` | estrada: 6,121 grupos de `name` duplicado | `find_similar()` clustering | M |
### 3.3 No vale la pena todavía (cold-start honesto)
- **Forecast de demanda por SKU, elasticidad/precios, predicción de quiebre, fraude/anomalías por SKU, churn/scoring por cliente**: hay **~70 líneas de venta en toda la plataforma** y **0 compras**. `cross_sell` estadístico y `anomaly_detector` se auto-marcan en `cold_start`. Dejar **diseñado, no invertir** hasta tener miles de transacciones por tenant.
- **IA de ops**: triage de logs con LLM y agrupación de alertas con ML = **overkill** (volumen KB/día, 1 receiver). Lo útil es **sin IA**: alerta de backup/celery/tenant caído y `predict_linear` para disco.
---
## 4. Roadmap propuesto
| Fase | Foco | Entregable verificable |
|---|---|---|
| **F5 — Cerrar lo crítico (2-3 días)** | Redis estable (hecho) · auth admin (hecho) · admin catálogo sin 500 · `expire_demos` a timer · unificar flags de módulo · reset de demo con PII · limpiar DB huérfanas · alerta de backup | 0 endpoint admin sin auth; módulos coherentes dashboard↔POS; alerta de backup llega |
| **F6 — MeLi de verdad (1 sem)** | Mapear los 52 listings → inventario · pipeline de imágenes · encender `module_marketplace` y `module_yonke` · publicar con descripción/categoría · persistir comisión · facturación automática al convertir | Publicar SKU con imagen desde el POS → vender en ML → orden convertida + CFDI encolado |
| **F7 — IA de catálogo (2 sem)** | Ligado masivo con embeddings · descripciones + imágenes · "alternativas/similares" funcional · SAT keys · limpieza de fitment | ≥70% de items ligados a catálogo; alternativas devuelven resultados; fitment por pieza < 20 filas |
| **F8 — Escala y cartera (2-3 sem)** | PgBouncer + pool · métricas de negocio por tenant + anomalías multi-tenant + salud de cartera · analytics en la cara pública · Yonke piloto · WA CRM por negocio · TLS/firewall/roles | panel de cartera; 30+ tenants sin agotar conexiones; 1 tenant Yonke en producción |
---
## 5. Métricas de éxito
- **Confiabilidad:** 0 servicios en bucle; backups diarios verificados + offsite; alertas que llegan.
- **Cierre:** 0 endpoints `/api/admin/*` sin auth; flags de módulo coherentes.
- **MeLi:** 100% de listings con `inventory_id`; >0 órdenes convertidas + facturadas; publicaciones con imagen y descripción.
- **IA de catálogo:** % items ligados a catálogo (0→≥70%); tiempo de alta de anuncio; búsquedas sin resultado.

View File

@@ -0,0 +1,125 @@
# Auditoría completa de la plataforma + IA no conversacional — Nexus Autoparts
> **Fecha:** 11-oct-2026 · **Base:** 6 investigaciones con evidencia en `archivo:línea`, SQL real y estado del host.
> **Énfasis solicitado:** áreas de mejora + **IA no conversacional** (predicción, clasificación, visión, OCR, anomalías, matching).
---
## 0. Resumen ejecutivo
La plataforma es hoy, en los datos, **un catálogo de autopartes con fitment masivo (≈21 M relaciones pieza-vehículo y ≈406 K equivalencias) más un POS funcional pero con poco volumen transaccional** (máximo 24 ventas en un tenant; la única serie temporal real son 4,582 ventas históricas de Atlas en 19 meses). El código de IA existente es **LLM + visión (QWEN/Hermes/Gemma) y Meilisearch**; no hay ML clásico instalado.
Hay tres frentes con problemas:
1. **Operación/seguridad (grave y barato de arreglar):** firewall abierto, Postgres con `trust` en toda la LAN, secretos en texto claro, **Redis caído 6 días → todo Celery muerto**, **alertas silenciosas** (sin `ALERT_PHONES`), backups que fallaron 5 días sin avisar.
2. **Bugs de producto en caja** (visibles al usuario hoy) y funciones construidas pero **sin cablear** (garantías, notificaciones de taller, lealtad, venta cruzada, flotillas).
3. **Calidad de datos** que bloquea el catálogo, la publicación en MercadoLibre y el SEO: **0 imágenes, 0 descripciones, `reorder_point` 100% NULL, 0 precios de proveedor, 52 listings sin `inventory_id`**.
**IA no conversacional: qué sí tiene sentido hoy** (no depende de volumen de ventas): auto-categorización de productos (ML y taxonomía), deduplicación/matching de SKUs, **OCR/VLM de facturas de proveedor**, **visión para graduar piezas usadas (Yonke)**, semantic search por embeddings, y **detección de anomalías de negocio** (descuentos/stock). **Qué NO vale la pena todavía:** forecast de demanda (cold start), triage de logs con LLM, agrupación de alertas con ML (overkill).
---
## 1. Operación y seguridad — P0 (hacer ya)
| # | Hallazgo | Evidencia | Acción |
|---|---|---|---|
| 1 | **Postgres con `trust` para toda la LAN** + puerto `5432` abierto en `0.0.0.0` | `pg_hba.conf`: `host all all 192.168.10.0/24 trust`; `iptables -S` → `INPUT ACCEPT` sin reglas; `ufw` no instalado | Quitar `trust` (usar `scram-sha-256`), `listen_addresses` a localhost/LAN acotada, firewall |
| 2 | **Secretos en claro en unit files world-readable** | `nexus-pos.service` (QWEN/FACTURAPI/JWT/INTERNAL/MEILI keys), `nexus-manager.service`, `nexus.service`, `meilisearch.service` | `EnvironmentFile` modo `600` o `systemd-creds`; rotar llaves |
| 3 | **Redis caído 6 días → Celery en bucle de muerte** | `systemctl status redis-server` = failed; `celery.log`: `Cannot connect to redis://localhost:6379/1 (55/100)` desde hace 6 días | Levantar Redis (o quitarlo del diseño); hoy **toda tarea Celery está caída** |
| 4 | **Alertas silenciosas** | `/etc/nexus/alert.env` sin `ALERT_PHONES` → `alerts.log: ALERT no ALERT_PHONES configured`; `RedisDown` activo 6 días sin consecuencia | Poblar `alert.env`, segundo canal (email/Slack), alertas de backup/Celery/app-down |
| 5 | **Backups: fallaron 5 días en silencio** (7–11 oct); solo locales; 3.5 GB zombi | `journalctl -u nexus-backup`; `BACKUP_S3_BUCKET` sin definir; cleanup no borra `.dump`/`.sql` viejos | Ya corregí el `((deleted++))`; **falta validar el timer 02:00**, offsite S3 y limpieza |
| 6 | **Sin TLS en este host** | solo `listen 80`; no hay 443 | Confirmar proxy/Cloudflare delante, o terminar TLS |
| 7 | **Endpoints expuestos** | Grafana `admin/nexus2026`; Prometheus/Alertmanager sin auth; webhook `/pos/api/notifications/webhook` POST **sin auth** | Auth + bind a localhost |
| 8 | **Servicios como `root`** y **timers muertos** (`nexus-quart`, `cache-warm`, `mv-refresh`, `cron.d` inexistente) | `systemctl status` | Erradicar jobs muertos / matrices obsoletas |
**Observabilidad:** Prometheus/Grafana/Alertmanager corren y scrapean infra, pero **la app no expone `/metrics`** (`:5001/metrics` → 404; `grep prometheus_client` = 0) → el dashboard "Nexus — Application" está **vacío**. Falta `pg_stat_statements` cargado (`shared_preload_libraries` vacío) y `log_min_duration_statement` activo.
**Escalabilidad:** el pool es **por proceso** (`tenant_db.py:24-32`, `minconn=2, maxconn=20` × 8 workers gunicorn) → ~160 conexiones/tenant en el peor caso; **sin PgBouncer**. 8 tenants van sobrados; **a ~15-20 tenants calientes revienta `max_connections=200`**. A 100 es insostenible.
---
## 2. Bugs de producto en caja — P0/P1
| Sev | Bug | Evidencia |
|---|---|---|
| P0 | **Venta offline rota**: `totals['total']` no existe → `ReferenceError`, el ticket offline nunca se muestra | `pos/static/js/pos.js:1587` |
| P0 | **Devolución muestra reembolso $0**: lee `res.refund_total`, el API devuelve `total_refund` | `pos.js:978` vs `pos_bp.py:2628` |
| P0 | **Apartado liquidado no muestra la venta**: lee `res.sale_id`, el API devuelve `{id,...}` | `pos.js:1054` vs `pos_bp.py:2377` |
| P0 | **"Facturar CFDI" no hace nada**: se manda `generate_cfdi` pero **ningún backend lo lee** | `pos.js:1519`; `grep generate_cfdi` en `pos/**/*.py` = 0 |
| P0 | **Reembolsos no entran al corte de caja** → efectivo esperado sobreestimado | `pos_bp.py:2454` no inserta `cash_movements`; `cashregister_bp.py:195-314` no resta devoluciones |
| P1 | **Apartado cancelado no revierte contabilidad** (pasivo fantasma 240) | `pos_bp.py:2389-2449` |
| P1 | **Apartado se liquida siempre como efectivo** (aunque sea tarjeta) | `pos_bp.py:2291,2332` |
| P1 | **Descuento/tope de precio solo se valida en venta directa** (cotización/apartado la evaden) | `pos_engine.py:290-295`; `pos_bp.py:822,1212,1704,1864` |
| P1 | **Devoluciones sin control de rol** (solo `pos.sell`) | `pos_bp.py:2455` |
| P1 | **500 por query param sin validar** (`?per_page=abc`) | `error.log`; `/pos/api/marketplace-ext/*` |
**Construido pero sin cablear:** garantías/RMA nunca se registran al vender (`warranty_bp.py:41`); `notify_order_ready`/`notify_maintenance_due` existen y no se llaman; **lealtad acumula pero no se puede canjear en el POS**; **venta cruzada muerta** (`part_kits.py` no se importa); flotillas fuera del menú (`sidebar.js` no incluye `fleet`); analytics de cliente (LTV/churn) calculado y no mostrado (`crm_bp.py:227`, `crm_engine.py:289`).
---
## 3. Calidad de datos y catálogo
- **Imágenes: 0 en todos los tenants** (`image_url` vacío; `inventory_images` 0 filas). Bloquea publicar en ML, el catálogo público y el SEO.
- **`supplier_catalog`**: 17,246 items, **0 con imagen, 0 con descripción**.
- **`reorder_point`/`reorder_qty` 100% NULL**; `min_stock=0` en ~95% → **las alertas de stock bajo nunca disparan**; `reorder_alerts` = 0 filas.
- **Filtro de sucursal roto en reorden**: filtra por `inventory.branch_id`, que es NULL al 100%.
- **Motor de Proveedores/Órdenes de Compra completo y sin uso** (`purchase_orders` = 0 filas en todos los tenants); `supplier_catalog_prices` = 0; `catalog_part_id` 0/14,568 → sin precios/margen de proveedor.
- **Fitment**: `inventory_vehicle_compat` de Estrada = **20.7 M filas / 129,101 items ≈ 160 por pieza** (99.5% salida QWEN, conf 0.843) — **ruidoso**; Capa/Rached usan `supplier_catalog` (0.95, limpio).
- **Búsqueda**: Meili cubre inventario y proveedor, pero **el índice `nexus_parts` (1.55 M docs TecDoc) sigue re-indexándose y está muerto**; los aliases/cross-refs **no entran** a `smart_search`; sin sinónimos.
- **Estrada y Originales Autopartes son clones exactos** (mismos 157,243 SKUs y 20,687,557 fitments) → cuéntalos una vez.
---
## 4. IA no conversacional — qué construir (y en qué orden)
### 4.1 Lo que ya existe (reutilizable)
- `ai_chat.classify_part()` (`ai_chat.py:379`) → número OEM a `{name, brand, vehicle, category}`.
- `ai_chat.chat_with_image()` (`ai_chat.py:309`) → **visión**: identifica una autoparte por foto (Gemma/QWEN).
- `qwen_fitment.py` → compatibilidad pieza-vehículo validada contra la maestra (con `confidence`).
- `whisper_local.py` → **ASR local** (faster-whisper, CPU) para notas de voz.
- Meilisearch (keyword + typos; soporta vectores) con índices poblados.
- `nexpart_taxonomy.py` → 558 etiquetas (dataset listo para clasificador); `wa_learning.get_learning_pairs_for_training()` (`:73`) → pares texto→producto etiquetados.
- **Infra lista para OCR**: `tesseract` + `pytesseract` + `ffmpeg` + `pdftotext` ya instalados.
- **No hay** `sklearn`/`torch`/`transformers`/`faiss`; **sin GPU** (12 vCPU / 47 GB). Inferencia en CPU o vía QWEN/Hermes.
### 4.2 Propuestas priorizadas por **viabilidad de datos** (no por moda)
| Prio | Implementación | Punto de integración | Datos (reales hoy) | Enfoque | Beneficio |
|---|---|---|---|---|---|
| **A1** | **Auto-categorización de productos** (taxonomía Nexpart + categoría ML) | `inventory_bp.py:129` (hoy no persiste) y `marketplace_external_service.py` (publish); ML ya tiene `domain_discovery` en `meli_service.py:280` | 171 K SKUs, 18 K sin categoría; taxonomía de 558 etiquetas | Reglas → embeddings (Meili) → LLM (`classify_part`) como fallback | Habilita navegación/filtros y **publicación en lote sin elegir categoría a mano** |
| **A2** | **Deduplicación / matching de SKUs** (poblar `catalog_part_id` + aliases) | `catalog_import_service.py:57-76` | 406 K interchange + 9 K + 4 K aliases | Normalización + blocking por prefijo + similitud de embeddings | Desbloquea precios de proveedor, margen y enriquecimiento (hoy link 0%) |
| **A3** | **OCR/VLM de facturas de proveedor** | `supplier_engine.receive_po:214` / compra directa `inventory_bp.py:1198` | Facturas PDF/foto (infra ya instalada) | `pdftotext`+Tesseract y VLM (`chat_with_image`) + validación contra catálogo | Elimina captura manual y **alimenta `supplier_catalog_prices`** (hoy vacío) |
| **A4** | **Visión para graduar piezas usadas (Yonke)** | `yonke_bp.py:145` (subida de fotos) | Fotos de piezas (hoy 0) | VLM que sugiera `condition_grade` (A/B/C/D) y defectos; validar encuadre antes de publicar | Alta consistente <30 s, menos devoluciones, confianza del comprador |
| **A5** | **Anomalías de negocio** (descuentos, merma de stock, precios mal capturados) | post-commit de venta `pos_engine.py:462` + job nocturno sobre `sale_items`/`inventory_operations`/`audit_log` | Ventas/ops por tenant | EWMA/z-score robusto o Isolation Forest por empleado/SKU/sucursal | Detecta fuga de margen que los topes no capturan (apartados/cotizaciones los evaden) |
| **A6** | **Semantic search** (embeddings + sinónimos) | `meili_search.py:23`, `index_meilisearch.py:47` | Nombres/SKUs/cross-refs | Vectores en Meili (híbrido) + sinónimos + indexar interchange | "balatas"→"Brake Pad"; búsqueda por número cruzado |
| **A7** | **Clasificación/priorización de preguntas de MeLi** (+ auto-respuesta por plantilla) | `service.py:1151` (`sync_questions` → `_upsert_question`) | 18 preguntas (4 sin responder desde mayo) | Clasificador de intención (compat/precio/envío) + plantillas + `vehicle_compatibility` | SLA (ML penaliza respuestas lentas) y más conversión |
| **A8** | **Enriquecimiento de imágenes** (scraping + tagging por nombre de parte) | `image_scraper.bulk_import:71`, `image_service.save_image:74` | `supplier_catalog` 0 imágenes | Scraping existente + VLM para validar/clasificar | Desbloquea catálogo público, SEO y publicación ML |
| **A9** | **Score de riesgo de crédito (fiado)** | validación en `pos_engine.py:314`, `get_customer` `customers_bp.py:133` | Clientes + aging (pocas ventas aún) | Regresión logística/heurística ponderada (mora>30d) | Menos incobrables; subir límites a buenos clientes |
| **A10** | **Venta cruzada "esto + esto"** | hook muerto `part_kits.build_kit_text`; `pos.js:403` al agregar al carrito | `sale_items` + `KIT_SUGGESTIONS` | Reglas de asociación/co-ocurrencia, fallback a kits | +5-15% ticket típico |
| **A11** | **Forecast de demanda / punto de reorden** | `reorder_engine.py:26,78` | **Cold start**: máx 24 ventas; única serie = Atlas `historical_sales` (4,582 / 19 meses) | Media móvil/Exponencial (ROP = μ·LT + z·σ); ML real cuando haya volumen | ⚠️ Hoy **bloqueado por datos**; arrancar con reglas y migrar |
| **A12** | **Churn / próxima compra** | `crm_engine.py:289` (hoy umbrales fijos) | 52% de compradores de Atlas recompran | BG/NBD o regresión de intervalo intercompra | Campañas de reactivación dirigidas |
### 4.3 Lo que NO vale la pena (honestidad)
- **Triage de logs con LLM**: el volumen de errores es de KB/día; deduplicar por firma con reglas gana y cuesta cero.
- **Agrupación de alertas con ML**: hoy hay **un** receiver y **cero** alertas útiles; arreglar el routing da el 100% del valor.
- **Predicción de capacidad con IA**: `predict_linear(disk[6h], 7d)` en PromQL ya responde; es una alerta, no un modelo.
- **Forecast de demanda hoy**: sin historial transaccional, cualquier modelo sería ruido.
---
## 5. Roadmap propuesto
| Fase | Foco | Entregable verificable |
|---|---|---|
| **F0 — Blindar (2-3 días)** | Seguridad (pg_hba/firewall/secretos), Redis arriba, alerting, validar backup timer + offsite, 500 de query params, borrar jobs muertos | Celery vivo; alerta de backup llega a un humano; `5432` cerrado |
| **F1 — Caja correcta (1 sem)** | Bugs P0 de `pos.js`, reembolsos→caja, reversa de apartado, forzar topes de descuento también en cotización/apartado, cablear/eliminar `generate_cfdi` | Corte de caja cuadra con devoluciones; ticket offline funciona |
| **F2 — IA de datos (2-3 sem)** | A1 auto-categorización, A2 dedup SKUs, A3 OCR facturas, A8 imágenes | ≥90% SKUs categorizados; factura de proveedor → líneas en <1 min; catálogo con imágenes |
| **F3 — IA en el flujo (2-3 sem)** | A4 visión Yonke, A6 semantic search, A5 anomalías, A7 preguntas, A10 venta cruzada | Grado sugerido por foto; búsqueda por sinónimo; alerta de descuento anómalo; sugeridos en carrito |
| **F4 — Escala y producto (3-4 sem)** | PgBouncer + pool, `/metrics` de app + `pg_stat_statements`, A9 crédito, A12 churn, A11 forecast cuando haya datos, WA CRM piloto | 30+ tenants sin agotar conexiones; dashboard con métricas reales; score de crédito en fiado |
---
## 6. Métricas de éxito
- **Seguridad/ops:** 0 servicios DB con `trust`; 0 secretos en unit files legibles; alertas de backup/Celery entregadas; 30 días de backups verificados (local + offsite).
- **Producto:** corte de caja cuadra con devoluciones; 0 `ReferenceError` en POS; 100% de SKUs categorizados; catálogo público con >80% de items con imagen.
- **IA:** ≥90% de facturas de proveedor parseadas sin captura manual; grado de pieza usada sugerido y aceptado en ≥70% de altas; búsquedas sin resultado reducidas >30% (semantic); ≥1 alerta real de anomalía accionada por mes.

View File

@@ -0,0 +1,199 @@
# 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_secret` regenerado 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 del `image_url` actual.
- `warranties`: `warranty_days` + política por defecto de usados en `tenant_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)**
- `condition` diná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|usadas` en 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_payload` existe 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:83` no lee `currency`).
**Contabilidad:**
- Pólizas para abonos de clientes, cortes de caja, devoluciones y apartados (4 funciones de `accounting_engine` existen y **nunca se llaman**).
- `record_return_entry` (no existe).
- Dejar de perder asientos en silencio: los hooks `try/except: pass` deben 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
1. **Arreglar el catálogo público** (visitante nuevo ve 500): `/api/catalog/search`, `/api/search`, `/api/categories`, modo OEM y detalle de parte. Modo `local` por defecto + degradación elegante (el POS ya tiene el guard `_oem_blocked`, el dashboard no).
2. **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).
3. **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).
4. **Imágenes masivas** (bloquea catálogo público y MeLi) — mismo pipeline de B1.
5. **Import/export round-trip** con compat, imágenes y min/max/reorder.
6. **Import con vehículo roto**: `bulk-import` devuelve 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".
7. **Multi-sucursal real**: todo el stock vive en la sucursal 1 y `inventory.branch_id` es NULL; transferencias/conteos/reorden implementados y **sin uso** (reorden además filtra por la columna equivocada y `reorder_point` es NULL al 100%).
8. **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.