Skip to content

Destino — imcrmdev (MongoDB)

Conexión

La migración escribe a una nueva instancia Mongo aislada que se levanta junto con el script:

ItemValor
Containercrm-migration-mongo-1
Puerto host27019 (27017 reservado, 27018 = dev SEMTEC)
DBimcrmdev
User / Passadmin / migration_pwd_change_me
URImongodb://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 | mongorestore al Mongo de producción de Trebol.

Colecciones del modelo existente que se llenan

CollectionOrigen GHLDoc count esperado
companiesGET /locations/{id}1
usersGET /users/?locationId=...~6
contacts_newPOST /contacts/search + GET /contacts/{id}44,918
opportunitiesGET /opportunities/search51,495
tasksGET /contacts/{id}/tasksvariable
roles, permissions, categoriesseed (copia del dev)bootstrap
branches, industries, service_sources, service_categoriesseed mínimobootstrap

meetings no 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 nuevaOrigenNotas
custom_valuesGET /locations/{id}/customValuesConstantes a nivel tenant
tagsGET /locations/{id}/tagsCatálogo de tags
sales_pipelinesGET /opportunities/pipelinesPipelines de ventas (NO tickets)
sales_pipeline_stagesderivado del anteriorStages explosionados
conversationsGET /conversations/searchConversaciones
messagesGET /conversations/{id}/messagesMensajes (alta cardinalidad)
formsGET /forms/?locationId=...Definiciones de formularios
form_submissionsGET /forms/submissionsRespuestas

Pausadas (ver TODO): custom_field_definitions, calendars, y la persistencia de citas en meetings.

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:

  1. Si email es null, "", o no existe → omitir el campo (no setear).
  2. Si hay duplicado: el primer contacto migrado se queda con el email; los siguientes se loguean en logs/dlq-contacts-duplicate-email.jsonl y 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).