diff --git a/api-gateway/src/controllers/clientes-progreso.controller.test.ts b/api-gateway/src/controllers/clientes-progreso.controller.test.ts index 5ac0750..f0e3725 100644 --- a/api-gateway/src/controllers/clientes-progreso.controller.test.ts +++ b/api-gateway/src/controllers/clientes-progreso.controller.test.ts @@ -10,6 +10,9 @@ const prismaMock = vi.hoisted(() => ({ findUnique: vi.fn(), update: vi.fn(), }, + usuario: { + findUnique: vi.fn(), + }, })) const mondayMock = vi.hoisted(() => ({ @@ -66,6 +69,7 @@ beforeEach(() => { vi.clearAllMocks() prismaMock.cliente.create.mockImplementation(async ({ data }) => ({ id: 'cli-1', ...data })) prismaMock.cliente.update.mockImplementation(async ({ data }) => ({ id: 'cli-1', ...data })) + prismaMock.usuario.findUnique.mockResolvedValue(null) // default: capturista sin Monday vinculado mondayMock.crearContacto.mockResolvedValue('cont-1') mondayMock.actualizarContacto.mockResolvedValue(undefined) mondayMock.crearApartado.mockResolvedValue('ap-1') @@ -282,6 +286,38 @@ describe('completarProgreso (POST /api/clientes/progreso/:id/completar)', () => }) }) + it('el Apartado lleva la ficha completa: contacto, precio, Forma de Pago y Vendedor del capturista original', async () => { + const { completarProgreso } = await import('./clientes.controller.js') + prismaMock.cliente.findUnique.mockResolvedValue({ + id: 'cli-1', + mondayContactoId: 'cont-1', + mondayApartadoId: null, + capturadoPorId: 'usr-9', // quien inició el alta progresiva + }) + prismaMock.usuario.findUnique.mockResolvedValue({ mondayUserId: 12345 }) + const altaConCredito = { + ...altaCompleta, + viviendaCredito: { ...altaCompleta.viviendaCredito, tiposCredito: ['COFI_FOVISSSTE'] }, + } + const { res, status } = resFake() + await completarProgreso(reqFake(altaConCredito, { id: 'cli-1' }), res) + expect(status).toHaveBeenCalledWith(201) + expect(mondayMock.crearApartado).toHaveBeenCalledWith( + expect.objectContaining({ + clienteNombre: 'Ana Demo Progreso', + unidadCodigo: 'U-999-DEMO2', + telefono: '6649990003', + email: 'ana@correo.com', + curp: 'AEDA900101MBCXXX00', + precioFinal: 1500000, + enganche: 20000, + vendedorId: 12345, + formaPagoLabel: 'Cofi-Fovissste', + fechaCompra: expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/), + }), + ) + }) + it('sin Contacto previo lo crea directo como CLIENTE (alta sin Monday al iniciar)', async () => { const { completarProgreso } = await import('./clientes.controller.js') prismaMock.cliente.findUnique.mockResolvedValue({ diff --git a/api-gateway/src/controllers/clientes.controller.test.ts b/api-gateway/src/controllers/clientes.controller.test.ts index 1d02f35..63f2824 100644 --- a/api-gateway/src/controllers/clientes.controller.test.ts +++ b/api-gateway/src/controllers/clientes.controller.test.ts @@ -14,6 +14,9 @@ const prismaMock = vi.hoisted(() => ({ create: vi.fn(), update: vi.fn(), }, + usuario: { + findUnique: vi.fn(), + }, })) const mondayMock = vi.hoisted(() => ({ @@ -134,6 +137,7 @@ describe('crearCliente (POST /api/clientes) — leadMondayId del funnel', () => vi.clearAllMocks() prismaMock.cliente.create.mockImplementation(async ({ data }) => ({ id: 'cli-1', ...data })) prismaMock.cliente.update.mockResolvedValue({}) + prismaMock.usuario.findUnique.mockResolvedValue(null) // default: capturista sin Monday vinculado mondayMock.crearContacto.mockResolvedValue('cont-1') funnelMock.moverAApartoConDedup.mockResolvedValue('op-1') }) @@ -176,3 +180,99 @@ describe('crearCliente (POST /api/clientes) — leadMondayId del funnel', () => ) }) }) + +describe('formaPagoLabel (tipos de crédito del alta → dropdown Forma de Pago)', () => { + it('mapea los 8 valores del enum del formato a los labels sembrados en Monday', async () => { + const { formaPagoLabel } = await import('./clientes.controller.js') + expect(formaPagoLabel(['INFONAVIT'])).toBe('Infonavit') + expect(formaPagoLabel(['APOYO_INFONAVIT'])).toBe('Apoyo Infonavit') + expect(formaPagoLabel(['COFINAVIT'])).toBe('Cofinavit') + expect(formaPagoLabel(['SEGUNDO_CREDITO'])).toBe('Segundo crédito') + expect(formaPagoLabel(['CONTADO'])).toBe('Contado') + expect(formaPagoLabel(['FOVISSSTE'])).toBe('Fovissste') + expect(formaPagoLabel(['COFI_FOVISSSTE'])).toBe('Cofi-Fovissste') + expect(formaPagoLabel(['BANCARIO'])).toBe('Bancario') + }) + + it('toma el PRIMERO reconocido del arreglo y tolera desconocidos', async () => { + const { formaPagoLabel } = await import('./clientes.controller.js') + expect(formaPagoLabel(['OTRA_COSA', 'CONTADO'])).toBe('Contado') + expect(formaPagoLabel(['OTRA_COSA'])).toBeUndefined() + expect(formaPagoLabel([])).toBeUndefined() + expect(formaPagoLabel(undefined)).toBeUndefined() + expect(formaPagoLabel('CONTADO')).toBeUndefined() // no es arreglo + }) +}) + +describe('crearCliente — Apartado con ficha completa', () => { + const altaConVenta = { + datosGenerales: { + nombreCompleto: 'Cliente Demo', + curp: 'CURP123', + rfc: 'RFC123', + email: 'cli@correo.com', + celular: '6641112233', + }, + viviendaCredito: { + proyecto: 'SENDA', + ubicacion: 'U-101', + valorVenta: 1500000, + enganche: 20000, + tiposCredito: ['INFONAVIT'], + }, + } + + const reqFake = (body: unknown, usuarioToken?: { sub: string }): Request => + ({ body, usuarioToken } as unknown as Request) + const resFake = () => { + const json = vi.fn() + const status = vi.fn().mockReturnValue({ json }) + return { res: { json, status } as unknown as Response, status, json } + } + + beforeEach(() => { + vi.clearAllMocks() + prismaMock.cliente.create.mockImplementation(async ({ data }) => ({ id: 'cli-1', ...data })) + prismaMock.cliente.update.mockResolvedValue({}) + prismaMock.usuario.findUnique.mockResolvedValue(null) + mondayMock.crearContacto.mockResolvedValue('cont-1') + mondayMock.crearApartado.mockResolvedValue('ap-1') + funnelMock.moverAApartoConDedup.mockResolvedValue('op-1') + }) + + it('el Apartado lleva contacto, precio, enganche, Fecha Compra (hoy) y Forma de Pago', async () => { + const { crearCliente } = await import('./clientes.controller.js') + const { res, status, json } = resFake() + await crearCliente(reqFake(altaConVenta), res) + expect(status).toHaveBeenCalledWith(201) + expect(mondayMock.crearApartado).toHaveBeenCalledWith( + expect.objectContaining({ + clienteNombre: 'Cliente Demo', + unidadCodigo: 'U-101', + montoApartado: 20000, + estadoLabel: 'Pago Apartado', + telefono: '6641112233', + email: 'cli@correo.com', + curp: 'CURP123', + precioFinal: 1500000, + enganche: 20000, + formaPagoLabel: 'Infonavit', + fechaCompra: expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/), + }), + ) + expect(json.mock.calls[0]?.[0]).toMatchObject({ mondayApartadoId: 'ap-1' }) + }) + + it('con capturista vinculado a Monday, el Apartado lo lleva como Vendedor (people)', async () => { + const { crearCliente } = await import('./clientes.controller.js') + prismaMock.usuario.findUnique.mockResolvedValue({ mondayUserId: 12345 }) + await crearCliente(reqFake(altaConVenta, { sub: 'usr-1' }), resFake().res) + expect(mondayMock.crearApartado).toHaveBeenCalledWith(expect.objectContaining({ vendedorId: 12345 })) + }) + + it('sin capturista vinculado, el Apartado se crea sin Vendedor', async () => { + const { crearCliente } = await import('./clientes.controller.js') + await crearCliente(reqFake(altaConVenta), resFake().res) + expect(mondayMock.crearApartado).toHaveBeenCalledWith(expect.objectContaining({ vendedorId: undefined })) + }) +}) diff --git a/api-gateway/src/controllers/clientes.controller.ts b/api-gateway/src/controllers/clientes.controller.ts index e93d2b5..cdae572 100644 --- a/api-gateway/src/controllers/clientes.controller.ts +++ b/api-gateway/src/controllers/clientes.controller.ts @@ -181,14 +181,55 @@ async function moverLeadAApartoSiAplica(body: Record): Promise< } } -/** Item en el tablero Apartado (pipeline Enkontrol); solo si hay valor de venta */ +/** Label del dropdown "Forma de Pago" (tablero Apartado) a partir del enum del formato de alta */ +const FORMA_PAGO_LABEL: Record = { + INFONAVIT: 'Infonavit', + APOYO_INFONAVIT: 'Apoyo Infonavit', + COFINAVIT: 'Cofinavit', + SEGUNDO_CREDITO: 'Segundo crédito', + CONTADO: 'Contado', + FOVISSSTE: 'Fovissste', + COFI_FOVISSSTE: 'Cofi-Fovissste', + BANCARIO: 'Bancario', +} + +/** + * Label de "Forma de Pago" a partir de viviendaCredito.tiposCredito (arreglo + * del wizard; se toma el PRIMERO reconocido — la columna del board es de una + * sola forma de pago). undefined si no hay ninguno reconocible. + */ +export function formaPagoLabel(tiposCredito: unknown): string | undefined { + if (!Array.isArray(tiposCredito)) return undefined + for (const t of tiposCredito) { + const label = FORMA_PAGO_LABEL[String(t).trim().toUpperCase()] + if (label !== undefined) return label + } + return undefined +} + +/** Item en el tablero Apartado (pipeline Enkontrol) con la ficha completa; solo si hay valor de venta */ async function crearApartadoSiAplica( - dg: { nombreCompleto: string }, - vc: { ubicacion?: string | undefined; valorVenta?: string | number | undefined; enganche?: string | number | undefined }, + dg: { nombreCompleto: string; celular?: string | undefined; email?: string | undefined; curp?: string | undefined }, + vc: { + ubicacion?: string | undefined + valorVenta?: string | number | undefined + enganche?: string | number | undefined + tiposCredito?: unknown + }, + capturadoPorId: string | null, ): Promise { const valorVenta = aNumero(vc.valorVenta) if (valorVenta <= 0) return null const enganche = aNumero(vc.enganche) + // Vendedor del apartado: el usuario Monday vinculado de quien capturó el alta + let vendedorId: number | undefined + if (capturadoPorId !== null) { + const capturista = await prisma.usuario.findUnique({ + where: { id: capturadoPorId }, + select: { mondayUserId: true }, + }) + vendedorId = capturista?.mondayUserId ?? undefined + } return mondayService.crearApartado({ clienteNombre: dg.nombreCompleto, unidadCodigo: vc.ubicacion ?? '', @@ -196,6 +237,14 @@ async function crearApartadoSiAplica( fechaApartado: hoyMas(0), fechaLimite: hoyMas(15), estadoLabel: 'Pago Apartado', // primer paso del pipeline Enkontrol + telefono: dg.celular, + email: dg.email, + curp: dg.curp, + precioFinal: valorVenta, + enganche: enganche > 0 ? enganche : undefined, + fechaCompra: hoyMas(0), + vendedorId, + formaPagoLabel: formaPagoLabel(vc.tiposCredito), }) } @@ -241,7 +290,7 @@ export async function crearCliente(req: Request, res: Response): Promise { ...{ actividadEconomicaLabel: actividadEconomicaLabel(body.empleo?.tipoEmpleo) }, }) - mondayApartadoId = await crearApartadoSiAplica(dg, vc) + mondayApartadoId = await crearApartadoSiAplica(dg, vc, req.usuarioToken?.sub ?? null) await prisma.cliente.update({ where: { id: cliente.id }, @@ -444,7 +493,8 @@ export async function completarProgreso(req: Request, res: Response): Promise { + it('los 5 códigos del enum mapean a los labels sembrados en la columna "Tipo propuesta"', async () => { + const { LABEL_PROPUESTA_MONDAY } = await import('./cotizaciones.controller.js') + // Labels reales verificados en Monday (difieren en guiones/espacios) + expect(LABEL_PROPUESTA_MONDAY['BANCARIO']).toBe('CREDITO BANCARIO') + expect(LABEL_PROPUESTA_MONDAY['INFONAVIT']).toBe('INFONAVIT-COFINAVIT') + expect(LABEL_PROPUESTA_MONDAY['COFINAVIT']).toBe('INFONAVIT-COFINAVIT') + expect(LABEL_PROPUESTA_MONDAY['CONTADO']).toBe('CONTADO') + expect(LABEL_PROPUESTA_MONDAY['SEMI_CONTADO']).toBe('SEMI CONTADO') + }) +}) diff --git a/api-gateway/src/controllers/cotizaciones.controller.ts b/api-gateway/src/controllers/cotizaciones.controller.ts index 09bcea4..fa70d7f 100644 --- a/api-gateway/src/controllers/cotizaciones.controller.ts +++ b/api-gateway/src/controllers/cotizaciones.controller.ts @@ -29,8 +29,10 @@ const hoyISO = (): string => { return `${d.getFullYear()}-${m}-${dia}` } -// Labels reales de la columna status "Tipo de Propuesta" del tablero Oportunidades -const LABEL_PROPUESTA_MONDAY: Record = { +// Labels reales de la columna status "Tipo de Propuesta" del tablero +// Oportunidades (verificados en Monday: difieren de los códigos del cotizador +// en guiones/espacios — el resolver case-insensitive solo no alcanza) +export const LABEL_PROPUESTA_MONDAY: Record = { BANCARIO: 'CREDITO BANCARIO', INFONAVIT: 'INFONAVIT-COFINAVIT', COFINAVIT: 'INFONAVIT-COFINAVIT', diff --git a/api-gateway/src/services/automatizaciones.service.test.ts b/api-gateway/src/services/automatizaciones.service.test.ts index dd84e03..ddf86ac 100644 --- a/api-gateway/src/services/automatizaciones.service.test.ts +++ b/api-gateway/src/services/automatizaciones.service.test.ts @@ -632,6 +632,25 @@ describe('AutomatizacionesService.escaneoOportunidades', () => { expect(state.mapa.get('contador:apartadosCreados')).toBe('1') }) + it('el Apartado hereda de la oportunidad: contacto, Precio Final y Vendedor (primer asesor)', async () => { + const monday = mondayMock([], [ + oportunidad('10', { etapa: 'Aparto', telefono: '6641112233', email: 'c@c.com', asesorIds: [101, 102] }), + ]) + const state = stateEnMemoria() + const svc = crearServicio(monday, state) + + const r = await svc.escaneoOportunidades() + expect(r.apartadosCreados).toHaveLength(1) + expect(monday.apartados[0]).toMatchObject({ + clienteNombre: 'Cliente 10', + unidadCodigo: 'U-101', + telefono: '6641112233', + email: 'c@c.com', + precioFinal: 2500000, + vendedorId: 101, + }) + }) + it('una oportunidad en Ganado crea su item en Ventas con unidad y valor', async () => { const monday = mondayMock([], [oportunidad('11', { etapa: 'GANADO' })]) const state = stateEnMemoria() diff --git a/api-gateway/src/services/automatizaciones.service.ts b/api-gateway/src/services/automatizaciones.service.ts index c797e71..273a7b2 100644 --- a/api-gateway/src/services/automatizaciones.service.ts +++ b/api-gateway/src/services/automatizaciones.service.ts @@ -541,12 +541,18 @@ export class AutomatizacionesService { for (const op of oportunidades) { if (esEtapa(op.etapa, ETAPA_APARTO) && (await this.state.get(claveApartadoCreado(op.id))) === null) { try { + // El Apartado hereda lo que la oportunidad ya trae (contacto, precio, + // asesor como Vendedor); lo que no exista se omite const apartadoItemId = await this.monday.crearApartado({ clienteNombre: op.nombre, unidadCodigo: op.unidadCodigo, fechaApartado: AutomatizacionesService.fechaHoyMas(0), fechaLimite: AutomatizacionesService.fechaHoyMas(7), estadoLabel: 'Pago Apartado', + telefono: op.telefono !== '' ? op.telefono : undefined, + email: op.email !== '' ? op.email : undefined, + precioFinal: op.precioFinal > 0 ? op.precioFinal : undefined, + vendedorId: op.asesorIds[0], }) await this.state.set(claveApartadoCreado(op.id), apartadoItemId) resultado.apartadosCreados.push({ oportunidadId: op.id, nombre: op.nombre, apartadoItemId }) diff --git a/api-gateway/src/services/mi-dia.service.test.ts b/api-gateway/src/services/mi-dia.service.test.ts index f429fed..e0e0f3c 100644 --- a/api-gateway/src/services/mi-dia.service.test.ts +++ b/api-gateway/src/services/mi-dia.service.test.ts @@ -310,6 +310,7 @@ describe('resumenMiDia (GET /api/mi-dia/resumen)', () => { nombreCompleto: 'Ana En Captura', proyecto: '', unidadCodigo: null, + mondayApartadoId: null, createdAt: new Date('2026-08-26T09:00:00Z'), }, { @@ -317,6 +318,7 @@ describe('resumenMiDia (GET /api/mi-dia/resumen)', () => { nombreCompleto: 'Luis Completado', proyecto: 'DIAGONAL', unidadCodigo: 'U-101', + mondayApartadoId: null, createdAt: new Date('2026-08-27T09:00:00Z'), }, ] @@ -329,6 +331,7 @@ describe('resumenMiDia (GET /api/mi-dia/resumen)', () => { unidadCodigo: 'U-101', completado: true, capturadoEn: '2026-08-27T09:00:00.000Z', + estadoApartado: null, }, { id: 'c1', @@ -337,13 +340,57 @@ describe('resumenMiDia (GET /api/mi-dia/resumen)', () => { unidadCodigo: '', completado: false, capturadoEn: '2026-08-26T09:00:00.000Z', + estadoApartado: null, }, ]) }) + it('mis clientes: estadoApartado cruza mondayApartadoId contra el tablero Apartado', async () => { + const clientes = [ + { + id: 'c1', + nombreCompleto: 'Con Apartado', + proyecto: 'DIAGONAL', + unidadCodigo: 'U-101', + mondayApartadoId: 'ap-1', + createdAt: new Date('2026-08-27T09:00:00Z'), + }, + { + id: 'c2', + nombreCompleto: 'Apartado Borrado', + proyecto: 'DIAGONAL', + unidadCodigo: 'U-102', + mondayApartadoId: 'ap-fantasma', // ya no existe en Monday + createdAt: new Date('2026-08-26T09:00:00Z'), + }, + { + id: 'c3', + nombreCompleto: 'Sin Apartado', + proyecto: '', + unidadCodigo: null, + mondayApartadoId: null, + createdAt: new Date('2026-08-25T09:00:00Z'), + }, + ] + const monday = mondayMock({ apartados: [apartado({ id: 'ap-1', estado: 'Alta Enkontrol' })] }) + const r = await resumenMiDia(monday, vendedor, new Map(), [], clientes, AHORA) + expect(r.misClientes.map((c) => [c.id, c.estadoApartado])).toEqual([ + ['c1', 'Alta Enkontrol'], // estado real del item en Monday + ['c2', null], // el apartado ya no existe + ['c3', null], // sin apartado ligado + ]) + }) + it('mis clientes: proyecto con solo espacios cuenta como en captura', async () => { const clientes = [ - { id: 'c1', nombreCompleto: 'Espacios', proyecto: ' ', unidadCodigo: null, createdAt: new Date('2026-08-26T09:00:00Z') }, + { + id: 'c1', + nombreCompleto: 'Espacios', + proyecto: ' ', + unidadCodigo: null, + mondayApartadoId: null, + createdAt: new Date('2026-08-26T09:00:00Z'), + }, ] const r = await resumenMiDia(mondayMock({}), vendedor, new Map(), [], clientes, AHORA) expect(r.misClientes[0]?.completado).toBe(false) diff --git a/api-gateway/src/services/mi-dia.service.ts b/api-gateway/src/services/mi-dia.service.ts index b80d0b5..768e76d 100644 --- a/api-gateway/src/services/mi-dia.service.ts +++ b/api-gateway/src/services/mi-dia.service.ts @@ -74,6 +74,8 @@ export interface ClienteMiDia { unidadCodigo: string completado: boolean // true cuando el alta ya tiene proyecto (pasó a CLIENTE) capturadoEn: string // ISO + /** Estado actual del item en el tablero Apartado (bidireccional Monday→app); null si no hay apartado ligado o ya no existe */ + estadoApartado: string | null } export interface ResumenMiDia { @@ -136,7 +138,14 @@ export async function resumenMiDia( /** Capturas locales del usuario (tabla LeadCaptura), ya filtradas por él */ capturas: { leadId: string; nombreLead: string; telefono: string | null; createdAt: Date }[] = [], /** Clientes cuyo alta inició el usuario (tabla Cliente), ya filtrados por él */ - clientes: { id: string; nombreCompleto: string; proyecto: string; unidadCodigo: string | null; createdAt: Date }[] = [], + clientes: { + id: string + nombreCompleto: string + proyecto: string + unidadCodigo: string | null + mondayApartadoId: string | null + createdAt: Date + }[] = [], ahora: Date = new Date(), ): Promise { const miId = usuario.mondayUserId @@ -217,8 +226,11 @@ export async function resumenMiDia( }) .sort((a, b) => b.capturadoEn.localeCompare(a.capturadoEn)) - // "Mis clientes" es 100% local (no requiere Monday): el alta en progreso - // lleva proyecto '', el alta completada lleva el proyecto lleno + // "Mis clientes" es local + cruce con Monday: el alta en progreso lleva + // proyecto '', el alta completada lleva el proyecto lleno. estadoApartado + // cruza Cliente.mondayApartadoId contra el tablero Apartado (null si no hay + // apartado ligado o ya no existe en Monday) + const apartadosPorId = new Map(apartados.map((a) => [a.id, a])) const misClientes: ClienteMiDia[] = clientes .map((c) => ({ id: c.id, @@ -227,6 +239,8 @@ export async function resumenMiDia( unidadCodigo: c.unidadCodigo ?? '', completado: c.proyecto.trim() !== '', capturadoEn: c.createdAt.toISOString(), + estadoApartado: + c.mondayApartadoId != null ? (apartadosPorId.get(c.mondayApartadoId)?.estado ?? null) : null, })) .sort((a, b) => b.capturadoEn.localeCompare(a.capturadoEn)) diff --git a/api-gateway/src/services/monday.service.test.ts b/api-gateway/src/services/monday.service.test.ts index 2fb99a9..7ad6319 100644 --- a/api-gateway/src/services/monday.service.test.ts +++ b/api-gateway/src/services/monday.service.test.ts @@ -743,4 +743,99 @@ describe('MondayService', () => { expect(Object.keys(parsed).sort()).toEqual(['color_estado', 'date_fecha']) }) }) + + describe('crearApartado (ficha completa del tablero Apartado)', () => { + // Columnas reales del board (verificadas en Monday 2026-09-04) + const COLUMNAS_APARTADO = [ + { id: 'text_u', title: 'Unidad', type: 'text' }, + { id: 'numeric_ma', title: 'Monto apartado', type: 'numbers' }, + { id: 'date_fa', title: 'Fecha apartado', type: 'date' }, + { id: 'date_fl', title: 'Fecha límite', type: 'date' }, + { id: 'color_e', title: 'Estado', type: 'status' }, + { id: 'phone_t', title: 'Teléfono', type: 'phone' }, + { id: 'email_c', title: 'Correo electrónico', type: 'email' }, + { id: 'text_curp', title: 'curp', type: 'text' }, // minúscula en el board + { id: 'numeric_pf', title: 'Precio Final', type: 'numbers' }, + { id: 'numeric_en', title: 'Enganche', type: 'numbers' }, + { id: 'date_fc', title: 'Fecha Compra', type: 'date' }, + { id: 'people_v', title: 'Vendedor', type: 'people' }, + { id: 'dropdown_fp', title: 'Forma de Pago', type: 'dropdown' }, + { id: 'long_obs', title: 'Observaciones', type: 'long_text' }, + ] + + it('serializa cada columna según su tipo (phone, email, numbers, date, people, dropdown, long_text)', async () => { + const llamadas = mockearFetch((body) => { + if (body.query.includes('settings_str')) { + return respuestaOk({ + boards: [ + { + columns: [ + { id: 'color_e', title: 'Estado', settings_str: '{"labels":{"1":"Pago Apartado"}}' }, + { id: 'dropdown_fp', title: 'Forma de Pago', settings_str: '{"labels":[{"name":"Infonavit"},{"name":"Contado"}]}' }, + ], + }, + ], + }) + } + if (body.query.includes('columns')) return respuestaOk({ boards: [{ columns: COLUMNAS_APARTADO }] }) + return respuestaOk({ create_item: { id: 'ap-1' } }) + }) + + const id = await crearServicio().crearApartado({ + clienteNombre: 'Cliente Demo', + unidadCodigo: 'U-101', + montoApartado: 20000, + fechaApartado: '2026-09-04', + fechaLimite: '2026-09-19', + estadoLabel: 'Pago Apartado', + telefono: '6641112233', + email: 'cli@correo.com', + curp: 'CURP123', + precioFinal: 1500000, + enganche: 20000, + fechaCompra: '2026-09-04', + vendedorId: 12345, + formaPagoLabel: 'INFONAVIT', // el resolver lo ajusta al label real + observaciones: 'Venta con descuento autorizado', + }) + expect(id).toBe('ap-1') + + const mutacion = llamadas.find((l) => l.body.query.includes('create_item')) + expect(mutacion?.body.variables['boardId']).toBe('666') // tablero Apartado + const parsed = JSON.parse(String(mutacion?.body.variables['columnValues'])) as Record + expect(parsed['phone_t']).toEqual({ phone: '6641112233', countryShortName: 'MX' }) + expect(parsed['email_c']).toEqual({ email: 'cli@correo.com', text: 'cli@correo.com' }) + expect(parsed['text_curp']).toBe('CURP123') + expect(parsed['numeric_pf']).toBe(1500000) + expect(parsed['numeric_en']).toBe(20000) + expect(parsed['date_fc']).toEqual({ date: '2026-09-04' }) + expect(parsed['people_v']).toEqual({ personsAndTeams: [{ id: 12345, kind: 'person' }] }) + expect(parsed['dropdown_fp']).toEqual({ labels: ['Infonavit'] }) // dropdown resuelto case-insensitive + expect(parsed['long_obs']).toEqual({ text: 'Venta con descuento autorizado' }) + }) + + it('omite las columnas opcionales no capturadas', async () => { + const llamadas = mockearFetch((body) => { + if (body.query.includes('settings_str')) { + return respuestaOk({ + boards: [{ columns: [{ id: 'color_e', title: 'Estado', settings_str: '{"labels":{"1":"Pago Apartado"}}' }] }], + }) + } + if (body.query.includes('columns')) return respuestaOk({ boards: [{ columns: COLUMNAS_APARTADO }] }) + return respuestaOk({ create_item: { id: 'ap-2' } }) + }) + + await crearServicio().crearApartado({ + clienteNombre: 'Cliente Demo', + unidadCodigo: 'U-101', + fechaApartado: '2026-09-04', + fechaLimite: '2026-09-19', + estadoLabel: 'Pago Apartado', + }) + + const mutacion = llamadas.find((l) => l.body.query.includes('create_item')) + const parsed = JSON.parse(String(mutacion?.body.variables['columnValues'])) as Record + expect(Object.keys(parsed).sort()).toEqual(['color_e', 'date_fa', 'date_fl', 'text_u']) + }) + }) }) diff --git a/api-gateway/src/services/monday.service.ts b/api-gateway/src/services/monday.service.ts index 689f90b..932cca1 100644 --- a/api-gateway/src/services/monday.service.ts +++ b/api-gateway/src/services/monday.service.ts @@ -557,12 +557,25 @@ export class MondayService { async crearApartado(datos: DatosApartado): Promise { const boardId = config.monday.boards.apartado + // Mapeo a las columnas reales del tablero Apartado (verificadas en Monday): + // contacto (Teléfono/Correo/curp), operación (Precio Final/Enganche/ + // Fecha Compra/Forma de Pago), asignación (Vendedor) y Observaciones. + // Todo lo opcional se omite si viene undefined (mismo patrón de siempre). const columnValues = await this.construirColumnValues(boardId, { Unidad: datos.unidadCodigo, 'Monto apartado': datos.montoApartado, 'Fecha apartado': datos.fechaApartado, 'Fecha límite': datos.fechaLimite, Estado: datos.estadoLabel, + Teléfono: datos.telefono, + 'Correo electrónico': datos.email, + curp: datos.curp, // el título de la columna es minúscula en el board + 'Precio Final': datos.precioFinal, + Enganche: datos.enganche, + 'Fecha Compra': datos.fechaCompra, + Vendedor: datos.vendedorId, + 'Forma de Pago': datos.formaPagoLabel, // dropdown; el resolver ajusta case + Observaciones: datos.observaciones, }) return this.crearItem(boardId, datos.clienteNombre, columnValues) } @@ -1001,12 +1014,16 @@ export class MondayService { return items.map((item) => { const porId = new Map(item.column_values.map((cv) => [cv.id, cv])) + // Misma corrección que mapearLead: status nunca escrito → value null y + // `text` muestra el default del tablero; el estado real es vacío + const estadoCv = colEstado ? porId.get(colEstado.id) : undefined + const estado = estadoCv?.value ? (estadoCv.text ?? '') : '' return { id: item.id, nombre: item.name, unidadCodigo: colUnidad ? (porId.get(colUnidad.id)?.text ?? '') : '', fechaLimite: colFechaLimite ? (porId.get(colFechaLimite.id)?.text ?? '') : '', - estado: colEstado ? (porId.get(colEstado.id)?.text ?? '') : '', + estado, } }) } @@ -1262,6 +1279,16 @@ export interface DatosApartado { fechaApartado: string // 'YYYY-MM-DD' fechaLimite: string // 'YYYY-MM-DD' estadoLabel: string // p. ej. 'Pago Apartado' (pipeline Enkontrol) + // Contacto y operación (opcionales; columnas reales del tablero Apartado) + telefono?: string | undefined // phone 'Teléfono' + email?: string | undefined // email 'Correo electrónico' + curp?: string | undefined // text 'curp' (título minúscula en el board) + precioFinal?: number | undefined // numbers 'Precio Final' + enganche?: number | undefined // numbers 'Enganche' + fechaCompra?: string | undefined // date 'Fecha Compra' ('YYYY-MM-DD') + vendedorId?: number | undefined // people 'Vendedor' (user ID de Monday) + formaPagoLabel?: string | undefined // dropdown 'Forma de Pago' (labels ya sembrados) + observaciones?: string | undefined // long_text 'Observaciones' } export interface DatosVenta { diff --git a/apps/mi-dia/src/lib/mi-dia.ts b/apps/mi-dia/src/lib/mi-dia.ts index df0ea63..4f71e03 100644 --- a/apps/mi-dia/src/lib/mi-dia.ts +++ b/apps/mi-dia/src/lib/mi-dia.ts @@ -45,6 +45,8 @@ export interface ClienteMiDia { unidadCodigo: string completado: boolean capturadoEn: string // ISO + /** Estado actual del item en el tablero Apartado (Monday); null si no hay apartado ligado */ + estadoApartado: string | null } export interface ResumenMiDia { diff --git a/apps/mi-dia/src/pages/MiDiaPage.tsx b/apps/mi-dia/src/pages/MiDiaPage.tsx index c445c27..c424251 100644 --- a/apps/mi-dia/src/pages/MiDiaPage.tsx +++ b/apps/mi-dia/src/pages/MiDiaPage.tsx @@ -439,6 +439,12 @@ export default function MiDiaPage({ accionesSesion }: { accionesSesion: Acciones

+ {/* Estado del apartado en Monday (bidireccional); solo si hay apartado ligado */} + {c.estadoApartado !== null && ( + + {c.estadoApartado} + + )}