# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Read first

Project conventions already live in **`AGENTS.md`** (project map + per-feature quick-reference), **`.cursor/rules/*.mdc`** (short always-on / file-scoped rules), and **`.cursor/skills/*/SKILL.md`** (multi-step workflow playbooks). They are plain markdown — read the ones covering the area you touch **before** editing. This file covers only what those do not: commands and cross-cutting architecture.

Per `.cursor/rules/maintain-agent-docs.mdc`: when the user establishes a durable convention, update the matching `.mdc` rule or `SKILL.md` — not this file.

## Commands

There is **no root `package.json`**. Every npm command runs inside `backend/` or `frontend/`.

### Backend (`cd backend`)

| Command | Purpose |
|---|---|
| `npm run start:dev` | API on `http://localhost:3000/api`; Swagger `/api/docs`; AdminJS `/admin` |
| `npm run build` | **Verification gate for cross-cutting backend changes** |
| `npm test` | Jest (`src/**/*.spec.ts`) |
| `npm test -- lab-case-task.generator` | Single suite by path fragment |
| `npm test -- -t "merges teeth"` | Single test by name |
| `npm run test:e2e` | Jest with `test/jest-e2e.json` |
| `npm run lint` | ESLint with `--fix` |
| `docker compose -f docker-compose.postgres.yml up -d` | Dev Postgres (host port from `POSTGRES_PORT` in `.env`) |
| `npm run prisma:generate` / `prisma:migrate` / `prisma:seed` | Client, dev migration, reference-data upsert (seed never wipes) |
| `npm run prisma:export-prosthesis-catalog` | Rewrite `docs/prosthesis-catalog.xlsx` from `catalog-seed-data.ts` |
| `npx prisma migrate reset` | Dev clean slate — drop, re-migrate, re-seed. Never against staging/prod |
| `npm run prisma:wipe-app-data` / `prisma:reset-treatment` / `prisma:regenerate-tasks` | Targeted dev data scripts |

`DATABASE_URL` must use `localhost` when Nest runs on the host and Postgres in Docker.

### Frontend (`cd frontend`)

| Command | Purpose |
|---|---|
| `npm run dev` | Dev server on **3001** (3000 is the API) |
| `npx tsc --noEmit` | **Verification gate for any type or cross-cutting frontend change** |
| `npm run build` | Production build (`output: 'standalone'`) |
| `npm run lint` | ESLint via Next |

`NEXT_PUBLIC_*` values are baked in at build time — restart `npm run dev` after changing `.env.local`.

### Git

Do not commit, push, amend, force-push, or skip hooks unless the user explicitly asks.

## Architecture

Dental **clinic ↔ lab** platform. Every user acts inside one `Organization` whose `type` is `CLINIC` (patients, appointments, treatment) or `LAB` (cases, tasks). Most features exist only for one side.

### Request identity: cookie JWT carrying the selected org

There is no `Authorization` header. `JwtStrategy` reads the httpOnly **`accessToken` cookie**, and the JWT payload carries `organizationId` — the org the user currently acts as. `POST /auth/select-organization` re-issues the token with a different org, so **switching orgs means a new token**, and every service scopes queries by `req.user.organizationId`.

On 401 the axios interceptor (`frontend/src/lib/api/client.ts`) refreshes, **re-selects** the org from `localStorage.currentOrganizationId`, then retries the original request — skipping that dance for auth endpoints and public invitation routes. `frontend/src/proxy.ts` (the Next middleware, exported as `proxy`) is a separate, cookie-only route gate that redirects unauthenticated users to `/{locale}/login?from=…`.

### Permissions

`TAB_*_READ` / `TAB_*_EDIT` codes in `backend/src/common/permissions.ts`; **EDIT implies READ**. Owners get org-type defaults merged with stored grants — always resolve via `hasEffectivePermission` / `getEffectivePermissionNames` in `common/membership-permissions.ts`, never by reading `membership.permissions` directly. Controllers stack `JwtAuthGuard` + `ClinicOrgGuard`/`LabOrgGuard`; feature-specific checks belong in the **service**.

### Error contract (spans 3 layers — change all of them)

`AppException(ErrorCode.X)` → `HttpExceptionFilter` → `{ success: false, error: { code } }` → axios normalizes to `ApiError` → `getUserFacingError(err, tErrors, fallback)` resolves `errors.X` from the message files. Adding a user-facing failure means: a code in `common/errors/error-codes.ts`, the throw site, and an `errors.X` key in **all three** of `frontend/messages/{en,fa,nl}.json`. Never throw raw English Nest exceptions for user-facing failures.

