feat: mis cobranzas, recordatorios de pago y agente IA con acciones
- PlanPago.vendedorId + filtro ?mios=1; toggle Mis planes/Todos en /pay - Motor de recordatorios: próximos a vencer (3d) y vencidos (re-cada 7d), canal Monday Actividad + stub WhatsApp listo para WA CRM, log persistente, cron diario 9:30, run manual con dry-run, historial en /api/cobranza/recordatorios - Agente IA: memoria por usuario (conversaciones aisladas) y tool calling: cotizar_unidad (motor real por desarrollo) y buscar_unidad (inventario Monday) - Tests: api 261, cobranza 75, agente-ia 13
This commit is contained in:
@@ -335,3 +335,44 @@ describe('repository async (API primero, fallback localStorage)', () => {
|
||||
expect(guardados[0].status).toBe('CANCELADO')
|
||||
})
|
||||
})
|
||||
|
||||
describe('mis cobranzas (toggle Mis planes / Todos)', () => {
|
||||
it('rutaListadoPlanes agrega ?mios=1 solo cuando se piden los míos', async () => {
|
||||
const { rutaListadoPlanes } = await import('./repository')
|
||||
expect(rutaListadoPlanes(true)).toBe('/planes?mios=1')
|
||||
expect(rutaListadoPlanes(false)).toBe('/planes')
|
||||
})
|
||||
|
||||
it('miosPorDefecto: vendedor arranca en "Mis planes"; el resto en "Todos"', async () => {
|
||||
const { miosPorDefecto } = await import('./repository')
|
||||
expect(miosPorDefecto('vendedor')).toBe(true)
|
||||
expect(miosPorDefecto('admin')).toBe(false)
|
||||
expect(miosPorDefecto('coordinador')).toBe(false)
|
||||
expect(miosPorDefecto('corporativo')).toBe(false)
|
||||
expect(miosPorDefecto(null)).toBe(false)
|
||||
expect(miosPorDefecto(undefined)).toBe(false)
|
||||
})
|
||||
|
||||
it('listarPlanes con mios=true llama al API con ?mios=1', async () => {
|
||||
const fetchMock = fetchQue(async () => respuestaJson([planApi()]))
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
|
||||
const r = await listarPlanes(crearStorageStub(), true)
|
||||
|
||||
expect(r.origen).toBe('api')
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'http://192.168.10.123:3000/api/planes?mios=1',
|
||||
expect.objectContaining({ headers: { 'Content-Type': 'application/json' } }),
|
||||
)
|
||||
})
|
||||
|
||||
it('listarPlanes con mios=true cae a localStorage igual que sin el filtro', async () => {
|
||||
vi.stubGlobal('fetch', fetchSinRed())
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
const r = await listarPlanes(crearStorageStub(), true)
|
||||
|
||||
expect(r.origen).toBe('local') // el respaldo local no tiene vendedorId
|
||||
expect(r.planes.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -172,14 +172,34 @@ function planesLocalesSinSembrar(storage: StorageLike): PlanPago[] {
|
||||
return planes.map((p) => (p.notas ? p : { ...p, notas: [] }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruta del listado de planes: con `mios` agrega ?mios=1 (el API filtra por el
|
||||
* sub del token; admin/coordinador ven todo de todos modos).
|
||||
*/
|
||||
export function rutaListadoPlanes(mios: boolean): string {
|
||||
return mios ? '/planes?mios=1' : '/planes'
|
||||
}
|
||||
|
||||
/**
|
||||
* Vista inicial del toggle "Mis planes / Todos": los vendedores arrancan en
|
||||
* "Mis planes"; el resto de roles (admin, coordinador, corporativo) en "Todos".
|
||||
*/
|
||||
export function miosPorDefecto(rol: string | null | undefined): boolean {
|
||||
return rol === 'vendedor'
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /planes. Ante error de red cae a localStorage (sembrando si está vacío).
|
||||
* Si el API responde vacío pero hay planes locales, los conserva solo localmente
|
||||
* (NO se migran automáticamente) y lo señala en `planesSoloLocales`.
|
||||
* `mios` solo afecta la llamada al API; el respaldo local no tiene vendedorId.
|
||||
*/
|
||||
export async function listarPlanes(storage: StorageLike = storageDefault()): Promise<ResultadoPlanes> {
|
||||
export async function listarPlanes(
|
||||
storage: StorageLike = storageDefault(),
|
||||
mios = false,
|
||||
): Promise<ResultadoPlanes> {
|
||||
try {
|
||||
const planes = (await request<PlanApi[]>('/planes')).map(desdeApi)
|
||||
const planes = (await request<PlanApi[]>(rutaListadoPlanes(mios))).map(desdeApi)
|
||||
if (planes.length === 0) {
|
||||
const locales = planesLocalesSinSembrar(storage)
|
||||
if (locales.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user