fix(auditoria): afluencia (validacion, update sin perder datos ni retroceder etapa, dedup cruzado, notas), alta cliente (registro tras firma, documentos por cliente, aviso de fallos) y reporte de clientes aplanado
This commit is contained in:
@@ -54,7 +54,7 @@ describe('services/api', () => {
|
||||
expect(resultado).toEqual({ ok: true, id: 'srv-123' })
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
|
||||
expect(url).toBe('http://192.168.10.123:3000/api/clientes')
|
||||
expect(url).toBe('/api/clientes')
|
||||
expect(init.method).toBe('POST')
|
||||
expect(init.headers).toEqual({ 'Content-Type': 'application/json' })
|
||||
|
||||
@@ -119,7 +119,7 @@ describe('services/api', () => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(2)
|
||||
|
||||
const [url1, init1] = fetchMock.mock.calls[0] as [string, RequestInit]
|
||||
expect(url1).toBe('http://192.168.10.123:3000/api/clientes/srv-123/documentos')
|
||||
expect(url1).toBe('/api/clientes/srv-123/documentos')
|
||||
expect(init1.method).toBe('POST')
|
||||
// Sin Content-Type manual: el navegador pone el boundary del multipart
|
||||
expect(init1.headers).toBeUndefined()
|
||||
@@ -143,11 +143,13 @@ describe('services/api', () => {
|
||||
expect(fetchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('ante un fallo de red avisa y continúa sin lanzar', async () => {
|
||||
it('ante un fallo de red avisa y devuelve los documentos que no subieron', async () => {
|
||||
fetchMock.mockRejectedValue(new TypeError('fetch failed'))
|
||||
|
||||
await expect(subirDocumentos('srv-123', altaConDocumentos())).resolves.toBeUndefined()
|
||||
expect(warnSpy).toHaveBeenCalledTimes(2)
|
||||
// No lanza: el alta ya quedó registrada; se reportan los fallos al usuario
|
||||
const { fallidos } = await subirDocumentos('srv-123', altaConDocumentos())
|
||||
expect(fallidos.length).toBeGreaterThanOrEqual(1)
|
||||
expect(warnSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -126,10 +126,17 @@ export async function completarProgreso(id: string, payload: AltaCliente): Promi
|
||||
* Sube los documentos reales del expediente (POST multipart
|
||||
* /clientes/:id/documentos, campo 'archivos'). El backend acepta un solo
|
||||
* `tipo` por petición, así que se envía una petición por documento con su
|
||||
* slot como tipo. Los fallos se reportan con console.warn sin interrumpir
|
||||
* el flujo local.
|
||||
* slot como tipo.
|
||||
*
|
||||
* Devuelve los documentos que NO se pudieron subir: el alta ya quedó
|
||||
* registrada, así que el usuario debe enterarse para reintentar (antes se
|
||||
* perdían en silencio).
|
||||
*/
|
||||
export async function subirDocumentos(clienteId: string, payload: AltaCliente): Promise<void> {
|
||||
export async function subirDocumentos(
|
||||
clienteId: string,
|
||||
payload: AltaCliente,
|
||||
): Promise<{ fallidos: { slot: string; nombre: string }[] }> {
|
||||
const fallidos: { slot: string; nombre: string }[] = []
|
||||
for (const doc of payload.documentos) {
|
||||
try {
|
||||
const form = new FormData()
|
||||
@@ -146,8 +153,10 @@ export async function subirDocumentos(clienteId: string, payload: AltaCliente):
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
} catch (err) {
|
||||
console.warn(`[api] subirDocumentos: no se pudo subir ${doc.nombre} (${doc.slot})`, err)
|
||||
fallidos.push({ slot: doc.slot, nombre: doc.nombre })
|
||||
}
|
||||
}
|
||||
return { fallidos }
|
||||
}
|
||||
|
||||
// ---------- Borradores del alta (auth) ----------
|
||||
@@ -264,6 +273,7 @@ export interface LeadCreado {
|
||||
export type ResultadoCrearLead =
|
||||
| { ok: true; lead: LeadCreado }
|
||||
| { ok: false; motivo: 'duplicado'; leadId: string; nombre: string }
|
||||
| { ok: false; motivo: 'validacion'; detalle: string }
|
||||
| { ok: false; motivo: 'conexion' }
|
||||
|
||||
/**
|
||||
@@ -308,13 +318,25 @@ export async function crearAfluencia(payload: {
|
||||
return { ok: false, motivo: 'conexion' }
|
||||
}
|
||||
manejar401(res)
|
||||
if (res.status === 201) {
|
||||
// 201 = afluencia nueva; 200 = actualización de una afluencia existente
|
||||
if (res.status === 201 || res.status === 200) {
|
||||
return { ok: true, lead: (await res.json()) as LeadCreado }
|
||||
}
|
||||
if (res.status === 409) {
|
||||
const cuerpo = (await res.json()) as { leadId: string; nombre: string }
|
||||
return { ok: false, motivo: 'duplicado', leadId: cuerpo.leadId, nombre: cuerpo.nombre }
|
||||
}
|
||||
if (res.status === 400) {
|
||||
// Validación del servidor: se muestra el motivo real, no "sin conexión"
|
||||
const cuerpo = (await res.json().catch(() => null)) as
|
||||
| { error?: string; issues?: { message?: string; path?: (string | number)[] }[] }
|
||||
| null
|
||||
const detalle =
|
||||
cuerpo?.issues?.map((i) => `${i.path?.join('.') ?? ''}: ${i.message ?? ''}`).filter(Boolean).join(' · ') ||
|
||||
cuerpo?.error ||
|
||||
'Datos incompletos o inválidos'
|
||||
return { ok: false, motivo: 'validacion', detalle }
|
||||
}
|
||||
console.warn(`[api] crearAfluencia: HTTP ${res.status}`)
|
||||
return { ok: false, motivo: 'conexion' }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user