1
This commit is contained in:
@@ -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
|
||||
@@ -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=
|
||||
@@ -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
@@ -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}"]
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
.venv
|
||||
venv
|
||||
.pytest_cache
|
||||
.ruff_cache
|
||||
.env*
|
||||
Dockerfile
|
||||
+8
-1
@@ -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=*"]
|
||||
|
||||
@@ -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:
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user