Files
cotizador/AGENTS.md
T
urieljarethandClaude Opus 5 20f535d485 Retirar el servidor MCP y fijar todas las dependencias del API
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]>
2026-07-28 17:51:21 -06:00

12 KiB
Raw Blame History

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.