# Jorge Cuadros & Asociados — Platform

Internal platform for a Baja California insurance brokerage and property-services
firm: a single expedient joining each client's **properties/services**,
**insurance policies**, **account statement**, and the firm's **checkbook**.
It replaces a legacy PHP/Access app (see `RESUME.md` and `PLAN.md` for the full
history and rebuild rationale, and [`docs/BACKLOG.md`](docs/BACKLOG.md) for
everything still outstanding).

The UI is Spanish-first; the codebase and this document are in English.

---

## Stack

| Layer     | Tech                                                           | Port |
| --------- | -------------------------------------------------------------- | ---- |
| Web       | Next.js 14 (App Router, React 18)                              | 3000 |
| API       | NestJS 10 · Passport local + `express-session` · Argon2        | 3001 |
| Database  | MySQL 8 via Prisma 5 (`@jorgecuadros/database` workspace pkg)  | 3306 |
| Migration | Python 3 pipeline (legacy Access → staging → transforms)       | —    |

Monorepo managed with **pnpm workspaces**. Node **>= 20**.

---

## Repository layout

```
apps/
  web/        Next.js frontend  (@jorgecuadros/web)
  api/        NestJS backend    (@jorgecuadros/api)
    scripts/seed-user.mjs   idempotent admin seeder
packages/
  database/   Prisma schema + generated client (@jorgecuadros/database)
    prisma/schema.prisma
migration/    One-off Python ETL from the legacy Access DB (run_all.py)
docker/       Dockerfiles for api + web
docker-compose.yml          mysql + api + web
.env.example                copy to .env
```

API feature modules: `auth`, `users`, `customers`, `policies`, `properties`,
`billing`, `bank`, `reports`, `notifications`, `renewals`, `mail`, `statements`,
`policy-ocr`, `ocr`, `storage`, `settings`, `ops`.

Web routes: `/inicio`, `/clientes`, `/polizas` (+ `/polizas/captura`, policy
PDF OCR capture), `/servicios`, `/estado-cuenta`, `/banco` (chequera),
`/recibos` (utility statement OCR capture), `/notificaciones` (mass email +
renewal avisos; `/renovaciones` is an alias onto its Pólizas tab), `/reportes`,
`/catalogos`, `/operaciones` (DB ingest/backup, ADMIN), `/usuarios`, `/login`.

Two OCR intakes share one `OcrProvider` seam (`src/ocr/`, Tesseract today):
utility statements → ledger rows ([`docs/STATEMENT_OCR.md`](docs/STATEMENT_OCR.md))
and carrier policy PDFs → `Policy` rows ([`docs/POLICY_OCR.md`](docs/POLICY_OCR.md)).
Both need `tesseract-ocr`, `tesseract-ocr-data-spa`, `poppler-utils` and object
storage; each reports its own availability and disables only itself if either
is missing.

---

## Prerequisites

- Node.js >= 20 and **pnpm** (`npm i -g pnpm`)
- Docker (for MySQL, or bring your own MySQL 8)
- Python 3 — only if you run the legacy data migration

---

## Run it locally (development)

### 1. Install

```bash
pnpm install
```

pnpm blocks postinstall build scripts by default; the trusted ones
(`argon2`, `prisma`, `@prisma/client`, `@prisma/engines`, `@nestjs/core`) are
allowlisted in `pnpm-workspace.yaml`, so the native builds run automatically.

### 2. Configure environment

```bash
cp .env.example .env
```

Then edit `.env`. For the Docker MySQL below the defaults already line up;
just set a real `SESSION_SECRET`:

```env
DATABASE_URL=mysql://jorgecuadros:jorgecuadros@localhost:3306/jorgecuadros
SESSION_SECRET=<any long random string>
WEB_ORIGIN=http://localhost:3000
NEXT_PUBLIC_API_ORIGIN=http://localhost:3001
```

The API loads `DATABASE_URL`, `SESSION_SECRET`, `WEB_ORIGIN`, and optional
`PORT` (default `3001`). The web app only needs `NEXT_PUBLIC_API_ORIGIN`.

### 3. Start MySQL

```bash
docker compose up -d mysql
```

(Or point `DATABASE_URL` at an existing MySQL 8 instance.)

### 4. Create the schema + generate the Prisma client

The schema is managed with `prisma db push` (no migration history committed):

```bash
pnpm --filter @jorgecuadros/database exec prisma db push
pnpm --filter @jorgecuadros/database generate
```

