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).
|
- **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.
|
- **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
|
## 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
|
```bash
|
||||||
npm run dev
|
# 1. Copia las variables de entorno
|
||||||
# or
|
copy .env.example .env # y define JWT_SECRET
|
||||||
yarn dev
|
|
||||||
# or
|
# 2. Levanta PostgreSQL + API Python y el dev server de Next.js
|
||||||
pnpm dev
|
start.bat
|
||||||
# or
|
|
||||||
bun dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
### Pasos
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
||||||
|
|
||||||
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
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
@@ -9,4 +12,8 @@ COPY . .
|
|||||||
|
|
||||||
EXPOSE 8000
|
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 { NextRequest, NextResponse } from "next/server";
|
||||||
import { verifySession } from "@/lib/auth";
|
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) {
|
export async function middleware(request: NextRequest) {
|
||||||
const { pathname } = request.nextUrl;
|
const { pathname } = request.nextUrl;
|
||||||
|
|||||||
Reference in New Issue
Block a user