This commit is contained in:
2026-06-10 02:16:08 -06:00
parent 4334052267
commit c3f4449296
10 changed files with 284 additions and 23 deletions
+19
View File
@@ -0,0 +1,19 @@
node_modules
.next
.git
.claude
api
docs
cotizador
*.xlsx
*.bat
.env*
!.env.example
*.tsbuildinfo
*.tmp.*
Dockerfile
docker-compose*.yml
README.md
AGENTS.md
CLAUDE.md
src/generated
+29
View File
@@ -0,0 +1,29 @@
# Cotizador E3 — variables de entorno
# Copia este archivo a .env (local) o configúralas en Coolify (producción).
# ── Base de datos (compartida por Next.js y el API Python) ──
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=cotizador_e3
# Solo la usa el CLI de Prisma (migraciones). Debe coincidir con DB_*.
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/cotizador_e3"
# ── Auth ──
# OBLIGATORIA. Genera una con: openssl rand -base64 32
JWT_SECRET=
# ── API Python (api/) — clave para agentes de IA / n8n ──
API_KEY=
# ── Seed (catálogo + usuarios). En producción: RUN_SEED=true solo el primer despliegue ──
RUN_SEED=false
SEED_ADMIN_EMAIL=[email protected]
SEED_ADMIN_PASSWORD=
SEED_ADMIN_NAME=Administrador
# Asesor opcional: solo se crea si defines el email
SEED_ASESOR_EMAIL=
SEED_ASESOR_PASSWORD=
SEED_ASESOR_NAME=
+9
View File
@@ -46,6 +46,15 @@
- **Bucéfalo CRM plan prices** (in `calculators.ts`, NOT the DB): basico=$1,000, estandar=$3,500, premium=$4,500, empresarial=$7,500 (monthly).
- **Financing** lives in the `FinanciamientoPlan` table (3/6/9/12 months). Formula: `comisionTotal = monto × comision%`, `pagoMensual = (monto + comisionTotal) × (1 + tasa) / meses`, then add 16% IVA. Both backends must keep this formula identical.
## Deployment (Coolify)
- Production stack: `docker-compose.coolify.yml` (postgres + web + api). In Coolify set **Docker Compose Location** to `/docker-compose.coolify.yml`. Local dev keeps using `docker-compose.yml` + `start.bat`.
- The `web` container runs `prisma migrate deploy` on every start, and the seed only when `RUN_SEED=true`. Seed users/passwords come from `SEED_*` env vars (see `.env.example`) — `prisma/seed.ts` never overwrites an existing password unless the env var is set.
- `DATABASE_URL` is only used by the Prisma CLI (migrations); the apps use individual `DB_*` vars. Both must point to the same DB.
- Healthchecks: web `GET /api/health` (public in middleware, pings DB), api `GET /health`.
- Root `Dockerfile` builds Next.js with `JWT_SECRET=placeholder-build-only` (auth.ts throws at import without it; all pages are force-dynamic so nothing gets baked).
- `*.xlsx` is gitignored (business files must never be committed).
## Architecture
```
+44
View File
@@ -0,0 +1,44 @@
# Cotizador E3 — Next.js (web)
# Multi-stage build. El runner conserva node_modules completo para que
# `prisma migrate deploy` y el seed (tsx) funcionen dentro del contenedor.
FROM node:22-slim AS base
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
# ── Dependencias ──
FROM base AS deps
COPY package.json package-lock.json ./
RUN npm ci
# ── Build ──
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Placeholder solo para el build: src/lib/auth.ts lanza error al importarse
# sin JWT_SECRET. Todas las páginas son force-dynamic, nada queda horneado.
ENV JWT_SECRET=placeholder-build-only
RUN npx prisma generate && npm run build
# ── Runtime ──
FROM base AS runner
ENV NODE_ENV=production
ENV PORT=3000
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/src/generated ./src/generated
COPY --from=builder /app/prisma.config.ts ./prisma.config.ts
COPY --from=builder /app/next.config.ts ./next.config.ts
COPY --from=builder /app/tsconfig.json ./tsconfig.json
COPY --from=builder /app/package.json ./package.json
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=5 \
CMD node -e "fetch('http://localhost:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
# Migraciones primero, seed opcional (RUN_SEED=true), luego el servidor.
CMD ["sh", "-c", "npx prisma migrate deploy && if [ \"$RUN_SEED\" = \"true\" ]; then npx tsx prisma/seed.ts; fi && exec node_modules/.bin/next start -p ${PORT:-3000}"]
+55 -21
View File
@@ -1,36 +1,70 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# Cotizador E3
## Getting Started
Sistema de cotizaciones de Consultoría E3. Dos backends sobre la misma base de datos PostgreSQL:
First, run the development server:
- **Web (Next.js 16)** — `src/`, puerto 3000. UI completa: cotizaciones, clientes, catálogo, configuración, export PDF/Excel. Prisma es dueño del esquema y las migraciones.
- **API (FastAPI + MCP)** — `api/`, puerto 8000. REST para integraciones (n8n, agentes de IA) con servidor MCP en `/mcp`. Referencia completa en `api/COTIZADOR_API_SKILL.md`.
## Desarrollo local (Windows)
Requisitos: Node 20+, Docker Desktop.
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
# 1. Copia las variables de entorno
copy .env.example .env # y define JWT_SECRET
# 2. Levanta PostgreSQL + API Python y el dev server de Next.js
start.bat
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
`start.bat` levanta el compose local (`docker-compose.yml`), aplica migraciones y arranca `npm run dev`. Para detener todo: `stop.bat`.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
Comandos útiles:
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
| Comando | Propósito |
|---------|-----------|
| `npm run dev` | Dev server Next.js (puerto 3000) |
| `npm run build` | Build de producción |
| `npx prisma migrate dev` | Crear/aplicar migración |
| `npx tsx prisma/seed.ts` | Seed (idempotente; usuarios vía `SEED_*`) |
| `npx prisma studio` | GUI de la base de datos |
## Learn More
## Despliegue en Coolify
To learn more about Next.js, take a look at the following resources:
El stack de producción está en [docker-compose.coolify.yml](docker-compose.coolify.yml): `postgres` + `web` (Next.js) + `api` (FastAPI). El contenedor `web` aplica las migraciones de Prisma automáticamente en cada arranque.
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
### Pasos
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
1. **Sube el repo a GitHub** (privado recomendado).
2. En Coolify: **+ New Resource → Docker Compose**, conecta el repo y la rama.
3. En la configuración del recurso, define **Docker Compose Location** = `/docker-compose.coolify.yml`.
4. Coolify detecta los servicios y asigna dominio a `web` (puerto 3000) y `api` (puerto 8000) vía las variables `SERVICE_FQDN_*` — configura los dominios deseados en la UI.
5. Define las variables de entorno en Coolify:
## Deploy on Vercel
| Variable | Obligatoria | Notas |
|----------|-------------|-------|
| `JWT_SECRET` | Sí | `openssl rand -base64 32` |
| `API_KEY` | Sí (para el API) | Clave para agentes/n8n |
| `DB_PASSWORD` | Recomendada | Password de PostgreSQL (default `postgres`) |
| `RUN_SEED` | Primer deploy | `true` solo la primera vez; luego `false` |
| `SEED_ADMIN_EMAIL` / `SEED_ADMIN_PASSWORD` / `SEED_ADMIN_NAME` | Primer deploy | Usuario admin inicial |
| `SEED_ASESOR_EMAIL` / `SEED_ASESOR_PASSWORD` / `SEED_ASESOR_NAME` | No | Asesor opcional |
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
6. **Deploy.** Orden de arranque: `postgres` (healthy) → `web` (migra + seed + sirve) → `api`.
7. Después del primer despliegue exitoso, cambia `RUN_SEED` a `false` y redeploya (el seed es idempotente, pero no hace falta correrlo cada vez).
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
### Healthchecks
- Web: `GET /api/health` (verifica también la conexión a la BD)
- API: `GET /health`
### Notas
- No expongas el puerto 5432: los servicios se comunican por la red interna del compose.
- El volumen `postgres_data` persiste la base de datos entre deploys. No lo borres.
- El servidor MCP queda en `https://<dominio-api>/mcp` (auth por `X-API-Key`).
- La fórmula de financiamiento y los cálculos viven duplicados en `src/lib/calculators.ts` y `api/app/services/calculators.py` — mantenlos en paridad.
## Arquitectura
Ver [AGENTS.md](AGENTS.md) para convenciones, gotchas de Prisma 7 / PDFKit / Next 16 y el modelo de datos.
+9
View File
@@ -0,0 +1,9 @@
__pycache__
*.pyc
*.pyo
.venv
venv
.pytest_cache
.ruff_cache
.env*
Dockerfile
+8 -1
View File
@@ -2,6 +2,9 @@ FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
@@ -9,4 +12,8 @@ COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=5 \
CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/health').status==200 else 1)"
# --proxy-headers: necesario detrás del reverse proxy (Traefik/Coolify)
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers", "--forwarded-allow-ips=*"]
+89
View File
@@ -0,0 +1,89 @@
# Cotizador E3 — stack de producción para Coolify
#
# En Coolify: crear recurso "Docker Compose" apuntando a este repo y configurar
# "Docker Compose Location" = /docker-compose.coolify.yml
#
# Las variables SERVICE_FQDN_* las resuelve Coolify automáticamente (asignan
# dominio a cada servicio). Define JWT_SECRET, API_KEY, DB_PASSWORD y las
# SEED_* en el panel de variables de entorno de Coolify.
#
# Para desarrollo local sigue usándose docker-compose.yml + start.bat.
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
POSTGRES_DB: ${DB_NAME:-cotizador_e3}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 20
web:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
environment:
- SERVICE_FQDN_WEB_3000
- DB_HOST=postgres
- DB_PORT=5432
- DB_USER=${DB_USER:-postgres}
- DB_PASSWORD=${DB_PASSWORD:-postgres}
- DB_NAME=${DB_NAME:-cotizador_e3}
# Solo la usa el CLI de Prisma (migrate deploy); debe coincidir con DB_*
- DATABASE_URL=postgresql://${DB_USER:-postgres}:${DB_PASSWORD:-postgres}@postgres:5432/${DB_NAME:-cotizador_e3}
- JWT_SECRET=${JWT_SECRET}
# Seed inicial: pon RUN_SEED=true solo en el primer despliegue
- RUN_SEED=${RUN_SEED:-false}
- SEED_ADMIN_EMAIL=${SEED_ADMIN_EMAIL:[email protected]}
- SEED_ADMIN_PASSWORD=${SEED_ADMIN_PASSWORD}
- SEED_ADMIN_NAME=${SEED_ADMIN_NAME:-Administrador}
- SEED_ASESOR_EMAIL=${SEED_ASESOR_EMAIL}
- SEED_ASESOR_PASSWORD=${SEED_ASESOR_PASSWORD}
- SEED_ASESOR_NAME=${SEED_ASESOR_NAME}
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://localhost:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
interval: 30s
timeout: 10s
start_period: 60s
retries: 5
api:
build:
context: ./api
dockerfile: Dockerfile
restart: unless-stopped
environment:
- SERVICE_FQDN_API_8000
- DB_HOST=postgres
- DB_PORT=5432
- DB_USER=${DB_USER:-postgres}
- DB_PASSWORD=${DB_PASSWORD:-postgres}
- DB_NAME=${DB_NAME:-cotizador_e3}
- API_KEY=${API_KEY}
- JWT_SECRET=${JWT_SECRET}
depends_on:
postgres:
condition: service_healthy
# web aplica las migraciones de Prisma; el api usa las mismas tablas
web:
condition: service_healthy
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/health').status==200 else 1)"]
interval: 30s
timeout: 10s
start_period: 30s
retries: 5
volumes:
postgres_data:
+16
View File
@@ -0,0 +1,16 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/db";
export const dynamic = "force-dynamic";
export async function GET() {
try {
await prisma.$queryRaw`SELECT 1`;
return NextResponse.json({ status: "ok", database: "ok" });
} catch {
return NextResponse.json(
{ status: "error", database: "unreachable" },
{ status: 503 }
);
}
}
+6 -1
View File
@@ -1,7 +1,12 @@
import { NextRequest, NextResponse } from "next/server";
import { verifySession } from "@/lib/auth";
const PUBLIC_PATHS = ["/login", "/api/auth/login", "/api/auth/logout"];
const PUBLIC_PATHS = [
"/login",
"/api/auth/login",
"/api/auth/logout",
"/api/health",
];
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;