### The core domain pipeline

```
Appointment ─┐
             ├→ Treatment (patient + day) → TreatmentDetail (treatment type + selected teeth)
Walk-in ─────┘                                   │
                                                 │ "send to lab" (clinic side)
                                                 ▼
                          LabCase + LabCaseToothProsthesis (per tooth, grouped by sourceKey)
                                                 │ generateLabCaseTasks()
                                                 ▼
                  ProsthesisType → ProsthesisTypeStep → LabWorkflowStep  ⇒  LabCaseTask rows
                                                 │
                                                 ▼
                                    LAB org: Cases tab + Tasks tab
```

`backend/src/modules/cases/lab-case-task.generator.ts` is the expansion point: it is **idempotent** (returns early if tasks exist) and drives the entire lab-side task list from catalog data. Teeth carry `selectionGroupId` so bridges/connected units survive into task grouping. A `LabCase` can also be lab-origin (`LabCaseOrigin`), created without any clinic treatment.

Clinics may only dispatch to labs they are linked to: `OrganizationLink` (A↔B, `LinkStatus`), plus `OrganizationInvitation` for counterparts not yet on the platform — the invite flow writes both rows in one transaction and stores only the token hash.

### Catalog is code-based and DB-translated

`TreatmentType`, `ProsthesisType`, and `LabWorkflowStep` store a stable `code` and **no label**. Labels come from `CatalogTranslation(entityKind, entityCode, locale)` resolved by `CatalogLabelService` (falls back locale → `en` → humanized code). So: never hardcode a catalog label in backend code, and pass the actor's locale into anything that materializes labels (task generation does). Frontend colors/labels for these codes live in `components/shared/treatmentTypeDisplay.ts` and `components/treatment/prosthesisTypeDisplay.ts`.

### Realtime and unread state

`modules/notifications/user-notification.service.ts` writes `UserNotification` rows and pushes them through the Socket.IO transport in `backend/src/realtime/` (`emitToUserOrg` → `notification.created`). On the frontend a single `notification.created` event drives three things: the header bell inbox, sidebar **tab badges**, and a *soft* refresh of whatever list is currently open — soft meaning it must not remount components or clear an in-progress treatment draft. Unread is per-user cursor state (`LabCaseUserReadState`, `LabCaseUserTabReadState`) plus the `LabCaseActivity` log — badges clear on opening a case, not on visiting a tab.

### Layout conventions worth knowing before you create a file

- **Prisma lives outside `src/`**: `backend/prisma/` holds `schema.prisma`, migrations, seeds *and* `prisma.module.ts` / `prisma.service.ts` — hence imports like `../../../prisma/prisma.service`. Register new Nest modules in `app.module.ts`.
- **AdminJS (`/admin`)** resources are a manual allowlist in `backend/src/admin/resources.ts` — update them whenever `schema.prisma` changes (see `.cursor/rules/adminjs.mdc`).
- **Frontend layering** (`.cursor/rules/frontend-components.mdc`): `app/**/page.tsx` is a thin wrapper only → route logic in `components/ui/{feature}/{Feature}Page.tsx` → JSX in `components/ui/**` → pure helpers in `components/{feature}/` or `components/shared/`. No JSX outside `ui/`, no pure helpers inside it.
- **i18n is mandatory, not a follow-up**: every user-visible string goes into `en.json`, `fa.json`, **and** `nl.json`. `fa` is RTL, so use logical `text-start`/`text-end`, never `text-left`/`text-right`. Dates/times/numbers go through `lib/i18n/format.ts`; form dates use `AppDateInput`, never a native date input.
- Treatment attachments are written to disk at `backend/uploads/treatments` relative to `process.cwd()`.

### Tests

Jest covers pure logic only — permission normalization, phone/timezone helpers, task generation, lab-send validation (7 suites in `backend/src/**`). There are no frontend tests; `npx tsc --noEmit` is the frontend gate.

## Deployment

Images are built on a dev machine and pulled by the server; Compose files and scripts are in `infrastructure/` (`docker-compose.{prod,staging,registry}.yml`). Full guide: `infrastructure/DEPLOY.md`. Root `README.md` covers the Docker Hub + Let's Encrypt path and the Gitea registry path. Frontend `NEXT_PUBLIC_*` are **build args** — changing the public domain requires rebuilding the frontend image. Next `output: 'standalone'` does **not** include `public/`; `frontend/Dockerfile` copies `/app/public` next to `server.js` (catalog icons at `/prosthesis-catalog/*.svg`). Production tags are immutable — CI clones `--branch $tag`; cut a new `v*` instead of moving an existing tag.
