Files
dyolink/CLAUDE.md
Amin Mousavi c57abbe895 docs: say component tests are allowed, not absent by rule
CLAUDE.md read as a prohibition — "Vitest for its own pure helpers only, no
React, no DOM". It was only ever a description of what was installed, and it
was talking future contributors out of a test they are allowed to write.

Tests now states what exists, that component tests are allowed, and what the
first one costs: jsdom or happy-dom, @testing-library/react v16+ for React 19,
environment and a *.spec.tsx include. Notes that babel-plugin-react-compiler is
in devDependencies but not enabled in next.config.ts, so there is no transform
mismatch to work around.

Adds which to reach for. A decision belongs in a pure test, extracted into
components/{feature}/. A defect only a real render shows — ref-versus-state
timing, effect ordering — belongs in a component test, because no pure test can
see it. VoiceReviewSheet is the cheap first one; TreatmentWorkspace needs six
axios mocks and earns its keep only for a bug that needs it.

Same correction in vitest.config.ts's docblock and the spec's §12 line, which
each carried their own copy of "no React, no DOM".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 15:49:36 +03:30

11 KiB

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
npx vitest run Vitest — today the pure helpers in components/treatment/*.spec.ts; component tests are allowed, see Tests

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.

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 ApiErrorgetUserFacingError(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/ (emitToUserOrgnotification.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, voice extraction contract (backend/src/**).

Frontend runs Vitest (npx vitest run). npx tsc --noEmit remains the cross-cutting gate.

What exists today is pure-helper specs under frontend/src/components/treatment/*.spec.tsprosthesisTree.ts, voiceReviewRows.ts, toothSelectionGroups.ts, voiceApply.ts. vitest.config.ts therefore sets no environment and includes *.spec.ts only.

Component tests are allowed. Nothing here forbids them; the setup simply is not installed yet. Adding the first one means jsdom (or happy-dom) and @testing-library/react, plus environment: 'jsdom' and a *.spec.tsx include in vitest.config.ts. React 19 needs testing-library v16+. The React compiler babel plugin is in devDependencies but not enabled in next.config.ts, so there is no transform mismatch to work around.

Which to reach for. Prefer a pure test when the thing under test is a decision: extract it into components/{feature}/ and leave the commit — state, refs, save order — in the component. voiceApply.ts is the worked example, pulled out of a callback in TreatmentWorkspace.tsx. Reach for a component test when the defect is only visible in a real render — ref-versus-state timing, effect ordering, a callback firing before a re-render — because no pure test can see those. Start with a small, prop-driven component (VoiceReviewSheet is the easy first one); rendering TreatmentWorkspace means mocking next-intl, the i18n router and six axios modules, so it earns its keep only for a bug that needs it.

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.