--- id: CASE-2026-05-29-origen-sucursal-contactos-usuario fecha: 2026-05-29 categoria: custom_field location_ids: ["nF1uEaYB3mCK5em9bPn2 (85974 - MP - Eugenia, piloto)", "todas las sucursales productivas (47, batch); excluye Marca GbKkBpCmKu2QmloKFHy3 y demos Vf7qQl3L9vakJ8hDtQ8e / Z64WQKORPVwXb5mn68Ef"] run_ids: ["fbuo-cc20241b7a6f (piloto Eugenia)", "fbuo-batch-8c31110b2d (batch 47 sucursales)"] snapshots: [] status: resuelto memorias: ["createdby_only_in_individual_get", "super_script_fix_branch_user_origin", "feedback_dry_run_protocol", "name_account_with_location_id"] playbooks: [] --- ## TRIGGERS - `createdBy.source`, `WEB_USER`, `MOBILE_USER`, `INTEGRATION` - `contactos creados por usuario`, `canal de origen sucursal`, `origen sucursal` - `createdBy no viene en el listado`, `GET /contacts/ omite createdBy` - `fix_web_user_branch_contacts roto`, `siempre detecta 0` - `fix_branch_user_origin.py`, `super script origen sucursal` - `Fuente de Prospecto ALIANZA`, `PROSPECCIÓN`, `no sobrescribir Fuente de Prospecto` - `tag formulario -> sucursal`, `etiqueta de origen única` - `Canal de Origen de la Oportunidad = Sucursal` ## SÍNTOMA Los contactos creados a mano en una sucursal (por un empleado) no traen canal de origen confiable: el campo nativo `source` llega vacío/None y no indica "sucursal". Esto ensucia el CF `Canal de Origen` (contacto y opp) y las etiquetas de origen. Objetivo: identificar los contactos creados 100% por usuario en sucursal y dejarlos con origen = Sucursal (CF + tag), propagando a sus oportunidades. Solo sucursales (no Marca, no demos). ## DIAGNÓSTICO Pasos read-only (todos con el helper `tag_canal_origen_workflow`): 1. Primer dry-run del super script leyendo `createdBy` del **listado** → `WEB_USER a corregir: 0`. Distribución: `(vacío): 129`. Sospecha: el listado no trae `createdBy`. 2. Comparación listado vs GET individual de un contacto: - Listado `GET /contacts/`: keys incluyen `source` (=None), `attributions`, pero **NO** `createdBy`. - Individual `GET /contacts/{id}`: trae `createdBy = {source: 'WEB_USER', sourceName: 'EUGENIA- 85974 MP', channel: 'APP', ...}` y `attributionSource = {medium: 'manual', sessionSource: 'CRM UI'}`. 3. Muestra de 20 GETs individuales en Eugenia: 3 WEB_USER + 17 INTEGRATION → el criterio discrimina perfecto. Proxy del listado: WEB_USER ≈ `attributions[0].medium == 'manual'`. 4. Audit log oficial del CRM para `maMw3C8QmhGVChRqL36y` (JUAN CARLOS RAMIREZ): "Action: Created, Modified by: Web user" → coincide con `createdBy.source == WEB_USER`. 5. Schema de contacto Eugenia (resolución de campos por alias, correcta): - `Canal de Origen` → `KLEZyRNR0jrldccerErV` (name real "CANAL DE ORIGEN") - `Fuente de Prospecto` → `QN1BNTKgCzcSOHa2wSZc` - `Sucursal` → `pmrGTW3tIa7oz7rQJMVx`, `TIENDA` → `H3g8J4NbgbcM4glyW9GZ` 6. Distribución de valores en Eugenia (SQLite): `Canal de Origen` {SUCURSAL 84, FORMULARIO 28, FACEBOOK 14, vacío 3}; `Fuente de Prospecto` {SUCURSAL 84, LEAD DIGITAL 42, **ALIANZA 2**, **PROSPECCIÓN 1**}. → `Fuente de Prospecto` contiene valores de negocio que NO deben pisarse. ## CAUSA RAÍZ 1. **`createdBy` solo viene en el GET individual** del contacto; el listado paginado lo omite (ver [[createdby_only_in_individual_get]]). El script previo `scripts/fix_web_user_branch_contacts.py` lo leía del listado → roto silenciosamente (siempre 0). 2. Los contactos creados por empleado quedan con `createdBy.source` ∈ {`WEB_USER` (UI web), `MOBILE_USER` (app móvil)}; los replicados desde Marca por n8n quedan `INTEGRATION`. ## ACCIÓN Super script nuevo `scripts/fix_branch_user_origin.py` (registrado en SCRIPTS_METADATA como "Origen Sucursal (contactos creados por usuario)"). Ver [[super_script_fix_branch_user_origin]]. Orden contacto→opp: - Contacto: tag único `sucursal` (quita `formulario`/`facebook-ads`), `Canal de Origen` = SUCURSAL. Si falta `Sucursal`/`TIENDA`, se completan desde el Verificador CSV (`load_verifier_map`). - TODAS las opps del contacto: `Canal de Origen de la Oportunidad` = Sucursal + propaga `Sucursal`/`TIENDA`. - **NO toca `Fuente de Prospecto`** (decisión del owner: preserva ALIANZA/PROSPECCIÓN). No sincroniza a Marca. Protocolo dry-run → piloto → batch ([[feedback_dry_run_protocol]]): ``` # Dry-run (Fase 1): python scripts/fix_branch_user_origin.py --location nF1uEaYB3mCK5em9bPn2 # Piloto: python scripts/fix_branch_user_origin.py --location nF1uEaYB3mCK5em9bPn2 --apply --run-id fbuo-cc20241b7a6f # Batch: python scripts/fix_branch_user_origin.py --all --apply --run-id fbuo-batch-8c31110b2d ``` ## VERIFICACIÓN - Piloto Eugenia (run `fbuo-cc20241b7a6f`, success): 89 creados por usuario, 6 contactos, 98 opps. JUAN CARLOS post-apply en vivo: tags=['sucursal'], Canal de Origen='SUCURSAL', Sucursal='Narvarte Oriente, Ciudad de México', TIENDA='EUGENIA', **Fuente de Prospecto='ALIANZA' intacto**; su opp Tpd964ztTwgNf1ipL5NC con Canal de Origen de la Oportunidad='Sucursal' + Sucursal propagado. - Consistencia Sucursal: 126/129 contactos ya tenían 'Narvarte Oriente, Ciudad de México'; los 3 vacíos quedaron con el MISMO valor del Verificador. Sin divergencia. - Batch (run `fbuo-batch-8c31110b2d`, success): 359 creados por usuario detectados en 47 sucursales, 48 contactos + 273 opps corregidos, 0 errores. Auditoría: 110 cambios contact + 284 opp, todos `applied`. ## EDGE-CASES / TRAMPAS - **No leer `createdBy` del listado** → siempre 0. Hay que GET individual por contacto (costoso pero fiel; el dashboard paraleliza por sucursal). - **No sobrescribir `Fuente de Prospecto`**: contiene ALIANZA/PROSPECCIÓN (valores de negocio), no solo SUCURSAL/LEAD DIGITAL. - Incluir **MOBILE_USER** además de WEB_USER (ambos = creación manual por empleado). - 8 sucursales tenían **0 contactos** (no tocadas) y 5 tienen **Verificador con Sucursal vacía** + 2 **no están en el Verificador**: si más adelante reciben contactos creados por usuario sin Sucursal, no se autocompletará hasta corregir el Verificador. ## REUTILIZABLE ```python # createdBy SOLO en GET individual: full = ghl_request("GET", f"/contacts/{cid}", token); inner = full.get("contact") or full src = (inner.get("createdBy") or {}).get("source") # WEB_USER | MOBILE_USER | INTEGRATION ``` ## PENDIENTES - Corregir el Verificador para las sucursales con Sucursal vacía / ausentes (Isidro Fabela, SENDERO, Grand Plaza, Independencia, Morelia 3, + las 2 ausentes) por si reciben contactos creados por usuario. - Confirmar si las 8 sucursales con 0 contactos es esperado (sucursales nuevas) o falta sync/acceso. - Identificación de origen Facebook Ads / formulario en sucursal (fuera de alcance de este caso). ## ENLACES - Memorias: [[createdby_only_in_individual_get]], [[super_script_fix_branch_user_origin]], [[feedback_dry_run_protocol]], [[name_account_with_location_id]] - Scripts: `scripts/fix_branch_user_origin.py`, helpers de `scripts/tag_canal_origen_workflow.py`, `scripts/fill_sucursal_tienda_from_location.py` (`load_verifier_map`) - Roto/superado: `scripts/fix_web_user_branch_contacts.py` - Logs: `generated/logs/fbuo_batch_8c31110b2d.log`