Restaurar funcionalidades desde stash + setup de despliegue Coolify

- Recupera el trabajo revertido (doble propuesta, retainer, niveles, API Python) desde el stash de GitHub Desktop
- Dockerfile multi-stage para Next.js (migraciones automáticas al arrancar, seed opcional via RUN_SEED)
- docker-compose.coolify.yml: postgres + web + api con healthchecks y SERVICE_FQDN_*
- Endpoint público /api/health (verifica BD) para healthchecks
- seed.ts parametrizado: conexión DB_* y credenciales SEED_* por entorno (sin passwords hardcodeadas)
- .env.example, .dockerignore, uvicorn con --proxy-headers
- Limpieza: .xlsx y __pycache__ fuera del repo

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
2026-06-10 02:21:18 -06:00
co-authored by Claude Fable 5
parent c3f4449296
commit 69c74faabb
43 changed files with 3536 additions and 1139 deletions
+44 -21
View File
@@ -74,6 +74,8 @@ export async function PUT(
esquemaPago,
incluirBonos,
incluirFinanciamiento,
esDoble,
opciones,
observaciones,
cliente,
servicios,
@@ -86,15 +88,15 @@ export async function PUT(
return NextResponse.json({ error: "No encontrada" }, { status: 404 });
}
let clienteIdFinal: string | undefined;
if (cliente) {
const existingCliente = await prisma.cliente.findFirst({
where: { nombre: cliente.nombre, empresa: cliente.empresa || null },
});
let clienteId = existing.clienteId;
if (existingCliente) {
clienteId = existingCliente.id;
clienteIdFinal = existingCliente.id;
await prisma.cliente.update({
where: { id: clienteId },
where: { id: clienteIdFinal },
data: {
email: cliente.email || existingCliente.email,
telefono: cliente.telefono || existingCliente.telefono,
@@ -109,18 +111,16 @@ export async function PUT(
telefono: cliente.telefono || null,
},
});
clienteId = newCliente.id;
clienteIdFinal = newCliente.id;
}
await prisma.cotizacion.update({
where: { id },
data: { clienteId },
});
}
const esDobleFinal = esDoble ?? existing.esDoble;
await prisma.cotizacion.update({
where: { id },
data: {
...(clienteIdFinal && { clienteId: clienteIdFinal }),
...(fecha && { fecha: new Date(fecha) }),
...(vigencia && { vigencia: new Date(vigencia) }),
...(moneda && { moneda }),
@@ -129,35 +129,58 @@ export async function PUT(
...(esquemaPago && { esquemaPago }),
...(incluirBonos !== undefined && { incluirBonos }),
...(incluirFinanciamiento !== undefined && { incluirFinanciamiento }),
...(esDoble !== undefined && { esDoble }),
...(esDoble !== undefined && { opcionesMetadata: esDoble ? opciones ?? {} : undefined }),
...(observaciones !== undefined && { observaciones }),
...(estado && ESTADOS_COTIZACION.includes(estado as typeof ESTADOS_COTIZACION[number]) && { estado }),
},
});
if (servicios && Array.isArray(servicios)) {
// El servicio Bucéfalo del catálogo se resuelve una sola vez, no por partida.
const necesitaBucefalo = servicios.some(
(s) => !s.esPersonalizado && s.catalogoId?.startsWith("bucefalo-")
);
const bucefaloCatalogoId = necesitaBucefalo
? (await prisma.servicioCatalogo.findFirst({
where: { categoriaRel: { nombre: "CRM" }, tipoPago: "mensual" },
select: { id: true },
}))?.id ?? null
: null;
await prisma.$transaction(async (tx) => {
await tx.servicioCotizado.deleteMany({ where: { cotizacionId: id } });
for (const serv of servicios) {
const catalogoId = serv.catalogoId?.startsWith("bucefalo-")
? (await tx.servicioCatalogo.findFirst({
where: { categoriaRel: { nombre: "CRM" }, tipoPago: "mensual" },
}))?.id || serv.catalogoId
: serv.catalogoId;
await tx.servicioCotizado.create({
data: {
await tx.servicioCotizado.createMany({
data: servicios.map((serv) => {
let catalogoId: string | null;
if (serv.esPersonalizado) {
catalogoId = null;
} else if (serv.catalogoId?.startsWith("bucefalo-")) {
catalogoId = bucefaloCatalogoId;
} else {
catalogoId = serv.catalogoId;
}
return {
cotizacionId: id,
servicioCatalogoId: catalogoId,
nombre: serv.esPersonalizado ? serv.nombre : null,
esPersonalizado: serv.esPersonalizado ?? false,
horas: serv.esPersonalizado ? serv.horas ?? null : null,
tarifaHora: serv.esPersonalizado ? serv.tarifaHora ?? null : null,
modeloCobro: serv.modeloCobro ?? (serv.esPersonalizado ? "horas" : "fijo"),
montoMinimo: serv.esPersonalizado ? serv.montoMinimo ?? null : null,
horasIncluidas: serv.esPersonalizado ? serv.horasIncluidas ?? null : null,
opcion: esDobleFinal ? serv.opcion ?? "ambas" : null,
fase: serv.fase,
tipoPago: serv.tipoPago,
precio: serv.precio,
tiempoEntrega: serv.tiempoEntrega,
entregables: serv.entregables,
seleccionado: true,
},
});
}
};
}),
});
});
}
+38 -11
View File
@@ -22,6 +22,8 @@ export async function POST(request: NextRequest) {
esquemaPago,
incluirBonos,
incluirFinanciamiento,
esDoble,
opciones,
observaciones,
cliente,
asesorId,
@@ -49,6 +51,18 @@ export async function POST(request: NextRequest) {
const clienteIdFinal = clienteRecord.id;
// El servicio Bucéfalo del catálogo es el mismo para todas las partidas:
// se resuelve una sola vez fuera del loop de inserción.
const necesitaBucefalo = servicios.some(
(s) => !s.esPersonalizado && s.catalogoId.startsWith("bucefalo-")
);
const bucefaloCatalogoId = necesitaBucefalo
? (await prisma.servicioCatalogo.findFirst({
where: { categoriaRel: { nombre: "CRM" }, tipoPago: "mensual" },
select: { id: true },
}))?.id ?? null
: null;
const cotizacion = await prisma.$transaction(async (tx) => {
const cot = await tx.cotizacion.create({
data: {
@@ -61,6 +75,8 @@ export async function POST(request: NextRequest) {
esquemaPago,
incluirBonos,
incluirFinanciamiento,
esDoble: esDoble ?? false,
opcionesMetadata: esDoble ? opciones ?? {} : undefined,
observaciones: observaciones || null,
clienteId: clienteIdFinal,
asesorId,
@@ -68,25 +84,36 @@ export async function POST(request: NextRequest) {
},
});
for (const servicio of servicios) {
const catalogoId = servicio.catalogoId.startsWith("bucefalo-")
? (await tx.servicioCatalogo.findFirst({
where: { categoriaRel: { nombre: "CRM" }, tipoPago: "mensual" },
}))?.id || servicio.catalogoId
: servicio.catalogoId;
await tx.servicioCotizado.create({
data: {
await tx.servicioCotizado.createMany({
data: servicios.map((servicio) => {
let catalogoId: string | null;
if (servicio.esPersonalizado) {
catalogoId = null;
} else if (servicio.catalogoId.startsWith("bucefalo-")) {
catalogoId = bucefaloCatalogoId;
} else {
catalogoId = servicio.catalogoId;
}
return {
cotizacionId: cot.id,
servicioCatalogoId: catalogoId,
nombre: servicio.esPersonalizado ? servicio.nombre : null,
esPersonalizado: servicio.esPersonalizado ?? false,
horas: servicio.esPersonalizado ? servicio.horas ?? null : null,
tarifaHora: servicio.esPersonalizado ? servicio.tarifaHora ?? null : null,
modeloCobro: servicio.modeloCobro ?? (servicio.esPersonalizado ? "horas" : "fijo"),
montoMinimo: servicio.esPersonalizado ? servicio.montoMinimo ?? null : null,
horasIncluidas: servicio.esPersonalizado ? servicio.horasIncluidas ?? null : null,
opcion: esDoble ? servicio.opcion ?? "ambas" : null,
fase: servicio.fase,
tipoPago: servicio.tipoPago,
precio: servicio.precio,
tiempoEntrega: servicio.tiempoEntrega,
entregables: servicio.entregables,
seleccionado: true,
},
});
}
};
}),
});
if (planBucefalo) {
await tx.planBucefaloCotizacion.create({