Appearance
Destino — imcrmdev (MongoDB)
Conexión
La migración escribe a una nueva instancia Mongo aislada que se levanta junto con el script:
| Item | Valor |
|---|---|
| Container | crm-migration-mongo-1 |
| Puerto host | 27019 (27017 reservado, 27018 = dev SEMTEC) |
| DB | imcrmdev |
| User / Pass | admin / migration_pwd_change_me |
| URI | mongodb://admin:migration_pwd_change_me@localhost:27019/imcrmdev?authSource=admin&directConnection=true |
Esta instancia es vacía y dedicada al cliente Trebol. Cuando la migración se valide, se cambia el endpoint del backend para apuntar a este Mongo, o se hace un
mongodump | mongorestoreal Mongo de producción de Trebol.
Colecciones del modelo existente que se llenan
| Collection | Origen GHL | Doc count esperado |
|---|---|---|
companies | GET /locations/{id} | 1 |
users | GET /users/?locationId=... | ~6 |
contacts_new | POST /contacts/search + GET /contacts/{id} | 44,918 |
opportunities | GET /opportunities/search | 51,495 |
tasks | GET /contacts/{id}/tasks | variable |
roles, permissions, categories | seed (copia del dev) | bootstrap |
branches, industries, service_sources, service_categories | seed mínimo | bootstrap |
meetingsno se llena en esta corrida — el módulo de citas/calendarios está pausado en TODO.
Colecciones nuevas que se crean para esta migración
Estas no existen en el imcrmdev actual y se agregan exclusivamente para no perder data del CRM origen:
| Collection nueva | Origen | Notas |
|---|---|---|
custom_values | GET /locations/{id}/customValues | Constantes a nivel tenant |
tags | GET /locations/{id}/tags | Catálogo de tags |
sales_pipelines | GET /opportunities/pipelines | Pipelines de ventas (NO tickets) |
sales_pipeline_stages | derivado del anterior | Stages explosionados |
conversations | GET /conversations/search | Conversaciones |
messages | GET /conversations/{id}/messages | Mensajes (alta cardinalidad) |
forms | GET /forms/?locationId=... | Definiciones de formularios |
form_submissions | GET /forms/submissions | Respuestas |
Pausadas (ver TODO):
custom_field_definitions,calendars, y la persistencia de citas enmeetings.
Convenciones para todos los docs migrados
js
{
"_id": ObjectId(), // generado por Mongo
"external_id": "kkzYOo4eCGQOvJYNQYXo", // GHL id (siempre presente)
"external_source": "ghl",
"company_id": ObjectId(...), // tenant id (Trebol)
// ... campos mapeados ...
"_ghl_raw": { /* JSON original */ }, // preservación total
"_migrated_at": ISODate(...)
}Índices garantizados por colección
js
db.contacts_new.createIndex({ company_id: 1, external_id: 1 }, { unique: true })
db.opportunities.createIndex({ company_id: 1, external_id: 1 }, { unique: true })
db.conversations.createIndex({ company_id: 1, external_id: 1 }, { unique: true })
db.messages.createIndex({ company_id: 1, external_id: 1 }, { unique: true })
db.messages.createIndex({ conversation_id: 1, dateAdded: 1 })
db.tasks.createIndex({ company_id: 1, external_id: 1 }, { unique: true })El
(company_id, external_id)único es lo que hace la migración idempotente.
Constraint crítico: índice único parcial sobre email
contacts_new tiene este índice del modelo existente:
js
{ email: 1, ... unique: true, partialFilterExpression: { email: { $exists: true, $gt: "" } } }Implicaciones:
- Si dos contactos en GHL tienen el mismo email, el segundo upsert falla con
E11000. - Si el email viene vacío, el índice no aplica → no hay conflicto.
Estrategia del migrador:
- Si
emailesnull,"", o no existe → omitir el campo (no setear). - Si hay duplicado: el primer contacto migrado se queda con el email; los siguientes se loguean en
logs/dlq-contacts-duplicate-email.jsonly se insertan con el email omitido (placeholder en_ghl_raw.duplicate_email). El usuario decide después qué hacer (merge manual, o aceptar pérdida de uniqueness).