From 04de7d703fff7a1c13b02d3c82a7b29eef1e4899 Mon Sep 17 00:00:00 2001
From: Consult AS Technology
Date: Thu, 27 Aug 2026 18:51:51 +0000
Subject: [PATCH] =?UTF-8?q?fix(alta):=20error=20400=20del=20servidor=20ya?=
=?UTF-8?q?=20no=20se=20reporta=20como=20'sin=20conexi=C3=B3n'?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- API: viviendaCredito.ubicacion ahora opcional (la app web no la exigía;
el desfase causaba 400 que la app mostraba como fallo de red)
- App: crearAltaCliente distingue conexion/validacion; el 400 muestra
banner rojo con el detalle de los issues en vez del banner ámbar
- Tests: api 160, alta-clientes 66
---
.../controllers/clientes.controller.test.ts | 19 ++++++++++
.../src/controllers/clientes.controller.ts | 8 ++--
.../src/pages/AltaClientePage.tsx | 23 ++++++++++--
apps/alta-clientes/src/services/api.test.ts | 30 ++++++++++++---
apps/alta-clientes/src/services/api.ts | 37 ++++++++++++++-----
5 files changed, 95 insertions(+), 22 deletions(-)
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' }
}
/**