feat(alta): guardado progresivo por etapa con réplica a Monday

- Al avanzar cada paso del wizard: upsert silencioso del borrador local
  + sync a Monday (iniciar → Contacto en PROSPECTO; pasos siguientes →
  PATCH con los campos mapeables: datos generales, dirección armada,
  actividad económica)
- Al completar: Contacto pasa a CLIENTE + Apartado + funnel (dedup)
- progresoId viaja con el borrador (se restaura solo); fallos de sync
  no bloquean el avance (aviso ámbar no bloqueante); submit final con
  fallback al endpoint legado si nunca hubo sync
- API: POST /clientes/progreso/iniciar, PATCH /clientes/progreso/:id,
  POST /clientes/progreso/:id/completar; POST /clientes legado intacto
- Tests: API 338 (+16), alta-clientes 112 (+4); smoke en vivo
  verificado (PROSPECTO → patch → CLIENTE + Apartado) y limpiado
This commit is contained in:
2026-09-01 21:03:47 +00:00
parent cd93aec91d
commit ccd4ca78ca
11 changed files with 876 additions and 40 deletions

View File

@@ -32,16 +32,16 @@ const serializarAlta = (payload: AltaCliente) => ({
})
/**
* Registra el alta en el API Gateway (POST /clientes). El backend persiste en
* SQLite y replica en Monday. Si el API no responde devuelve motivo 'conexion'
* y el flujo continúa solo local: los PDFs se generan igual. Si el API rechaza
* por validación (400) devuelve motivo 'validacion' con el detalle.
* POST JSON del alta contra `ruta` (`/clientes` o `/clientes/progreso/:id/completar`).
* Si el API no responde devuelve motivo 'conexion' y el flujo continúa solo
* local: los PDFs se generan igual. Si el API rechaza por validación (400)
* devuelve motivo 'validacion' con el detalle.
*/
export async function crearAltaCliente(payload: AltaCliente): Promise<ResultadoAlta> {
async function enviarAlta(ruta: string, payload: AltaCliente): Promise<ResultadoAlta> {
let res: Response
try {
res = await fetch(
`${API_URL}/clientes`,
`${API_URL}${ruta}`,
conAuth({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -49,7 +49,7 @@ export async function crearAltaCliente(payload: AltaCliente): Promise<ResultadoA
}),
)
} catch (err) {
console.warn('[api] crearAltaCliente: sin conexión al servidor, el expediente se genera solo localmente', err)
console.warn('[api] alta: sin conexión al servidor, el expediente se genera solo localmente', err)
return { ok: false, motivo: 'conexion' }
}
manejar401(res)
@@ -63,13 +63,65 @@ export async function crearAltaCliente(payload: AltaCliente): Promise<ResultadoA
issues?: { path: (string | number)[]; message: string }[]
} | null
const detalle = cuerpo?.issues?.map((i) => `${i.path.join('.')}: ${i.message}`).join(' · ')
console.warn('[api] crearAltaCliente: validación rechazada', detalle)
console.warn('[api] alta: validación rechazada', detalle)
return { ok: false, motivo: 'validacion', detalle: detalle ?? 'Datos incompletos o inválidos' }
}
console.warn(`[api] crearAltaCliente: HTTP ${res.status}`)
console.warn(`[api] alta: HTTP ${res.status}`)
return { ok: false, motivo: 'conexion' }
}
/**
* Registra el alta en el API Gateway (POST /clientes). El backend persiste en
* SQLite y replica en Monday.
*/
export async function crearAltaCliente(payload: AltaCliente): Promise<ResultadoAlta> {
return enviarAlta('/clientes', payload)
}
// ---------- Progreso del alta etapa por etapa (auth) ----------
export interface ProgresoIniciado {
id: string
mondayContactoId: string | null
}
/** POST /clientes/progreso/iniciar — crea el alta en progreso (Contacto PROSPECTO en Monday) */
export async function iniciarProgreso(payload: {
datosGenerales: unknown
leadMondayId?: string
}): Promise<ProgresoIniciado> {
const res = await fetch(
`${API_URL}/clientes/progreso/iniciar`,
conAuth({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}),
)
manejar401(res)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return (await res.json()) as ProgresoIniciado
}
/** PATCH /clientes/progreso/:id — guarda el avance del wizard y lo replica en el Contacto */
export async function actualizarProgreso(id: string, datos: unknown): Promise<void> {
const res = await fetch(
`${API_URL}/clientes/progreso/${id}`,
conAuth({
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(datos),
}),
)
manejar401(res)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
}
/** POST /clientes/progreso/:id/completar — cierra el alta (mismo contrato que POST /clientes) */
export async function completarProgreso(id: string, payload: AltaCliente): Promise<ResultadoAlta> {
return enviarAlta(`/clientes/progreso/${id}/completar`, payload)
}
/**
* Sube los documentos reales del expediente (POST multipart
* /clientes/:id/documentos, campo 'archivos'). El backend acepta un solo