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

@@ -0,0 +1,121 @@
-- CreateTable
CREATE TABLE "Cotizacion" (
"id" TEXT NOT NULL PRIMARY KEY,
"unidadCodigo" TEXT NOT NULL,
"clienteNombre" TEXT NOT NULL,
"clienteEmail" TEXT,
"clienteTelefono" TEXT,
"tipoPropuesta" TEXT NOT NULL,
"acabados" BOOLEAN NOT NULL,
"apartado" REAL NOT NULL,
"descuento" REAL NOT NULL,
"precioLista" REAL NOT NULL,
"precioFinal" REAL NOT NULL,
"enganche" REAL NOT NULL,
"numMensualidades" INTEGER NOT NULL,
"mensualidad" REAL,
"pagoFinal" REAL NOT NULL,
"mondayItemId" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateTable
CREATE TABLE "Cliente" (
"id" TEXT NOT NULL PRIMARY KEY,
"nombreCompleto" TEXT NOT NULL,
"curp" TEXT NOT NULL,
"rfc" TEXT NOT NULL,
"email" TEXT,
"celular" TEXT,
"proyecto" TEXT NOT NULL,
"unidadCodigo" TEXT,
"payloadJson" TEXT NOT NULL,
"firmaPng" TEXT,
"mondayContactoId" TEXT,
"mondayApartadoId" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateTable
CREATE TABLE "Documento" (
"id" TEXT NOT NULL PRIMARY KEY,
"clienteId" TEXT NOT NULL,
"tipo" TEXT NOT NULL,
"nombreArchivo" TEXT NOT NULL,
"ruta" TEXT NOT NULL,
"mime" TEXT NOT NULL,
"tamano" INTEGER NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Documento_clienteId_fkey" FOREIGN KEY ("clienteId") REFERENCES "Cliente" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "PlanPago" (
"id" TEXT NOT NULL PRIMARY KEY,
"ventaRef" TEXT,
"clienteNombre" TEXT NOT NULL,
"clienteTelefono" TEXT,
"unidadCodigo" TEXT NOT NULL,
"proyecto" TEXT NOT NULL,
"fechaInicio" DATETIME NOT NULL,
"engancheTotal" REAL NOT NULL,
"numPagos" INTEGER NOT NULL,
"mensualidad" REAL NOT NULL,
"tasaMoraMensual" REAL NOT NULL DEFAULT 0,
"status" TEXT NOT NULL DEFAULT 'ACTIVO',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateTable
CREATE TABLE "Pago" (
"id" TEXT NOT NULL PRIMARY KEY,
"planId" TEXT NOT NULL,
"numPago" INTEGER NOT NULL,
"fechaVencimiento" DATETIME NOT NULL,
"monto" REAL NOT NULL,
"montoPagado" REAL NOT NULL DEFAULT 0,
CONSTRAINT "Pago_planId_fkey" FOREIGN KEY ("planId") REFERENCES "PlanPago" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "AplicacionPago" (
"id" TEXT NOT NULL PRIMARY KEY,
"pagoId" TEXT NOT NULL,
"fechaPago" DATETIME NOT NULL,
"monto" REAL NOT NULL,
"referencia" TEXT NOT NULL,
CONSTRAINT "AplicacionPago_pagoId_fkey" FOREIGN KEY ("pagoId") REFERENCES "Pago" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "NotaPlan" (
"id" TEXT NOT NULL PRIMARY KEY,
"planId" TEXT NOT NULL,
"fecha" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"texto" TEXT NOT NULL,
CONSTRAINT "NotaPlan_planId_fkey" FOREIGN KEY ("planId") REFERENCES "PlanPago" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateIndex
CREATE INDEX "Cotizacion_unidadCodigo_idx" ON "Cotizacion"("unidadCodigo");
-- CreateIndex
CREATE INDEX "Cliente_curp_idx" ON "Cliente"("curp");
-- CreateIndex
CREATE INDEX "Documento_clienteId_idx" ON "Documento"("clienteId");
-- CreateIndex
CREATE INDEX "PlanPago_unidadCodigo_idx" ON "PlanPago"("unidadCodigo");
-- CreateIndex
CREATE INDEX "Pago_planId_idx" ON "Pago"("planId");
-- CreateIndex
CREATE INDEX "Pago_fechaVencimiento_idx" ON "Pago"("fechaVencimiento");
-- CreateIndex
CREATE INDEX "AplicacionPago_pagoId_idx" ON "AplicacionPago"("pagoId");
-- CreateIndex
CREATE INDEX "NotaPlan_planId_idx" ON "NotaPlan"("planId");

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "sqlite"

View File

@@ -0,0 +1,123 @@
// FRASA API Gateway — persistencia local
// Desarrollo: SQLite. Producción: Postgres 16 (cambiar provider y DATABASE_URL).
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite" // prod: "postgresql"
url = env("DATABASE_URL")
}
model Cotizacion {
id String @id @default(uuid())
unidadCodigo String
clienteNombre String
clienteEmail String?
clienteTelefono String?
tipoPropuesta String // BANCARIO | INFONAVIT | CONTADO | SEMI_CONTADO
acabados Boolean
apartado Float // TODO prod: Decimal en Postgres
descuento Float // fracción (0.02 = 2%)
precioLista Float // TODO prod: Decimal en Postgres
precioFinal Float
enganche Float
numMensualidades Int
mensualidad Float? // null => N/A (Contado)
pagoFinal Float
mondayItemId String?
createdAt DateTime @default(now())
@@index([unidadCodigo])
}
model Cliente {
id String @id @default(uuid())
nombreCompleto String
curp String
rfc String
email String?
celular String?
proyecto String
unidadCodigo String?
// Payload completo del alta como JSON serializado (SQLite no tiene Json nativo;
// en Postgres puede migrarse a la columna Json sin cambiar el contrato).
payloadJson String
firmaPng String? // base64 opcional
mondayContactoId String?
mondayApartadoId String?
createdAt DateTime @default(now())
documentos Documento[]
@@index([curp])
}
model Documento {
id String @id @default(uuid())
clienteId String
cliente Cliente @relation(fields: [clienteId], references: [id], onDelete: Cascade)
tipo String // INE | CONSTANCIA_RFC | CURP | COMPROBANTE_DOMICILIO | ACTA_NACIMIENTO | OTRO
nombreArchivo String
ruta String
mime String
tamano Int
createdAt DateTime @default(now())
@@index([clienteId])
}
model PlanPago {
id String @id @default(uuid())
ventaRef String?
clienteNombre String
clienteTelefono String?
unidadCodigo String
proyecto String
fechaInicio DateTime // se normaliza a medianoche local
engancheTotal Float // TODO prod: Decimal en Postgres
numPagos Int
mensualidad Float
tasaMoraMensual Float @default(0)
status String @default("ACTIVO") // ACTIVO | CANCELADO (LIQUIDADO se deriva)
createdAt DateTime @default(now())
notas NotaPlan[]
pagos Pago[]
@@index([unidadCodigo])
}
model Pago {
id String @id @default(uuid())
planId String
plan PlanPago @relation(fields: [planId], references: [id], onDelete: Cascade)
numPago Int
fechaVencimiento DateTime
monto Float
montoPagado Float @default(0)
aplicaciones AplicacionPago[]
@@index([planId])
@@index([fechaVencimiento])
}
model AplicacionPago {
id String @id @default(uuid())
pagoId String
pago Pago @relation(fields: [pagoId], references: [id], onDelete: Cascade)
fechaPago DateTime
monto Float
referencia String
@@index([pagoId])
}
model NotaPlan {
id String @id @default(uuid())
planId String
plan PlanPago @relation(fields: [planId], references: [id], onDelete: Cascade)
fecha DateTime @default(now())
texto String
@@index([planId])
}