feat(mejoras): 25 mejoras UX en todos los módulos + purga Monday
Backend: mi-dia siempre local, vocabulario canónico, AplicacionPago con usuario Mi día: modal español real, cita desde modal, score falso eliminado Cotizador: barra sticky, persistencia local con folio Alta Clientes: redirect eliminado, foto conectada, asesor desde sesión, refs dinámicas Cobranza: contrato Mora arreglado, link a Plan, abono próximo pago, WhatsApp Landing: contactos locales, ModalContacto eliminado, ModoTrabajo eliminado Dashboards: MiTablero unificado, funnel con descartados, drill-down, rango fechas Chat FRASA: corpus 5 desarrollos, 6 herramientas, historial conversaciones 69 archivos, 7 apps construidas
This commit is contained in:
@@ -12,9 +12,6 @@ const prismaMock = vi.hoisted(() => ({
|
||||
leadCaptura: {
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
leadOportunidad: {
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
cliente: {
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
@@ -36,7 +33,6 @@ vi.mock('../services/prisma.js', () => ({ prisma: prismaMock }))
|
||||
vi.mock('../services/monday.service.js', () => ({ mondayService: mondayMock }))
|
||||
|
||||
import { resumenMiDiaEndpoint } from './mi-dia.controller.js'
|
||||
import { saludMonday } from '../services/salud-monday.service.js'
|
||||
|
||||
const usuarioFake = {
|
||||
id: 'u-1',
|
||||
@@ -80,12 +76,9 @@ const reqFake = { usuarioToken: { sub: 'u-1' } } as unknown as Request
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.restoreAllMocks() // restaura los spyOn(saludMonday, 'estaDisponible')
|
||||
saludMonday.restablecer()
|
||||
prismaMock.usuario.findUnique.mockResolvedValue(usuarioFake)
|
||||
prismaMock.usuario.findMany.mockResolvedValue([])
|
||||
prismaMock.leadCaptura.findMany.mockResolvedValue([])
|
||||
prismaMock.leadOportunidad.findMany.mockResolvedValue([])
|
||||
prismaMock.cliente.findMany.mockResolvedValue([])
|
||||
prismaMock.actividadLocal.findMany.mockResolvedValue([])
|
||||
mondayMock.getUsuariosWorkspace.mockResolvedValue([])
|
||||
@@ -96,9 +89,8 @@ beforeEach(() => {
|
||||
mondayMock.getUltimosCambios.mockResolvedValue(new Map())
|
||||
})
|
||||
|
||||
describe('resumenMiDiaEndpoint (modo local y unión)', () => {
|
||||
it('Monday caído (circuit breaker): resumen 100% local, sin llamadas a Monday', async () => {
|
||||
vi.spyOn(saludMonday, 'estaDisponible').mockReturnValue(false)
|
||||
describe('resumenMiDiaEndpoint (siempre local, Monday purgado)', () => {
|
||||
it('responde 100% desde LeadCaptura: Monday no se llama nunca', async () => {
|
||||
prismaMock.leadCaptura.findMany.mockResolvedValue([
|
||||
capturaFake({ leadId: 'loc_l1', board: 'LEADS', etapa: 'NUEVO', pendienteSync: true }),
|
||||
capturaFake({ leadId: 'loc_o1', board: 'OPORTUNIDADES', etapa: 'Afluencia', pendienteSync: true }),
|
||||
@@ -109,15 +101,16 @@ describe('resumenMiDiaEndpoint (modo local y unión)', () => {
|
||||
|
||||
expect(mondayMock.getLeads).not.toHaveBeenCalled()
|
||||
expect(mondayMock.getOportunidades).not.toHaveBeenCalled()
|
||||
expect(mondayMock.getApartados).not.toHaveBeenCalled()
|
||||
expect(mondayMock.getUsuariosWorkspace).not.toHaveBeenCalled()
|
||||
const resumen = json.mock.calls[0]?.[0] as Record<string, unknown>
|
||||
expect((resumen["misLeads"] as unknown[]).length).toBe(1) // solo leads, no oportunidades
|
||||
expect((resumen['misLeads'] as unknown[]).length).toBe(1) // solo leads, no oportunidades
|
||||
expect((resumen['misOportunidades'] as { items: unknown[] }).items).toHaveLength(1)
|
||||
expect(resumen['misActividadesHoy']).toEqual([])
|
||||
expect(resumen['conteos']).toMatchObject({ actividadesAbiertas: 0 })
|
||||
})
|
||||
|
||||
it('Monday se cae a media lectura (502): cae al resumen local', async () => {
|
||||
mondayMock.getLeads.mockRejectedValue(new AppError(502, 'Monday no respondió'))
|
||||
it('las capturas locales llegan con pendienteSync (badge ⏳ del front)', async () => {
|
||||
prismaMock.leadCaptura.findMany.mockResolvedValue([
|
||||
capturaFake({ leadId: 'loc_l1', board: 'LEADS', pendienteSync: true }),
|
||||
])
|
||||
@@ -126,41 +119,11 @@ describe('resumenMiDiaEndpoint (modo local y unión)', () => {
|
||||
await resumenMiDiaEndpoint(reqFake, res)
|
||||
|
||||
const resumen = json.mock.calls[0]?.[0] as Record<string, unknown>
|
||||
expect((resumen['misLeads'] as { id: string }[]).map((l) => l.id)).toEqual(['loc_l1'])
|
||||
expect((resumen['misLeads'] as { id: string; pendienteSync?: boolean }[]).map((l) => l.id)).toEqual(['loc_l1'])
|
||||
expect((resumen['misLeads'] as { pendienteSync?: boolean }[])[0]?.pendienteSync).toBe(true)
|
||||
})
|
||||
|
||||
it('Monday vivo: unión — las capturas pendientes se agregan con pendienteSync (badge ⏳)', async () => {
|
||||
mondayMock.getLeads.mockResolvedValue([
|
||||
{
|
||||
id: '9001',
|
||||
nombre: 'Lead Monday',
|
||||
createdAt: '2026-09-01T10:00:00.000Z',
|
||||
updatedAt: '2026-09-02T10:00:00.000Z',
|
||||
estado: 'Nuevo',
|
||||
origen: 'Web',
|
||||
telefono: '',
|
||||
email: '',
|
||||
vendedorIds: [123],
|
||||
},
|
||||
])
|
||||
prismaMock.leadCaptura.findMany.mockResolvedValue([
|
||||
capturaFake({ leadId: 'loc_n1', board: 'LEADS', etapa: 'NUEVO', pendienteSync: true, updatedAt: new Date('2026-09-05T10:00:00') }),
|
||||
])
|
||||
const { res, json } = resFake()
|
||||
|
||||
await resumenMiDiaEndpoint(reqFake, res)
|
||||
|
||||
const resumen = json.mock.calls[0]?.[0] as Record<string, unknown>
|
||||
const leads = resumen['misLeads'] as { id: string; pendienteSync?: boolean }[]
|
||||
expect(leads.map((l) => l.id)).toEqual(['loc_n1', '9001']) // el local, más reciente, primero
|
||||
expect(leads[0]?.pendienteSync).toBe(true)
|
||||
expect(leads[1]?.pendienteSync).toBeUndefined() // el de Monday no lleva flag
|
||||
// Y la captura quedó marcada en Mis capturas
|
||||
const capturas = resumen['misCapturas'] as { leadId: string; pendienteSync?: boolean; estado: string }[]
|
||||
expect(capturas.find((c) => c.leadId === 'loc_n1')).toMatchObject({ pendienteSync: true, estado: 'NUEVO' })
|
||||
})
|
||||
|
||||
it('Monday vivo y sin pendientes: respuesta idéntica a la de hoy (sin flags)', async () => {
|
||||
it('sin capturas: listas vacías y sin flags de sync', async () => {
|
||||
const { res, json } = resFake()
|
||||
|
||||
await resumenMiDiaEndpoint(reqFake, res)
|
||||
@@ -170,8 +133,20 @@ describe('resumenMiDiaEndpoint (modo local y unión)', () => {
|
||||
expect(JSON.stringify(resumen)).not.toContain('pendienteSync')
|
||||
})
|
||||
|
||||
it('un error no-Monday se propaga (no se traga nada)', async () => {
|
||||
mondayMock.getLeads.mockRejectedValue(new AppError(500, 'bug'))
|
||||
it('los usuarios del equipo solo se consultan para roles coordinador/admin/corporativo', async () => {
|
||||
await resumenMiDiaEndpoint(reqFake, resFake().res)
|
||||
expect(prismaMock.usuario.findMany).not.toHaveBeenCalled()
|
||||
|
||||
prismaMock.usuario.findUnique.mockResolvedValue({ ...usuarioFake, rol: 'coordinador' })
|
||||
await resumenMiDiaEndpoint(reqFake, resFake().res)
|
||||
expect(prismaMock.usuario.findMany).toHaveBeenCalledWith({
|
||||
where: { activo: true },
|
||||
select: { id: true, nombre: true, rol: true, activo: true },
|
||||
})
|
||||
})
|
||||
|
||||
it('un error de BD se propaga (no se traga nada)', async () => {
|
||||
prismaMock.leadCaptura.findMany.mockRejectedValue(new AppError(500, 'bug'))
|
||||
await expect(resumenMiDiaEndpoint(reqFake, resFake().res)).rejects.toThrow('bug')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user