Spec: diseño de Fase 0 y Fase 1 del plugin de propuesta consultiva con IA
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]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
26e6d3a9bf
commit
0ff783fd65
@@ -7,15 +7,19 @@
|
||||
| `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) |
|
||||
| `npx tsx prisma/seed.ts` | Run seed (upserts all data, idempotent) |
|
||||
| `npx prisma migrate dev` | Create/apply migration |
|
||||
| `npx prisma generate` | Regenerate Prisma client |
|
||||
| `npx prisma studio` | Prisma Studio GUI |
|
||||
| `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`.
|
||||
@@ -46,6 +50,25 @@
|
||||
- **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).
|
||||
@@ -66,23 +89,33 @@
|
||||
```
|
||||
src/
|
||||
app/
|
||||
(app)/ # Authed route group: dashboard, cotizaciones, clientes, catalogo, configuracion (has its own layout.tsx + Sidebar)
|
||||
api/ # Next.js route handlers (REST): auth, catalogo, categorias, cotizaciones, configuracion, paquetes, export, import
|
||||
(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, ExportButtons, EstadoBadge, layout/Sidebar
|
||||
lib/ # auth, db, store, calculators, pdf-generator, schemas, config-helpers
|
||||
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 # 13 models (User, Cliente, Cotizacion, Categoria, Paquete, FasePaquete,
|
||||
schema.prisma # 14 models (User, Cliente, Cotizacion, Categoria, Paquete, FasePaquete,
|
||||
# ServicioCatalogo, ServicioPaquete, ServicioCotizado, PlanBucefaloCotizacion,
|
||||
# Configuracion, Bono, FinanciamientoPlan)
|
||||
# RegistroHoras, Configuracion, Bono, FinanciamientoPlan)
|
||||
seed.ts # All catalog data (services, categorias, bonos, planes, config) — idempotent upserts
|
||||
migrations/ # 3 migrations
|
||||
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 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user