---
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`