### 5. Seed a sign-in user

```bash
node apps/api/scripts/seed-user.mjs
```

Idempotent (upsert by email). Defaults — override with `SEED_EMAIL`,
`SEED_PASSWORD`, `SEED_NAME`:

- email: `admin@jorgecuadros.local`
- password: `ChangeMe!2026`
- role: `ADMIN`

### 6. Run the apps (two terminals)

```bash
# API  → http://localhost:3001
pnpm --filter @jorgecuadros/api start:dev

# Web  → http://localhost:3000
pnpm --filter @jorgecuadros/web dev
```

Root shortcuts also exist: `pnpm dev:api`, `pnpm dev:web`.

### 7. Log in

Open http://localhost:3000, sign in with the seeded credentials.
Sessions are cookie-based and last 8 hours.

---

## Run it with Docker (full stack)

Builds MySQL + API + web from `docker-compose.yml`:

```bash
export SESSION_SECRET=$(openssl rand -hex 32)
docker compose up --build
```

Web on http://localhost:3000, API on http://localhost:3001. `SESSION_SECRET`
is required (compose fails without it). After first boot, push the schema and
seed a user against the container DB:

```bash
docker compose exec api node apps/api/scripts/seed-user.mjs
```

---

## Common commands

| Task                     | Command                                                        |
| ------------------------ | -------------------------------------------------------------- |
| Install                  | `pnpm install`                                                 |
| Dev — API                | `pnpm dev:api`                                                 |
| Dev — Web                | `pnpm dev:web`                                                 |
| Build all                | `pnpm build`                                                   |
| Generate Prisma client   | `pnpm prisma:generate`                                         |
| Push schema (dev)        | `pnpm --filter @jorgecuadros/database exec prisma db push`     |
| Prisma Studio            | `pnpm prisma:studio`                                           |
| API tests                | `pnpm --filter @jorgecuadros/api test`                         |
| Lint (web / api)         | `pnpm --filter @jorgecuadros/web lint` · `... /api lint`       |
| Seed admin user          | `node apps/api/scripts/seed-user.mjs`                          |

---

## Auth & roles

Session auth via Passport local strategy; passwords hashed with Argon2 (no
plaintext, unlike the legacy app). Roles gate the UI and API — e.g. managing
`/catalogos` and `/usuarios` requires the appropriate ability (`ADMIN` /
`MANAGER`). New users are created by an admin in `/usuarios`; the first admin
comes from the seed script above.

---

## Legacy data migration (optional)

`migration/` holds the one-off Python ETL that lifts data out of the old
Microsoft Access database into MySQL.

```bash
pip install -r migration/requirements.txt
python migration/run_all.py
```

> ⚠️ `run_all.py` truncates and reloads **all** downstream tables. Never run an
> individual `transform_*.py` in isolation — it orphans dependent tables. See
> `migration/RECONCILIATION.md` for details.

---

## Scheduled jobs

The API runs two automatic email sweeps. Neither cadence is in the source:
both are stored in `app_settings` and edited at `/notificaciones` →
"Programación de envíos" (ADMIN, `setting:manage`), taking effect immediately
without a restart. Shipped defaults:

| Job | Default | What it does |
| --- | ------- | ------------ |
| Pólizas | **on**, 06:00 daily (America/Tijuana) | Renewal avisos at 30/15 days before expiry and 7 days after. |
| Servicios | **off** | All four mass-email jobs in order, same as "Ejecutar todos". |

A scheduled run never uses the UI's send flags — in particular it ignores
`debug`, so a forgotten test toggle cannot silently stop customer mail. Full
detail in [`docs/MASS_EMAIL_NOTIFICATIONS.md`](docs/MASS_EMAIL_NOTIFICATIONS.md).

Sending needs `SES_*` in the environment. Without it the API still boots and
logs mail to stdout in dev; in production every send fails loudly and is
recorded as `FAILED` rather than quietly going nowhere.

---

## Production notes

- Use `pnpm --filter @jorgecuadros/database exec prisma migrate deploy` if/when
  a committed migration history is adopted; today dev uses `db push`.
- Set a strong `SESSION_SECRET` and a locked-down `DATABASE_URL`.
- The API expects `WEB_ORIGIN` to match the browser origin for session cookies.
- Documents are stored in MinIO in the deployed environment (see `RESUME.md`).
