Convierte el análisis de negocio de docs/BI-propuesta-consultiva-IA.md en una especificación cerrada para las dos primeras fases del roadmap. El análisis previo destapó cuatro problemas que el documento de negocio no anticipaba, los cuatro verificados en disco: - POST /mcp no tiene autenticación (api/main.py:107) y _obtener_cotizacion hace SELECT c.* sin lista blanca, con dominio público en producción. Publicaría la columna de notas internas sin tocar una línea de código. - No existe una fuente de verdad para los totales: el cálculo está duplicado en siete consumidores y el IVA está hardcodeado como * 1.16 en PDF y Excel. - El Excel es un documento del cliente, no una herramienta interna. El diseño inicial proponía poner ahí las notas internas; se corrigió. - Las transcripciones se sincronizarían a la nube de MEGA: .megaignore solo excluye .next, node_modules y .turbo. También reconcilia nueve contradicciones de contrato entre los diseños explorados (clave de partida, forma de la evidencia, nombres del valor anual) que habrían hecho que la capa de validación validara el vacío. Actualiza AGENTS.md, que había quedado desfasado: 14 modelos y 10 migraciones (decía 13 y 3), el registro de horas, la doble propuesta, los modelos de cobro y la convención de no usar diálogos nativos del navegador. 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 + MCP server (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, plus an MCP server at/mcpfor AI agents (n8n, Claude, ChatGPT). Tools/resources defined inapi/app/mcp/. - 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.