Paquete Compartido @repo/crm-services
Objetivo
Section titled “Objetivo”Concentrar lógica de dominio CRM reusable entre web y mobile (mocks, reglas y entrada HTTP), sin duplicar datos en pantallas.
La regla canónica del monorepo es: normalizar una vez en shared, orquestar en web cuando haga falta, pintar específico por plataforma.
Factories públicas
Section titled “Factories públicas”createMockCrmServices(options?)createHttpCrmServices({ apiClient })
Módulos expuestos
Section titled “Módulos expuestos”dashboardagendacarteracatalogointeractionscampaignskanbanchatsSinAsignarcustomFields
Contratos
Section titled “Contratos”- Todos los métodos están tipados con
@repo/shared-types. - Implementación HTTP usa
@repo/api-client.
Estrategia actual
Section titled “Estrategia actual”- Mock-first como default para paridad funcional y desarrollo.
- Mock-first controlado: válido mientras backend todavía no completa un módulo o el flujo real aún no está listo para producto.
- Web mantiene entrypoints existentes en
apps/web/app/services/*.service.tscomo wrappers (compatibilidad de imports). - Mobile consume singleton
getMobileCrmServices()y evita latencia mock artificial. - La prioridad de conexión real es:
- backend
- web
- mobile
- Cuando un módulo todavía no está listo en backend o no ha sido validado en web, mantenerlo mock-first en native no cuenta como deuda automática.
- Regla permanente de performance:
- preferir endpoints filtrados del backend antes que overfetch + filtro local
- solo hacer fallback más amplio cuando el contrato backend todavía no exista o una validación temporal lo exija
- no cargar datos secundarios si la UI no los va a renderizar
Guardrails
Section titled “Guardrails”- No introducir dependencias de UI (React, RN, Next) dentro del paquete.
- Mantener funciones de dominio puras y adapters por app para mapear view-models.
- Cualquier endpoint nuevo debe agregar primero contrato en
@repo/shared-types. - No convertir este paquete en BFF: auth, [REDACTED]s, secretos y policy de sesión siguen fuera, en adapters por plataforma.
- No dejar que la normalización reusable se disperse en
route.tso componentes si puede vivir aquí.
Estado actual de cartera
Section titled “Estado actual de cartera”getCarteraItems()mezcla backend real + mock de forma explícita:- reales:
grupo,cliente/lead,creacion,groupId - render derivado:
vendedor - mock temporal:
contacto,ultimaCompra,frecuencia,agenda,interaccion
- reales:
- La implementación HTTP de cartera consume:
GET /leadsGET /employeesGET /lead-assignmentsGET /group-assignments
- La columna
vendedorpuede fusionar asignaciones directas a lead y heredadas por grupo para render. - La mutación canónica para asignar vendedor desde cartera es
assignSellerToGroups()y creaPOST /group-assignments. - El paquete expone
groupIddentro de cadaCarteraItempara que cualquier app convierta selección de filas agroupIdsúnicos sin duplicar lógica. getCarteraItemById()se usa hoy como detalle base de cartera y resuelveGET /groups/:id, porque la ruta interna/cartera/[id]representa el grupo visible, no un lead individual.- El paquete ahora también expone:
getGroupInvoices(groupId, { page })getGroupContacts(groupId)updateContact(contactId, input)
getGroupInvoices(groupId, { page })consume el contrato oficial:GET /groups/:id/invoices?page=n
- Esa respuesta ya llega con:
datapaginationpor díadaysettingsemptyState
- Regla de performance para este método:
- nunca sobretraer historial completo si backend ya pagina por día
- el consumidor debe pedir el siguiente lote solo cuando la UI lo necesite
getGroupContacts(groupId)resuelve la jerarquía real de BoostAPI:- usa
GET /contacts?group_id=...como path canónico y más barato para el detalle de cartera - no asume
1 group : 1 lead
- usa
updateContact(contactId, input)hacePATCH /contacts/:idcon el contrato oficial de canales/manual channels; no toca el grupo legacyContactos.CarteraContacttambién refleja metadatos de performance para UI:hasCustomFieldValuescustomFieldValuesCount
- Esos flags permiten que web/mobile solo llamen
GET /contacts/:id/custom-field-valuescuando el contacto realmente trae extras.
Estado actual de customFields
Section titled “Estado actual de customFields”- El módulo
customFieldsya expone contratos HTTP + mock para:getCustomFieldTypes()getCustomFieldGroups()getCustomFields()getGroupCustomFieldValues(groupId)saveGroupCustomFieldValues(groupId, input)getContactCustomFieldValues(contactId)saveContactCustomFieldValues(contactId, input)getCurrencyCatalog()getDepartmentCatalog()getMunicipalityCatalog(departmentId?)requestMediaUpload(input)getMediaDownloadUrl(input)
- La implementación HTTP consume directamente BoostAPI:
GET /groups/:id/invoicesGET /custom-field-typesGET /custom-field-groupsGET /custom-fieldsGET /groups/:id/custom-field-valuesPUT /groups/:id/custom-field-valuesGET /contacts/:id/custom-field-valuesPUT /contacts/:id/custom-field-valuesGET /catalogs/currenciesGET /catalogs/departmentsGET /catalogs/municipalitiesPOST /media/presign-uploadGET /media/presign-download
- El paquete no interpreta todavía
settings.readonlypara bloquear edición por sí mismo; esa decisión de UI sigue en las apps consumidoras. - Aun así, la capa ya transporta esos settings nuevos sin romper contratos:
readonlyoverride_permission_codecontexts
- Regla compartida para futuras apps consumidoras:
- los campos readonly siguen visibles
- la UI podrá decidir bloquearlos antes de mutar
- pero el enforcement real sigue viniendo del
403de BoostAPI
contextsqueda solo como preparación futura;@repo/crm-servicesno debe empezar a filtrar por pantalla sin un contrato backend explícito.- La jerarquía de ownership que esta capa debe seguir manteniendo es:
group > lead/client > contact- con
clienttratado comoleadconzauru_id, no como tipo nuevo
- Regla de consumo importante:
GET /custom-field-groupsyGET /custom-fieldsen BoostAPI son paginados y usanlimit=10por default.- La implementación HTTP del monorepo fuerza
limitalto (100y200) para evitar perder campos cuando el schema de una entidad crece. - Si en el futuro el catálogo crece más, el siguiente paso correcto es paginar explícitamente o filtrar
custom_fieldsporcustom_field_group_id; no volver a confiar en defaults del backend.
- Los mocks de
customFieldsya reflejan el blueprint dev actual de BoostAPI paraInformacion personalizada del cliente, incluyendocurrency,department,municipality,image,videoypdf. - Los mocks de cartera ahora también incluyen páginas de
Facturaspara no romper la pestaña cuando web trabaja sinboostToken. - La capa HTTP y los mocks ya cubren también el segundo entry de facturas:
GET /groups/:id/invoices/:invoiceId
- Ese detalle se carga on-demand al abrir el
ojito; no se overfetchea junto con la tabla principal. - El mapper HTTP del detalle ya expone el shape normalizado de línea que frontend debe preferir:
lineKindcodenameproductTypetrackingNameexpirationDateexpirationLabel
- Los campos raw (
item,bundle,serial,lot,serialId,lotId) se preservan para debugging y futuras mejoras sin romper el contrato compartido. - Para cartera detalle, el paquete entrega estructura y valores suficientes para renderizar:
- la sección dinámica
Informacion personalizada del clientea nivel group - los extras dinámicos
record_type = contactdentro de cada card superior de contacto
- la sección dinámica
- La regla operativa en detalle es:
- pedir grupos por
record_type - pedir campos por
custom_field_group_id - pedir valores del contacto solo cuando
hasCustomFieldValues = true
- pedir grupos por
- La administración de schema (
custom_field_groups.*/custom_fields.*) sigue fuera de alcance por ahora.
Resiliencia de integración
Section titled “Resiliencia de integración”- Fallos al leer
employees,lead-assignmentsogroup-assignmentsno deben tumbar la tabla. - En la implementación HTTP actual esos fallos degradan a arrays vacíos y hacen
console.warn, permitiendo que la pantalla siga renderizando. - El
console.logtemporal del payload deGET /leadsvive en la implementación web server-side mientras se valida el contrato con backend. - En detalle de cartera, si fallan
custom-field-groups,custom-fields, catálogos o URLs firmadas, la app degrada la sección dinámica sin bloquear la entrada al grupo mientras existancartera+groups.view.
Uso futuro en mobile
Section titled “Uso futuro en mobile”- Mobile ya puede consumir los mismos contratos y métodos porque
@repo/crm-servicesno depende de Next ni de React Native. - Hoy
apps/native/lib/crm-services.tsusacreateMockCrmServices({ simulateDelay: false }). - Cuando native migre cartera a datos reales, el cambio esperado es reemplazar ese adapter por
createHttpCrmServices({ apiClient }), manteniendo el mismo API de métodos. - La primera UI mobile de asignación de vendedor debe llamar
services.cartera.assignSellerToGroups(); no debe crear una variante propia por lead. - La auditoría
auth-bff-shared-auditdefine el criterio oficial para decidir qué flujos pueden salir de mocks haciashared-with-platform-authy cuáles deben quedarse detrás del BFF web. - La transición de native a real data debe respetar este orden:
- contrato estable en backend
- validación real en web
- salida de mocks en native por slice
