@

docs(wiki): organise into folders + migrate retired Docs/

- Group pages into Overview/, Build-and-Run/, System/, Product/, Quality/, Ops/
  so the on-disk layout mirrors the sidebar hierarchy.
- Migrate retired design/test docs from the app repo Docs/ into the wiki:
  Product/Workflows, Product/Design-System, and the Quality/ test references
  (Test-Plan, E2E-Test-Framework, E2E-Test-Plan, Playwright-Test-Design).
- Rewrite internal links to folder-qualified, root-relative targets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
Hardik 2026-06-19 13:48:03 +05:30
parent e7882f07db
commit 864141c105
27 changed files with 1506 additions and 96 deletions

@ -38,14 +38,14 @@ Server-side env on pms1 lives in `~/pms/App/.env`; locally in `App/.env.local`
- **SSO at module load**`auth.ts` evaluates the `AZURE_AD_*` vars when the
module loads, so they must be *present* (even as placeholders) for the app to
start in non-SSO environments. See [Architecture](Architecture#auth--authorisation).
start in non-SSO environments. See [Architecture](System/Architecture#auth--authorisation).
- **Storage / email auto-toggle** — with R2/Resend unset in dev, uploads go to
`.dev-uploads/` and emails print to the terminal. See
[File Storage](File-Storage) and [Notifications](Notifications).
[File Storage](Product/File-Storage) and [Notifications](Product/Notifications).
- **Inventory flag** — `INVENTORY_ENABLED = NEXT_PUBLIC_INVENTORY_ENABLED !==
"false"`, i.e. enabled unless explicitly `"false"`.
- **Env banner**`EnvBanner` renders nothing when `NEXT_PUBLIC_ENV_LABEL` is
unset, so production is unaffected; staging sets it to the
"INTERNAL DEV / STAGING - NOT PRODUCTION" string.
- **GstService** has its own `PORT` (default 3003); the portal reaches it via
`GST_SERVICE_URL`. See [Vendors and GST Lookup](Vendors-and-GST-Lookup).
`GST_SERVICE_URL`. See [Vendors and GST Lookup](Product/Vendors-and-GST-Lookup).

@ -21,7 +21,7 @@ In development the app needs **only a database and an auth secret**. Cloudflare
R2 and Resend are **not** required — file uploads land in `.dev-uploads/` and
emails are printed to the terminal (lines prefixed `📧 [DEV EMAIL]`). The switch
is automatic, driven by `NODE_ENV` (`next dev` → development, `next build/start`
→ production). See [File Storage](File-Storage) and [Notifications](Notifications).
→ production). See [File Storage](Product/File-Storage) and [Notifications](Product/Notifications).
## Setup
@ -46,7 +46,7 @@ pnpm dev # Next.js + Turbopack at http://localhost:3000
> `auth.ts` reads the Azure/Entra SSO variables at **module load**. In a non-SSO
> dev environment, set placeholder values for `AZURE_AD_*` so the app boots. See
> [Environment Variables](Environment-Variables).
> [Environment Variables](Build-and-Run/Environment-Variables).
## Seed credentials
@ -95,7 +95,7 @@ pnpm email:preview # live-preview email templates at http://localhost:3001
- **GstService** (`GstService/`) — a small Express + Playwright microservice that
proxies the GST portal CAPTCHA/lookup. Optional in dev; defaults to
`http://localhost:3003`. See [Vendors and GST Lookup](Vendors-and-GST-Lookup).
`http://localhost:3003`. See [Vendors and GST Lookup](Product/Vendors-and-GST-Lookup).
## Project layout
@ -112,4 +112,4 @@ App/
└── tests/ # unit (Vitest), integration (Vitest+DB), e2e (Playwright)
```
See [Architecture](Architecture) for the full layer breakdown and [Data Model](Data-Model) for the schema.
See [Architecture](System/Architecture) for the full layer breakdown and [Data Model](System/Data-Model) for the schema.

15
Home.md

@ -29,11 +29,12 @@ This wiki is the project's living reference. It is synthesised from the in-repo
## Start here
- **New to the codebase?** → [Getting Started](Getting-Started)
- **Understanding the system?** → [Architecture](Architecture) · [Data Model](Data-Model) · [PO Lifecycle](PO-Lifecycle)
- **Who can do what?** → [Roles and Permissions](Roles-and-Permissions)
- **What's been built?** → [Feature Catalogue](Feature-Catalogue) · [Pages and Navigation](Pages-and-Navigation)
- **Operating it?** → [Deployment and Operations](Deployment-and-Operations) · [Issue-to-Deploy Pipeline](Issue-to-Deploy-Pipeline)
- **Unsure of a term?** → [Glossary](Glossary)
- **New to the codebase?** → [Getting Started](Build-and-Run/Getting-Started)
- **Understanding the system?** → [Architecture](System/Architecture) · [Data Model](System/Data-Model) · [PO Lifecycle](System/PO-Lifecycle)
- **Who can do what?** → [Roles and Permissions](System/Roles-and-Permissions)
- **What's been built?** → [Feature Catalogue](Product/Feature-Catalogue) · [Pages and Navigation](Product/Pages-and-Navigation) · [Workflows](Product/Workflows)
- **Designing UI?** → [Design System](Product/Design-System)
- **Operating it?** → [Deployment and Operations](Ops/Deployment-and-Operations) · [Issue-to-Deploy Pipeline](Ops/Issue-to-Deploy-Pipeline)
- **Unsure of a term?** → [Glossary](Overview/Glossary)
> The full page list is in the sidebar. See [Changelog](Changelog) for what shipped recently and [Open Questions](Open-Questions) for decisions still pending sign-off.
> The full page list is in the sidebar. See [Changelog](Overview/Changelog) for what shipped recently and [Open Questions](Overview/Open-Questions) for decisions still pending sign-off.

@ -37,7 +37,7 @@ same host.
All production env vars must be set (auth, DB, R2, Resend, optionally
Forgejo/GST). Server-side env lives in `~/pms/App/.env`. The full list and the
dev/prod split is on [Environment Variables](Environment-Variables).
dev/prod split is on [Environment Variables](Build-and-Run/Environment-Variables).
## Release & deploy flow
@ -66,7 +66,7 @@ Watch progress under **Actions** in Forgejo, or `pm2 logs forgejo-runner`.
> not-yet-migrated column yields `P2022 … column does not exist` at runtime. The
> deploy workflow runs `migrate deploy` for you; for manual deploys, run it (and
> restart) before/with the swap. This was a real production incident — see
> [Changelog](Changelog).
> [Changelog](Overview/Changelog).
## Staging (smoke test before deploy)
@ -106,5 +106,5 @@ data.**
to missing fork information" even when `mergeable: true`. Fix: close and reopen
the PR (UI or API). Resolves on upgrade past v10.
See [Issue-to-Deploy Pipeline](Issue-to-Deploy-Pipeline) for the automation, and
See [Issue-to-Deploy Pipeline](Ops/Issue-to-Deploy-Pipeline) for the automation, and
`automation/README.md` for the full runbook.

@ -91,5 +91,5 @@ So the fix stage verifies against realistic data without touching production:
- Schema-migration issues are routed to `interactive`, so the unattended fixer
should not be altering the schema.
See [Deployment and Operations](Deployment-and-Operations) for the deploy
See [Deployment and Operations](Ops/Deployment-and-Operations) for the deploy
workflow and staging, and `automation/README.md` for the authoritative runbook.

@ -2,7 +2,7 @@
Mirrors `CHANGELOG.md` at the repo root (the authoritative copy). Releases are
tagged `vX.Y.Z`; the deployed production version is whichever tag is currently
checked out in `~/pms`. See [Deployment and Operations](Deployment-and-Operations).
checked out in `~/pms`. See [Deployment and Operations](Ops/Deployment-and-Operations).
## [Unreleased]
@ -12,10 +12,10 @@ checked out in `~/pms`. See [Deployment and Operations](Deployment-and-Operation
`/admin/companies` CRUD. A PO is billed under a selected company (name, short
`code`, GST number, address, phone/mobile, contact + invoice email, invoice
address); details populate the exported PO header / invoice block. See
[Purchase Orders](Purchase-Orders#companies-multi-company-invoicing).
[Purchase Orders](Product/Purchase-Orders#companies-multi-company-invoicing).
- **Structured PO numbers** (`lib/po-number.ts`) — `COMPANY/VESSEL/ID/FY`; Indian
financial year; system IDs start at 9000; imported POs keep their original
number. See [Purchase Orders](Purchase-Orders#po-numbering).
number. See [Purchase Orders](Product/Purchase-Orders#po-numbering).
- **3-level accounting-code hierarchy**`Account.parentId` self-relation
(Top → Sub → Leaf), 6-digit codes seeded from
`prisma/accounting-codes-data.ts`. Only leaf codes are PO-selectable, via a
@ -26,24 +26,24 @@ checked out in `~/pms`. See [Deployment and Operations](Deployment-and-Operation
`poDate ?? approvedAt ?? createdAt`.
- **Submitter vendor creation**`create_vendor` lets Technical/Manning add
vendors; created **unverified**, verified when a PO closes/pays with them, on
import, or via Manager/Accounts/Admin. See [Vendors](Vendors-and-GST-Lookup).
import, or via Manager/Accounts/Admin. See [Vendors](Product/Vendors-and-GST-Lookup).
- **Import PO → Closed**`/po/import` saves a parsed Excel PO directly as
`CLOSED`, auto-detecting company, matching vessel, auto-creating vendor,
products, and per-vendor prices.
- **Inventory feature flag** (`NEXT_PUBLIC_INVENTORY_ENABLED`) — site
stock/consumption gated; PO catalogue stays available. Inventory increments at
**PO approval**. See [Inventory and Catalogue](Inventory-and-Catalogue).
**PO approval**. See [Inventory and Catalogue](Product/Inventory-and-Catalogue).
- **Dashboards** — Accounts gains a "Payments Completed This Month" card.
- **Automated issue-to-deploy pipeline** — Report Issue button → Forgejo issue →
Claude watcher triage/fix → PR → tag-triggered deploy. See
[Issue-to-Deploy Pipeline](Issue-to-Deploy-Pipeline).
[Issue-to-Deploy Pipeline](Ops/Issue-to-Deploy-Pipeline).
### Changed
- **Cost centre is now a Vessel only.** The Vessel-or-Site cost-centre model was
removed: `PurchaseOrder.vesselId` is required, `costCentreRef` is gone, and
`Vessel` no longer links to a `Site`. Vessels are surfaced as **"Cost Centre"**
(`/admin/vessels` → "Cost Centre Management"). See [Glossary](Glossary).
(`/admin/vessels` → "Cost Centre Management"). See [Glossary](Overview/Glossary).
- **Closed PO list** — submitters see only their own `CLOSED` POs;
Managers/SuperUsers see all.
- **Sidebar** reorganised into **Purchasing** and **Administration** (role-aware);
@ -60,7 +60,7 @@ checked out in `~/pms`. See [Deployment and Operations](Deployment-and-Operation
- Production `P2022 … column does not exist` after deploy — caused by shipping
code whose Prisma client expected a column before `migrate deploy` ran.
Migrations must be applied before the new build serves traffic (now in the
README and the [deploy workflow](Deployment-and-Operations#release--deploy-flow)).
README and the [deploy workflow](Ops/Deployment-and-Operations#release--deploy-flow)).
---

@ -6,7 +6,7 @@ original design — the definitions below are the **shipped** meanings.
| Term | Meaning |
|---|---|
| **PPMS** | "Pelagia Payment Management System" — the in-UI brand for Pelagia Portal (login, sidebar, title). |
| **Purchase Order (PO)** | The central record: a request to buy goods/services, tracked through its [lifecycle](PO-Lifecycle) from DRAFT to CLOSED. |
| **Purchase Order (PO)** | The central record: a request to buy goods/services, tracked through its [lifecycle](System/PO-Lifecycle) from DRAFT to CLOSED. |
| **Cost Centre** | **A Vessel.** Every PO is raised against a Vessel (`PurchaseOrder.vesselId`, required). Surfaced as "Cost Centre" everywhere in the UI (`/admin/vessels` → "Cost Centre Management"). The earlier Vessel-or-Site cost-centre model was removed. |
| **Vessel** | A ship; the cost centre a PO is charged to. Has a unique `code` used in PO numbers. |
| **Accounting Code** | A budget head: a leaf in the 3-level `Account` hierarchy (Top Category → Sub-Category → Leaf), 6-digit numeric. Only leaf codes are PO-selectable. Previously labelled "Account". |
@ -14,7 +14,7 @@ original design — the definitions below are the **shipped** meanings.
| **PO Number** | Auto-formatted `COMPANY/VESSEL/ID/FY` (e.g. `PMS/HNR1/9000/2024-25`). System IDs start at 9000; imported POs keep their original number. |
| **FY** | Indian financial year (AprMar), rendered `YYYY-YY` in PO numbers. |
| **Vendor** | A supplier. Submitter-created vendors are **unverified** until a PO closes/pays with them, on import, or a Manager/Accounts/Admin verifies. The formal `vendorId` is the verified code. |
| **GSTIN** | 15-char Indian GST identification number; looked up via the [GST microservice](Vendors-and-GST-Lookup) to auto-fill vendor details. |
| **GSTIN** | 15-char Indian GST identification number; looked up via the [GST microservice](Product/Vendors-and-GST-Lookup) to auto-fill vendor details. |
| **Site** | A port/depot/office that holds inventory; used for delivery and vendor-distance sourcing. **Not** a cost centre. |
| **Product / Item** | A catalogue entry (`code`, `name`). Tracks `lastPrice`/`lastVendor` and per-vendor prices, updated on payment. |
| **Line Item** | A row on a PO: name, qty, unit, unit price, GST rate; optional product link and per-line accounting code. |
@ -27,5 +27,5 @@ original design — the definitions below are the **shipped** meanings.
| **pms1** | The single Ubuntu server hosting the app, DB, Forgejo, and CI runner. |
| **`ppms`** | The pm2 process running the production app on port 3000. |
| **`pelagia` / `pelagia_test`** | Production DB / its daily mirror used for staging + autofix verification. |
| **Report Issue** | Header button that files a Forgejo issue, kicking off the [issue-to-deploy pipeline](Issue-to-Deploy-Pipeline). |
| **Report Issue** | Header button that files a Forgejo issue, kicking off the [issue-to-deploy pipeline](Ops/Issue-to-Deploy-Pipeline). |
| **Staging** | A deployed instance of latest `master` (pm2 `ppms-staging`, port 3200, SSH-tunnel only) for pre-release smoke testing. |

@ -12,10 +12,10 @@ product — annotated below. Update as they resolve.
| 4 | Is SSO required, or is internal credential management enough? | **Resolved** — both: Microsoft Entra SSO **and** a credentials provider; SSO users have nullable passwords. |
| 5 | What currency/currencies? Multi-currency with FX in scope? | **Partly**`currency` defaults to `INR`; multi-currency/FX not implemented. |
| 6 | Hard-delete vs permanent archive for rejected POs; retention window? | **Open**. |
| 7 | Public document URLs vs always-signed/authenticated downloads? | **Resolved** — downloads are auth-gated/presigned (dev route 404s in prod). See [File Storage](File-Storage). |
| 7 | Public document URLs vs always-signed/authenticated downloads? | **Resolved** — downloads are auth-gated/presigned (dev route 404s in prod). See [File Storage](Product/File-Storage). |
| 8 | Row-level vessel/account restrictions per submitter? | **Open** — any submitter can raise a PO against any cost centre. |
| 9 | Expected volume (POs/day, concurrent users) — for pool sizing / `pms1` resourcing? | **Open**. |
| 10 | Should manager analytics count only CLOSED POs, or all from MGR_APPROVED onwards? | **Resolved** — "Approved this month" counts by `approvedAt` (all POs approved in the period), not just those currently in `MGR_APPROVED`. See [Changelog](Changelog). |
| 10 | Should manager analytics count only CLOSED POs, or all from MGR_APPROVED onwards? | **Resolved** — "Approved this month" counts by `approvedAt` (all POs approved in the period), not just those currently in `MGR_APPROVED`. See [Changelog](Overview/Changelog). |
For the design-era spec context, see `Docs/01-design-document.md` and
`Docs/DESIGN.md`.

74
Product/Design-System.md Normal file

@ -0,0 +1,74 @@
# Design System
Visual language and UI conventions, migrated from the original design spec
(`Docs/01-design-document.md` §7, §9 and `Docs/DESIGN.md` §9, now retired). These
are the design intentions; the implemented source of truth is Tailwind v4 +
shadcn/ui in `App/components/`.
## Colour palette
| Token | Hex | Usage |
|---|---|---|
| `primary` | `#2563EB` | Primary actions, active states, links |
| `primary-dark` | `#1D4ED8` | Hover on primary |
| `success` | `#16A34A` | Approved, paid, closed states |
| `warning` | `#D97706` | Pending review, edits requested |
| `danger` | `#DC2626` | Rejected, destructive actions |
| `neutral-50` | `#F9FAFB` | Page background |
| `neutral-100` | `#F3F4F6` | Card / panel background |
| `neutral-700` | `#374151` | Body text |
| `neutral-900` | `#111827` | Headings |
## Typography
| Element | Font | Weight | Size |
|---|---|---|---|
| Headings (H1H3) | Inter | 600700 | 24 / 20 / 16 px |
| Body | Inter | 400 | 14 px |
| Labels / captions | Inter | 500 | 12 px |
| Data / mono values | JetBrains Mono | 400 | 13 px |
## Component conventions
- Cards: `rounded-lg`, `shadow-sm`, 16 px padding.
- Status badges: pill shape, colour-coded to match the
[PO lifecycle](System/PO-Lifecycle#status-badges) state colours.
- Tables: alternating row shading, sticky header on scroll.
- Forms: floating labels; validation errors below the field in `danger` colour.
- Buttons: primary = blue fill, secondary = white with border, danger = red fill.
- Admin action buttons (Edit / Delete) are **bordered** (asserted in E2E).
## Key UI patterns
- **Status badges** — every PO status has a distinct colour (see the lifecycle
page); colour is never the sole signal (icon + label accompany it).
- **Confirm-before-destruction** — Delete uses a two-step *inline* confirm
("Delete X? Confirm / Cancel") that replaces the button in place — no modal.
- **Inline table editing** — manager line-item editing happens inline on the
approval page (not a modal) so the rest of the PO stays visible.
- **Live GST summary** — below the line-items table: Taxable, GST, Grand Total
update as you type. See [Purchase Orders](Product/Purchase-Orders#gst-calculation).
- **Product autocomplete** — typing in a line-item name field fuzzy-searches the
catalogue; the dropdown shows product name/code and per-vendor price hints
(e.g. "Vendor A: ₹1,200 · Vendor B: ₹1,050").
- **Cheapest / ★ Closest tags** — computed independently so both can show at
once. See [Inventory and Catalogue](Product/Inventory-and-Catalogue#cheapest--closest-tags).
- **Cart persistence**`localStorage` under a fixed key; survives navigation,
local to the device/user; a `cart-updated` event lets components react live.
- **Mobile** — Manager/Accounts get mobile cards + a bottom nav; other roles get
a "Desktop Required" overlay. See
[Pages and Navigation](Product/Pages-and-Navigation#mobile).
- **Environment banner** — a fixed non-prod banner via `NEXT_PUBLIC_ENV_LABEL`
(`EnvBanner`); renders nothing in production.
## Accessibility & i18n
- Target **WCAG 2.1 AA**.
- All interactive elements keyboard-navigable with a visible focus ring.
- Colour is never the sole conveyor of meaning (icons + labels accompany status
colours).
- English-only for v1; an i18n architecture (react-i18next) was envisioned to be
wired but not populated.
> A non-functional **Reports** UX mockup also exists in the wiki
> (`Reports-Mockup`) — proposed layouts and chart treatments for spend reporting.

@ -7,12 +7,12 @@ with the detail, or names the code that implements it.
- **Full PO lifecycle** — DRAFT → … → CLOSED with manager approval, vendor
validation, payment, and receipt confirmation. Enforced by
[the state machine](PO-Lifecycle); every change is an audit row.
[the state machine](System/PO-Lifecycle); every change is an audit row.
- **Partial payments & partial receipts**`PARTIALLY_PAID` /
`PARTIALLY_CLOSED` loop until fully settled (`deliveredQuantity` per line).
- **Structured PO numbers**`COMPANY/VESSEL/ID/FY` (e.g. `PMS/HNR1/9000/2024-25`);
system IDs start at 9000; imported POs keep their original number.
See [Purchase Orders](Purchase-Orders#po-numbering).
See [Purchase Orders](Product/Purchase-Orders#po-numbering).
- **GST-inclusive totals** — per-line `gstRate` (default 18%); live taxable/GST/
grand-total summary in the form.
- **Manager line-item editing** — managers adjust quantities, prices, GST,
@ -28,18 +28,18 @@ with the detail, or names the code that implements it.
## Catalogue, vendors, inventory
- **Multi-company invoicing** — bill a PO under a sister company (PMS/HNR/DEI);
details flow to the exported PO. See [Data Model](Data-Model#company--multi-company-invoicing).
details flow to the exported PO. See [Data Model](System/Data-Model#company--multi-company-invoicing).
- **3-level accounting codes** — Top → Sub → Leaf (6-digit); only leaf codes
selectable, via a searchable combobox.
- **Vendor management** — submitter-created (unverified) vendors;
auto-verify-on-payment; **GSTIN lookup** via the GST microservice; geocoded
vendor-distance sourcing. See [Vendors and GST Lookup](Vendors-and-GST-Lookup).
vendor-distance sourcing. See [Vendors and GST Lookup](Product/Vendors-and-GST-Lookup).
- **Product catalogue** — editable at `/admin/products`, read-only at
`/inventory/items`; per-vendor price history; "Cheapest" / "★ Closest" tags.
- **Cart** — collect items (localStorage) → create a PO pre-filled.
- **Site inventory (feature-flagged)** — stock per site, daily consumption log;
inventory incremented at PO **approval**. Gated by
`NEXT_PUBLIC_INVENTORY_ENABLED`. See [Inventory and Catalogue](Inventory-and-Catalogue).
`NEXT_PUBLIC_INVENTORY_ENABLED`. See [Inventory and Catalogue](Product/Inventory-and-Catalogue).
## Platform
@ -64,8 +64,8 @@ with the detail, or names the code that implements it.
- **Report Issue button** — any signed-in user files a Forgejo issue from the
header (`lib/forgejo.ts`).
- **Automated issue→fix→PR pipeline** + **tag-triggered deploy**. See
[Issue-to-Deploy Pipeline](Issue-to-Deploy-Pipeline).
[Issue-to-Deploy Pipeline](Ops/Issue-to-Deploy-Pipeline).
- **Staging instance** against a daily **prod-mirror test DB** for smoke testing.
For the screen-by-screen breakdown, see [Pages and Navigation](Pages-and-Navigation).
For what shipped recently, see [Changelog](Changelog).
For the screen-by-screen breakdown, see [Pages and Navigation](Product/Pages-and-Navigation).
For what shipped recently, see [Changelog](Overview/Changelog).

@ -58,4 +58,4 @@ R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET_NAME, R2_PUBLIC
```
In development these can be left as placeholders. See
[Environment Variables](Environment-Variables).
[Environment Variables](Build-and-Run/Environment-Variables).

@ -50,7 +50,7 @@ regardless of whether the list is sorted by Price or Distance. Selecting a site
also auto-switches the active sort to **Distance** (a `useEffect` keyed on the
site id resets it on every site change — important because Next.js soft
navigation preserves React state). With no site selected, neither distance tag
shows. See [Testing](Testing) for the specs pinning this down.
shows. See [Testing](Quality/Testing) for the specs pinning this down.
## Cart

@ -32,7 +32,7 @@ The switch is automatic on `NODE_ENV`. Preview templates live with
## Event → recipient matrix
Driven by the side-effects declared per transition in the
[state machine](PO-Lifecycle#transition-table):
[state machine](System/PO-Lifecycle#transition-table):
| Event | Side-effect | Notified |
|---|---|---|
@ -64,5 +64,5 @@ Separate from PO notifications: any signed-in user can file a bug from the heade
via the **Report Issue** button (`components/layout/report-issue-button.tsx`
`report-issue-actions.ts``lib/forgejo.ts`), which creates a Forgejo issue
labelled `portal`. That kicks off the
[Issue-to-Deploy Pipeline](Issue-to-Deploy-Pipeline). Requires `FORGEJO_URL`,
[Issue-to-Deploy Pipeline](Ops/Issue-to-Deploy-Pipeline). Requires `FORGEJO_URL`,
`FORGEJO_REPO`, `FORGEJO_TOKEN` (token scope `write:issue`).

@ -2,8 +2,8 @@
This page covers the mechanics specific to purchase orders: numbering, GST,
the create/edit forms, company invoicing, accounting codes, and Excel import.
For the status graph see [PO Lifecycle](PO-Lifecycle); for the schema see
[Data Model](Data-Model).
For the status graph see [PO Lifecycle](System/PO-Lifecycle); for the schema see
[Data Model](System/Data-Model).
## PO numbering
@ -57,7 +57,7 @@ PO `totalAmount`.
(`/api/products/search`).
3. **Terms & Conditions** — Delivery, Dispatch, Inspection, Transit Insurance,
Payment Terms, Others (all optional text → `tc*` fields).
4. **Documents** — drag-and-drop / browse uploader (see [File Storage](File-Storage)).
4. **Documents** — drag-and-drop / browse uploader (see [File Storage](Product/File-Storage)).
Footer: **Save as Draft** / **Submit for Approval** (and **Update & Resubmit**
when editing an `EDITS_REQUESTED` PO, which returns it to `MGR_REVIEW`).
@ -68,7 +68,7 @@ Validation lives in `lib/validations/po.ts` (Zod), which also exports
> **Form selector gotcha** (for tests): the PO form labels are visual-only — no
> `htmlFor`/`id` binding. Use `name`-attribute selectors
> (`input[name="title"]`, `select[name="vesselId"]`). See [Testing](Testing).
> (`input[name="title"]`, `select[name="vesselId"]`). See [Testing](Quality/Testing).
## Companies (multi-company invoicing)
@ -87,7 +87,7 @@ groups leaf codes by sub-category in a searchable, portal-rendered combobox
`accountId`. Seed data: `prisma/accounting-codes-data.ts`.
> "Accounting Code" replaces the older "Account" label. The **Cost Centre** is a
> separate concept — it is the Vessel. See [Glossary](Glossary).
> separate concept — it is the Vessel. See [Glossary](Overview/Glossary).
## Payments

@ -1,7 +1,7 @@
# Vendors and GST Lookup
Vendors are suppliers a PO can be raised against. The model and its evolution
are on [Data Model](Data-Model#vendor--vendorcontact); this page covers the
are on [Data Model](System/Data-Model#vendor--vendorcontact); this page covers the
verification lifecycle, distance-based sourcing, and the GST microservice that
backs GSTIN lookup.
@ -33,7 +33,7 @@ and a `VendorContact[]` list (name, role, mobile, email, isPrimary). Managed at
detail / items pages, selecting a **Site** re-sorts vendors by proximity to that
site; the nearest vendor gets a **★ Closest** tag and the lowest price gets a
**Cheapest** tag — computed independently so both can show at once, regardless
of the active sort. See [Inventory and Catalogue](Inventory-and-Catalogue).
of the active sort. See [Inventory and Catalogue](Product/Inventory-and-Catalogue).
## GSTIN lookup (GstService)

157
Product/Workflows.md Normal file

@ -0,0 +1,157 @@
# Workflows
Step-by-step user workflows, migrated from the original UI/UX spec
(`Docs/DESIGN.md`, now retired). For the screen list see
[Pages and Navigation](Product/Pages-and-Navigation); for the status graph see
[PO Lifecycle](System/PO-Lifecycle).
## Submit a purchase order (Technical / Manning)
1. Click **New PO** in the sidebar.
2. Select **Cost Centre** (Vessel) and **Accounting Code**.
3. Add line items — type a name to search the catalogue; previous vendor prices
appear as hints.
4. Optionally attach documents and fill in Terms & Conditions.
5. Click **Submit for Approval** → Manager is emailed; status shows "Under
Review" on My Orders.
6. If the manager **requests edits**: the PO shows `EDITS_REQUESTED` with the
note; edit and resubmit.
7. If the manager **requests a vendor ID**: select a vendor and submit; the PO
returns to the manager queue.
8. On **approval**: you're emailed and the PO appears in the Accounts payment
queue.
## Approve a purchase order (Manager)
1. Open **Approvals**; see the pending count.
2. Click **Review** on a PO; read line items, vendor, documents, notes.
3. Optionally **Edit** to adjust quantities/prices/GST or change
vendor/vessel/account inline (logged as `MANAGER_LINE_EDIT`).
4. Choose an action:
- **Approve** → moves to the Accounts payment queue.
- **Approve with Note** → same, with a note visible to the submitter.
- **Request Edits** → write a note; PO returns to the submitter.
- **Request Vendor ID** → PO returns to the submitter to select a vendor.
- **Reject** → write a reason; PO is closed permanently.
> A vendor must be assigned before approval, and only **verified** vendors may be
> assigned. See [Roles and Permissions](System/Roles-and-Permissions).
## Process a payment (Accounts)
1. Open **Payments**; see cards for `MGR_APPROVED` POs.
2. **Send for Payment** → notifies submitter and manager.
3. When the bank/finance confirms, **Mark as Paid** (capturing the **compulsory
payment date** — no future dates) → notifies all parties. Partial payments
loop via `PARTIALLY_PAID` until fully settled.
4. The submitter can now upload a delivery receipt.
## Confirm receipt (Technical / Manning)
1. Goods are delivered to the vessel/site.
2. Open the PO detail (status `PAID_DELIVERED`).
3. **Confirm Receipt** → upload the delivery receipt and optionally add notes.
4. Submit → PO is `CLOSED` (or `PARTIALLY_CLOSED` for a partial receipt);
accounts and manager are notified.
## Look up a vendor by GSTIN (Manager / Admin)
1. Open the Add/Edit Vendor form.
2. Type the 15-digit GSTIN and click **Look up** → a CAPTCHA image loads from the
GST microservice.
3. Type the 6-digit CAPTCHA and click **Verify** → the form auto-fills legal
name, address, and pincode.
4. Save; location is geocoded silently from the pincode for distance sorting.
Detail: [Vendors and GST Lookup](Product/Vendors-and-GST-Lookup).
## Source items by proximity (Manager)
1. Open **Items** → click an item name.
2. See all vendors that supply it with their last quoted price.
3. Select a **site** from the "Sort by distance from" dropdown.
4. The table re-sorts by proximity; distance shows per row; the closest vendor is
marked **★** and the cheapest is tagged **Cheapest**.
5. **Add to Cart** on the desired vendor row.
## Create a PO from the cart (Manager / Technical)
1. Add items to the cart from item detail pages.
2. Open **Cart**; adjust quantities, remove items, select a delivery site.
3. **Create PO** → opens New PO pre-filled with the cart line items and vendor.
4. Fill in title, cost centre, accounting code; submit normally.
## Track inventory at a site (Manager / Admin)
1. Open **Sites** → click a site.
2. View the current-stock bar chart and the 30-day consumption line chart.
3. Use **Log Consumption** to record a daily drawdown (product, date, quantity,
note). See [Inventory and Catalogue](Product/Inventory-and-Catalogue).
## Import a PO from Excel (Manager / SuperUser / Admin)
1. Open **Import PO** and upload a Pelagia-format `.xlsx`.
2. The parser extracts line items, vendor, and quotation details; the company is
auto-detected and the vessel auto-matched by code.
3. The PO is saved **directly as `CLOSED`** (a historical record), auto-creating
the vendor and any unknown products. See
[Purchase Orders](Product/Purchase-Orders#import-po--closed).
## Export PO history (Auditor / Manager)
1. Open **History**; apply filters (date range, cost centre, one or more
statuses).
2. **Export PDF** or **Export CSV** → downloads the matching POs (up to ~200).
---
## User stories (priority reference)
From the original spec (`Docs/01-design-document.md` §8); IDs are referenced by
the [Test Plan](Quality/Test-Plan). P0 = must-have, P1 = should-have.
**Submitter (Technical / Manning)**
| ID | Story | Pri |
|---|---|---|
| S-01 | Create a PO with line items and attach documents | P0 |
| S-02 | Save a PO as draft before submitting | P0 |
| S-03 | Submit a draft PO for approval | P0 |
| S-04 | Receive email when my PO is approved/rejected | P0 |
| S-05 | View status and history of all my POs | P0 |
| S-06 | Provide a vendor ID when requested | P0 |
| S-07 | Edit and resubmit when edits are requested | P0 |
| S-08 | Confirm receipt and upload a receipt to close a PO | P0 |
**Manager**
| ID | Story | Pri |
|---|---|---|
| M-01 | See all POs awaiting my approval | P0 |
| M-02 | Approve / reject / request edits | P0 |
| M-03 | Add a note when approving or rejecting | P0 |
| M-04 | Flag a PO for vendor-ID verification | P0 |
| M-05 | View spend analytics by cost centre and month | P1 |
| M-06 | Export a full PO history report (CSV/PDF) | P1 |
**Accounts**
| ID | Story | Pri |
|---|---|---|
| A-01 | See all manager-approved POs ready for payment | P0 |
| A-02 | Mark a PO as paid with a reference | P0 |
| A-03 | Receive email when a PO enters my payment queue | P0 |
**Admin**
| ID | Story | Pri |
|---|---|---|
| AD-01 | Create, edit, deactivate user accounts | P0 |
| AD-02 | Manage cost centres, accounting codes, vendors | P0 |
| AD-03 | Manage the product catalogue | P1 |
## Non-goals (out of scope)
Mobile-native app (web is desktop-first), public-facing pages, self-registration
/ generic OAuth, a vendor self-service portal, and automated
bank/payment-gateway integration (payment is marked manually).

@ -0,0 +1,311 @@
# PPMS — E2E Test Framework Reference
> _Migrated from the retired `Docs/e2e-test-framework.md`. Index: [Testing](Quality/Testing)._
This document describes the Playwright-based end-to-end test framework for the
PPMS portal: its stack, directory layout, configuration, shared utilities, and
the conventions every spec must follow.
---
## Stack
| Layer | Tool | Version |
|---|---|---|
| Test runner | `@playwright/test` | 1.60 |
| Browser | Chromium (headless) | bundled with Playwright |
| Language | TypeScript | inherits from app `tsconfig.json` |
| Package manager | pnpm | same as portal app |
| App server | Next.js 15 dev server (`pnpm dev`) | auto-started by Playwright config |
---
## Directory Layout
```
App/pelagia-portal/
├── playwright.config.ts # Root config — workers, retries, baseURL, webServer
└── tests/
├── e2e/
│ ├── helpers/
│ │ ├── login.ts # Shared login(), createDraftPo(), submitPo(), USERS
│ │ └── auth.js # Legacy plain-JS login helper (pre-existing)
│ ├── dashboard/
│ │ └── po-status-badges.js
│ ├── inventory/
│ │ ├── items-tags.spec.ts
│ │ └── cart-icon.spec.ts
│ ├── mobile/
│ │ ├── desktop-required.spec.ts
│ │ ├── manager-approvals.spec.ts
│ │ ├── accounts-payments.spec.ts
│ │ └── bottom-nav.spec.ts
│ ├── admin-bordered-buttons.spec.ts
│ ├── approvals-edit-highlight.spec.ts
│ ├── export-gate.spec.ts
│ ├── notification-bell.spec.ts
│ ├── partial-receipt.spec.ts
│ ├── payment-history.spec.ts
│ ├── po-submit-button.spec.ts
│ ├── profile.spec.ts
│ ├── rebrand.spec.ts
│ └── vendor-auto-verify.spec.ts
├── integration/ # Vitest integration tests (separate suite)
└── unit/ # Vitest unit tests (separate suite)
```
---
## Configuration (`playwright.config.ts`)
```ts
export default defineConfig({
testDir: "./tests/e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 1, // 1 local retry reduces flakiness from auth concurrency
workers: process.env.CI ? 1 : 2, // 2 local workers — more causes NextAuth bcrypt flooding
reporter: "html",
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
},
webServer: {
command: "pnpm dev",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI, // reuse running dev server locally
},
});
```
### Why workers: 2
The app uses NextAuth v5 with bcrypt password hashing for every login. Under high
parallelism (the default of ~50% CPU cores) all workers attempt to authenticate
simultaneously, overwhelming the dev server and causing login redirects to time out.
Two workers provide enough parallelism to keep the suite fast without triggering
the concurrency limit.
---
## Shared Helpers (`tests/e2e/helpers/login.ts`)
### `USERS` — seed credentials
```ts
export const USERS = {
TECH: { email: "tech@pelagia.local", password: "tech1234" },
MANNING: { email: "manning@pelagia.local", password: "manning1234" },
ACCOUNTS: { email: "accounts@pelagia.local", password: "accounts1234" },
MANAGER: { email: "manager@pelagia.local", password: "manager1234" },
SUPERUSER: { email: "superuser@pelagia.local", password: "super1234" },
AUDITOR: { email: "auditor@pelagia.local", password: "audit1234" },
ADMIN: { email: "admin@pelagia.local", password: "admin1234" },
};
```
### `login(page, creds)`
Navigates to `/login`, fills credentials, and waits up to **20 s** for the
redirect away from `/login`. The 20 s timeout is intentional — the bcrypt hash
check plus DB round-trip can exceed the Playwright default 5 s under any load.
```ts
await login(page, USERS.MANAGER);
```
### `createDraftPo(page, title)`
Creates a minimal PO as DRAFT and returns the absolute PO URL. Uses
**`name`-attribute selectors** because the PO form labels have no `htmlFor`/`id`
binding — `getByLabel()` will not resolve.
```ts
const poUrl = await createDraftPo(page, "Test PO - boiler parts");
```
### `submitPo(page, title)`
Same as `createDraftPo` but clicks the **Submit for Approval** button instead of
Save as Draft. Returns the PO URL after redirect.
---
## Selector Conventions
### Critical: PO form has no accessible label bindings
The new-PO form (`/po/new`) and the edit form use `<label>` elements that are
**visual only** — they have no `for` attribute and the inputs have no `id`.
`page.getByLabel()` will not find them.
**Always use name-attribute selectors for PO form fields:**
```ts
// CORRECT
page.locator('input[name="title"]')
page.locator('select[name="vesselId"]')
page.locator('select[name="accountId"]')
page.locator('input[name="projectCode"]')
// WRONG — will time out
page.getByLabel(/title/i)
page.getByLabel(/vessel/i)
```
### Role-badge selectors (profile page)
The user's role appears in both the desktop sidebar/header and the profile page.
`getByText("Technical")` will fail with a strict-mode violation. Scope to `<dd>`:
```ts
// CORRECT — scoped to the profile <dd> role badge
await expect(page.locator("dd span").filter({ hasText: "Technical" })).toBeVisible();
// WRONG — strict-mode violation (role appears in header too)
await expect(page.getByText("Technical")).toBeVisible();
```
### Mobile viewport
Mobile tests must set the viewport explicitly before `login()`:
```ts
const MOBILE_VIEWPORT = { width: 375, height: 812 };
test("...", async ({ page }) => {
await page.setViewportSize(MOBILE_VIEWPORT);
await login(page, USERS.MANAGER);
// ...
});
```
### Checking CSS classes (not computed styles)
For visual-only assertions (bordered buttons, colored badges), check the element's
`class` attribute rather than computed CSS, since Tailwind classes are the source
of truth:
```ts
const cls = await page.locator("button", { hasText: "Edit" }).first().getAttribute("class");
expect(cls).toMatch(/border/);
```
---
## Writing a New Spec
### File naming
- Feature specs: `tests/e2e/<section>/<feature>.spec.ts`
- Pre-`@playwright/test` scripts: `tests/e2e/<section>/<feature>.js` (legacy format)
New specs must use `@playwright/test` format (`import { test, expect } from "@playwright/test"`).
### Header comment
Every new spec file must open with a JSDoc block:
```ts
/**
* User stories covered: Feature N — <Name>
* - Story 1
* - Story 2
*
* Created: YYYY-MM-DD
*/
```
### Step logging
Log every meaningful assertion with a `✓` prefix so CI output is scannable:
```ts
console.log("✓ Export buttons hidden on DRAFT PO");
console.log(`✓ Logged in as ${creds.email}`);
```
### Graceful skips for seed-dependent tests
Tests that require specific seed data (e.g., an item with multiple vendors, or a
PO in a particular status) should skip rather than fail hard if the precondition
is absent:
```ts
const poRow = page.locator("[data-status='MGR_APPROVED']").first();
if ((await poRow.count()) === 0) {
test.skip(true, "No MGR_APPROVED PO in seed data");
return;
}
```
### HTTP-level assertions (no browser needed)
Use `request.get()` for API-level checks (status codes, headers) rather than
driving the full browser:
```ts
test("export returns 403 for DRAFT PO", async ({ request }) => {
// Must first obtain a session cookie — use page-based login or apiRequestContext
const resp = await request.get(`/api/po/${draftPoId}/export?format=pdf`);
expect(resp.status()).toBe(403);
});
```
---
## Running the Suite
```bash
cd App/pelagia-portal
pnpm test:e2e # headless, 2 workers
pnpm test:e2e:ui # Playwright interactive UI
pnpm test:e2e -- --headed # watch the browser
# Single file
pnpm test:e2e -- tests/e2e/mobile/bottom-nav.spec.ts
# Name filter
pnpm test:e2e -- --grep "Feature 20"
# All tests with trace on every run (debugging)
pnpm test:e2e -- --trace on
```
HTML report at `playwright-report/index.html` after every run.
---
## Known Gotchas
| Situation | Symptom | Fix |
|---|---|---|
| Many workers, all trying to log in at once | Login times out; page stays on `/login` | Keep `workers ≤ 2` locally; use `storageState` for auth |
| `getByLabel(/title/i)` on PO form | Locator times out — no `htmlFor` binding | Use `locator('input[name="title"]')` |
| `getByText("Technical")` on profile page | Strict-mode violation — appears in header AND profile | Scope to `page.locator("dd span").filter(...)` |
| Multi-role flow in one test (submit → approve → pay) | Flaky under 2 workers; competing for same user | Use `beforeAll` + a dedicated seed PO or run single-threaded |
| Viewport-dependent `md:hidden` elements | Element found at desktop viewport but not mobile, or vice versa | Always set viewport before login for mobile tests |
| `router.push` soft navigation | `waitForLoadState('networkidle')` still sees old URL | Use `page.waitForURL(pattern)` concurrently with the click/select |
---
## Future Improvements
1. **Auth state sharing** — Save one `storageState` per role in a global setup file.
This eliminates ~100 login round-trips and should cut suite time from 25 min to
under 5 min.
2. **Fix pre-existing specs** — Update `submitter-journey.spec.ts` and
`po-export.spec.ts` to use the shared helper's name-based selectors (FIX-1 in
test report).
3. **`data-testid` attributes** — Add sparse `data-testid` attributes to
ambiguous elements (unit price input, line-item rows) so specs don't depend on
implementation details like placeholder text or CSS class names.
4. **CI integration** — Run `pnpm test:e2e` in Forgejo Actions (runner on `pms1`) on every PR.
Use `workers: 1` and `retries: 2` (already wired for `process.env.CI`).
5. **Visual regression** — Add Percy or Playwright's built-in screenshot comparison
for the status badge colors and mobile card layout.

242
Quality/E2E-Test-Plan.md Normal file

@ -0,0 +1,242 @@
# PPMS — E2E Test Plan
> _Migrated from the retired `Docs/e2e-test-plan.md`. Index: [Testing](Quality/Testing)._
**Version:** 1.0
**Date:** 2026-05-17
**Scope:** PPMS portal (`App/pelagia-portal`)
**Test type:** Browser-level end-to-end (Playwright / Chromium)
---
## 1 · Objectives
1. Verify that each shipped feature behaves correctly from a user's perspective in
a real browser session against a live Next.js dev server and PostgreSQL database.
2. Catch regressions introduced by new features before they reach production.
3. Document the expected user experience for each role so that future developers
have a runnable specification, not just written prose.
---
## 2 · Scope
### In scope
- All authenticated portal routes under `/(portal)/`
- Login / logout flows
- Role-based access control (page redirects, element visibility)
- Mobile-specific layout and navigation (375 × 812 viewport)
- API-level gate checks (HTTP status codes on export endpoint)
### Out of scope
- Unit tests for individual components and utilities → `tests/unit/` (Vitest)
- Integration tests for Server Actions and database mutations → `tests/integration/` (Vitest + real DB)
- Email delivery (Resend is console-logged in dev; not browser-testable)
- File storage (R2 is mocked to `.dev-uploads/` in dev)
- GstService GST-number lookup (separate Node.js service; tested independently)
- Visual pixel-perfect regression (not yet implemented)
---
## 3 · Test Environment
| Item | Value |
|---|---|
| Base URL | `http://localhost:3000` |
| App server | Next.js 15 dev server (`pnpm dev`) — auto-started by Playwright webServer config |
| Database | Local PostgreSQL populated with `pnpm db:seed` |
| Browser | Chromium (headless by default) |
| Auth | Fresh login per test using seeded credentials |
**Prerequisite:** run `pnpm db:seed` before the first test run to ensure all users,
vessels, accounts, vendors, and POs are present.
---
## 4 · User Roles Under Test
| Role | Email | Capabilities tested |
|---|---|---|
| TECHNICAL | tech@pelagia.local | Create/submit POs, view status, receipt confirmation |
| MANNING | manning@pelagia.local | Same as TECHNICAL; separate user for isolation |
| ACCOUNTS | accounts@pelagia.local | Payment queue, mark paid, payment history, partial receipt |
| MANAGER | manager@pelagia.local | Approval queue, approve/reject/request-edits, mobile |
| SUPERUSER | superuser@pelagia.local | All manager capabilities + admin read |
| ADMIN | admin@pelagia.local | Admin CRUD pages (users, vendors, vessels, etc.) |
| AUDITOR | auditor@pelagia.local | Desktop Required overlay (non-mobile role) |
---
## 5 · Feature Coverage Matrix
Each row maps a shipped feature (linked to its git commit) to the spec file
that verifies it, the roles exercised, and the current test status.
| # | Feature | Spec File | Roles | Status |
|---|---|---|---|---|
| 1 | PPMS rebrand — login, sidebar, title | `rebrand.spec.ts` | TECH | ✅ Pass |
| 2 | Color-coded PO status badges on dashboard | `dashboard/po-status-badges.js` | TECH, MANAGER | ✅ Pass |
| 3 | Submit for Approval button on DRAFT PO detail | `po-submit-button.spec.ts` | TECH | ⚠️ Selector fix needed |
| 4 | In-app notification bell with unread badge | `notification-bell.spec.ts` | TECH, MANAGER, ACCOUNTS | ✅ Pass |
| 5 | Export gate — PDF/XLSX only on MGR_APPROVED+ | `export-gate.spec.ts` | TECH, MANAGER | ✅ Pass |
| 6 | Approver name as signatory on exported docs | `export-gate.spec.ts` | ACCOUNTS | ✅ Pass |
| 7 | Payment history page at `/payments/history` | `payment-history.spec.ts` | ACCOUNTS, MANAGER, TECH | ✅ Pass |
| 8 | Partial receipt confirmation (per-item delivery) | `partial-receipt.spec.ts` | ACCOUNTS, TECH | ✅ Pass |
| 9 | Auto-verify vendor on first successful payment | `vendor-auto-verify.spec.ts` | ADMIN, ACCOUNTS | ✅ Pass (UI only; full flow skipped) |
| 10 | Bordered buttons on admin pages | `admin-bordered-buttons.spec.ts` | ADMIN | ✅ Pass |
| 11 | User profile page and manager signature | `profile.spec.ts` | TECH, ACCOUNTS, MANAGER, SUPERUSER | ✅ 6/7 pass |
| 12 | Cheapest / ★ Closest tags on inventory items | `inventory/items-tags.spec.ts` | TECH | ✅ Pass |
| 13 | Auto-sort by distance when site is selected | `inventory/items-tags.spec.ts` | TECH | ✅ Pass |
| 14 | Cart icon in header with item count badge | `inventory/cart-icon.spec.ts` | TECH | ✅ Pass |
| 15 | Item and vendor detail pages at `/inventory/…/[id]` | `inventory/cart-icon.spec.ts` | TECH | ✅ Pass |
| 16 | Desktop Required overlay for non-mobile roles | `mobile/desktop-required.spec.ts` | AUDITOR, TECH | ✅ Pass |
| 17 | Manager approval queue as mobile cards | `mobile/manager-approvals.spec.ts` | MANAGER | ✅ Pass |
| 18 | Accounts payment actions on mobile | `mobile/accounts-payments.spec.ts` | ACCOUNTS | ✅ Pass |
| 19 | Sign-out button on Desktop Required overlay | `mobile/desktop-required.spec.ts` | AUDITOR | ✅ Pass |
| 20 | Home tab in mobile bottom navigation | `mobile/bottom-nav.spec.ts` | MANAGER, ACCOUNTS | ✅ Pass |
| 21 | Edit-highlight diff on resubmitted POs | `approvals-edit-highlight.spec.ts` | TECH, MANAGER | ⚡ Flaky |
---
## 6 · Test Case Descriptions
### Feature 1 — PPMS Rebrand
| ID | Description | Expected |
|---|---|---|
| US-1a | Visit `/login` | Page shows text "PPMS" and "Pelagia Payment Management System" |
| US-1a | Visit `/login` | Page does NOT show "Pelagia Portal" |
| US-1b | Log in as any user | Sidebar displays "PPMS" |
| US-1c | Log in as any user | Browser tab title matches `/PPMS/i` |
### Feature 2 — Dashboard Status Badges
| ID | Description | Expected |
|---|---|---|
| US-2a | TECHNICAL logs in, visits `/dashboard` | Each PO row has a visible badge element with a background-color class |
| US-2b | MANAGER logs in, visits `/dashboard` | Same — badges present on manager view |
### Feature 4 — Notification Bell
| ID | Description | Expected |
|---|---|---|
| US-4a | Any user logs in | A bell icon button is visible in the header |
| US-4b | User has unread notifications | A numeric badge or dot is visible on/near the bell |
| US-4c | User clicks the bell | A dropdown/panel appears containing notification items |
### Feature 5 & 6 — Export Gate
| ID | Description | Expected |
|---|---|---|
| US-5a | Visit a DRAFT PO detail page | No "Export PDF" or "Export XLSX" buttons visible |
| US-5b | Visit a MGR_APPROVED PO detail page | Export buttons are visible |
| US-5c | `GET /api/po/[draftId]/export?format=pdf` | HTTP 403 with error JSON |
| US-6a | `GET /api/po/[approvedId]/export?format=xlsx` | HTTP 200, content-type `application/vnd.openxmlformats…` |
| US-6b | `GET /api/po/[approvedId]/export?format=pdf` | HTTP 200, content-type `application/pdf` |
### Feature 7 — Payment History
| ID | Description | Expected |
|---|---|---|
| US-7a | ACCOUNTS visits `/payments/history` | Page loads; shows table or empty-state |
| US-7a | MANAGER visits `/payments/history` | Page loads (MANAGER has `view_all_pos` permission) |
| US-7b | TECHNICAL visits `/payments/history` | Redirected to `/dashboard` |
| US-7b | MANNING visits `/payments/history` | Redirected to `/dashboard` |
### Feature 10 — Admin Bordered Buttons
| ID | Description | Expected |
|---|---|---|
| US-10a | ADMIN visits `/admin/vendors` | Edit and Delete/Deactivate buttons have a CSS class containing `border` |
| US-10b | ADMIN visits `/admin/users` | Same |
| US-10c | ADMIN visits `/admin/vessels` | Same |
| US-10d | ADMIN visits `/admin/accounts` | Same |
### Feature 1619 — Mobile Experience
| ID | Description | Viewport | Expected |
|---|---|---|---|
| US-16a | AUDITOR logs in | 375 × 812 | "Desktop Required" overlay covers the page |
| US-16a | TECHNICAL logs in | 375 × 812 | "Desktop Required" overlay visible |
| US-16a | MANAGER logs in | 375 × 812 | No overlay — portal content visible |
| US-16a | ACCOUNTS logs in | 375 × 812 | No overlay — portal content visible |
| US-19a | AUDITOR on Desktop Required screen | 375 × 812 | "Sign out" button present in overlay |
| US-19b | AUDITOR clicks "Sign out" | 375 × 812 | Redirected to `/login` |
| US-17a | MANAGER visits `/approvals` | 375 × 812 | PO cards rendered (not a table) |
| US-17b | MANAGER taps a PO card | 375 × 812 | Navigates to `/approvals/[id]` |
| US-17c | MANAGER on `/approvals/[id]` | 375 × 812 | Edit form hidden; Approve/Reject buttons visible |
| US-18a | ACCOUNTS visits `/payments` | 375 × 812 | Payment queue loads; no Desktop Required overlay |
| US-18b | ACCOUNTS sees MGR_APPROVED PO | 375 × 812 | "Start Payment Processing" button visible |
| US-18c | ACCOUNTS sees SENT_FOR_PAYMENT PO | 375 × 812 | Reference input + "Confirm Payment Sent" button visible |
### Feature 20 — Mobile Bottom Navigation
| ID | Description | Viewport | Expected |
|---|---|---|---|
| US-20a | MANAGER logs in | 375 × 812 | Bottom nav has links to `/dashboard`, `/approvals`, `/profile` |
| US-20b | MANAGER taps Home tab | 375 × 812 | Navigates to `/dashboard` |
| US-20c | ACCOUNTS logs in | 375 × 812 | Bottom nav has links to `/dashboard`, `/payments`, `/profile` |
| US-20c | ACCOUNTS taps Home tab | 375 × 812 | Navigates to `/dashboard` |
---
## 7 · Regression Checklist
Run after any change to the following areas:
| Area changed | Specs to run |
|---|---|
| Auth / login / NextAuth config | `auth.spec.ts`, `rebrand.spec.ts` |
| Portal layout (sidebar, header, mobile nav) | `mobile/bottom-nav.spec.ts`, `mobile/desktop-required.spec.ts`, `rebrand.spec.ts` |
| PO state machine / status transitions | `export-gate.spec.ts`, `po-submit-button.spec.ts`, `approvals-edit-highlight.spec.ts` |
| Payment / Accounts flows | `accounts-payment.spec.ts`, `payment-history.spec.ts`, `mobile/accounts-payments.spec.ts` |
| Approval / Manager flows | `manager-approvals.spec.ts`, `mobile/manager-approvals.spec.ts` |
| Admin pages | `admin-bordered-buttons.spec.ts` |
| Inventory / Items | `inventory/items-tags.spec.ts`, `inventory/cart-icon.spec.ts` |
| Profile page | `profile.spec.ts` |
| Notifications | `notification-bell.spec.ts` |
| Export endpoint | `export-gate.spec.ts`, `po-export.spec.ts` |
---
## 8 · Gaps & Future Test Coverage
The following areas are not yet covered by automated E2E tests:
| Gap | Priority | Notes |
|---|---|---|
| Full vendor auto-verify flow (TECH → submit → MANAGER → approve → ACCOUNTS → pay → verify) | Medium | Requires `beforeAll` multi-role setup; skip currently in place |
| PO edit form (`/po/[id]/edit`) — field pre-population | High | `submitter-journey.spec.ts` covers this but currently fails due to selector issue |
| Edits-requested email trigger | Low | Email is console-logged in dev; not directly testable in browser |
| AUDITOR read-only views | Medium | AUDITOR can view all POs; no spec yet |
| Superuser access requests on profile page | Low | UI exists; no spec |
| PDF/XLSX content verification (signature name, PO fields) | Medium | API returns correct status; content inspection not yet asserted |
| MANNING/TECHNICAL Desktop Required overlay | Done | Covered in `desktop-required.spec.ts` |
| Password change flow | Low | Form exists on profile page; not yet exercised |
---
## 9 · Continuous Integration (Planned)
When wired into CI (Forgejo Actions, runner on `pms1`), the following configuration applies:
```yaml
# .forgejo/workflows/e2e.yml
- name: Install Playwright browsers
run: pnpm exec playwright install --with-deps chromium
- name: Run E2E tests
run: pnpm test:e2e
env:
CI: "true"
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }} # e.g. the pelagia_test mirror
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
NEXTAUTH_URL: "http://localhost:3000"
```
In CI mode (`process.env.CI = "true"`), the config uses:
- `workers: 1` — no concurrency, avoids auth flooding on constrained runners
- `retries: 2` — two retry attempts before marking a test as failed
- `forbidOnly: true` — fails the run if any `test.only` is left in the code

@ -0,0 +1,343 @@
# Playwright Test Design — Pelagia Portal
> _Migrated from the retired `Docs/PLAYRIGHT_TEST_DESIGN.md`. Index: [Testing](Quality/Testing)._
This document describes how to save, structure, and extend the Playwright verification
scripts written during development sessions. Every script here was used to confirm a
bug fix before committing; they should be promoted to a permanent test suite.
---
## Setup
Playwright is currently installed in `GstService/` (a sibling service). For the Portal's
own test suite, install it once:
```bash
cd App/pelagia-portal
pnpm add -D playwright @playwright/test
npx playwright install chromium
```
Then place tests in `App/pelagia-portal/tests/e2e/` and run with:
```bash
npx playwright test # headless
npx playwright test --headed # headed (watch the browser)
npx playwright test --ui # interactive Playwright UI
```
---
## Design Principles
### 1. Log every step with a symbol prefix
Use `✓` for passing assertions, `✗` for failures, and plain text for context.
This makes CI output scannable without opening a full trace.
```js
console.log('✓ Logged in');
console.log('✓ Expanded item with', vendorCount, 'vendors');
console.log('✗ Could not find item with multiple vendors');
```
### 2. Wait for URLs, not just network idle
Client-side `router.push` navigations finish asynchronously. Always pair a
`selectOption` / `click` that triggers navigation with `page.waitForURL(...)`:
```js
const nav = page.waitForURL('**/inventory/items?siteId=**', { timeout: 8000 });
await page.locator('select').first().selectOption({ index: 1 });
await nav;
await page.waitForLoadState('networkidle');
```
### 3. Account for preserved React state across soft navigation
Next.js App Router soft-navigates between pages that share a layout. Client component
state (`useState`) is **preserved** — a row that was expanded before `router.push` stays
expanded after. Tests must model this or they will double-click a row and accidentally
close it.
```js
// Expand BEFORE selecting a site — row stays open through navigation
await page.locator('tbody tr').first().click();
await page.waitForTimeout(300);
// select site → navigate → row is still expanded, no second click needed
```
### 4. Find items with enough data to test
Not every item has multiple vendors or a known distance. Loop over rows until one
with sufficient vendors is found rather than assuming the first row is suitable:
```js
for (let i = 0; i < Math.min(rowCount, 10); i++) {
await rows.nth(i).click();
await page.waitForTimeout(400);
const vendorCount = await page.locator('table table tbody tr').count();
if (vendorCount > 1) { expanded = true; break; }
await rows.nth(i).click(); // close and try next
}
```
### 5. Exit with a non-zero code on failure
Scripts run in CI; call `process.exit(1)` so a failed check surfaces as a build error.
```js
if (!allGood) process.exit(1);
```
---
## Test Scripts
### AUTH — helpers used by every test
```js
// tests/e2e/helpers/auth.js
async function login(page, email = 'tech@pelagia.local', password = 'tech1234') {
await page.goto('http://localhost:3000/login');
await page.fill('#email', email);
await page.fill('#password', password);
await page.click('button[type=submit]');
await page.waitForURL('**/dashboard', { timeout: 8000 });
console.log(`✓ Logged in as ${email}`);
}
module.exports = { login };
```
---
### TEST 1 — Auto-sort by distance when site is selected or changed
**Bug:** Sorting did not automatically switch to "Distance" when a site was selected
from the site dropdown on the Items page. `useState` only evaluates its initial value
once on mount. Next.js soft navigation preserves component state, so changing the
`?siteId=` URL param never re-ran the initialiser. A `useEffect` keyed on
`currentSiteId` was added to reset `sortBy` whenever the selected site changes.
**File:** `tests/e2e/inventory/items-sort-by-site.js`
```js
const { chromium } = require('playwright');
const { login } = require('../helpers/auth');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await login(page);
// ── 1. No site selected → Price should be the active sort ──────────────
await page.goto('http://localhost:3000/inventory/items');
await page.waitForLoadState('networkidle');
// Expand first row to reveal the sort toggle
await page.locator('tbody tr').first().click();
await page.waitForTimeout(300);
const priceActiveNoSite = await page
.locator('button:has-text("Price")')
.evaluate(el => el.classList.contains('bg-primary-100'));
console.log('1. No site → Price active:', priceActiveNoSite);
if (!priceActiveNoSite) { console.error('✗ Expected Price to be active'); process.exit(1); }
console.log('✓ Pass');
// ── 2. Select a site → Distance should become active automatically ──────
// Row stays expanded through soft navigation — do NOT click again
const nav1 = page.waitForURL('**/inventory/items?siteId=**', { timeout: 8000 });
await page.locator('select').first().selectOption({ index: 1 });
await nav1;
await page.waitForTimeout(400); // allow useEffect to run
console.log('2. Navigated to:', new URL(page.url()).search);
const distanceActiveSite = await page
.locator('button:has-text("Distance")')
.evaluate(el => el.classList.contains('bg-primary-100'));
console.log(' Distance auto-active:', distanceActiveSite);
if (!distanceActiveSite) { console.error('✗ Expected Distance to be auto-active'); process.exit(1); }
console.log('✓ Pass');
// ── 3. Manual switch to Price still works ───────────────────────────────
await page.locator('button:has-text("Price")').click();
await page.waitForTimeout(200);
const priceManual = await page
.locator('button:has-text("Price")')
.evaluate(el => el.classList.contains('bg-primary-100'));
console.log('3. Manual switch → Price active:', priceManual);
if (!priceManual) { console.error('✗ Manual switch to Price did not work'); process.exit(1); }
console.log('✓ Pass');
// ── 4. Change to a different site → Distance resets automatically ───────
const options = await page.locator('select option').all();
if (options.length > 2) {
const nav2 = page.waitForURL('**/inventory/items?siteId=**', { timeout: 8000 });
await page.locator('select').first().selectOption({ index: 2 });
await nav2;
await page.waitForTimeout(400);
const distanceReset = await page
.locator('button:has-text("Distance")')
.evaluate(el => el.classList.contains('bg-primary-100'));
console.log('4. Different site → Distance reset:', distanceReset);
if (!distanceReset) { console.error('✗ Expected Distance to reset on site change'); process.exit(1); }
console.log('✓ Pass');
} else {
console.log('4. Skipped — only one site available in seed data');
}
await browser.close();
console.log('\n✓ All checks passed — items-sort-by-site');
})().catch(e => { console.error('✗', e.message); process.exit(1); });
```
---
### TEST 2 — Cheapest and Closest tags appear independent of sort order
**Bug:** The `★ Closest` tag was only rendered when `sortBy === "distance"` and the
`Cheapest` tag only when `sortBy === "price"`. Switching sort order hid one of the
tags entirely. The fix computes each tag independently — `minPrice` for cheapest,
`closestVendorId` for nearest by `distanceKm` — so both can appear simultaneously
on whichever vendor qualifies, regardless of the active sort.
**File:** `tests/e2e/inventory/items-vendor-tags.js`
```js
const { chromium } = require('playwright');
const { login } = require('../helpers/auth');
(async () => {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await login(page);
// ── Setup: navigate to items with a site selected ───────────────────────
await page.goto('http://localhost:3000/inventory/items');
await page.waitForLoadState('networkidle');
const nav = page.waitForURL('**/inventory/items?siteId=**', { timeout: 8000 });
await page.locator('select').first().selectOption({ index: 1 });
await nav;
await page.waitForLoadState('networkidle');
await page.waitForTimeout(300);
console.log('✓ Site selected:', new URL(page.url()).searchParams.get('siteId'));
// ── Find an item with multiple vendors ──────────────────────────────────
const rows = page.locator('tbody tr');
const rowCount = await rows.count();
let expanded = false;
let vendorCount = 0;
for (let i = 0; i < Math.min(rowCount, 10); i++) {
await rows.nth(i).click();
await page.waitForTimeout(400);
vendorCount = await page.locator('table table tbody tr').count();
if (vendorCount > 1) {
expanded = true;
console.log(`✓ Expanded item ${i + 1} with ${vendorCount} vendors`);
break;
}
await rows.nth(i).click(); // close and try next
await page.waitForTimeout(200);
}
if (!expanded) {
console.error('✗ Could not find item with multiple vendors — check seed data');
process.exit(1);
}
// ── 1. Distance sort (default): both tags must be visible ───────────────
const distanceActiveBefore = await page
.locator('button:has-text("Distance")')
.evaluate(el => el.classList.contains('bg-primary-100'));
console.log(' Active sort:', distanceActiveBefore ? 'Distance' : 'Price');
const closestDistSort = await page.locator('text=★ Closest').count();
const cheapestDistSort = await page.locator('text=Cheapest').count();
console.log(`1. Distance sort → ★ Closest: ${closestDistSort} Cheapest: ${cheapestDistSort}`);
if (closestDistSort < 1) { console.error(' Closest tag missing under Distance sort'); process.exit(1); }
if (cheapestDistSort < 1) { console.error(' Cheapest tag missing under Distance sort'); process.exit(1); }
console.log('✓ Pass');
// ── 2. Price sort: both tags must still be visible ──────────────────────
await page.locator('button:has-text("Price")').click();
await page.waitForTimeout(300);
const closestPriceSort = await page.locator('text=★ Closest').count();
const cheapestPriceSort = await page.locator('text=Cheapest').count();
console.log(`2. Price sort → ★ Closest: ${closestPriceSort} Cheapest: ${cheapestPriceSort}`);
if (closestPriceSort < 1) { console.error(' Closest tag missing under Price sort'); process.exit(1); }
if (cheapestPriceSort < 1) { console.error(' Cheapest tag missing under Price sort'); process.exit(1); }
console.log('✓ Pass');
// ── 3. No site: neither tag should appear ───────────────────────────────
const navBack = page.waitForURL(/\/inventory\/items$/, { timeout: 8000 });
await page.locator('select').first().selectOption({ value: '' });
await navBack;
await page.waitForLoadState('networkidle');
await page.waitForTimeout(300);
// Expand the same row
await page.locator('tbody tr').first().click();
await page.waitForTimeout(400);
const closestNoSite = await page.locator('text=★ Closest').count();
const cheapestNoSite = await page.locator('text=Cheapest').count();
console.log(`3. No site → ★ Closest: ${closestNoSite} Cheapest: ${cheapestNoSite}`);
if (closestNoSite > 0) { console.error('✗ ★ Closest should not appear without a site'); process.exit(1); }
if (cheapestNoSite > 0) { console.error('✗ Cheapest should not appear when only one vendor visible without site sort'); }
// Cheapest may legitimately appear if item still has multiple vendor prices — not a hard failure
console.log('✓ Pass');
await browser.close();
console.log('\n✓ All checks passed — items-vendor-tags');
})().catch(e => { console.error('✗', e.message); process.exit(1); });
```
---
## Running all e2e scripts manually
```bash
# From GstService directory (current Playwright install location)
node ../App/pelagia-portal/tests/e2e/inventory/items-sort-by-site.js
node ../App/pelagia-portal/tests/e2e/inventory/items-vendor-tags.js
```
Once Playwright is installed in the portal itself:
```bash
cd App/pelagia-portal
npx playwright test tests/e2e/
```
---
## Adding new tests
When a bug is fixed and browser-verified during a dev session, follow this checklist:
1. **Name the file after the feature area**`tests/e2e/<section>/<feature>.js`
2. **Open with a comment block** describing the bug, the fix, and what the script checks
3. **Log every decision point** with `✓`/`✗` prefix and plain-English labels
4. **Use `waitForURL`** (not `waitForLoadState`) for router.push-triggered navigations
5. **Account for preserved state** — React state survives soft nav; model that explicitly
6. **Exit non-zero** on any assertion failure so CI catches it
7. **Add an entry to this document** under `## Test Scripts` with the bug description
---
## Known gotchas
| Situation | Symptom | Fix |
|---|---|---|
| Clicking a row that is already expanded | Row closes, sort toggle disappears, selectors time out | Expand row *before* triggering soft navigation so state is preserved |
| `waitForLoadState('networkidle')` after `router.push` | URL still shows old path | Use `page.waitForURL(pattern)` concurrently with the action |
| `button:has-text("Distance")` times out | Sort toggle only renders when `expandedId` is truthy | Ensure a row is expanded before asserting on the sort toggle |
| Tags not found after switching sites | `sortBy` state did not reset (stale closure) | `useEffect` on `currentSiteId` resets it — confirm the effect dependency is correct |

266
Quality/Test-Plan.md Normal file

@ -0,0 +1,266 @@
# Pelagia Portal — Test Plan
> _Migrated from the retired `Docs/TEST_PLAN.md`. Index: [Testing](Quality/Testing)._
**Version:** 1.0
**Date:** 2026-05-09
**Project:** Pelagia Marine Services PO Portal
**Scope:** Unit, Integration, and E2E test coverage across all portal features
---
## 1. Overview
This document describes the testing strategy, scope, tooling, and coverage matrix for the Pelagia Portal. It is intended as the authoritative reference for what is tested, why, and how to run each layer.
The portal manages the full lifecycle of purchase orders: creation, submission, manager review, vendor assignment, payment, and receipt confirmation. Testing focuses on correctness of state transitions, permission enforcement, and data integrity.
---
## 2. Testing Stack
| Layer | Tool | Environment | Command |
|---|---|---|---|
| Unit | Vitest 2.x | jsdom | `pnpm test` |
| Integration | Vitest 2.x | Node (real DB) | `pnpm test:integration` |
| E2E | Playwright 1.49 | Chromium (dev server) | `pnpm test:e2e` |
**Key libraries:** `@testing-library/react`, `@testing-library/jest-dom`, `@testing-library/user-event`.
Unit tests live in `tests/unit/`. Integration tests live in `tests/integration/`. E2E specs live in `tests/e2e/`.
Integration tests run serially in a single fork (`poolOptions.forks.singleFork = true`) to avoid database conflicts. Each test suite cleans up its own data via `afterEach` using the `deletePosByTitle(PREFIX)` helper.
---
## 3. Test Data & Environment
### 3.1 Seeded Data (prisma/seed.ts)
| Entity | Records | Notes |
|---|---|---|
| Users | 5 | admin, manager, tech, accounts, manning |
| Vessels | 3 | MV Pelagia Star, MV Aegean Wind, MV Poseidon |
| Accounts | 3 | TECH-OPS, CREW-MGT, FUEL-BNK |
| Vendors | 12 | VND-0001 to VND-0012; VND-0003 and VND-0012 are unverified |
| Products | 25 | Spanning lubricants, filters, safety, rope, electrical, paint, navigation |
Re-run with `npx tsx prisma/seed.ts` before integration tests if the database is reset.
### 3.2 Authentication Mocking
Integration tests mock `@/auth` to inject a session without real credentials:
```typescript
vi.mock("@/auth", () => ({ auth: vi.fn() }));
vi.mocked(auth).mockResolvedValue(makeSession(userId, "MANAGER"));
```
`makeSession(userId, role)` is defined in `tests/integration/helpers.ts`.
### 3.3 Side-Effect Mocking
All integration and unit tests mock:
- `@/lib/notifier` — prevents email dispatch
- `next/cache` (`revalidatePath`) — avoids Next.js cache calls outside a server context
---
## 4. Coverage Matrix
### 4.1 Unit Tests
| File | Test File | Cases Covered |
|---|---|---|
| `lib/permissions.ts` | `tests/unit/permissions.test.ts` | All 7 roles × key permissions; `requirePermission` throws |
| `lib/po-state-machine.ts` | `tests/unit/po-state-machine.test.ts` | `canPerformAction`, `getTransition`, `requiresNote`, `getAvailableActions`; MANAGER/ACCOUNTS expansions |
| `lib/po-import-parser.ts` | `tests/unit/po-import-parser.test.ts` | `cellStr`, `cellNum`, `parseSheet` (real + synthetic), `parseWorkbook` |
| `lib/validations/po.ts` | `tests/unit/validations.test.ts` | `lineItemSchema`, `createPoSchema`, TC defaults |
| `components/po/po-line-items-editor.tsx` | `tests/unit/po-line-items-editor.test.tsx` | Edit mode, read-only mode, totals, add/remove |
| `components/po/po-status-badge.tsx` | `tests/unit/po-status-badge.test.tsx` | All status labels |
| `lib/utils.ts` | `tests/unit/utils.test.ts` | `formatCurrency`, `formatDate`, `generatePoNumber`, status maps |
### 4.2 Integration Tests
| Test File | Feature | Scenarios |
|---|---|---|
| `create-po.test.ts` | S-01, S-02, S-03 | Draft, submit, line items, totals, optional fields, notifications |
| `approval-actions.test.ts` | M-02, M-03, M-04, S-06, S-07 | Approve, reject, request edits, vendor ID flow, resubmit |
| `payment-actions.test.ts` | A-01, A-02 | Payment queue, mark paid |
| `discard-po.test.ts` | Discard draft | Owner, MANAGER, SUPERUSER can discard; ACCOUNTS and non-owners denied; status guard; cascade cleanup |
| `vendor-approval.test.ts` | Vendor gate + provide vendor ID | Approval blocked without vendor; ACCOUNTS can provide vendor ID; unverified vendor rejected; AUDITOR denied |
| `manager-po-creation.test.ts` | Manager creates POs | MANAGER can create, submit, discard; ACCOUNTS denied; role documented for self-approval |
| `products-search.test.ts` | Product search API | Auth, min-length validation, name/code/description search, case-insensitive, max 10, inactive excluded, Decimal serialised |
| `import-api.test.ts` | Excel import API | Auth (TECHNICAL/ACCOUNTS → 403), no file, invalid file, correct parse of Sample_PO.xlsx |
### 4.3 E2E Tests (Playwright)
| Spec File | Scenarios |
|---|---|
| `auth.spec.ts` | Login, redirect on bad creds, role badge, sign-out |
| `submitter-journey.spec.ts` | Create draft, add line items, submit, see status transitions |
| `manager-approvals.spec.ts` | Review PO, approve with/without note, reject, request edits |
| `accounts-payment.spec.ts` | Payment queue, process payment, confirm receipt |
| `po-export.spec.ts` | PDF and XLSX export buttons and content |
---
## 5. Permission Test Matrix
The table below documents every role's expected access to key operations. ✓ = allowed, ✗ = denied.
| Operation | TECHNICAL | MANNING | ACCOUNTS | MANAGER | SUPERUSER | AUDITOR | ADMIN |
|---|---|---|---|---|---|---|---|
| Create PO | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ |
| Submit PO | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ |
| Edit own draft | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ |
| Discard own draft | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ |
| Discard any draft | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ |
| Approve PO | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ |
| Reject PO | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ |
| Request edits | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ |
| Provide vendor ID | Own PO only | Own PO only | ✓ | ✓ | ✓ | ✗ | ✗ |
| Process payment | ✗ | ✗ | ✓ | ✗ | ✓ | ✗ | ✗ |
| Confirm receipt | Own PO only | Own PO only | ✗ | ✗ | ✓ | ✗ | ✗ |
| Manage vendors | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ | ✓ |
| Manage products | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ |
| Import PO | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✓ |
| View analytics | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ |
**Business rules tested explicitly:**
- A vendor must be assigned before a manager can approve a PO.
- Only verified vendors (those with a `vendorId` field) may be assigned via `provideVendorId`.
- Discarding is only possible on `DRAFT` status POs.
---
## 6. Feature-Level Test Scenarios
### F-01: PO Creation & Draft Management
| ID | Scenario | Type | File |
|---|---|---|---|
| S-01 | Create PO with multiple line items; verify totals | Integration | `create-po.test.ts` |
| S-02 | Save as draft; verify status = DRAFT | Integration | `create-po.test.ts` |
| S-02a | ACCOUNTS role denied creation | Integration | `create-po.test.ts` |
| S-02b | MANAGER can create and save a draft | Integration | `manager-po-creation.test.ts` |
| S-03 | Submit for approval; status = MGR_REVIEW | Integration | `create-po.test.ts` |
| S-04 | Discard draft by owner | Integration | `discard-po.test.ts` |
| S-04a | MANAGER discards any draft | Integration | `discard-po.test.ts` |
| S-04b | ACCOUNTS cannot discard | Integration | `discard-po.test.ts` |
| S-04c | Cannot discard a submitted PO | Integration | `discard-po.test.ts` |
### F-02: Approval Workflow
| ID | Scenario | Type | File |
|---|---|---|---|
| M-01 | Manager sees pending POs | E2E | `manager-approvals.spec.ts` |
| M-02 | Approve PO → MGR_APPROVED | Integration / E2E | `approval-actions.test.ts` |
| M-02a | Approve with note stores managerNote | Integration | `approval-actions.test.ts` |
| M-02b | Approval blocked — no vendor assigned | Integration | `vendor-approval.test.ts` |
| M-03 | Reject PO with note | Integration / E2E | `approval-actions.test.ts` |
| M-04 | Request edits → EDITS_REQUESTED | Integration | `approval-actions.test.ts` |
| M-04a | Request vendor ID → VENDOR_ID_PENDING | Integration | `approval-actions.test.ts` |
| M-04b | TECHNICAL denied approval | Integration | `approval-actions.test.ts` |
### F-03: Vendor ID Assignment
| ID | Scenario | Type | File |
|---|---|---|---|
| S-06 | TECHNICAL provides vendor ID on own PO | Integration | `approval-actions.test.ts` |
| S-06a | ACCOUNTS provides vendor ID | Integration | `vendor-approval.test.ts` |
| S-06b | Unverified vendor rejected | Integration | `vendor-approval.test.ts` |
| S-06c | AUDITOR cannot provide vendor ID | Integration | `vendor-approval.test.ts` |
| S-06d | Wrong status → error | Integration | `vendor-approval.test.ts` |
### F-04: Payment & Receipt
| ID | Scenario | Type | File |
|---|---|---|---|
| A-01 | Accounts processes payment | Integration / E2E | `payment-actions.test.ts` |
| A-02 | Mark as paid with reference | Integration / E2E | `payment-actions.test.ts` |
### F-05: Excel Import
| ID | Scenario | Type | File |
|---|---|---|---|
| I-01 | Parser extracts 1 line item from Sample_PO.xlsx | Unit | `po-import-parser.test.ts` |
| I-02 | T&C rows not included in line items | Unit | `po-import-parser.test.ts` |
| I-03 | Vendor name, PI quotation, place of delivery extracted | Unit | `po-import-parser.test.ts` |
| I-04 | GST rate > 1 normalised to fraction | Unit | `po-import-parser.test.ts` |
| I-05 | INSTRUCTIONS TO VENDORS row stops parsing | Unit | `po-import-parser.test.ts` |
| I-06 | TECHNICAL / ACCOUNTS denied (403) | Integration | `import-api.test.ts` |
| I-07 | Unauthenticated denied (401) | Integration | `import-api.test.ts` |
| I-08 | No file → 400 | Integration | `import-api.test.ts` |
| I-09 | Invalid binary → 400 | Integration | `import-api.test.ts` |
| I-10 | MANAGER receives parsed results (200) | Integration | `import-api.test.ts` |
| I-11 | Correct line item values in API response | Integration | `import-api.test.ts` |
### F-06: Product Fuzzy Search
| ID | Scenario | Type | File |
|---|---|---|---|
| P-01 | Unauthenticated → 401 | Integration | `products-search.test.ts` |
| P-02 | Query < 2 chars empty array | Integration | `products-search.test.ts` |
| P-03 | Search by name substring | Integration | `products-search.test.ts` |
| P-04 | Search by product code | Integration | `products-search.test.ts` |
| P-05 | Search by description text | Integration | `products-search.test.ts` |
| P-06 | Case-insensitive matching | Integration | `products-search.test.ts` |
| P-07 | Max 10 results returned | Integration | `products-search.test.ts` |
| P-08 | lastPrice serialised as `number` not Prisma Decimal | Integration | `products-search.test.ts` |
| P-09 | Inactive products excluded | Integration | `products-search.test.ts` |
---
## 7. Known Gaps & Out-of-Scope Items
### Currently untested (acceptable gaps)
| Area | Reason |
|---|---|
| File upload to S3 / storage | Requires live AWS credentials; tested manually in staging |
| Email notification content | `notify()` is mocked; email body format tested via review |
| PDF/XLSX export content | Snapshot-tested manually; E2E checks endpoint responds |
| Receipt confirmation workflow | Happy path covered in E2E; integration test pending |
| Admin CRUD (users, vessels, accounts, products) | Standard CRUD; covered by E2E smoke tests |
### Out of scope
- Performance / load testing
- Accessibility (a11y) automated checks
- Cross-browser testing (Chromium only)
- Mobile viewport testing
---
## 8. Running the Tests
```bash
# All unit tests (fast, no DB needed)
pnpm test
# Unit tests in watch mode during development
pnpm test:watch
# Integration tests (requires seeded DB)
pnpm test:integration
# All unit + integration
pnpm test:all
# E2E tests (requires running dev server)
pnpm test:e2e
# E2E with interactive Playwright UI
pnpm test:e2e:ui
```
### Pre-requisites for integration tests
1. A PostgreSQL instance running and `.env` pointing to it (`DATABASE_URL`).
2. Schema applied: `npx prisma migrate deploy` (or `npx prisma db push` in dev).
3. Data seeded: `npx tsx prisma/seed.ts`.
### CI behaviour
Integration tests and E2E tests run on every PR. E2E tests retry twice on failure (`playwright.config.ts`). The `test:all` script is used for pre-merge validation.
---
## 9. Test Authorship Conventions
- **Naming:** `describe` blocks map to feature scenarios (e.g., `"S-02 — save as draft"`). `it` blocks describe the outcome, not the action.
- **Prefix isolation:** Every integration test uses a `PREFIX` constant (e.g., `"INTTEST_DISCARD_"`) and cleans up with `afterEach(() => deletePosByTitle(PREFIX))`.
- **No test interdependence:** Each test creates its own data. Tests must pass in isolation and in any order.
- **Negative tests first:** Each describe block should include at least one negative (denial/error) case before or after the happy path.
- **Avoid `any`:** Type assertions in tests should use `as { id: string }` or similar narrow casts, not `as any`.

@ -1,5 +1,13 @@
# Testing
The index for the test suite. Deep references (migrated from the retired
`Docs/`):
- [Test Plan](Quality/Test-Plan) — unit/integration/E2E coverage + permission matrix
- [E2E Test Framework](Quality/E2E-Test-Framework) — Playwright stack, helpers, selector conventions
- [E2E Test Plan](Quality/E2E-Test-Plan) — feature-coverage matrix and test cases
- [Playwright Test Design](Quality/Playwright-Test-Design) — verification-script design principles
Three layers, all run on PRs. Commands run from `App/`.
| Layer | Tool | Env | Command |
@ -66,8 +74,8 @@ suite isolates with a `PREFIX` constant and cleans up via
## E2E tests (Playwright)
Browser-level checks against a live dev server + Postgres. The full E2E
framework reference is in `Docs/e2e-test-framework.md`; the feature-coverage
matrix is in `Docs/e2e-test-plan.md`.
framework reference is the [E2E Test Framework](Quality/E2E-Test-Framework) page;
the feature-coverage matrix is the [E2E Test Plan](Quality/E2E-Test-Plan).
### Config (`playwright.config.ts`)
@ -88,7 +96,7 @@ login redirect.
### Shared helpers (`tests/e2e/helpers/login.ts`)
- `USERS` — the seed credentials (see [Getting Started](Getting-Started#seed-credentials)).
- `USERS` — the seed credentials (see [Getting Started](Build-and-Run/Getting-Started#seed-credentials)).
- `login(page, creds)` — fills `/login`, waits up to **20 s** for the redirect
(bcrypt + DB can exceed Playwright's 5 s default).
- `createDraftPo(page, title)` / `submitPo(page, title)` — minimal PO setup.
@ -113,8 +121,8 @@ Rebrand, dashboard status badges, submit button, notification bell, export gate
vendor auto-verify, admin bordered buttons, profile + signature, inventory
Cheapest/★Closest tags, cart icon, item/vendor detail pages, mobile
(Desktop-Required overlay, manager cards, accounts payment, bottom nav),
edit-highlight diff. See `Docs/e2e-test-plan.md` for the full matrix and known
flaky/needs-fix items.
edit-highlight diff. See the [E2E Test Plan](Quality/E2E-Test-Plan) for the full
matrix and known flaky/needs-fix items.
## Out of scope / known gaps

@ -116,8 +116,8 @@ lib/
└── validations/{po.ts,user.ts} # Zod schemas
```
See the [Data Model](Data-Model), [PO Lifecycle](PO-Lifecycle), and
[Roles and Permissions](Roles-and-Permissions) for the core domain logic.
See the [Data Model](System/Data-Model), [PO Lifecycle](System/PO-Lifecycle), and
[Roles and Permissions](System/Roles-and-Permissions) for the core domain logic.
## API surface
@ -147,12 +147,12 @@ All data **mutations** are Server Actions co-located with their page
- Authorisation is centralised in `lib/permissions.ts`
(`hasPermission` / `requirePermission`). **Server Actions call
`requirePermission()` at the top before any DB write**; Server Components gate
whole page segments. Full matrix on [Roles and Permissions](Roles-and-Permissions).
whole page segments. Full matrix on [Roles and Permissions](System/Roles-and-Permissions).
## Development conventions
- **Trunk**: `master`. Work lands via PRs (`feat/`/`fix/`/`chore/`, or
`claude/issue-N` from the [automated pipeline](Issue-to-Deploy-Pipeline)).
`claude/issue-N` from the [automated pipeline](Ops/Issue-to-Deploy-Pipeline)).
Production is whatever `vX.Y.Z` tag is deployed; staging is a deployed instance
of latest `master`, not a branch.
- **Commits**: Conventional Commits (`feat:`, `fix:`, `refactor:`).

@ -25,7 +25,7 @@ enum ActionType {
enum RequestStatus { PENDING APPROVED DENIED }
```
`POStatus` drives the [PO Lifecycle](PO-Lifecycle); `ActionType` rows form the
`POStatus` drives the [PO Lifecycle](System/PO-Lifecycle); `ActionType` rows form the
per-PO audit trail.
## Entity relationships
@ -64,7 +64,7 @@ The central entity. Key fields:
| Field | Notes |
|---|---|
| `poNumber` (unique) | `COMPANY/VESSEL/ID/FY` — see [Purchase Orders](Purchase-Orders) |
| `poNumber` (unique) | `COMPANY/VESSEL/ID/FY` — see [Purchase Orders](Product/Purchase-Orders) |
| `status` | `POStatus`, default `DRAFT` |
| `totalAmount` | `Decimal(12,2)`, **GST-inclusive** sum of line items |
| `paidAmount?` | accumulates across partial payments |
@ -86,7 +86,7 @@ totalPrice Decimal(12,2), gstRate Decimal(5,4) default 0.18, sortOrder, size?,
deliveredQuantity? , productId?, accountId?` + `poId` (cascade).
- `totalPrice = quantity × unitPrice × (1 + gstRate)`; the PO `totalAmount` is
the sum of line `totalPrice`. See [GST](Purchase-Orders#gst-calculation).
the sum of line `totalPrice`. See [GST](Product/Purchase-Orders#gst-calculation).
- `deliveredQuantity` supports **partial receipt**.
- `accountId` allows a **per-line accounting code** override.
@ -104,7 +104,7 @@ Every state transition and notable event writes one row. `metadata` is flexible
### Notification
`subject, body, link?, isRead (default false), sentAt, status (default "sent")`
+ optional `poId`, `userId`. Backs the in-app notification bell; every email
event is also persisted here. See [Notifications](Notifications).
event is also persisted here. See [Notifications](Product/Notifications).
## Reference / catalogue models
@ -141,13 +141,13 @@ latitude?, longitude?, isVerified (default false), isActive`. `VendorContact[]`:
- Submitters can **create vendors** (unverified). A vendor becomes verified on a
closing/paying PO, on import, or via Manager/Accounts/Admin.
- `latitude`/`longitude` geocoded from `pincode` for vendor-distance sorting.
- See [Vendors and GST Lookup](Vendors-and-GST-Lookup).
- See [Vendors and GST Lookup](Product/Vendors-and-GST-Lookup).
### Product + ProductVendorPrice
`Product`: `code (unique), name, description?, lastPrice?, lastVendorId?,
isActive`. `ProductVendorPrice`: one row per `(productId, vendorId)` with
`price`. On payment confirmation, `lastPrice`/`lastVendorId` and per-vendor
prices are updated. See [Inventory and Catalogue](Inventory-and-Catalogue).
prices are updated. See [Inventory and Catalogue](Product/Inventory-and-Catalogue).
### Site / ItemInventory / ItemConsumption (inventory, feature-flagged)
- `Site`: `name, code (unique), address?, latitude?, longitude?, isActive`.
@ -186,4 +186,4 @@ chronology is a useful changelog of schema evolution, e.g.:
> **Always apply migrations before new code serves traffic.** `pnpm build` runs
> only `prisma generate`; deploying code whose client expects a not-yet-migrated
> column yields `P2022 … column does not exist`. The deploy workflow runs
> `migrate deploy`. See [Deployment and Operations](Deployment-and-Operations).
> `migrate deploy`. See [Deployment and Operations](Ops/Deployment-and-Operations).

@ -20,7 +20,7 @@ DRAFT → SUBMITTED → MGR_REVIEW → MGR_APPROVED → SENT_FOR_PAYMENT → PAI
- **Partial payments** (`PARTIALLY_PAID`) and **partial receipts**
(`PARTIALLY_CLOSED`) loop until the full amount/quantity is settled.
- **Imported POs** are created directly in `CLOSED` (historical record,
bypassing approval). See [Purchase Orders](Purchase-Orders#import-po--closed).
bypassing approval). See [Purchase Orders](Product/Purchase-Orders#import-po--closed).
- Terminal states: **REJECTED**, **CLOSED**.
## Transition table
@ -71,14 +71,14 @@ for the current PO status and the signed-in user's role.
Side-effects are declared per transition (`EMAIL_MANAGER`, `EMAIL_SUBMITTER`,
`EMAIL_ACCOUNTS`, `EMAIL_SUBMITTER_AND_MANAGER`) and dispatched via
`lib/notifier.ts` — never directly from UI handlers. See
[Notifications](Notifications) for the event→recipient matrix and templates.
[Notifications](Product/Notifications) for the event→recipient matrix and templates.
Two non-email side-effects worth calling out, applied in the server actions:
- **Product price auto-update** — on payment confirmation, each line item with a
`productId` updates `Product.lastPrice`/`lastVendorId` and upserts the
per-vendor price; a `PRODUCT_PRICE_UPDATED` action is logged. See
[Inventory and Catalogue](Inventory-and-Catalogue).
[Inventory and Catalogue](Product/Inventory-and-Catalogue).
- **Inventory increment** — at **approval**, ordered quantities are added to
`ItemInventory` when the PO has a `siteId`.

@ -4,7 +4,7 @@ Authorisation is centralised in `lib/permissions.ts`. Server Actions call
`requirePermission(role, permission)` at the top before any DB write;
`hasPermission(role, permission)` gates UI and page segments. The PO state
machine adds a second gate (status + role) on top of permissions — see
[PO Lifecycle](PO-Lifecycle).
[PO Lifecycle](System/PO-Lifecycle).
## The seven roles
@ -73,5 +73,5 @@ Beyond the permission map, server actions and the state machine enforce:
- **Receipt confirmation** is the submitter's own PO (or SUPERUSER/MANAGER).
- **Payment date** is compulsory and cannot be in the future.
See the [Testing](Testing) page for the permission test matrix that pins these
See the [Testing](Quality/Testing) page for the permission test matrix that pins these
rules down with integration tests.

@ -1,33 +1,41 @@
### Pelagia Portal (PPMS)
**Overview**
- [Home](Home)
- [Glossary](Glossary)
- [Changelog](Changelog)
- [Open Questions](Open-Questions)
**Build & Run**
- [Getting Started](Getting-Started)
- [Environment Variables](Environment-Variables)
**Overview/**
- [Glossary](Overview/Glossary)
- [Changelog](Overview/Changelog)
- [Open Questions](Overview/Open-Questions)
**System**
- [Architecture](Architecture)
- [Data Model](Data-Model)
- [PO Lifecycle](PO-Lifecycle)
- [Roles and Permissions](Roles-and-Permissions)
**Build-and-Run/**
- [Getting Started](Build-and-Run/Getting-Started)
- [Environment Variables](Build-and-Run/Environment-Variables)
**Product**
- [Feature Catalogue](Feature-Catalogue)
- [Pages and Navigation](Pages-and-Navigation)
- [Purchase Orders](Purchase-Orders)
- [Vendors and GST Lookup](Vendors-and-GST-Lookup)
- [Inventory and Catalogue](Inventory-and-Catalogue)
- [Notifications](Notifications)
- [File Storage](File-Storage)
**System/**
- [Architecture](System/Architecture)
- [Data Model](System/Data-Model)
- [PO Lifecycle](System/PO-Lifecycle)
- [Roles and Permissions](System/Roles-and-Permissions)
**Quality**
- [Testing](Testing)
**Product/**
- [Feature Catalogue](Product/Feature-Catalogue)
- [Pages and Navigation](Product/Pages-and-Navigation)
- [Workflows](Product/Workflows)
- [Purchase Orders](Product/Purchase-Orders)
- [Vendors and GST Lookup](Product/Vendors-and-GST-Lookup)
- [Inventory and Catalogue](Product/Inventory-and-Catalogue)
- [Notifications](Product/Notifications)
- [File Storage](Product/File-Storage)
- [Design System](Product/Design-System)
- [Reports Mockup](Reports-Mockup)
**Ops**
- [Deployment and Operations](Deployment-and-Operations)
- [Issue-to-Deploy Pipeline](Issue-to-Deploy-Pipeline)
**Quality/**
- [Testing](Quality/Testing)
- [Test Plan](Quality/Test-Plan)
- [E2E Test Framework](Quality/E2E-Test-Framework)
- [E2E Test Plan](Quality/E2E-Test-Plan)
- [Playwright Test Design](Quality/Playwright-Test-Design)
**Ops/**
- [Deployment and Operations](Ops/Deployment-and-Operations)
- [Issue-to-Deploy Pipeline](Ops/Issue-to-Deploy-Pipeline)