feat(monday): alinear Frasa-CAS con CRM real del cliente (7 mejoras)
- Vocabulario funnel: labels Intento de contacto/Abandono en Leads, etapas completas en Oportunidades (Cotizador, Ganado, Perdido...) - Apartados: pipeline Enkontrol (8 estados) + checkboxes CURP/RFC/Comprobante que la app de alta prende al subir documentos - Motivos No Compra: board catálogo + dropdown + endpoint /oportunidades/:id/perder - Perfil demográfico en Contactos mapeado desde el alta (estado civil, vivienda, propósito, fecha nac., rango de edad calculado) - Desarrollos: catálogo maestro sincronizado (precios, m2, tipo, entrega) - Actividades: board nuevo + creación automática en round-robin e inactividad - Cobranza: plan liquidado mueve el apartado a 'X Escriturar' en Monday - Scripts: setup:crm y sync:desarrollos (idempotentes) - Tests 119/119 verdes
This commit is contained in:
@@ -279,10 +279,12 @@ export class MondayService {
|
||||
* column_values SIEMPRE se envía con JSON.stringify en variables (nunca
|
||||
* template literals dentro del query).
|
||||
*/
|
||||
private valorPorTipo(col: ColumnaMeta, crudo: string | number): unknown {
|
||||
private valorPorTipo(col: ColumnaMeta, crudo: string | number | boolean): unknown {
|
||||
switch (col.type) {
|
||||
case 'status':
|
||||
return { label: String(crudo) }
|
||||
case 'dropdown':
|
||||
return { labels: [String(crudo)] }
|
||||
case 'date':
|
||||
return { date: String(crudo) }
|
||||
case 'numbers':
|
||||
@@ -291,6 +293,14 @@ export class MondayService {
|
||||
return { email: String(crudo), text: String(crudo) }
|
||||
case 'phone':
|
||||
return { phone: String(crudo), countryShortName: 'MX' }
|
||||
case 'checkbox':
|
||||
return { checked: crudo === true || crudo === 'true' ? 'true' : 'false' }
|
||||
case 'people':
|
||||
return { personsAndTeams: [{ id: Number(crudo), kind: 'person' }] }
|
||||
case 'board_relation':
|
||||
return { item_ids: [Number(crudo)] }
|
||||
case 'long_text':
|
||||
return { text: String(crudo) }
|
||||
default:
|
||||
return String(crudo)
|
||||
}
|
||||
@@ -298,7 +308,7 @@ export class MondayService {
|
||||
|
||||
private async construirColumnValues(
|
||||
boardId: string,
|
||||
valores: Record<string, string | number | undefined>,
|
||||
valores: Record<string, string | number | boolean | undefined>,
|
||||
): Promise<string> {
|
||||
const out: Record<string, unknown> = {}
|
||||
for (const [titulo, crudo] of Object.entries(valores)) {
|
||||
@@ -346,6 +356,14 @@ export class MondayService {
|
||||
RFC: datos.rfc,
|
||||
Dirección: datos.direccion,
|
||||
Estado: datos.estadoLabel,
|
||||
// Perfil demográfico (vocabulario CRM FRASA); solo se envía lo capturado
|
||||
Sexo: datos.sexoLabel,
|
||||
'Estado Civil': datos.estadoCivilLabel,
|
||||
'Rango de Edad': datos.rangoEdadLabel,
|
||||
'Actividad Economica': datos.actividadEconomicaLabel,
|
||||
'Vivienda Actual': datos.viviendaActualLabel,
|
||||
'Proposito de Compra': datos.propositoCompraLabel,
|
||||
'Fecha Nacimiento': datos.fechaNacimiento,
|
||||
})
|
||||
return this.crearItem(boardId, datos.nombre, columnValues)
|
||||
}
|
||||
@@ -378,6 +396,74 @@ export class MondayService {
|
||||
this.invalidarCacheUnidades()
|
||||
}
|
||||
|
||||
// ---------- Escrituras genéricas ----------
|
||||
|
||||
/**
|
||||
* Cambia UNA columna de un item, resolviendo el column ID por título y
|
||||
* serializando según el tipo de columna (status, dropdown, checkbox, date...).
|
||||
*/
|
||||
async cambiarValorColumna(
|
||||
boardId: string,
|
||||
itemId: string,
|
||||
tituloColumna: string,
|
||||
valor: string | number | boolean,
|
||||
): Promise<void> {
|
||||
const col = await this.columnaId(boardId, tituloColumna)
|
||||
await this.graphql(
|
||||
`mutation ($boardId: ID!, $itemId: ID!, $columnId: String!, $value: JSON!) {
|
||||
change_column_value(board_id: $boardId, item_id: $itemId, column_id: $columnId, value: $value) { id }
|
||||
}`,
|
||||
{ boardId, itemId, columnId: col.id, value: JSON.stringify(this.valorPorTipo(col, valor)) },
|
||||
)
|
||||
}
|
||||
|
||||
/** Nombre de un item por ID (cualquier tablero) */
|
||||
async obtenerNombreItem(itemId: string): Promise<string> {
|
||||
const data = await this.graphql<{ items: { name: string }[] }>(
|
||||
`query ($ids: [ID!]) { items(ids: $ids) { name } }`,
|
||||
{ ids: [itemId] },
|
||||
)
|
||||
const nombre = data.items[0]?.name
|
||||
if (nombre === undefined) throw new AppError(404, `Item ${itemId} no encontrado en Monday`)
|
||||
return nombre
|
||||
}
|
||||
|
||||
// ---------- Motivos No Compra / Actividades (tableros CRM FRASA) ----------
|
||||
|
||||
/** Item en el tablero Motivos No Compra (nombre = "<motivo> — <item origen>") */
|
||||
async crearMotivoNoCompra(datos: DatosMotivoNoCompra): Promise<string> {
|
||||
const boardId = config.monday.boards.motivos
|
||||
if (boardId === '') throw new AppError(500, 'MONDAY_BOARD_MOTIVOS no está configurado')
|
||||
const columnValues = await this.construirColumnValues(boardId, {
|
||||
Categoria: datos.categoria,
|
||||
Descripcion: datos.descripcion,
|
||||
})
|
||||
return this.crearItem(boardId, datos.nombre, columnValues)
|
||||
}
|
||||
|
||||
/** Actividad de seguimiento ligada a un lead (bitácora del CRM) */
|
||||
async crearActividad(datos: DatosActividad): Promise<string> {
|
||||
const boardId = config.monday.boards.actividades
|
||||
if (boardId === '') throw new AppError(500, 'MONDAY_BOARD_ACTIVIDADES no está configurado')
|
||||
// La columna board_relation "Lead" no puede crearse vía API (limitación de
|
||||
// Monday): si existe en el tablero se liga, si no, la actividad se crea igual.
|
||||
let lead: string | undefined
|
||||
try {
|
||||
await this.columnaId(boardId, 'Lead')
|
||||
lead = datos.leadItemId
|
||||
} catch {
|
||||
lead = undefined
|
||||
}
|
||||
const columnValues = await this.construirColumnValues(boardId, {
|
||||
Tipo: datos.tipoLabel,
|
||||
Responsable: datos.responsableId,
|
||||
Fecha: datos.fecha,
|
||||
Estado: datos.estadoLabel,
|
||||
Lead: lead,
|
||||
})
|
||||
return this.crearItem(boardId, datos.nombre, columnValues)
|
||||
}
|
||||
|
||||
// ---------- Leads (automatizaciones) ----------
|
||||
|
||||
/** Items del tablero Leads con estado, fecha de actualización y vendedor asignado */
|
||||
@@ -510,6 +596,14 @@ export interface DatosContacto {
|
||||
rfc?: string
|
||||
direccion?: string
|
||||
estadoLabel: string // p. ej. 'CLIENTE'
|
||||
// Perfil demográfico (labels del dropdown correspondiente)
|
||||
sexoLabel?: string | undefined
|
||||
estadoCivilLabel?: string | undefined
|
||||
rangoEdadLabel?: string | undefined
|
||||
actividadEconomicaLabel?: string | undefined
|
||||
viviendaActualLabel?: string | undefined
|
||||
propositoCompraLabel?: string | undefined
|
||||
fechaNacimiento?: string | undefined // 'YYYY-MM-DD'
|
||||
}
|
||||
|
||||
export interface DatosApartado {
|
||||
@@ -518,7 +612,22 @@ export interface DatosApartado {
|
||||
montoApartado?: number
|
||||
fechaApartado: string // 'YYYY-MM-DD'
|
||||
fechaLimite: string // 'YYYY-MM-DD'
|
||||
estadoLabel: string // p. ej. 'VIGENTE'
|
||||
estadoLabel: string // p. ej. 'Pago Apartado' (pipeline Enkontrol)
|
||||
}
|
||||
|
||||
export interface DatosMotivoNoCompra {
|
||||
nombre: string // '<motivo> — <nombre del item de origen>'
|
||||
categoria: string // una de MOTIVOS_NO_COMPRA
|
||||
descripcion?: string
|
||||
}
|
||||
|
||||
export interface DatosActividad {
|
||||
nombre: string
|
||||
tipoLabel: string // 'Llamada' | 'Reunion' | 'Tarea'
|
||||
responsableId: number // user ID de Monday
|
||||
fecha: string // 'YYYY-MM-DD'
|
||||
estadoLabel: string // 'Abierto' | 'Listo'
|
||||
leadItemId: string // item del tablero Leads relacionado
|
||||
}
|
||||
|
||||
/** Singleton usado por los controllers (los tests instancian el suyo) */
|
||||
|
||||
Reference in New Issue
Block a user