Files
MP-Manager/docs/casos/2026-05-30-comparativa-auditoria-completa-buckets.md
T
2026-05-30 20:16:12 -06:00

107 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: CASE-2026-05-30-comparativa-auditoria-completa-buckets
fecha: 2026-05-30
categoria: descuadre | cascada_n8n | config_location
location_ids: ["GbKkBpCmKu2QmloKFHy3 (Monte Providencia / Marca)", "jE41bVhhnb5T505BFm4F (85964 - MP - Morelia 1)", "uZnMH5bO6MXTHcgHeyq9 (85935 - MP - Pilares, hub)", "NSDniGzjxotVDNa5YxqW (85937 - MP - METEPEC, shell)"]
run_ids: ["45554292-1b2d-491c-8107-b0ebf81c0b86 (cleanup Patricia)", "fill-temixco-20260530", "bf-cristhian-20260530", "bf-hugo-20260530", "isai-tienda-20260530", "isai-opp-20260530", "fix-identity-collisions-20260530 (sarahi name + miguel TIENDA)", "fix-sarahi-tienda-20260530", "cleanup link 9i1rDQa (isai dup rpDH)", "create-miguel-brand-opp-20260530"]
snapshots: ["generated/migrations/cleanup_brand_duplicate_replica_opps_20260530_122326.json", "generated/migrations/fix_identity_collisions_20260530_130329.json", "generated/migrations/create_miguel_brand_opp_20260530_132718.json"]
status: resuelto
memorias: ["[[positive_opp_descuadre_double_replica]]", "[[verificador_tipo_de_tienda_colapso]]", "[[positive_descuadre_stale_cache]]", "[[audit_hub_map_metepec_pilares]]"]
playbooks: ["docs/PLAYBOOK_DESCUADRE.md"]
---
## TRIGGERS
- `Comparativa Marca vs Sucursales sigo viendo discrepancia`
- `auditar por completo los buckets`, `optimizar toda la sección Comparativa`
- `opps +1 descuadre positivo`, `opportunities_in_brand_duplicate_link 2`
- `contacts_in_brand_present_in_other_branch_not_assigned 84 falsos positivos`
- `TIENDA=METEPEC vive en Pilares 85935`, `Metepec 85937 vacío hub digital`
- `audit lee Verificador CSV no Baserow`, `falta fila METEPEC→85935 digital`
- `DIGITAL_HUB_BY_SHELL`, `hub-map en código audit_brand_vs_branches_totals`
- `PATRICIA PARRA NAVARRO zzBzWC4adBrdTA8WhQph`, `réplica abandoned $0 updatedAt==createdAt`
## SÍNTOMA
Comparativa del dashboard: contactos cuadraban (diff 0) pero **opps +1** (Marca 1341 vs sucursales 1340). El usuario reportó discrepancia persistente y pidió auditar TODOS los buckets + optimizar la sección. Cache fresco (sync 2026-05-30 11:1811:19).
## DIAGNÓSTICO
1. Frescura de cache: `sync_logs` → todas sincronizaron 11:1811:19. No es stale.
2. Audit completo a JSON (read-only):
```
python scripts/audit_brand_vs_branches_totals.py --json > generated/agent/runs/descuadre_audit_20260530.json
# OJO: el redirect en Windows escribe cp1252, no utf-8. Leer con .decode('cp1252').
```
3. Buckets con items: `opportunities_in_brand_duplicate_link=2`, `present_in_other_branch_not_assigned=84`, `contacts_missing_id_field=4`, `without_tienda=2`, `in_branch_not_in_brand=1`, `not_in_any_branch=1`, `opportunities_missing_id_field=1`. Resto 0.
4. **El +1 de opps** = bucket `opportunities_in_brand_duplicate_link`: 2 opps de Marca con el MISMO link `zzBzWC4adBrdTA8WhQph` (Morelia 1). `extra_opps=1`.
5. **El bucket 84** descompuesto por (expected→actual): **82/84 = TIENDA=METEPEC esperado 85937, actual Pilares 85935.** Verificador CSV mapea METEPEC→85937 (vacío, físico), pero los leads digitales viven en el hub Pilares. Las otras tiendas del cluster (GRAND PLAZA/ISIDRO/SENDERO) SÍ tienen fila digital →85935; METEPEC no. → falsos positivos.
6. Los contactos cuadraban a 0 **enmascarando** 2 problemas que se cancelan: `in_branch_not_in_brand=1` (sarahi sarabia, real) + `not_in_any_branch=1` (test21 harness, prueba).
## CAUSA RAÍZ
1. **+1 opps**: réplica duplicada en Marca (n8n `Cfgwp0bOtDW8zuKW` hizo CREATE en vez de UPDATE por carrera de indexado). La sobrante: `sAxBY01AQNwSr0OExQof` *abandoned* $0 created==updated 2026-05-30 03:02, colgada de contacto distinto (edgar morales). Mismo patrón que [[positive_opp_descuadre_double_replica]].
2. **84 ruido**: el audit lee el Verificador CSV, que NO codifica la consolidación de hub (Toluca/Metepec/Lerma→Pilares 85935) que Baserow 750 sí tiene ([[verificador_tipo_de_tienda_colapso]]). Falta la fila digital METEPEC→85935.
## ACCIÓN
**A) Optimización del audit (código, sin tocar Bucéfalo)** — autorizada "hub-map en código":
- En `scripts/audit_brand_vs_branches_totals.py`: constante `DIGITAL_HUB_BY_SHELL` (shell loc → hub Pilares `uZnMH5bO6MXTHcgHeyq9`) + check en el bucket `present_in_other_branch`: si la sucursal asignada es shell de un hub y el contacto está en el hub, se considera bien asignado.
- Resultado: bucket 84 → **2** (los 2 reales: luis fernando tienda=PILARES quirk; miguel angel EUGENIA→Temixco).
**B) Corrección +1 opps** — autorizada "borrar opp duplicada":
```
# dry-run (verifica en vivo, snapshot):
python scripts/cleanup_brand_duplicate_replica_opps.py --only-link zzBzWC4adBrdTA8WhQph
# apply:
python scripts/cleanup_brand_duplicate_replica_opps.py --apply --only-link zzBzWC4adBrdTA8WhQph
# run_id=45554292-1b2d-491c-8107-b0ebf81c0b86 borró sAxBY01AQNwSr0OExQof (conservó OGQtfmjFk31M5eXKDBpO open $70k)
# re-sync Marca para refrescar cache:
python -c "import sync_engine as se; a=next(x for x in se.parse_accounts_csv() if x['location_id']=='GbKkBpCmKu2QmloKFHy3'); se.sync_account(a['location_id'], a['token'])"
```
**C) Pendientes (2ª tanda, misma sesión)** — todos aplicados con dry-run+snapshot+audit:
- `fill_contact_id_sucursal.py --location Temixco --apply`: cristhian/hugo lado sucursal.
- `backfill_brand_contact_id_sucursal.py --only-contact <U9DWipe|bUNqMZaL> --apply`: cristhian/hugo lado Marca.
- `fix_brand_tienda_from_sucursal.py --only-contact JV9g9tWO --apply`: isai TIENDA→ECATEPEC (reporta "Errores:1" cosmético al persistir local; el PUT a GHL sí aplica, verificado en vivo).
- `backfill_opp_sucursal_link.py --only-opp 0eYLJ6 --apply`: enlazó la opp huérfana de isai → **destapó** que isai tenía 2 réplicas en Marca (rpDH+0eYLJ) de 1 opp en Ecatepec.
- `cleanup_brand_duplicate_replica_opps.py --only-link 9i1rDQa --apply`: borró rpDH (réplica nueva), conservó 0eYLJ (original, más antigua). → destapó el faltante 1.
- **sarahi** (colisión de teléfono con "luis enrique suchil"): el contacto Marca m6QK tenía email+opp de sarahi pero nombre de luis → UPDATE firstName/lastName→"Sarahí Sarabia" (NO sync, evita 3er duplicado) + TIENDA ATLACOMULCO→ATIZAPAN. jBaK (luis real) intacto.
- **miguel** (TIENDA EUGENIA→TEMIXCO) + el faltante 1 era su empeño físico Temixco OQBrOQN9 ($56,671) sin réplica. NO es multi-empeño: son 2 opps DISTINTAS (empeño físico ene-08 $56,671 vs lead digital Marca abr-24 $80,200 sin link, CF vacíos). Se **creó** réplica en Marca (8HITkGkOn3gN23Tl8LBr, con link a OQBrOQN9) SIN tocar la digital — script ad-hoc reusando `resolve_brand_pipeline_and_stage`+`create_opportunity`; el link se setea con PUT separado (el POST no guarda customFields). Gotcha: tras el PUT el GET inmediato da link=None (latencia indexado GHL); re-sync confirma válido.
- **luis fernando** (TIENDA=PILARES, vive en Pilares): se agregó 85940 Isidro Fabela (0 contactos) a `DIGITAL_HUB_BY_SHELL` → resuelto.
## VERIFICACIÓN
| | Inicial | Tras 1ª tanda | Final |
|---|---|---|---|
| Contactos diff | 0 | 0 | **0** |
| Opps diff | **+1** | 0 | **0** (1340=1340) |
| `opportunities_in_brand_duplicate_link` | 2 | 0 | 0 |
| `present_in_other_branch_not_assigned` | 84 | 2 | **0** |
| `contacts_missing_id_field` | 4 | 4 | **0** |
| `opportunities_missing_id_field` | 1 | 1 | **0** |
| `contacts_in_branch_not_in_brand` | 1 | 1 | **0** |
| Items accionables totales | ~95 | — | **2 (solo test21, fantasma)** |
> Lección clave: el "diff 0" inicial de opps era engañoso — enmascaraba el duplicado de isai (+1) contra el faltante de miguel (1). El faltante NO aparecía en `opportunities_in_branch_not_in_brand` (=0) porque el bucket matchea por contacto y miguel ya tenía una opp en Marca (gap multi-opp). Se cazó con un diff 1:1 de links sucursal↔Marca (parsear `fieldValueString`, no `value`).
## EDGE-CASES / TRAMPAS
- El redirect `> file.json` en Windows NO escribe utf-8; el JSON sale cp1252. Decodificar con `cp1252`.
- `sync_account(location_id, token)` — son DOS args posicionales, no el dict de cuenta.
- El audit lee SQLite (cache). Tras un DELETE en GHL hay que **re-sync de la location** antes de re-auditar o el número no cambia.
- El hub-map mapea varios shells→Pilares pero solo METEPEC generaba falsos positivos (los demás ya resolvían al hub vía CSV o tienen 0 contactos). Mapearlos todos es inocuo y a prueba de cambios de orden en el CSV.
## REUTILIZABLE
- Descomponer cualquier bucket grande por (expected_branch → actual_branch) con Counter antes de concluir "error de datos": muchas veces es mapeo del Verificador, no datos.
- `cleanup_brand_duplicate_replica_opps.py --only-link <link>` es el camino seguro para el descuadre positivo de opps por doble réplica (verifica en vivo + snapshot + script_audit).
## ACCIÓN (3ª tanda — reasignación opp por homónimos)
Al ir a "crear la opp de miguel en Temixco" se descubrió que **NO procedía**: el lead digital $80,200 es de OTRO "Miguel Angel" (Eugenia `+525530454950`, contacto Marca `hE9U9Q`), distinto del Miguel-Temixco (`+527775114949`, contacto `RwxMQr0`). Ese lead ya estaba replicado (Eugenia `kGda02`↔Marca `1A3P5b`), pero `1A3P5b` colgaba del contacto EQUIVOCADO (`RwxMQr0`) porque la réplica n8n matcheó por nombre. `hE9U9Q` (su dueño real) tenía 0 opps.
- **Fix:** `PUT /opportunities/1A3P5b` con `contactId=hE9U9Q` (+ name/pipeline/stage/mv). **GHL v2 SÍ acepta cambiar contactId vía PUT** (status 200) — el gotcha de `build_brand_opp_payload` no aplicó. run_id `reassign-miguel-opp-20260530`, snapshot `reassign_miguel_opp_20260530_155526.json`. Verificado: RwxMQr0 solo $56,671, hE9U9Q solo $80,200.
## PENDIENTES
- **test21** (fantasma de índice): GET 400 / search 200. No accionable; se auto-corrige cuando GHL reconcilie el índice.
- **Causa raíz n8n homónimos**: la réplica Sucursal→Marca debe matchear por teléfono/`id_contacto_sucursal` antes que por nombre (si no, vuelve a colgar opps de homónimos en el contacto equivocado). Ver [[matching_rules]], [[n8n_workflows_v2_hardened]].
- ~~Leads digitales nuevos~~ **RESUELTO**: `David Arturo Vega` (Satélite) y `PER,LA MARIA VILLA` (La Viga) aparecieron ~15:56 (actividad viva) como descuadre +2. NO eran leads sin cascar: sus contactos+opps YA estaban en sucursal; el +2 eran **réplicas duplicadas en Marca sin link** (mismo patrón isai/Patricia, pero sin link compartido → caían en `missing_id_field`, no `duplicate_link`). Fix: (1) TIENDA llenada (`leads-tienda-20260530`); (2) borradas las 2 duplicadas sin link `LJPKIsd`+`OyRtrlGb`, conservadas las enlazadas `Mt9Dafz3`+`aZf3pzuc` (run_id `delete-dup-leads-opps-20260530`, snapshot `delete_dup_leads_opps_20260530_160744.json`). → opps 1342=1342, diff 0.
- Patrón a vigilar: el n8n de opps sigue generando réplicas duplicadas en Marca (Patricia, isai, PER,LA, David — 4 en un día). Unas con link compartido, otras sin link. La causa raíz (idempotencia) está en [[n8n_opp_idempotency_baserow_mapping]]; revisar por qué siguen apareciendo.
## ENLACES
- Memorias: [[positive_opp_descuadre_double_replica]], [[verificador_tipo_de_tienda_colapso]], [[positive_descuadre_stale_cache]], [[name_account_with_location_id]], [[audit_hub_map_metepec_pilares]]
- Playbook: docs/PLAYBOOK_DESCUADRE.md
- Scripts: scripts/audit_brand_vs_branches_totals.py, scripts/cleanup_brand_duplicate_replica_opps.py
- Caso previo relacionado (réplica duplicada, quedó parcial): docs/casos/2026-05-30-descuadre-opp-replica-duplicada-marca.md