feat(pendientes): cotizador (mensualidades, bloqueo de unidad, totales, numMensualidades), cobranza (recibos, atrasos, mora configurable, recordatorios), soporte (adjuntos, filtros, estados), clientes maestro (catalogos reales), contactos (ficha y duplicados) y permisos de captura por rol

This commit is contained in:
2026-09-21 08:34:53 +00:00
parent b5a20aed4e
commit 2fc80e7f9e
66 changed files with 3792 additions and 436 deletions

View File

@@ -379,3 +379,68 @@ describe('mis cobranzas (toggle Mis planes / Todos)', () => {
expect(r.planes.length).toBeGreaterThan(0)
})
})
describe('edición del plan y alcance sin planes asignados', () => {
it('sinPlanesAsignados: solo cuando "míos" vino vacío desde el API', async () => {
const { sinPlanesAsignados } = await import('./repository')
expect(sinPlanesAsignados({ planes: [], origen: 'api', planesSoloLocales: false }, true)).toBe(true)
// con resultados, con vista "todos" o en modo offline no aplica
expect(sinPlanesAsignados({ planes: [planLocal()], origen: 'api', planesSoloLocales: false }, true)).toBe(false)
expect(sinPlanesAsignados({ planes: [], origen: 'api', planesSoloLocales: false }, false)).toBe(false)
expect(sinPlanesAsignados({ planes: [], origen: 'local', planesSoloLocales: false }, true)).toBe(false)
})
it('puedeEditarMora: admin/manager/dirección sí; vendedor y roles de operación no', async () => {
const { puedeEditarMora } = await import('./repository')
expect(puedeEditarMora('admin')).toBe(true)
expect(puedeEditarMora('manager')).toBe(true)
expect(puedeEditarMora('direccion_general')).toBe(true)
expect(puedeEditarMora('director_ventas')).toBe(true)
expect(puedeEditarMora('coordinador')).toBe(true) // legacy de dirección
expect(puedeEditarMora('vendedor')).toBe(false)
expect(puedeEditarMora('administracion_venta')).toBe(false)
expect(puedeEditarMora('escrituracion')).toBe(false)
expect(puedeEditarMora(null)).toBe(false)
expect(puedeEditarMora(undefined)).toBe(false)
})
it('actualizarPlanAsync hace PATCH /planes/:id con la tasa de mora', async () => {
const { actualizarPlanAsync } = await import('./repository')
const fetchMock = fetchQue(async () => respuestaJson(planApi({ tasaMoraMensual: 2.5 })))
vi.stubGlobal('fetch', fetchMock)
const r = await actualizarPlanAsync('api-1', { tasaMoraMensual: 2.5 })
expect(r.origen).toBe('api')
expect(r.plan.tasaMoraMensual).toBe(2.5)
expect(fetchMock).toHaveBeenCalledWith(
`${API_URL}/planes/api-1`,
expect.objectContaining({ method: 'PATCH', body: JSON.stringify({ tasaMoraMensual: 2.5 }) }),
)
})
it('actualizarPlanAsync propaga el 403 del API (rol sin permiso) sin tocar localStorage', async () => {
const { actualizarPlanAsync } = await import('./repository')
vi.stubGlobal(
'fetch',
fetchQue(async () => respuestaJson({ error: 'Solo admin, manager o dirección pueden modificar el plan' }, 403)),
)
await expect(actualizarPlanAsync('api-1', { tasaMoraMensual: 2 }, crearStorageStub())).rejects.toBeInstanceOf(ErrorApi)
})
it('actualizarPlanAsync cae a localStorage cuando falla la red', async () => {
const { actualizarPlanAsync } = await import('./repository')
vi.stubGlobal('fetch', fetchSinRed())
vi.spyOn(console, 'warn').mockImplementation(() => {})
const storage = crearStorageStub()
storage.setItem(CLAVE_STORAGE, JSON.stringify([planLocal()]))
const r = await actualizarPlanAsync('local-1', { tasaMoraMensual: 3 }, storage)
expect(r.origen).toBe('local')
expect(r.plan.tasaMoraMensual).toBe(3)
const guardado = JSON.parse(storage.getItem(CLAVE_STORAGE) ?? '[]') as PlanPago[]
expect(guardado[0].tasaMoraMensual).toBe(3)
})
})