MCP: se retira por completo (endpoint, paquete api/app/mcp/ y dependencia). Tres razones, en orden de peso: - Nadie lo usa. - Llevaba roto desde antes de este trabajo. Con credencial valida devolvia 500: el handler construia un StreamableHTTPServerTransport nuevo por peticion, sin manejo de sesion. Lo que estaba expuesto a internet era la puerta abierta de un cuarto averiado. - Su SDK sin fijar tumbo el API entero en produccion al saltar a 2.0.0. Retirarlo es una mitigacion mas fuerte que autenticarlo, que fue lo que hizo el commit anterior. Recuperable con `git show edb500f5:api/app/mcp/server.py`. Se limpia tambien la ruta "/mcp" que el endpoint raiz seguia anunciando, y las menciones a MCP del docstring y la descripcion de Swagger. Dependencias: once de las doce eran rangos `>=` sin techo, o sea que cada reconstruccion era una tirada de dados contra PyPI. `pydantic>=2.0` habria aceptado pydantic 3 con la misma alegria con la que `mcp>=1.0.0` acepto 2.0.0. Ahora todas van fijadas a la version exacta que corre sana en produccion, capturada con pip freeze del contenedor healthy. El lado Next.js ya era reproducible via package-lock.json; por eso el web nunca se cayo durante el incidente y el api si. Docs actualizados: AGENTS.md, README.md, api/COTIZADOR_API_SKILL.md y el spec, que ademas registra en su seccion 0 las tres desviaciones de Fase 0 respecto a lo disenado (el Despliegue B cancelado, la retirada del MCP y la deriva de dependencia). Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
132 lines
12 KiB
Markdown
132 lines
12 KiB
Markdown
# AGENTS.md — Cotizador E3
|
||
|
||
## Quick Reference
|
||
|
||
| Command | Purpose |
|
||
|---------|---------|
|
||
| `npm run dev` | Start Next.js dev server on port 3000 |
|
||
| `npm run build` | Production build (runs TypeScript check) |
|
||
| `npm run lint` | ESLint (flat config, eslint-config-next) |
|
||
| `npm run db:seed` | Run seed (upserts all data, idempotent) |
|
||
| `npm run db:migrate` | Create/apply migration (`prisma migrate dev`) |
|
||
| `npm run db:generate` | Regenerate Prisma client |
|
||
| `npm run db:studio` | Prisma Studio GUI |
|
||
|
||
**There is no test suite** — no runner, no test files, in either backend. Verification is `npm run build` (typechecks) + `npm run lint`. Don't claim a change is verified on the strength of a build alone; exercise the affected route or page.
|
||
|
||
**Windows environment.** Use `start.bat` / `stop.bat` to manage Docker PostgreSQL + Next.js together. PowerShell is the shell. Paths with brackets (e.g. `[id]`) require `-LiteralPath` in PowerShell, not `-Path`.
|
||
|
||
**Two backends, one database.** The Next.js app (`src/`, port 3000) and a standalone Python FastAPI service (`api/`, port 8000) both talk to the same PostgreSQL DB. Prisma owns the schema/migrations; the Python API reads/writes the same tables independently. `docker-compose.yml` runs `postgres` + the `api` service; Next.js is run separately via `npm run dev` / `start.bat`.
|
||
|
||
**Three compose files, and two of them are the same file.** `docker-compose.yaml` is a byte-identical copy of `docker-compose.coolify.yml` — it exists only so Coolify's default detection finds the production stack. Local dev is `docker-compose.yml` (postgres with port 5432 published + api). Because `.yml` and `.yaml` both exist, a bare `docker compose` warns about ambiguity before resolving to `docker-compose.yml`; pass `-f docker-compose.yml` explicitly, as `start.bat` does. **When you edit the Coolify stack, edit both `docker-compose.coolify.yml` and `docker-compose.yaml`** or Coolify deploys the stale copy.
|
||
|
||
## Prisma 7 — Critical Gotchas
|
||
|
||
- **Prisma client is NOT at `@prisma/client`.** It's generated to `src/generated/prisma/` and imported as `@/generated/prisma/client`.
|
||
- **Uses `PrismaPg` driver adapter** (from `@prisma/adapter-pg`) with individual connection params (host/port/user/password/database), NOT a connection string. The `connectionString` approach causes SCRAM auth errors with postgres:postgres.
|
||
- **`prisma.config.ts`** has no `seed` property (unsupported by PrismaConfig type). Run seed manually with `npx tsx prisma/seed.ts`.
|
||
- After changing `schema.prisma`: run `npx prisma migrate dev`, then `npx prisma generate`.
|
||
- The `globalForPrisma` singleton pattern in `src/lib/db.ts` prevents hot-reload connection leaks in dev.
|
||
|
||
## PDFKit — Server-Side PDF
|
||
|
||
- `pdfkit` is excluded from Next.js bundling via `serverExternalPackages` in `next.config.ts`. Without this, font metric files (`.afm`) can't be resolved at runtime.
|
||
- **Footer auto-page-break bug:** `doc.text()` at `y > page.height - margins.bottom` triggers pdfkit's automatic page addition. To write footers in the bottom margin, temporarily set `doc.page.margins.bottom = 0`, write, then restore. Use `bufferPages: true` + `switchToPage()` to add footers after all content.
|
||
- Returns `Promise<Buffer>` — convert to `Uint8Array` for `NextResponse` body.
|
||
|
||
## Next.js 16 Conventions
|
||
|
||
- **Async params:** Dynamic route params are `Promise<{ id: string }>` — must `await params` before use.
|
||
- **All pages are `force-dynamic`** — no static generation or ISR.
|
||
- **Tailwind CSS v4** — no `tailwind.config.*` file. Config is in `postcss.config.mjs` (using `@tailwindcss/postcss`) and CSS custom properties via `@theme inline` in `src/app/globals.css`.
|
||
|
||
## Data Model Notes
|
||
|
||
- **Configuracion** is a key-value store (`clave`/`valor`), not a traditional settings model. New config keys: `color_primario`, `color_secundario`, `logo_base64` (branding for PDF export).
|
||
- **Cascading deletes:** Cotizacion → ServicioCotizado, PlanBucefaloCotizacion use `onDelete: Cascade`.
|
||
- Cotización number format: `UJ{YY}{MM}{AsesorInitials}{seq}` (e.g. `UJ2605AG001`) — see `generarNumeroCotizacion` in `src/lib/calculators.ts`. Sequence is 3 digits, zero-padded.
|
||
- Vigencia = 15 business days from fecha (excludes Sat/Sun) — `calcularVigencia`.
|
||
- IVA_RATE = 0.16 (16%).
|
||
- **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.
|
||
|
||
### Charge models (`ServicioCotizado.modeloCobro`)
|
||
|
||
A quoted line item is `fijo` (default), `horas`, `retainer`, or `demanda` — see `MODELOS_COBRO` in `calculators.ts`. The time-based three are `MODELOS_COBRO_TIEMPO`. Supporting fields: `esPersonalizado`, `horas`, `tarifaHora`, `montoMinimo`, `horasIncluidas`. `TARIFA_HORA_DEFAULT = 700`.
|
||
|
||
**`precio` is always the authoritative total.** For retainer and demanda lines, `horas × tarifaHora` is display/comparison metadata only — never re-derive a total from it. `calcularTotalesOpcion` and the PDF/Excel builders all sum `precio`.
|
||
|
||
### Doble propuesta (two-option quotes)
|
||
|
||
`Cotizacion.esDoble` turns one quote into two comparable proposals. Each `ServicioCotizado.opcion` is `"1"`, `"2"`, or `"ambas"` (shared by both). `opcionesMetadata` (Json) holds per-option `titulo` / `descripcion` / `noIncluye` (`MetaOpcion`). `calcularTotalesOpcion(servicios, "1" | "2")` sums lines matching that option **plus** all `"ambas"` lines. Anything that renders or totals a quote must handle both the single and double shape.
|
||
|
||
### Registro de horas (hours log → payment notes)
|
||
|
||
`RegistroHoras` tracks worked time against a quote so the client can be billed for it. Only offered when `esCotizacionPorTiempo(servicios)` is true (i.e. some line uses a time-based `modeloCobro`).
|
||
|
||
- `estadoPago` is `"por_pagar"` (default) or `"pagada"`, with `fechaPago` stamped on transition. The two buckets must stay separated in totals — pending is what goes on the payment note, paid is history. `resumenPagoHoras` / `esPagada` in `calculators.ts` are the only place that logic should live.
|
||
- Routes: `GET|POST /api/cotizaciones/[id]/horas` (GET takes `?from=&to=` day filters), `PATCH|DELETE .../horas/[registroId]`, and `POST .../nota-horas` which renders the PDF **server-side** via `src/lib/nota-horas-pdf.ts` (no browser URL).
|
||
- Hours are entered as `horaInicio`/`horaFin` strings and converted by `calcularHorasRango`. Dates come in as `YYYY-MM-DD` and must go through `fechaRegistroDesdeISO` to avoid UTC off-by-one — don't `new Date(iso)` directly.
|
||
- Grouping for display/PDF: `ModoAgrupacion` = `detalle | dia | semana | mes` via `periodoAgrupacion`.
|
||
|
||
## Auth
|
||
|
||
- **JWT sessions** (`src/lib/auth.ts`) signed with `jose` (HS256, 7-day expiry), stored in the `cotizador-session` httpOnly cookie. `JWT_SECRET` env var is **required** (throws at startup if missing).
|
||
- **`src/middleware.ts`** gates everything except `PUBLIC_PATHS` (`/login`, `/api/auth/login`, `/api/auth/logout`, `/api/health`). Unauthenticated API calls → 401 JSON; pages → redirect to `/login`. On success it injects `x-user-id` / `x-user-role` request headers for downstream handlers.
|
||
- Passwords hashed with `bcryptjs`. Roles: `asesor` (default), and others stored as free-form strings.
|
||
|
||
## 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
|
||
|
||
```
|
||
src/
|
||
app/
|
||
(app)/ # Authed route group: dashboard, cotizaciones, clientes, catalogo, configuracion (has its own layout.tsx + Sidebar + DialogProvider)
|
||
api/ # Next.js route handlers (REST): auth, catalogo, categorias, cotizaciones (+ horas, nota-horas, precio),
|
||
# configuracion, paquetes, export, import, health
|
||
login/ # Public login page
|
||
components/ # CotizacionForm (~1.4k lines), ExportButtons, EstadoBadge, layout/Sidebar, ui/DialogProvider
|
||
lib/ # auth, db, store, calculators, schemas, config-helpers,
|
||
# pdf-generator (quote PDF), nota-horas-pdf (hours-note PDF), excel-builder
|
||
generated/prisma/ # Prisma client output (gitignored)
|
||
prisma/
|
||
schema.prisma # 14 models (User, Cliente, Cotizacion, Categoria, Paquete, FasePaquete,
|
||
# ServicioCatalogo, ServicioPaquete, ServicioCotizado, PlanBucefaloCotizacion,
|
||
# RegistroHoras, Configuracion, Bono, FinanciamientoPlan)
|
||
seed.ts # All catalog data (services, categorias, bonos, planes, config) — idempotent upserts
|
||
migrations/ # 10 migrations
|
||
api/ # Standalone Python FastAPI service (see below)
|
||
docs/ # Business/product notes (Spanish), not code docs
|
||
```
|
||
|
||
- **Zustand store** (`src/lib/store.ts`) holds the cotización draft. Used by both the new (`cotizaciones/nueva`) and edit (`cotizaciones/[id]/editar`) pages, both of which render `CotizacionForm.tsx`.
|
||
- **ExportButtons.tsx** has 4 variants: `ExportExcelButtonSaved` / `ExportPDFButtonSaved` (GET by ID) and `ExportExcelButtonDraft` / `ExportPDFButtonDraft` (POST with body).
|
||
- **Business logic belongs in `calculators.ts`**, not in components or route handlers. It's the shared source of truth for totals, charge models, hours, phases, and formatting — and the file the Python `calculators.py` mirrors.
|
||
|
||
## UI Conventions
|
||
|
||
- **Never use the browser's native `confirm()` / `alert()` / `prompt()`.** They render as "«domain» dice…" and break the brand. Use the platform's own dialogs: `useConfirm()`, `usePrompt()`, `useToast()` from `@/components/ui/DialogProvider`, mounted once in `src/app/(app)/layout.tsx`. `confirm` and `prompt` return promises (`boolean` / `string | null`); pass `danger: true` for destructive actions.
|
||
- Everything user-facing is in **Spanish**. Code identifiers are Spanish too (`cotizacion`, `servicio`, `horas`) — match the surrounding naming rather than introducing English terms.
|
||
- Icons come from `lucide-react`. Colors use the CSS custom properties from `globals.css` (`bg-card-bg`, `border-border`, `text-primary`, `text-muted`), not hardcoded Tailwind palette values.
|
||
|
||
## Python API (`api/`) — Optional Second Backend
|
||
|
||
- **FastAPI app** (`api/main.py`) exposing the same domain as REST, for n8n and other integrations.
|
||
- **The MCP server was removed on 2026-07-28.** It was unused, its transport had been broken for some time (it built a `StreamableHTTPServerTransport` per request with no session handling and returned 500 even with valid credentials), and its unpinned SDK took the whole API down in production when `mcp` jumped to 2.0.0. To revive it: `git show edb500f5:api/app/mcp/server.py`.
|
||
- **`api/requirements.txt` pins exact versions on purpose.** That outage is why. Bump a dependency deliberately — never by accident on a redeploy.
|
||
- Auth: **API key** (`X-API-Key` or `Authorization: Bearer`) for agents; JWT for human login. Routers in `api/app/routers/`, business logic in `api/app/services/` (its own `calculators.py`, `pdf_generator.py`, `excel_generator.py` — mirror the TS versions).
|
||
- Run: `cd api && pip install -r requirements.txt && uvicorn main:app --reload --port 8000`. Swagger at `/docs`. Full endpoint reference in `api/COTIZADOR_API_SKILL.md`.
|
||
- It reads `DB_*`, `API_KEY`, `JWT_SECRET` env vars (same DB as Prisma).
|
||
|
||
## Domain
|
||
|
||
Quotation system for Consultoría E3 (digital marketing agency in Querétaro, MX). Services organized in 4 phases: Fase 0 (Auditorías), Fase 1 (Setup/Infra), Fase 2 (Publicidad/Manejo), Fase 3 (Contenido/SEO). Two payment types: `unico` (one-time) and `mensual` (recurring). Optional Bucéfalo CRM plans and Openpay financing.
|