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]>
12 KiB
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 tosrc/generated/prisma/and imported as@/generated/prisma/client. - Uses
PrismaPgdriver adapter (from@prisma/adapter-pg) with individual connection params (host/port/user/password/database), NOT a connection string. TheconnectionStringapproach causes SCRAM auth errors with postgres:postgres. prisma.config.tshas noseedproperty (unsupported by PrismaConfig type). Run seed manually withnpx tsx prisma/seed.ts.- After changing
schema.prisma: runnpx prisma migrate dev, thennpx prisma generate. - The
globalForPrismasingleton pattern insrc/lib/db.tsprevents hot-reload connection leaks in dev.
PDFKit — Server-Side PDF
pdfkitis excluded from Next.js bundling viaserverExternalPackagesinnext.config.ts. Without this, font metric files (.afm) can't be resolved at runtime.- Footer auto-page-break bug:
doc.text()aty > page.height - margins.bottomtriggers pdfkit's automatic page addition. To write footers in the bottom margin, temporarily setdoc.page.margins.bottom = 0, write, then restore. UsebufferPages: true+switchToPage()to add footers after all content. - Returns
Promise<Buffer>— convert toUint8ArrayforNextResponsebody.
Next.js 16 Conventions
- Async params: Dynamic route params are
Promise<{ id: string }>— mustawait paramsbefore use. - All pages are
force-dynamic— no static generation or ISR. - Tailwind CSS v4 — no
tailwind.config.*file. Config is inpostcss.config.mjs(using@tailwindcss/postcss) and CSS custom properties via@theme inlineinsrc/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) — seegenerarNumeroCotizacioninsrc/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
FinanciamientoPlantable (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).
estadoPagois"por_pagar"(default) or"pagada", withfechaPagostamped on transition. The two buckets must stay separated in totals — pending is what goes on the payment note, paid is history.resumenPagoHoras/esPagadaincalculators.tsare the only place that logic should live.- Routes:
GET|POST /api/cotizaciones/[id]/horas(GET takes?from=&to=day filters),PATCH|DELETE .../horas/[registroId], andPOST .../nota-horaswhich renders the PDF server-side viasrc/lib/nota-horas-pdf.ts(no browser URL). - Hours are entered as
horaInicio/horaFinstrings and converted bycalcularHorasRango. Dates come in asYYYY-MM-DDand must go throughfechaRegistroDesdeISOto avoid UTC off-by-one — don'tnew Date(iso)directly. - Grouping for display/PDF:
ModoAgrupacion=detalle | dia | semana | mesviaperiodoAgrupacion.
Auth
- JWT sessions (
src/lib/auth.ts) signed withjose(HS256, 7-day expiry), stored in thecotizador-sessionhttpOnly cookie.JWT_SECRETenv var is required (throws at startup if missing). src/middleware.tsgates everything exceptPUBLIC_PATHS(/login,/api/auth/login,/api/auth/logout,/api/health). Unauthenticated API calls → 401 JSON; pages → redirect to/login. On success it injectsx-user-id/x-user-rolerequest 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 usingdocker-compose.yml+start.bat. - The
webcontainer runsprisma migrate deployon every start, and the seed only whenRUN_SEED=true. Seed users/passwords come fromSEED_*env vars (see.env.example) —prisma/seed.tsnever overwrites an existing password unless the env var is set. DATABASE_URLis only used by the Prisma CLI (migrations); the apps use individualDB_*vars. Both must point to the same DB.- Healthchecks: web
GET /api/health(public in middleware, pings DB), apiGET /health. - Root
Dockerfilebuilds Next.js withJWT_SECRET=placeholder-build-only(auth.ts throws at import without it; all pages are force-dynamic so nothing gets baked). *.xlsxis 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 renderCotizacionForm.tsx. - ExportButtons.tsx has 4 variants:
ExportExcelButtonSaved/ExportPDFButtonSaved(GET by ID) andExportExcelButtonDraft/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 Pythoncalculators.pymirrors.
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 insrc/app/(app)/layout.tsx.confirmandpromptreturn promises (boolean/string | null); passdanger: truefor 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 fromglobals.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
StreamableHTTPServerTransportper request with no session handling and returned 500 even with valid credentials), and its unpinned SDK took the whole API down in production whenmcpjumped to 2.0.0. To revive it:git show edb500f5:api/app/mcp/server.py. api/requirements.txtpins exact versions on purpose. That outage is why. Bump a dependency deliberately — never by accident on a redeploy.- Auth: API key (
X-API-KeyorAuthorization: Bearer) for agents; JWT for human login. Routers inapi/app/routers/, business logic inapi/app/services/(its owncalculators.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 inapi/COTIZADOR_API_SKILL.md. - It reads
DB_*,API_KEY,JWT_SECRETenv 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.