--- 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:18–11:19). ## DIAGNÓSTICO 1. Frescura de cache: `sync_logs` → todas sincronizaron 11:18–11: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 --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 ` 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