diff --git a/api-gateway/src/controllers/clientes.controller.test.ts b/api-gateway/src/controllers/clientes.controller.test.ts index cba8cd1..421f419 100644 --- a/api-gateway/src/controllers/clientes.controller.test.ts +++ b/api-gateway/src/controllers/clientes.controller.test.ts @@ -73,3 +73,22 @@ describe('actividadEconomicaLabel', () => { expect(actividadEconomicaLabel(undefined)).toBeUndefined() }) }) + +describe('crearClienteSchema', () => { + const base = { + datosGenerales: { nombreCompleto: 'Cliente Demo', curp: 'CURP123', rfc: 'RFC123' }, + viviendaCredito: { proyecto: 'SENDA' }, + } + + it('acepta un alta sin ubicación (opcional en el formato web)', async () => { + const { crearClienteSchema } = await import('./clientes.controller.js') + expect(() => crearClienteSchema.parse(base)).not.toThrow() + }) + + it('sigue exigiendo nombre, curp, rfc y proyecto', async () => { + const { crearClienteSchema } = await import('./clientes.controller.js') + expect(() => + crearClienteSchema.parse({ datosGenerales: { nombreCompleto: '', curp: '', rfc: '' }, viviendaCredito: {} }), + ).toThrow() + }) +}) diff --git a/api-gateway/src/controllers/clientes.controller.ts b/api-gateway/src/controllers/clientes.controller.ts index 7df7e0b..e08d20f 100644 --- a/api-gateway/src/controllers/clientes.controller.ts +++ b/api-gateway/src/controllers/clientes.controller.ts @@ -11,7 +11,7 @@ import { NotFoundError, BadRequestError } from '../utils/errors.js' // ---------- Alta de clientes ---------- -const crearClienteSchema = z +export const crearClienteSchema = z .object({ datosGenerales: z .object({ @@ -32,7 +32,7 @@ const crearClienteSchema = z viviendaCredito: z .object({ proyecto: z.string().min(1), - ubicacion: z.string().min(1), + ubicacion: z.string().optional(), valorVenta: z.union([z.string(), z.number()]).optional(), enganche: z.union([z.string(), z.number()]).optional(), }) @@ -169,7 +169,7 @@ export async function crearCliente(req: Request, res: Response): Promise { email: dg.email ?? null, celular: dg.celular ?? null, proyecto: vc.proyecto, - unidadCodigo: vc.ubicacion, // la "ubicación" del formato es el código de unidad (U-101, etc.) + unidadCodigo: vc.ubicacion ?? '', // la "ubicación" del formato es el código de unidad (U-101, etc.) payloadJson: JSON.stringify(body), firmaPng: body.firma?.firmaDataUrl ?? null, }, @@ -199,7 +199,7 @@ export async function crearCliente(req: Request, res: Response): Promise { const enganche = aNumero(vc.enganche) mondayApartadoId = await mondayService.crearApartado({ clienteNombre: dg.nombreCompleto, - unidadCodigo: vc.ubicacion, + unidadCodigo: vc.ubicacion ?? '', ...(enganche > 0 ? { montoApartado: enganche } : {}), fechaApartado: hoyMas(0), fechaLimite: hoyMas(15), diff --git a/apps/alta-clientes/src/pages/AltaClientePage.tsx b/apps/alta-clientes/src/pages/AltaClientePage.tsx index e97869e..ebfc32a 100644 --- a/apps/alta-clientes/src/pages/AltaClientePage.tsx +++ b/apps/alta-clientes/src/pages/AltaClientePage.tsx @@ -16,18 +16,25 @@ export default function AltaClientePage() { useAltaCliente() const [registro, setRegistro] = useState('') const [sinConexion, setSinConexion] = useState(false) + const [errorValidacion, setErrorValidacion] = useState('') // Registra el alta en el API al llegar al paso de firmas; si el API no // responde, el flujo continúa solo local (los PDFs se generan igual) const registrarAlta = async () => { - const creada = await crearAltaCliente(alta) - if (creada) { - await subirDocumentos(creada.id, alta) - setRegistro(creada.id) + const res = await crearAltaCliente(alta) + if (res.ok) { + await subirDocumentos(res.id, alta) + setRegistro(res.id) setSinConexion(false) + setErrorValidacion('') + } else if (res.motivo === 'validacion') { + setRegistro('') + setSinConexion(false) + setErrorValidacion(res.detalle ?? 'Datos incompletos o inválidos') } else { setRegistro('') setSinConexion(true) + setErrorValidacion('') } } @@ -134,6 +141,14 @@ export default function AltaClientePage() { Sin conexión al servidor — el expediente se generó solo localmente

)} + {errorValidacion && ( +

+ El servidor rechazó el alta: {errorValidacion} +

+ )} actualizar('firma', f)} diff --git a/apps/alta-clientes/src/services/api.test.ts b/apps/alta-clientes/src/services/api.test.ts index 1b90416..8fa54df 100644 --- a/apps/alta-clientes/src/services/api.test.ts +++ b/apps/alta-clientes/src/services/api.test.ts @@ -51,7 +51,7 @@ describe('services/api', () => { const alta = altaConDocumentos() const resultado = await crearAltaCliente(alta) - expect(resultado).toEqual({ id: 'srv-123' }) + 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') @@ -71,23 +71,43 @@ describe('services/api', () => { ) }) - it('ante un fallo de red devuelve null y avisa con console.warn', async () => { + it('ante un fallo de red devuelve motivo conexion y avisa con console.warn', async () => { fetchMock.mockRejectedValue(new TypeError('fetch failed')) const resultado = await crearAltaCliente(altaConDocumentos()) - expect(resultado).toBeNull() + expect(resultado).toEqual({ ok: false, motivo: 'conexion' }) expect(warnSpy).toHaveBeenCalled() }) - it('ante una respuesta HTTP de error devuelve null y avisa', async () => { + it('ante una respuesta HTTP de error devuelve motivo conexion y avisa', async () => { fetchMock.mockResolvedValue(new Response('error', { status: 500 })) const resultado = await crearAltaCliente(altaConDocumentos()) - expect(resultado).toBeNull() + expect(resultado).toEqual({ ok: false, motivo: 'conexion' }) expect(warnSpy).toHaveBeenCalled() }) + + it('ante un 400 devuelve motivo validacion con el detalle de los issues (no es un fallo de red)', async () => { + fetchMock.mockResolvedValue( + new Response( + JSON.stringify({ + error: 'Validación fallida', + issues: [{ code: 'too_small', path: ['viviendaCredito', 'ubicacion'], message: 'Required' }], + }), + { status: 400, headers: { 'Content-Type': 'application/json' } }, + ), + ) + + const resultado = await crearAltaCliente(altaConDocumentos()) + + expect(resultado).toEqual({ + ok: false, + motivo: 'validacion', + detalle: 'viviendaCredito.ubicacion: Required', + }) + }) }) describe('subirDocumentos', () => { diff --git a/apps/alta-clientes/src/services/api.ts b/apps/alta-clientes/src/services/api.ts index e9df43a..afa6e58 100644 --- a/apps/alta-clientes/src/services/api.ts +++ b/apps/alta-clientes/src/services/api.ts @@ -8,6 +8,11 @@ export interface AltaClienteCreada { id: string } +/** Resultado del registro en el API: distingue fallo de red de rechazo por validación. */ +export type ResultadoAlta = + | { ok: true; id: string } + | { ok: false; motivo: 'conexion' | 'validacion'; detalle?: string } + interface DocumentoMeta { tipo: string nombre: string @@ -28,12 +33,14 @@ 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 null (con - * console.warn) y el flujo continúa solo local: los PDFs se generan igual. + * 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. */ -export async function crearAltaCliente(payload: AltaCliente): Promise { +export async function crearAltaCliente(payload: AltaCliente): Promise { + let res: Response try { - const res = await fetch( + res = await fetch( `${API_URL}/clientes`, conAuth({ method: 'POST', @@ -41,14 +48,26 @@ export async function crearAltaCliente(payload: AltaCliente): Promise null)) as { + 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) + return { ok: false, motivo: 'validacion', detalle: detalle ?? 'Datos incompletos o inválidos' } + } + console.warn(`[api] crearAltaCliente: HTTP ${res.status}`) + return { ok: false, motivo: 'conexion' } } /**