feat: API Gateway (Express 5 + Prisma + Monday) y conexion de apps al API

- api-gateway: servicio Monday (retry 429/complejidad, paginacion cursor,
  column IDs por titulo), endpoints unidades/cotizaciones/clientes/planes,
  webhook con handshake challenge, Prisma SQLite dev, 55 tests
- cotizador: inventario desde /api/unidades con fallback a seed
- alta-clientes: POST /api/clientes + upload multipart de documentos
- cobranza: persistencia via API con respaldo offline en localStorage
- .env.example con board IDs del workspace Frasa-CAS
This commit is contained in:
2026-08-21 23:39:17 +00:00
parent ecd4edac02
commit 0546f35d2a
48 changed files with 7129 additions and 66 deletions

View File

@@ -1,21 +1,71 @@
import type { AltaCliente } from '../types'
import { uuid } from '../lib/uuid'
const API_URL = import.meta.env.VITE_API_URL ?? 'http://192.168.10.123:3000/api'
// TODO produccion: '/api' same-origin detras de NGINX
export interface AltaClienteCreada {
id: string
}
/** Registra el alta de cliente en el backend */
export async function crearAltaCliente(payload: AltaCliente): Promise<AltaClienteCreada> {
// TODO: POST /api/clientes
await new Promise((resolve) => setTimeout(resolve, 400))
console.log('[api] crearAltaCliente (stub):', payload.datosGenerales.nombreCompleto)
return { id: uuid() }
interface DocumentoMeta {
tipo: string
nombre: string
tamano: number
mime: string
}
/** Sube los documentos del expediente del cliente */
export async function subirDocumentos(clienteId: string, payload: AltaCliente): Promise<void> {
// TODO: POST multipart a /api/clientes/:id/documentos
await new Promise((resolve) => setTimeout(resolve, 200))
console.log(`[api] subirDocumentos (stub) cliente=${clienteId}:`, payload.documentos.length, 'archivos')
/**
* Serializa el alta para el POST JSON: los File no viajan en el cuerpo,
* solo sus metadatos; la firma ya es un string base64 (data URL).
*/
const serializarAlta = (payload: AltaCliente) => ({
...payload,
documentos: payload.documentos.map(
(d): DocumentoMeta => ({ tipo: d.slot, nombre: d.nombre, tamano: d.tamano, mime: d.tipoMime }),
),
})
/**
* 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.
*/
export async function crearAltaCliente(payload: AltaCliente): Promise<AltaClienteCreada | null> {
try {
const res = await fetch(`${API_URL}/clientes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(serializarAlta(payload)),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = (await res.json()) as { id: string }
return { id: data.id }
} catch (err) {
console.warn('[api] crearAltaCliente: sin conexión al servidor, el expediente se genera solo localmente', err)
return null
}
}
/**
* 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.
*/
export async function subirDocumentos(clienteId: string, payload: AltaCliente): Promise<void> {
for (const doc of payload.documentos) {
try {
const form = new FormData()
form.append('tipo', doc.slot)
form.append('archivos', doc.archivo, doc.nombre)
const res = await fetch(`${API_URL}/clientes/${clienteId}/documentos`, {
method: 'POST',
body: form,
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
} catch (err) {
console.warn(`[api] subirDocumentos: no se pudo subir ${doc.nombre} (${doc.slot})`, err)
}
}
}