# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Commands ```bash # Development pnpm dev # Next.js + Turbopack at localhost:3000 pnpm lint # ESLint pnpm type-check # tsc --noEmit # Tests pnpm test # Unit tests (Vitest, jsdom) pnpm test:watch # Unit tests in watch mode pnpm test:integration # Integration tests (Vitest, node + real DB) pnpm test:e2e # E2E tests (Playwright, headless) pnpm test:e2e:ui # E2E tests with interactive UI pnpm test:all # All test suites # Run a single test file pnpm test -- tests/unit/po-line-items-editor.test.tsx pnpm test:integration -- tests/integration/create-po.test.ts # Database pnpm db:migrate # Create + apply migration (dev) pnpm db:migrate:deploy # Apply migrations (CI/prod) pnpm db:seed # Populate sample data pnpm db:studio # Prisma GUI at localhost:5555 pnpm db:reset # Drop + recreate + seed (dev) ``` ## Architecture ### Overview Internal purchase order management system for a maritime company. Full-stack Next.js 15 App Router app with Prisma + PostgreSQL, NextAuth v5 credentials auth, and Tailwind CSS v4. **Key design decisions:** - Server Components for all data-fetching pages; Client Components only where interactivity is needed - Server Actions for all mutations (form submissions, approvals, etc.) - Prisma `Decimal` fields **cannot** be passed directly to Client Components — convert with `Number()` in the Server Component before passing as props (see `po-detail.tsx` → `lineItemsForEditor` pattern) - File storage toggles automatically: Cloudflare R2 in production, `.dev-uploads/` directory in development - Email toggles automatically: Resend in production, console log in development ### PO Lifecycle (State Machine) `lib/po-state-machine.ts` enforces all status transitions. The canonical flow: ``` DRAFT → SUBMITTED → MGR_REVIEW → MGR_APPROVED → SENT_FOR_PAYMENT → PAID_DELIVERED → CLOSED ↓↑ ↕ ↕ EDITS_REQUESTED / REJECTED PARTIALLY_PAID PARTIALLY_CLOSED / VENDOR_ID_PENDING ``` Partial payments (`PARTIALLY_PAID`) and partial receipts (`PARTIALLY_CLOSED`) loop until the full amount/quantity is settled. Imported POs are created directly in `CLOSED`. Every status change is validated against the state machine and recorded as a `POAction` row (audit trail). ### Role-Based Permissions `lib/permissions.ts` defines `hasPermission(role, permission)` and `requirePermission(role, permission)`. Roles: `TECHNICAL`, `MANNING`, `ACCOUNTS`, `MANAGER`, `SUPERUSER`, `AUDITOR`, `ADMIN`. Permissions include (non-exhaustive): `create_po`, `approve_po`, `process_payment`, `confirm_receipt`, `create_vendor`, `manage_vendors`, `manage_products`, `manage_sites`, `manage_vessels_accounts`, `manage_users`. `create_vendor` is held by submitters too; `manage_*` by Manager/Admin. **Pattern:** Server Actions call `requirePermission()` (or `hasPermission()`) at the top before any DB write. **Auth:** NextAuth v5 with a Microsoft Entra SSO provider **and** a credentials provider. SSO-only users have no `passwordHash` (it is nullable) — the profile page lets them optionally set one, and is reachable by every role. Only approvers (`approve_po`) can upload a signature. ### Key Directories - `app/(portal)/` — All authenticated pages (portal layout with sidebar) - `app/api/po/[id]/export/` — PDF and XLSX export endpoint - `lib/validations/po.ts` — Zod schemas for PO forms; exports `TC_FIXED_LINE` and `TC_DEFAULTS` - `lib/po-state-machine.ts` — All valid status transitions with required roles - `lib/notifier.ts` — Email dispatch (Resend in prod, console in dev) - `lib/storage.ts` — File upload/download (R2 in prod, local in dev) - `components/po/` — PO-specific components (line items editor, status badge, etc.) - `tests/integration/helpers.ts` — `makeSession()`, `makePoForm()`, `fd()` for integration test setup ### Cost Centre Model A PO's **cost centre is a Vessel** (the `Vessel` model). `PurchaseOrder.vesselId` is **required**. POs no longer reference a Site as a cost centre — that earlier dual Vessel-or-Site design was removed. **Form field:** PO create/edit/import forms use a plain `vesselId` select (no more `costCentreRef` encoding). **Display pattern:** `po.vessel?.name ?? "—"`. **URL pre-select:** `/po/new?vesselId=`. **Terminology:** "Vessel" is surfaced as **"Cost Centre"** everywhere in the UI, including the admin page (`/admin/vessels` → "Cost Centre Management"). `Site` still exists as a separate construct (used for vendor-distance and inventory), but is not a PO cost centre. Budget heads are labelled "Accounting Code" (not "Account"). ### Accounting Code Hierarchy `Account` is a self-referential 3-level tree via `parentId` (`AccountHierarchy` relation): **Top Category (6-digit, e.g. `100000`) → Sub-Category (`100100`) → Leaf Item (`100101`)**. Codes are 6-digit numeric strings. Seed data lives in `prisma/accounting-codes-data.ts`. - **Only leaf items** (accounts with no children) are selectable on a PO. - PO forms group leaf codes by their sub-category in a searchable dropdown (`components/ui/searchable-select.tsx`, a portal-rendered combobox used in the line-items editor and the main accounting-code field). ### Companies (multi-company invoicing) `Company` represents the sister company a PO is billed under (`PurchaseOrder.companyId`, optional). Fields: `name`, `code` (unique short code, e.g. `PMS`), `gstNumber`, `address`, `telephone`, `mobile`, `email`, `invoiceEmail`, `invoiceAddress`. Managed at `/admin/companies`. The selected company's details populate the **exported PO header / invoice block** (falling back to hardcoded Pelagia defaults when no company is linked). ### Delivery Locations (issue #19) `DeliveryLocation` (a `Company` FK + free-text `address` + `isActive`) is an admin-managed list that backs the PO **Place of Delivery** dropdown. Managed at `/admin/delivery-locations`, gated by the **`manage_delivery_locations`** permission (Manager + SuperUser + Admin — explicitly **not** admin-only, per the issue). The CRUD mirrors `/admin/sites` (table + Add/Edit dialogs + activate/deactivate + delete). The three PO forms (`new-po-form`, `edit-po-form`, `manager-edit-po-form`) render a shared `` — a native `