# 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=<id>`.

**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).

### PO Numbering (`lib/po-number.ts`)

Structured format: **`COMPANY/VESSEL/PO_ID/FY`** — e.g. `PMS/HNR1/9000/2024-25`. The financial year is Indian (Apr–Mar) rendered `YYYY-YY`. System-generated `PO_ID` starts at **9000** to avoid clashing with historical numbers. **Imported POs keep their original PO number** verbatim; `parsePoNumber()` extracts the company/vessel/id parts on import.

### Payments

When Accounts records a payment, a **compulsory payment date** is captured (`PurchaseOrder.paymentDate`) — the input defaults to today and rejects future dates (validated in `processPaymentSchema` and `markPaid`). There is also an editable **`poDate`** field; the exported PO "Date" shows `poDate ?? approvedAt ?? createdAt` (i.e. the approval date once approved, not creation).

### Vendors

`Vendor` carries `isVerified`, `gstin`, `pincode` + `latitude`/`longitude` (geocoded for vendor-distance sorting from a Site), and a `VendorContact[]` list. **Submitters can create vendors** (permission `create_vendor`) but they are created **unverified**; a vendor becomes verified when a PO is closed/paid with it, on import, or when a Manager/Accounts/Admin runs `verifyVendor`. Only `manage_vendors` holders may assign a `vendorId` (the formal verified code).

### Inventory (feature-flagged)

Inventory (`ItemInventory`, keyed by `productId` + `siteId`) is **incremented at PO approval** — not on close — for the ordered quantities, when the PO has a `siteId`. The whole inventory surface (site stock, consumption) is gated by `NEXT_PUBLIC_INVENTORY_ENABLED` (see `lib/feature-flags.ts`); the vendor/product catalogue used for PO creation stays available regardless.

### Import → Closed

`/po/import` parses a Pelagia-format Excel PO and saves it **directly as `CLOSED`** (historical record, bypasses approval). It auto-detects the company (by header/code), auto-matches the vessel by code, **auto-creates the vendor and any unknown products**, and upserts per-vendor prices.

### GST Calculation

`totalAmount = sum(quantity × unitPrice × (1 + gstRate))` for each line item. The `gstRate` is stored as a decimal on `POLineItem` (e.g., `0.18` = 18%). This applies in Server Actions when computing `totalPrice` per line and the PO `totalAmount`.

### Environment Variables

```
NEXTAUTH_SECRET         # Required always
NEXTAUTH_URL            # Required always (e.g., http://localhost:3000)
DATABASE_URL            # PostgreSQL connection string

AZURE_AD_CLIENT_ID, AZURE_AD_CLIENT_SECRET, AZURE_AD_TENANT_ID
                        # Microsoft Entra SSO (prod). auth.ts reads them at module
                        # load — set placeholders in non-SSO/dev envs so it boots.

# Optional in dev (defaults to local storage + console email):
R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET_NAME, R2_PUBLIC_URL
RESEND_API_KEY, EMAIL_FROM, EMAIL_FROM_NAME

# Report Issue button (lib/forgejo.ts); token needs write:issue:
FORGEJO_URL, FORGEJO_REPO, FORGEJO_TOKEN

GST_SERVICE_URL         # GstService microservice (defaults to localhost:3003)
NEXT_PUBLIC_INVENTORY_ENABLED   # Inventory feature flag
NEXT_PUBLIC_ENV_LABEL   # When set, shows a non-prod banner (EnvBanner). Leave unset in prod.
```

### Operations & automation

This repo runs a self-hosted issue-to-deploy pipeline on the `pms1` server (Forgejo +
headless Claude Code). See [`../automation/README.md`](../automation/README.md). Relevant
when working in this codebase:

- The **Report Issue** button (portal header) files a Forgejo issue; a watcher triages it
  and, for auto-fixable ones, implements a fix and opens a PR. Deploys are gated on a
  human merging the PR and pushing a `vX.Y.Z` tag.
- Automated fixes and the **staging** instance run against `pelagia_test`, a **daily mirror
  of the production database**, in dev mode (console email, local storage). Migrations are
  applied to it, so its schema tracks `master`. Never assume an empty DB — it holds prod-like data.
