Appearance
Mapping campo a campo
Convención del CRM destino
El CRM imcrmdev modela "company" (B2B) como un contacto con service_type=corporate. Por eso GHL Businesses y GHL Contacts ambos terminan en la colección contacts_new, diferenciados por service_type:
service_type=residential— personas (las 44,918 contactos de GHL)service_type=corporate— empresas (los 6 GHL Businesses, ver sección Business)
El vínculo persona→empresa se preserva en custom_fields.parent_business_external_id.
Contact (persona, service_type=residential)
GHL GET /contacts/{id} → imcrmdev contacts_new
| Campo origen | Campo destino | Transformación / nota |
|---|---|---|
id | external_id | guardado tal cual |
| — | _id | generado por Mongo |
| — | company_id | tenant Trebol (bootstrap) |
| — | service_type | siempre "residential" |
assignedTo | assignedAgentId | si existe en users.external_id, se reemplaza por su publicid; si no, se guarda crudo |
assignedTo | ownerAgentId | mismo valor — el responsable actual del lead es el dueño de la cartera en imcrmdev |
assignedTo | created_by | si nulo → fallback al system_user_id de bootstrap |
firstName | first_name | |
lastName | last_name | |
firstName + lastName | name | concatenación; si vacío → phone o email |
email | email | omitir si vacío, omitir y loguear si duplicado |
email | email_opted_in | false si dndSettings.Email.status == "active" (active = bloqueado) |
phone | phone | E.164 tal cual |
country | address.country | |
state | address.state | |
city | address.city | |
address1 | address.address | |
postalCode | address.postcode | |
dateOfBirth | custom_fields.date_of_birth | (no hay campo top-level en destino) |
companyName | custom_fields.current_insurance | En GHL Trebol este campo guarda la aseguradora actual del prospecto ("Geico", "Progressive"…), NO su empleador |
businessId | custom_fields.parent_business_external_id | Si está set, además se denormaliza el name del business migrado en custom_fields.parent_business_name |
tags[] | custom_fields.tags | array tal cual |
customFields[] | custom_fields.{fieldKey} | resolver id → fieldKey usando el catálogo cargado en memoria al inicio de la pasada |
dndSettings | custom_fields.dnd_settings | objeto completo |
additionalEmails[] | custom_fields.additional_emails | |
additionalPhones[] | custom_fields.additional_phones | |
dateAdded | created_at | parse ISO |
dateUpdated | updated_at | parse ISO |
| (todo el JSON) | _ghl_raw | preservación |
| (timestamp ahora) | _migrated_at |
Campos del destino que quedan vacíos
billing_account_id, account_id, network_information, linked_status, network_status, contract_info, onus, website (si no viene), whatsapp_opted_in.
Business (B2B Company → contact corporate)
GHL GET /businesses/?locationId={id} → imcrmdev contacts_new (service_type=corporate).
Volumen real en Trebol: 6 docs (LLCs de clientes commercial). Por eso este recurso se ejecuta como catálogo (full refresh siempre, antes de la fase de contacts).
| Campo origen | Campo destino | Transformación / nota |
|---|---|---|
id | external_id | |
| — | service_type | siempre "corporate" |
| — | created_by | system_user_id (los businesses no tienen owner asignado por GHL) |
name | name | trim de espacios extra |
name | first_name | full nombre — el destino requiere first_name no vacío |
| — | last_name | string vacío |
phone | phone | |
email | email | omitir si vacío |
address | address.address | |
city | address.city | |
state | address.state | |
postalCode | address.postcode | |
country | address.country | |
customFields[] | custom_fields.{fieldKey} | resolver con el catálogo en memoria |
| — | custom_fields.is_business | true (flag para queries y UI) |
createdAt | created_at | |
updatedAt | updated_at | |
| (todo) | _ghl_raw |
Vínculo persona → business
Cuando un contacto persona en GHL tiene businessId apuntando a uno de estos businesses (en Trebol son ~6 casos, 0.01% de los contactos), el script:
- Lee el business primero (corre antes en
SYNC_ORDER). - Cachea
business_name_map: { ghl_business_id → name }en memoria. - Al insertar el contacto persona, agrega:js
custom_fields: { parent_business_external_id: "6977a0bb1d90c36acab33ed9", // GHL id del business parent_business_name: "Moreno's Contractors LLC" // denormalizado }
Para resolver el FK a _id de Mongo posteriormente, basta una query: db.contacts_new.findOne({ external_id: parent_business_external_id, service_type: 'corporate' }). El índice (custom_fields.parent_business_external_id) hace esa lookup eficiente.
Note (embebido en contacts_new.notes[])
GHL GET /contacts/{id}/notes → imcrmdev contacts_new.notes (array embebido tipo ContactNote)
| Campo origen | Campo destino |
|---|---|
id | _id (string, NO objectid — llega como GHL id) |
body | content |
| — | title (vacío, GHL no tiene título) |
userId | agentId |
userId resuelto | agentName |
dateAdded | createdAt |
dateAdded | updatedAt |
Opportunity (opportunities)
GHL GET /opportunities/search → imcrmdev opportunities
| Campo origen | Campo destino |
|---|---|
id | external_id |
name | name |
monetaryValue | amount |
pipelineId | pipeline_id (FK a sales_pipelines.external_id) |
pipelineId resuelto | pipeline (string, nombre legible) |
pipelineStageId | stage_id (FK a sales_pipeline_stages.external_id) |
pipelineStageId resuelto | stage (string, nombre legible) |
effectiveProbability | probability |
status | status (open / won / lost / abandoned) |
source | source |
assignedTo resuelto | assigned_to (publicid del user destino) |
contactId resuelto | contact_id (ObjectId del contacto destino) |
contact.name | contact_name (denormalizado) |
customFields[] | custom_fields.{fieldKey} |
lastStatusChangeAt | last_status_change_at |
lastStageChangeAt | last_stage_change_at |
createdAt | created_at |
updatedAt | updated_at |
| (todo) | _ghl_raw |
Campos destino vacíos
accountExecutive, serviceBundle, installationSLA, mrcType (todos ISP-specific).
Pipeline (sales_pipelines)
GHL GET /opportunities/pipelines → imcrmdev sales_pipelines (NUEVA)
| Campo origen | Campo destino |
|---|---|
id | external_id |
name | name |
dateAdded | created_at |
dateUpdated | updated_at |
showInFunnel, showInPieChart | display.show_in_funnel, .show_in_pie_chart |
useOpportunityProbability | use_opportunity_probability |
Cada stage se explosiona en sales_pipeline_stages:
| Origen | Destino |
|---|---|
stages[].id | external_id |
stages[].name | name |
stages[].position | order |
stages[].stageWinProbability | win_probability |
| pipeline.id | pipeline_id (FK external_id) |
Conversation (conversations)
| Campo origen | Campo destino |
|---|---|
id | external_id |
contactId resuelto | contact_id |
lastMessageDate (epoch ms) | last_message_date (Date) |
lastMessageType | last_message_type |
lastMessageBody | last_message_body |
lastMessageDirection | last_message_direction |
unreadCount | unread_count |
assignedTo resuelto | assigned_to |
tags[] | tags |
type | type (TYPE_PHONE, TYPE_EMAIL, etc.) |
Message (messages)
| Campo origen | Campo destino |
|---|---|
id | external_id |
conversationId | conversation_id (FK) |
contactId resuelto | contact_id |
direction | direction |
status | status |
messageType | message_type |
body | body |
from | from |
to | to |
dateAdded | created_at |
attachments[] | attachments[] (URLs, sin descargar) |
Task (tasks)
GHL GET /contacts/{id}/tasks → imcrmdev tasks
| Campo origen | Campo destino |
|---|---|
id | external_id |
title | title |
body | description |
dueDate | dueDate |
completed | status (true → COMPLETED, false → PENDING) |
assignedTo resuelto | assignedTo |
contactId | associations[] ({ associatedType: "contact", associatedId }) |