Updated directory structure

This commit is contained in:
Hardik 2026-05-19 10:35:26 +05:30
parent d689ef8893
commit a7f3b315c1
9 changed files with 2690 additions and 0 deletions

237
Docs/01-design-document.md Normal file
View file

@ -0,0 +1,237 @@
# Pelagia Portal — Design Document
## 1. Overview
Pelagia Portal is an internal purchase order (PO) management web application for a maritime / vessel-operations company. It digitises the entire PO lifecycle — from a crew member raising a requisition, through manager approval and vendor validation, to accounts payment processing and final receipt confirmation — replacing ad-hoc email chains and spreadsheets with a single, auditable system.
---
## 2. Goals & Non-Goals
### Goals
- Provide role-specific dashboards and workflows so every actor only sees what is relevant to their job.
- Enforce a structured, auditable approval chain for every purchase order.
- Notify all stakeholders at each state transition via email without manual action.
- Give management real-time spend visibility by vessel, project, and time period.
- Surface vendor information deficiencies before payment is blocked.
### Non-Goals
- Direct integration with external accounting or ERP software (out of scope for v1).
- Mobile-native apps (the web app is expected to be accessed on desktop/tablet).
- Supplier-facing self-service portal.
- Automated payment processing (Accounts team confirms payment manually).
---
## 3. Actors & Roles
| Role | Description | Key Permissions |
|---|---|---|
| **Technical** | Deck / engine crew raising POs for technical vessel needs | Create, edit draft, submit, confirm receipt |
| **Manning** | Crew-management staff raising POs for manning / crew needs | Same as Technical |
| **Manager** | Approves or rejects POs; can request edits, add vendor IDs, or directly amend line items during review | Review, approve, reject, request edits, edit line items (versioned), view all POs, history reports |
| **Accounts** | Processes payment for approved POs | View payment queue, mark as paid, view all POs |
| **SuperUser** | Elevated user with cross-team operational authority | All Technical + Manning + Manager permissions |
| **Auditor** | Read-only audit access across all records | View all POs, download audit trail, export reports |
| **Admin** | System administrator | Manage users, vessels, accounts, vendors; full CRUD on all entities |
---
## 4. PO Lifecycle & State Machine
```
DRAFT ──(submit)──► SUBMITTED ──(system auto-move)──► MGR_REVIEW
┌──────────────────────────────────────┤
│ │ │
(no vendor ID) (request edits) (reject)
▼ ▼ ▼
VENDOR_ID_PENDING EDITS_REQUESTED REJECTED
│ │
(ID provided) (resubmit)
└────────────────────┘
(approve / approve+note)
MGR_APPROVED
(accounts picks up)
SENT_FOR_PAYMENT
(payment confirmed)
PAID_DELIVERED
(submitter confirms receipt)
CLOSED
```
### Allowed State Transitions
| From | To | Actor | Trigger |
|---|---|---|---|
| DRAFT | SUBMITTED | Technical / Manning / SuperUser | Submit button |
| SUBMITTED | MGR_REVIEW | System | Auto on submit |
| MGR_REVIEW | VENDOR_ID_PENDING | Manager | Missing vendor ID |
| VENDOR_ID_PENDING | MGR_REVIEW | Submitter / Manager | Vendor ID supplied |
| MGR_REVIEW | EDITS_REQUESTED | Manager | Request edits action |
| EDITS_REQUESTED | SUBMITTED | Technical / Manning / SuperUser | Resubmit after edits |
| MGR_REVIEW | REJECTED | Manager | Reject action |
| MGR_REVIEW | MGR_APPROVED | Manager / SuperUser | Approve or Approve+Note |
| MGR_APPROVED | SENT_FOR_PAYMENT | Accounts | Pick up payment |
| SENT_FOR_PAYMENT | PAID_DELIVERED | Accounts | Confirm payment |
| PAID_DELIVERED | CLOSED | Technical / Manning / SuperUser | Confirm receipt |
---
## 5. Email Notification Matrix
| Event | Notified Parties |
|---|---|
| PO submitted | Manager(s), Submitter (confirmation) |
| Vendor ID requested | Submitter |
| Vendor ID supplied | Manager |
| Edits requested | Submitter (includes manager note) |
| PO resubmitted after edits | Manager |
| PO approved | Submitter, Accounts (with PO attachment) |
| PO approved with note | Submitter (with note), Accounts |
| PO rejected | Submitter (with rejection reason) |
| Payment sent | Submitter, Manager |
| Receipt confirmed | Manager, Accounts |
| PO closed | Submitter, Manager, Accounts |
---
## 6. Screen Inventory
### 6.1 Authentication
- **Login** — Employee ID / email + password. Role badge hints displayed. No self-registration; accounts provisioned by Admin.
### 6.2 Dashboards (role-specific landing pages)
- **Technical / Manning Dashboard** — My open POs count, pending approvals, completed orders, quick-access "New PO" CTA. Full list of all POs (open and historical) is accessible and each PO is openable from the dashboard.
- **Manager Dashboard** — Approvals awaiting count, approved POs listing with per-PO expense breakdown (line items + totals), spend by vessel (bar chart), spend by month (bar chart), recent activity feed.
- **Accounts Dashboard** — Payment queue total value, ready-for-payment item count, recently processed items.
### 6.3 PO Creation & Editing
- **New PO Form** — Multi-section form:
- Order Info: title, vessel, account, project code, date required
- Line Items: add / remove rows (description, qty, unit, unit price, total)
- Vendor: vendor name, vendor ID (optional at creation), contact
- Documents: drag-and-drop upload, file list with remove
- Approval Flow: read-only visual showing who will review
- **Edit PO** — Same form, pre-populated; only available when PO is in DRAFT or EDITS_REQUESTED.
### 6.4 Manager Approval
- **Approval Queue** — Paginated list with search (PO number, vessel, submitter) and filters (date range, vessel). Each row shows PO number, submitter, vessel, amount, days waiting.
- **PO Detail / Decision View** — Full PO summary, line items, attached documents, vendor info with verification callout (NEW if no ID). 4-action bar: Reject | Request Edits | Approve | Approve + Note.
### 6.5 Accounts Payment Queue
- **Payment Queue** — Approved POs ready for payment. Shows PO summary, total amount, bank / payment ref fields. "Mark as Paid" action.
### 6.6 Order Tracking (Submitter)
- **My Orders** — Card list with live status indicator, progress step-bar, latest manager note, and "Confirm Receipt" CTA when in PAID_DELIVERED.
### 6.7 Receipt Confirmation
- **Receipt Screen** — Upload receipt / invoice image, delivery confirmation checklist, optional notes. Submits to close the PO.
### 6.8 Manager History / Reports
- **History** — Full PO audit list with date, submitter, vessel, status, amount. Export to CSV / PDF. Filter by date range, vessel, status.
### 6.9 Administration (Admin role)
- **User Management** — CRUD for user accounts, role assignment.
- **Vessel Management** — CRUD for vessels.
- **Account Management** — CRUD for accounts / cost centres.
- **Vendor Management** — CRUD for approved vendor registry.
- **Product Catalogue** — CRUD for products: product code, name, description. Last known unit price and associated vendor are read-only in this view — they are auto-populated when a PO containing that product is marked as paid.
---
## 7. Design System
### 7.1 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 |
### 7.2 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 |
### 7.3 Component Conventions
- Cards use `rounded-lg`, `shadow-sm`, 16 px padding.
- Status badges use pill shape with colour-coded background matching state machine colours.
- Tables use alternating row shading, sticky header on scroll.
- Forms use floating labels; validation errors appear below the field in `danger` colour.
- Action buttons: primary = blue fill, secondary = white with border, danger = red fill.
---
## 8. User Stories (Priority P0 = must-have, P1 = should-have, P2 = nice-to-have)
### Submitter (Technical / Manning)
| ID | Story | Priority |
|---|---|---|
| S-01 | As a submitter, I can create a PO with line items and attach documents. | P0 |
| S-02 | As a submitter, I can save a PO as draft before submitting. | P0 |
| S-03 | As a submitter, I can submit a draft PO for manager approval. | P0 |
| S-04 | As a submitter, I receive an email when my PO is approved or rejected. | P0 |
| S-05 | As a submitter, I can view the current status and history of all my POs. | P0 |
| S-06 | As a submitter, I can provide a vendor ID when requested by a manager. | P0 |
| S-07 | As a submitter, I can edit and resubmit a PO when edits are requested. | P0 |
| S-08 | As a submitter, I can confirm receipt and upload a receipt document to close a PO. | P0 |
### Manager
| ID | Story | Priority |
|---|---|---|
| M-01 | As a manager, I see all POs awaiting my approval in a queue. | P0 |
| M-02 | As a manager, I can approve, reject, or request edits on a PO. | P0 |
| M-03 | As a manager, I can add a note when approving or rejecting. | P0 |
| M-04 | As a manager, I can flag a PO for vendor ID verification. | P0 |
| M-05 | As a manager, I can view spend analytics by vessel and month. | P1 |
| M-06 | As a manager, I can export a full PO history report as CSV or PDF. | P1 |
### Accounts
| ID | Story | Priority |
|---|---|---|
| A-01 | As an accounts user, I see all manager-approved POs ready for payment. | P0 |
| A-02 | As an accounts user, I can mark a PO as paid with a reference number. | P0 |
| A-03 | As an accounts user, I receive email when a new PO enters my payment queue. | P0 |
### Admin
| ID | Story | Priority |
|---|---|---|
| AD-01 | As an admin, I can create, edit, and deactivate user accounts. | P0 |
| AD-02 | As an admin, I can manage vessels, accounts, and vendors. | P0 |
| AD-03 | As an admin, I can manage the product catalogue (codes, names, descriptions). Last known prices and vendors are automatically updated when a PO is paid. | P1 |
---
## 9. Accessibility & Internationalisation
- WCAG 2.1 AA compliance target.
- All interactive elements keyboard-navigable with visible focus ring.
- Colour is never the sole conveyor of meaning (icons + labels accompany status colours).
- English only for v1; i18n architecture (react-i18next) to be wired up but not populated.
---
## 10. Open Questions
- Should managers be able to directly edit a PO (bypass submitter) in exceptional circumstances?
- What is the approval chain for high-value POs — single manager or dual sign-off?
- Should the vendor registry be editable by managers, or Admin-only?
- Is SSO (e.g., Azure AD) required for login, or internal credential management is sufficient?

564
Docs/02-architecture.md Normal file
View file

@ -0,0 +1,564 @@
# Pelagia Portal — Architecture Document
## 1. Technology Stack
### 1.1 Decision Summary
The portal is an internal line-of-business app with a well-defined data model, multi-role access, and transactional workflows. The stack below optimises for **developer velocity**, **type safety end-to-end**, and **operational simplicity** (minimal infrastructure to manage).
| Layer | Choice | Rationale |
|---|---|---|
| **Framework** | Next.js 15 (App Router) | Full-stack React; server components reduce client JS; built-in API routes; excellent TypeScript support |
| **Language** | TypeScript 5 (strict mode) | Shared types between frontend and backend; catches contract mismatches at compile time |
| **UI Library** | React 19 | Concurrent rendering, Server Components |
| **Component Library** | shadcn/ui + Radix UI primitives | Accessible, unstyled primitives; copy-owned source, no black-box upgrade surprises |
| **Styling** | Tailwind CSS v4 | Utility-first; consistent design tokens; no CSS specificity battles |
| **ORM** | Prisma 5 | Type-safe DB client; schema-first migrations; Prisma Studio for admin data inspection |
| **Database** | PostgreSQL 16 | ACID transactions; JSON columns for flexible line-item metadata; mature RBAC at row level |
| **Auth** | NextAuth.js v5 (Auth.js) | Session-cookie auth; credentials provider for internal accounts; easy SSO adapter upgrade path |
| **File Storage** | Cloudflare R2 (S3-compatible) in production; local filesystem in development | Cheap egress; S3 API compatibility; presigned URLs keep uploads off the app server; dev mode avoids paid services |
| **Email** | Resend + React Email in production; console log in development | Transactional email with React-rendered templates; generous free tier; reliable deliverability; dev mode requires no API key |
| **Charts** | Recharts | Lightweight; composable; works well with server-fetched data in RSC |
| **Validation** | Zod | Schema validation shared between server actions and client form validation |
| **Testing** | Vitest + React Testing Library + Playwright | Unit/integration fast with Vitest; E2E critical paths with Playwright |
| **CI/CD** | GitHub Actions | Lint, type-check, test, build on every PR; deploy on merge to main |
| **Hosting** | Vercel (app) + Supabase (Postgres + Storage fallback) | Zero-config deploys; Vercel serverless functions match Next.js well |
---
## 2. High-Level System Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Browser │
│ React 19 + shadcn/ui + Tailwind │
│ Server Components (read) + Client Components (forms) │
└──────────────────┬──────────────────────────────────────┘
│ HTTPS
┌──────────────────▼──────────────────────────────────────┐
│ Next.js 15 App Server │
│ │
│ ┌─────────────────────┐ ┌─────────────────────────┐ │
│ │ App Router Pages │ │ Server Actions / API │ │
│ │ (RSC rendering) │ │ Route Handlers │ │
│ └─────────────────────┘ └──────────┬──────────────┘ │
│ │ │
│ ┌────────────────────────────────────▼──────────────┐ │
│ │ Business Logic Layer │ │
│ │ (PO state machine, permission checks, notifier) │ │
│ └──────────────────────┬────────────────────────────┘ │
└─────────────────────────┼────────────────────────────────┘
┌───────────────┼───────────────┐
│ │ │
┌─────────▼────┐ ┌───────▼──────┐ ┌────▼──────────┐
│ PostgreSQL │ │ Cloudflare R2│ │ Resend │
│ (Prisma) │ │ (documents, │ │ (transact- │
│ │ │ receipts) │ │ ional email) │
└──────────────┘ └──────────────┘ └───────────────┘
```
---
## 3. Application Layer Structure
```
pelagia-portal/
├── app/ # Next.js App Router
│ ├── (auth)/
│ │ └── login/
│ ├── (portal)/ # Authenticated shell
│ │ ├── layout.tsx # Sidebar + header shell
│ │ ├── dashboard/
│ │ ├── po/
│ │ │ ├── new/
│ │ │ ├── [id]/
│ │ │ │ ├── page.tsx # Detail view
│ │ │ │ └── edit/
│ │ ├── approvals/
│ │ ├── payments/
│ │ ├── history/
│ │ └── admin/
│ │ ├── users/
│ │ ├── vessels/
│ │ ├── accounts/
│ │ └── vendors/
│ └── api/
│ ├── auth/[...nextauth]/
│ └── files/
│ ├── sign/ # Generate presigned upload URL (production)
│ └── dev/[...key]/ # Local file upload/download handler (dev only)
├── components/
│ ├── ui/ # shadcn/ui primitives (owned copies)
│ ├── po/ # PO-specific composite components
│ ├── dashboard/
│ └── layout/
├── lib/
│ ├── db.ts # Prisma client singleton
│ ├── auth.ts # NextAuth config
│ ├── po-state-machine.ts # State transition logic + guards
│ ├── permissions.ts # Role → allowed-action map
│ ├── notifier.ts # Email dispatch (wraps Resend)
│ ├── storage.ts # R2 presigned URL helpers
│ └── validations/ # Zod schemas
├── emails/ # React Email templates
│ ├── po-submitted.tsx
│ ├── po-approved.tsx
│ ├── po-rejected.tsx
│ ├── edits-requested.tsx
│ ├── vendor-id-needed.tsx
│ ├── payment-processed.tsx
│ └── receipt-confirmed.tsx
├── prisma/
│ ├── schema.prisma
│ └── migrations/
└── tests/
├── unit/
├── integration/
└── e2e/
```
---
## 4. Data Model
### 4.1 Entity Relationship (Prisma Schema)
```prisma
// prisma/schema.prisma
enum Role {
TECHNICAL
MANNING
ACCOUNTS
MANAGER
SUPERUSER
AUDITOR
ADMIN
}
enum POStatus {
DRAFT
SUBMITTED
MGR_REVIEW
VENDOR_ID_PENDING
EDITS_REQUESTED
REJECTED
MGR_APPROVED
SENT_FOR_PAYMENT
PAID_DELIVERED
CLOSED
}
enum ActionType {
CREATED
SUBMITTED
APPROVED
APPROVED_WITH_NOTE
REJECTED
EDITS_REQUESTED
VENDOR_ID_REQUESTED
VENDOR_ID_PROVIDED
PAYMENT_SENT
RECEIPT_CONFIRMED
CLOSED
REASSIGNED
PRODUCT_PRICE_UPDATED
}
model User {
id String @id @default(cuid())
employeeId String @unique
email String @unique
name String
passwordHash String
role Role
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
submittedPOs PurchaseOrder[] @relation("Submitter")
actions POAction[]
notifications Notification[]
}
model Vessel {
id String @id @default(cuid())
name String
imoNumber String? @unique
isActive Boolean @default(true)
purchaseOrders PurchaseOrder[]
}
model Account {
id String @id @default(cuid())
code String @unique
name String
description String?
isActive Boolean @default(true)
purchaseOrders PurchaseOrder[]
}
model Vendor {
id String @id @default(cuid())
name String
vendorId String? @unique
contactName String?
contactEmail String?
isVerified Boolean @default(false)
isActive Boolean @default(true)
createdAt DateTime @default(now())
purchaseOrders PurchaseOrder[]
products Product[] @relation("ProductLastVendor")
}
model Product {
id String @id @default(cuid())
code String @unique
name String
description String?
lastPrice Decimal? @db.Decimal(12, 2)
lastVendorId String?
lastVendor Vendor? @relation("ProductLastVendor", fields: [lastVendorId], references: [id])
isActive Boolean @default(true)
updatedAt DateTime @updatedAt
createdAt DateTime @default(now())
lineItems POLineItem[]
}
model PurchaseOrder {
id String @id @default(cuid())
poNumber String @unique @default(cuid()) // formatted in app layer
title String
status POStatus @default(DRAFT)
totalAmount Decimal @db.Decimal(12, 2)
currency String @default("USD")
dateRequired DateTime?
projectCode String?
managerNote String?
paymentRef String?
submittedAt DateTime?
approvedAt DateTime?
paidAt DateTime?
closedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
submitterId String
submitter User @relation("Submitter", fields: [submitterId], references: [id])
vesselId String
vessel Vessel @relation(fields: [vesselId], references: [id])
accountId String
account Account @relation(fields: [accountId], references: [id])
vendorId String?
vendor Vendor? @relation(fields: [vendorId], references: [id])
lineItems POLineItem[]
documents PODocument[]
actions POAction[]
receipt Receipt?
notifications Notification[]
}
model POLineItem {
id String @id @default(cuid())
description String
quantity Decimal @db.Decimal(10, 3)
unit String
unitPrice Decimal @db.Decimal(12, 2)
totalPrice Decimal @db.Decimal(12, 2)
sortOrder Int @default(0)
productId String?
product Product? @relation(fields: [productId], references: [id])
poId String
po PurchaseOrder @relation(fields: [poId], references: [id], onDelete: Cascade)
}
model PODocument {
id String @id @default(cuid())
fileName String
fileSize Int
mimeType String
storageKey String // R2 object key
uploadedAt DateTime @default(now())
poId String
po PurchaseOrder @relation(fields: [poId], references: [id], onDelete: Cascade)
}
model POAction {
id String @id @default(cuid())
actionType ActionType
note String?
metadata Json? // flexible: payment ref, vendor ID, etc.
createdAt DateTime @default(now())
poId String
po PurchaseOrder @relation(fields: [poId], references: [id])
actorId String
actor User @relation(fields: [actorId], references: [id])
}
model Receipt {
id String @id @default(cuid())
storageKey String // R2 object key
fileName String
notes String?
confirmedAt DateTime @default(now())
poId String @unique
po PurchaseOrder @relation(fields: [poId], references: [id])
}
model Notification {
id String @id @default(cuid())
subject String
body String
sentAt DateTime @default(now())
status String @default("sent") // sent | failed | bounced
poId String?
po PurchaseOrder? @relation(fields: [poId], references: [id])
userId String
user User @relation(fields: [userId], references: [id])
}
```
---
## 5. Authentication & Authorisation
### 5.1 Authentication
- Session-cookie based via NextAuth.js v5, `CredentialsProvider`.
- Passwords hashed with bcrypt (cost factor 12).
- Sessions stored server-side (database adapter); JWT not used to avoid stale role tokens.
- Session contains: `userId`, `role`, `name`, `email`.
### 5.2 Authorisation Model
Role permissions are enforced in a central `lib/permissions.ts` module and checked in Server Actions / Route Handlers before any data mutation. React Server Components also gate entire page segments server-side.
```
Action | Technical | Manning | Accounts | Manager | SuperUser | Auditor | Admin
----------------------------|-----------|---------|----------|---------|-----------|---------|-------
create_po | ✓ | ✓ | | | ✓ | |
submit_po | ✓ | ✓ | | | ✓ | |
edit_own_draft_po | ✓ | ✓ | | | ✓ | |
view_own_pos | ✓ | ✓ | | | ✓ | ✓ | ✓
view_all_pos | | | ✓ | ✓ | ✓ | ✓ | ✓
approve_po | | | | ✓ | ✓ | |
reject_po | | | | ✓ | ✓ | |
request_edits | | | | ✓ | ✓ | |
request_vendor_id | | | | ✓ | ✓ | |
process_payment | | | ✓ | | | |
confirm_receipt | ✓ | ✓ | | | ✓ | |
view_analytics | | | | ✓ | ✓ | ✓ | ✓
export_reports | | | | ✓ | ✓ | ✓ | ✓
manage_users | | | | | | | ✓
manage_vendors | | | | | | | ✓
manage_vessels_accounts | | | | | | | ✓
```
---
## 6. PO State Machine Implementation
The state machine lives entirely in `lib/po-state-machine.ts`. No state transition may be performed without going through this module, ensuring the graph is enforced in one place.
```typescript
// lib/po-state-machine.ts (illustrative)
export type POStatus =
| 'DRAFT' | 'SUBMITTED' | 'MGR_REVIEW' | 'VENDOR_ID_PENDING'
| 'EDITS_REQUESTED' | 'REJECTED' | 'MGR_APPROVED'
| 'SENT_FOR_PAYMENT' | 'PAID_DELIVERED' | 'CLOSED';
interface Transition {
to: POStatus;
allowedRoles: Role[];
requiresNote?: boolean;
sideEffects: SideEffect[];
}
const transitions: Record<POStatus, Record<string, Transition>> = {
DRAFT: {
submit: { to: 'SUBMITTED', allowedRoles: ['TECHNICAL','MANNING','SUPERUSER'], sideEffects: ['EMAIL_MANAGER'] },
},
MGR_REVIEW: {
approve: { to: 'MGR_APPROVED', allowedRoles: ['MANAGER','SUPERUSER'], sideEffects: ['EMAIL_SUBMITTER','EMAIL_ACCOUNTS'] },
approve_note: { to: 'MGR_APPROVED', allowedRoles: ['MANAGER','SUPERUSER'], requiresNote: true, sideEffects: ['EMAIL_SUBMITTER','EMAIL_ACCOUNTS'] },
reject: { to: 'REJECTED', allowedRoles: ['MANAGER','SUPERUSER'], requiresNote: true, sideEffects: ['EMAIL_SUBMITTER'] },
request_edits: { to: 'EDITS_REQUESTED', allowedRoles: ['MANAGER','SUPERUSER'], requiresNote: true, sideEffects: ['EMAIL_SUBMITTER'] },
request_vendor: { to: 'VENDOR_ID_PENDING', allowedRoles: ['MANAGER','SUPERUSER'], sideEffects: ['EMAIL_SUBMITTER'] },
},
SENT_FOR_PAYMENT: {
confirm_payment: { to: 'PAID_DELIVERED', allowedRoles: ['ACCOUNTS'], sideEffects: ['EMAIL_SUBMITTER','EMAIL_MANAGER','UPDATE_PRODUCT_PRICES'] },
},
// ...
};
export function canTransition(from: POStatus, action: string, role: Role): boolean { ... }
export async function applyTransition(poId: string, action: string, actor: User, note?: string): Promise<PurchaseOrder> { ... }
```
### Product Price Auto-Update (`UPDATE_PRODUCT_PRICES` side effect)
When `confirm_payment` fires on a `SENT_FOR_PAYMENT` PO, `applyTransition` iterates every line item that carries a `productId`. For each one it sets `Product.lastPrice = lineItem.unitPrice` and `Product.lastVendorId = po.vendorId`. A `PRODUCT_PRICE_UPDATED` `POAction` is logged per updated product. Line items without a `productId` are skipped.
---
## 7. File Upload Flow
To avoid routing large files through the app server, uploads use **presigned URLs** in production. Development uses a local equivalent to avoid requiring Cloudflare credentials.
**Production (`NODE_ENV=production`) — Cloudflare R2:**
```
Client App Server Cloudflare R2
│ │ │
│── POST /api/files/sign ──►│ │
│ { fileName, mimeType } │ │
│ │── generate presigned ─►│
│ │◄─── presigned URL ─────│
│◄── { uploadUrl, key } ────│ │
│ │ │
│─────── PUT uploadUrl ──────────────────────────────►│
│ │ │
│── Server Action: link ───►│ │
│ { poId, key, meta } │── INSERT PODocument ──►│ (DB)
```
**Development (`NODE_ENV=development`) — local filesystem:**
```
Client App Server .dev-uploads/
│ │ │
│── POST /api/files/sign ──►│ │
│ { fileName, mimeType } │ │
│◄── { uploadUrl, key } ────│ │
│ uploadUrl = /api/files/dev/<key>
│ │ │
│── PUT /api/files/dev/<key>►│ │
│ │── write to disk ───────►│
│ │ │
│── Server Action: link ───►│ │
│ { poId, key, meta } │── INSERT PODocument ──►│ (DB)
```
Downloads follow the same pattern: `generateDownloadUrl` returns a `/api/files/dev/<key>` GET URL in development and an R2 presigned URL in production. The `app/api/files/dev/[...key]/route.ts` route is auth-gated and returns 404 in production.
---
## 8. Notification System
`lib/notifier.ts` is the single point for dispatching emails. It is called exclusively from within state-machine side-effects, never directly from UI handlers.
```
notifier.notify({
event: 'PO_APPROVED',
po: PurchaseOrder, // full PO with relations
recipients: User[], // resolved from event matrix
})
```
**In production**, email templates live in `/emails/` as React Email components, rendered server-side with `@react-email/render` and sent via the Resend SDK.
**In development**, the email content (recipient, subject, body) is printed to the terminal instead of being sent. No Resend API key is required.
In both modes, all notification events are persisted in the `Notification` table for audit purposes.
---
## 9. API Surface
All data mutations are implemented as **Next.js Server Actions** (no separate REST endpoints for mutations). Queries use React Server Components where possible; client components call `fetch` against route handlers only for dynamic/paginated data.
| Route Handler | Method | Purpose |
|---|---|---|
| `/api/auth/[...nextauth]` | GET/POST | Auth.js session endpoints |
| `/api/files/sign` | POST | Generate R2 presigned upload URL |
| `/api/po/[id]/export` | GET | Export single PO as PDF |
| `/api/reports/export` | GET | Export history report as CSV/PDF |
All other data operations (create PO, approve, reject, etc.) are Server Actions in `app/(portal)/*/actions.ts` co-located with their page.
---
## 10. Deployment Architecture
```
┌────────────────────────────────────────────────┐
│ Vercel │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Next.js App (Edge + Node.js) │ │
│ │ - Static assets via Vercel CDN │ │
│ │ - Server Components on Node.js runtime │ │
│ │ - API routes / Server Actions │ │
│ └──────────────────────────────────────────┘ │
└────────────────────────────────────────────────┘
│ │
┌────────▼──────┐ ┌────────▼──────────────┐
│ Supabase │ │ Cloudflare R2 │
│ PostgreSQL │ │ (document storage) │
│ (managed, │ │ │
│ auto-backup)│ └────────────────────────┘
└───────────────┘
┌────────▼──────┐
│ Resend │
│ (email API) │
└───────────────┘
```
### Environment Variables
The set of required variables differs between development and production. The switch is automatic — controlled by `NODE_ENV` (set to `development` by `next dev` and `production` by `next build/start`).
| Variable | Dev Required | Prod Required | Notes |
|---|---|---|---|
| `NEXTAUTH_SECRET` | Yes | Yes | 32-char random secret |
| `NEXTAUTH_URL` | Yes | Yes | Full app URL |
| `DATABASE_URL` | Yes | Yes | PostgreSQL connection string |
| `R2_ACCOUNT_ID` | No | Yes | Cloudflare account ID |
| `R2_ACCESS_KEY_ID` | No | Yes | R2 access key |
| `R2_SECRET_ACCESS_KEY` | No | Yes | R2 secret key |
| `R2_BUCKET_NAME` | No | Yes | R2 bucket name |
| `R2_PUBLIC_URL` | No | Yes | Public R2 bucket URL |
| `RESEND_API_KEY` | No | Yes | Resend API key |
| `EMAIL_FROM` | No | Yes | Sender address |
| `EMAIL_FROM_NAME` | No | No | Display name (default: "Pelagia Portal") |
In development, uploaded files are stored in `.dev-uploads/` at the project root and emails are printed to the terminal.
---
## 11. Testing Strategy
| Layer | Tool | What is tested |
|---|---|---|
| Unit | Vitest | State machine transitions, permission checks, Zod validators, utility functions |
| Integration | Vitest + Prisma test DB | Server Actions against a real test database; seeded with fixture data |
| E2E | Playwright | Full happy paths per role: create PO → approve → pay → confirm receipt |
| Accessibility | axe-core + Playwright | WCAG violations on key pages |
CI runs all tests on every pull request. Playwright E2E runs against a preview deployment.
---
## 12. Development Conventions
- **Branch strategy**: `main` (production) ← `staging` ← feature branches (`feat/`, `fix/`, `chore/`).
- **Commit style**: Conventional Commits (`feat:`, `fix:`, `refactor:`).
- **Code quality**: ESLint (Next.js config) + Prettier + TypeScript strict mode; enforced via husky pre-commit hook.
- **Database migrations**: Never edit `schema.prisma` without generating and committing a migration (`prisma migrate dev`). Migration files are committed and reviewed in PRs.
- **Secrets**: Never committed; managed via Vercel environment variable UI and `.env.local` locally (`.env.local` is git-ignored).

16
Docs/03-open-questions.md Normal file
View file

@ -0,0 +1,16 @@
# Pelagia Portal — Open Questions & Decisions Log
Track decisions that need sign-off before the corresponding feature is built. Update the Status column when resolved.
| # | Question | Raised By | Status | Decision |
|---|---|---|---|---|
| 1 | Should a manager be able to directly edit a PO (bypass the submitter edit cycle) in exceptional circumstances? | Design review | Open | — |
| 2 | Is dual sign-off required for POs above a certain value threshold? If so, what is the threshold and how is the second approver selected? | Design review | Open | — |
| 3 | Is the vendor registry Admin-only, or can Managers also add/edit vendors? | Design review | Open | — |
| 4 | Is SSO (Azure AD / Google Workspace) required for login, or is internal credential management sufficient for v1? | Architecture review | Open | — |
| 5 | What currency / currencies does the system need to support? Is multi-currency (with FX rates) in scope? | Design review | Open | — |
| 6 | Should rejected POs be hard-deleted after a retention period or permanently archived? How long is the retention window? | Legal / compliance | Open | — |
| 7 | Should documents (PO attachments, receipts) be publicly accessible via URL, or always served through a signed/authenticated download? | Security review | Open | — |
| 8 | Are there specific vessels or accounts that certain submitters are restricted to (i.e., row-level vessel permissions), or is any submitter able to raise a PO against any vessel? | Design review | Open | — |
| 9 | What is the expected volume? (POs per day, concurrent users) — affects connection-pool sizing and whether Vercel serverless is sufficient. | Architecture review | Open | — |
| 10 | Should Manager analytics (spend by vessel/month) include only CLOSED POs, or all POs from MGR_APPROVED onwards? | Design review | Open | — |

719
Docs/DESIGN.md Normal file
View file

@ -0,0 +1,719 @@
# Pelagia Portal — Design Document
Internal purchase-order management system for a maritime company.
This document describes every feature, page, workflow, and user story to guide UI/UX design.
---
## 1. Purpose
Pelagia Portal digitises the full purchase-order lifecycle — from a crew member raising a requisition aboard a vessel, through manager approval and payment by accounts, to receipt confirmation on delivery. It replaces paper and email-based processes with a traceable, role-gated workflow.
---
## 2. User Roles
Seven roles exist. Each role represents a real job function in the company.
| Role | Who they are | Core capability |
|------|-------------|-----------------|
| **TECHNICAL** | Ship technical crew | Create, submit, and track their own POs; confirm delivery |
| **MANNING** | Manning crew | Same as TECHNICAL |
| **ACCOUNTS** | Finance / accounts team | Process payments, manage vendor registry |
| **MANAGER** | Department manager | Review and approve POs, edit line items before approval, view analytics |
| **SUPERUSER** | Power user / ops lead | All PO actions across the board |
| **AUDITOR** | Internal auditor | Read-only view of all POs; export reports |
| **ADMIN** | System administrator | Manage users, vendors, vessels, accounts, products, and sites |
### Role Access Matrix
| Feature area | TECH / MANNING | ACCOUNTS | MANAGER | SUPERUSER | AUDITOR | ADMIN |
|---|:---:|:---:|:---:|:---:|:---:|:---:|
| Create / edit own POs | ✓ | | ✓ | ✓ | | |
| Approve / reject POs | | | ✓ | ✓ | | |
| Process payments | | ✓ | | ✓ | | |
| Confirm receipt | ✓ | | | ✓ | | |
| View all POs | | ✓ | ✓ | ✓ | ✓ | ✓ |
| View analytics / export | | | ✓ | ✓ | ✓ | ✓ |
| Vendor registry | | ✓ | ✓ | | | ✓ |
| Item catalogue | | | ✓ | | | ✓ |
| Vessel management | | | ✓ | | | ✓ |
| Site management | | | ✓ | | | ✓ |
| User management | | | | | | ✓ |
| Account management | | | ✓ | | | ✓ |
---
## 3. Navigation Structure
The left sidebar adapts to the signed-in user's role.
```
Dashboard ← all users
─── Purchase Orders ──────────────────
New PO ← TECH, MANNING, MANAGER, SUPERUSER
My Orders ← TECH, MANNING, MANAGER, SUPERUSER
Approvals ← MANAGER, SUPERUSER
Import PO ← MANAGER, SUPERUSER, ADMIN
Payments ← ACCOUNTS
History / Export ← MANAGER, SUPERUSER, ACCOUNTS, AUDITOR, ADMIN
─── Inventory ───────────────────────
Vendors ← MANAGER, ACCOUNTS, ADMIN
Items ← MANAGER, ADMIN
Vessels ← MANAGER, ADMIN
Sites ← MANAGER, ADMIN
Cart ← TECH, MANNING, MANAGER, SUPERUSER
─── Administration ────────────────── (ADMIN only)
Users
Accounts
```
---
## 4. Authentication
### Login Page `/login`
- Email + password form
- Validates credentials against bcrypt hash
- On success: redirects to `/dashboard` (or pre-login destination)
- No self-registration; accounts are created by an ADMIN
---
## 5. Page Catalogue
### 5.1 Dashboard `/dashboard`
Entry point after login. Content varies by role.
**Submitter view (TECHNICAL / MANNING / SUPERUSER)**
- Stat cards: Open orders count, Pending approval count, Completed orders
- Quick "New PO" call-to-action
- Link to full order list
**Manager view**
- Stat cards: Awaiting approval (clickable → approval queue), Approved this month, Total approved spend
- Recent approved POs table: PO number, title, vessel, amount, date
- Spend trend chart (monthly bar chart, last 612 months)
- Vessel spend breakdown chart (pie or bar)
**Accounts view**
- Stat cards: Ready for payment count, Total value awaiting payment
- Quick link to payment queue
**Auditor / Admin view**
- Total PO count with link to history
---
### 5.2 My Purchase Orders `/my-orders`
Personal PO list for submitters.
**Open orders table** (DRAFT, SUBMITTED, MGR_REVIEW, VENDOR_ID_PENDING, EDITS_REQUESTED)
- Columns: PO Number, Title, Vessel, Status badge, Amount, Last updated
- Manager note displayed inline if status = EDITS_REQUESTED
**Past orders table** (MGR_APPROVED through CLOSED / REJECTED)
- Same columns
Actions:
- "New PO" button (top right)
- Click any row → PO detail page
---
### 5.3 Approval Queue `/approvals`
All POs awaiting manager decision (status = MGR_REVIEW).
Filter bar:
- Search (PO number, submitter name, title)
- Vessel dropdown
- Date from picker
Table columns: PO Number, Title, Submitter, Vessel, Amount, Submitted date
Actions:
- "Review" link per row → approval detail page
- Pending count shown in heading
---
### 5.4 PO Detail `/po/[id]`
Full read view of a single PO. Accessible to: the submitter (own POs), ACCOUNTS, MANAGER, SUPERUSER, AUDITOR, ADMIN.
**Header band**
- PO number (monospace)
- Status badge (colour-coded)
- Export PDF button
**Body sections**
*Summary panel*
- Title, vessel, account, vendor (if assigned), project code, date required, currency, total amount
*Line items table*
- Columns: Item name, Description, Qty, Unit, Unit price, GST rate, Total (incl. GST)
- Read-only
*Terms & Conditions*
- Delivery, Dispatch, Inspection, Transit insurance, Payment terms, Others
*Documents*
- Uploaded files with download links
*Audit trail*
- Chronological list of every action on the PO
- Each row: actor name, action type, timestamp, optional note
*Timestamps sidebar (or footer)*
- Created, Submitted, Approved, Paid, Closed
**Contextual action buttons** (shown/hidden based on status and role)
| Condition | Button |
|-----------|--------|
| Status = DRAFT or EDITS_REQUESTED + own submitter | Edit |
| Status = DRAFT + own submitter or MANAGER/SUPERUSER | Discard (delete draft) |
| Status = VENDOR_ID_PENDING + can provide vendor | Vendor selection form inline |
| Status = PAID_DELIVERED + own submitter or SUPERUSER | Confirm Receipt |
---
### 5.5 Approval Detail `/approvals/[id]`
Full PO view with approval action panel. MANAGER / SUPERUSER only.
Same content as PO detail, plus:
**Manager action panel**
- Approve button
- Approve with Note button (opens note textarea, then approves)
- Reject button (requires mandatory note)
- Request Edits button (requires mandatory note)
- Request Vendor ID button (sends back to submitter to supply vendor)
**Manager line-item edit form**
- Inline form allowing manager to adjust quantities, unit prices, GST rate, add/remove line items and change vessel, account, vendor before approving
---
### 5.6 New PO `/po/new`
Multi-section form to create a purchase order.
**Section 1 — Header**
- Title (required)
- Description / remarks
- Vessel (required, dropdown)
- Account / Cost Centre (required, dropdown)
- Vendor (optional, dropdown — can be added later)
- Date Required (date picker)
- Project Code
**Section 2 — Line Items**
- Dynamic table; rows can be added and removed
- Per-row fields: Name (searchable against item catalogue), Description, Qty, Unit, Size, Unit Price, GST Rate
- As-you-type name search shows matching products with per-vendor prices as hints
- Running totals shown below table: Taxable, GST, Grand Total
**Section 3 — Terms & Conditions**
- Delivery, Dispatch, Inspection, Transit Insurance, Payment Terms, Others (all text, optional)
**Section 4 — Documents**
- Drag-and-drop or browse file uploader
- Shows list of attached files
**Footer actions**
- Save as Draft
- Submit for Approval
---
### 5.7 Edit PO `/po/[id]/edit`
Identical form to New PO, pre-filled with existing data.
Available only when status = DRAFT or EDITS_REQUESTED, and the user is the submitter or SUPERUSER.
Footer actions:
- Save as Draft
- Update & Resubmit (only shown when status = EDITS_REQUESTED; transitions back to MGR_REVIEW)
---
### 5.8 Import PO `/po/import`
Upload an Excel file in Pelagia's standard PO template format.
Steps (wizard-style or single page):
1. Drop / upload .xlsx file
2. System parses line items, vendor, quotation details
3. User selects Vessel and Account (not parsed from file)
4. Preview of extracted line items in editable table
5. Save as Draft
---
### 5.9 Confirm Receipt `/po/[id]/receipt`
Receipt confirmation form. Shown only when status = PAID_DELIVERED.
- PO number and title shown as context
- File upload for delivery receipt document
- Optional notes field
- Submit button → transitions PAID_DELIVERED → CLOSED
---
### 5.10 Payment Queue `/payments`
ACCOUNTS role only.
Card list of POs in MGR_APPROVED and SENT_FOR_PAYMENT statuses.
**Per card**
- PO number, title
- Vessel, Submitter, Vendor
- Approved date
- Amount (prominent)
- Status badge: "Ready for Payment" or "Processing — awaiting confirmation"
**Per card actions**
- MGR_APPROVED → "Send for Payment" button
- SENT_FOR_PAYMENT → "Mark as Paid" button
- View PO detail link
---
### 5.11 History & Export `/history`
All POs in all statuses. MANAGER, SUPERUSER, ACCOUNTS, AUDITOR, ADMIN.
**Filter bar**
- Date range (from / to)
- Vessel dropdown
- Status dropdown
**Table columns**: PO Number, Title, Vessel, Submitter, Status badge, Amount, Created date
**Export buttons** (apply current filters to export)
- Export PDF
- Export CSV
---
### 5.12 Vendor Registry `/admin/vendors`
Vendor list. MANAGER, ACCOUNTS, ADMIN.
**Table columns**: Vendor ID (or "Pending"), Name, Contact (name + email), Item count, Verified badge, Status badge
**Actions**
- Add Vendor button → modal form (GSTIN lookup, name, address, pincode auto-filled via GST portal captcha; manual contact fields)
- Edit / Delete per row
- Click vendor name → Vendor Detail page
---
### 5.13 Vendor Detail `/admin/vendors/[id]`
**Header**
- Vendor name, vendor ID, verified / active badges
- Edit button
**Info card**
- GSTIN, address, pincode, contact name, mobile, email
**Items supplied table**
- Product code, name, last quoted price, last updated
- Click product name → Item Detail page
**Recent POs table**
- PO number, status, amount, created date (last 10)
---
### 5.14 GSTIN Lookup (modal / inline within vendor form)
Two-step flow embedded in the Add / Edit Vendor form:
1. User types a 15-character GSTIN and clicks "Look up"
2. System loads GST portal captcha image from the microservice → displays inline
3. User types the 6-digit captcha answer
4. User clicks "Verify" → microservice submits to GST portal → returns taxpayer data
5. Form auto-fills: name, address, pincode (lat/lng geocoded silently from pincode)
Error states: wrong captcha (shows error, resets), session expired (auto-reset), GST portal unavailable.
---
### 5.15 Item Catalogue `/admin/products`
MANAGER, ADMIN.
**Table columns**: Name, Code, Description, Vendor count, Last price, Last vendor, Updated date, Status badge
Footer note: "Items are added automatically when a PO is marked as paid."
**Actions** (ADMIN only)
- Add Product → modal form (code, name, description)
- Toggle Active / Inactive per row
- Delete per row
- Click name → Item Detail page
---
### 5.16 Item Detail `/admin/products/[id]`
**Header**
- Name, code, status badge, description
- Add to Cart button
- Toggle Active button (ADMIN only)
**Stat cards**
- Vendor count, Lowest price, Highest price, Sites with stock
**Price comparison bar chart**
- One bar per vendor, Y-axis = unit price
**Site distance filter**
- Dropdown: "Sort by distance from site" — re-sorts vendor table by proximity
- Uses geocoded pincode of vendor vs site lat/lng for distance
**Vendor pricing table**
- Columns: Vendor (link to vendor detail), Verified badge, Unit price, Distance (if site selected), Last updated, Add to Cart
- Closest vendor gets a ★ marker when a site is selected
**Stock by site**
- Chip list: site name + quantity on hand (link to site detail)
---
### 5.17 Vessel Management `/admin/vessels`
MANAGER, ADMIN.
**Table columns**: Name, IMO Number, Status badge
**Actions**
- Add / Edit / Delete per row (all modal)
---
### 5.18 Account / Cost Centre Management `/admin/accounts`
MANAGER, ADMIN.
**Table columns**: Code, Name, Description, Status badge
**Actions**
- Add / Edit / Delete per row (all modal)
---
### 5.19 Sites `/admin/sites`
MANAGER, ADMIN (ADMIN-only for add/edit/delete).
Ports, depots, and offices that hold inventory.
**Table columns**: Name, Code, Address, Vessels, Items tracked, Location (lat/lon from pincode), Status badge
**Actions**
- Add Site → modal form (name, code, address, pincode for auto-geocoding)
- Edit / Delete per row
- Click name → Site Detail page
---
### 5.20 Site Detail `/admin/sites/[id]`
**Header**
- Name, code, address, geocoded location
- Edit button (ADMIN only)
**Stat cards**
- Vessels at site, Items tracked, Total inventory value (if calculable)
**Inventory bar chart**
- X-axis = product name, Y-axis = quantity on hand
**Consumption line chart**
- Last 30 days of daily consumption, one line per product
**Inventory table**
- Product name, quantity on hand, last updated; link to item detail
**Log consumption form**
- Fields: Product (dropdown), Date (date picker), Quantity, Note
- Submits immediately; chart and table refresh
**Assigned vessels**
- Chip list linking to vessel detail
**Recent POs for this site**
- Last 8 POs with status, vendor, amount
---
### 5.21 User Management `/admin/users`
ADMIN only.
**Table columns**: Employee ID, Name, Email, Role badge, Status badge, Created date
**Actions**
- Add User → modal form (employee ID, name, email, role, initial password)
- Edit → modal form (same fields, password optional)
- Delete per row
---
### 5.22 Cart `/inventory/cart`
Persistent cart collecting items selected from product detail pages. Stored in localStorage.
**Cart view**
- Item list: product name, description, vendor (if selected), unit price, quantity (editable inline)
- Summary: subtotal, GST, grand total
- Site selector (to indicate delivery site)
**Actions**
- Remove item
- Clear cart
- Create PO → opens New PO form pre-filled with cart line items and selected site/vendor
---
## 6. PO Lifecycle State Machine
```
┌──────────────────────────┐
▼ │
[DRAFT] ──submit──► [SUBMITTED] ──auto──► [MGR_REVIEW]
│ │ │ │
approve ◄───────┘ │ │ └──── reject ──► [REJECTED]
│ │ │
│ request_edits─┘ └── request_vendor_id ──► [VENDOR_ID_PENDING]
│ │
│ ◄──── provide_vendor_id ──────────────────────┘
[MGR_APPROVED]
process_payment
[SENT_FOR_PAYMENT]
mark_paid
[PAID_DELIVERED]
confirm_receipt
[CLOSED]
```
States that allow re-entry into the flow:
- **EDITS_REQUESTED** → submitter edits PO → re-submits → MGR_REVIEW
- **VENDOR_ID_PENDING** → submitter selects vendor → MGR_REVIEW
Terminal states: **REJECTED**, **CLOSED**
---
## 7. Workflows
### 7.1 Submit a Purchase Order (TECHNICAL / MANNING)
1. Click **New PO** in sidebar
2. Select vessel and account
3. Add line items (type name to search item catalogue; previous vendor prices appear as hints)
4. Optionally attach documents and fill in T&C fields
5. Click **Submit for Approval**
6. Manager receives email notification
7. Status shows as "Under Review" on My Orders page
8. If manager requests edits: submitter sees EDITS_REQUESTED status with manager note; edits form; resubmits
9. If manager requests vendor ID: submitter selects a vendor and submits; returns to manager queue
10. On approval: submitter notified by email; accounts team can see PO in payment queue
### 7.2 Approve a Purchase Order (MANAGER)
1. Click **Approvals** in sidebar; see count of pending POs
2. Click **Review** on a PO
3. Read full detail: line items, vendor, documents, submitter notes
4. Optionally: click **Edit** to adjust line items, change vendor, vessel, or account
5. Choose action:
- **Approve** → immediately moves to accounts payment queue
- **Approve with Note** → same, with a note visible to submitter
- **Request Edits** → write note explaining required changes; PO returned to submitter
- **Request Vendor ID** → PO returned to submitter to select vendor; then returns to manager queue
- **Reject** → write reason; PO is closed permanently
### 7.3 Process a Payment (ACCOUNTS)
1. Click **Payments** in sidebar
2. See cards for all MGR_APPROVED POs
3. Click **Send for Payment** → initiates payment; notifies submitter and manager
4. When payment is confirmed by bank/finance: click **Mark as Paid** → notifies all parties
5. Submitter can now upload delivery receipt
### 7.4 Confirm Receipt (TECHNICAL / MANNING)
1. Goods are delivered on site / to vessel
2. Navigate to PO detail page (status = PAID_DELIVERED)
3. Click **Confirm Receipt**
4. Upload delivery receipt document and optionally add notes
5. Submit → PO is CLOSED; accounts and manager notified
### 7.5 Look Up a Vendor by GSTIN (MANAGER / ADMIN)
1. Open Add/Edit Vendor modal
2. Type the 15-digit GSTIN
3. Click **Look up** → captcha image loads from GST portal (via microservice)
4. Type the 6-digit captcha shown in the image
5. Click **Verify** → form auto-fills with legal name, trade name, registered address, pincode
6. Review and save; location is geocoded silently from pincode for distance calculations
### 7.6 Source Items by Proximity (MANAGER)
1. Navigate to **Items** → click an item name
2. See all vendors that supply the item with their last quoted price
3. Select a **site** from the "Sort by distance from" dropdown
4. Table re-sorts: vendors nearest to the site appear first; distance shown per row; closest vendor marked ★
5. Click **Add to Cart** on the desired vendor row → item added to cart
### 7.7 Create a PO from the Cart (MANAGER / TECHNICAL)
1. Browse Item catalogue and add items to cart (Add to Cart button per vendor row)
2. Click **Cart** in sidebar
3. Review cart: adjust quantities inline; remove items; select delivery site
4. Click **Create PO** → opens New PO form pre-filled with all cart items and vendor
5. Fill in title, vessel, account; submit normally
### 7.8 Track Inventory at a Site (MANAGER / ADMIN)
1. Navigate to **Sites** → click a site
2. View bar chart of current stock (quantity per product)
3. View consumption line chart (last 30 days)
4. Use **Log Consumption** form to record daily drawdown: select product, pick date, enter quantity
### 7.9 Auto-sync Catalogue on Payment Confirmation (ACCOUNTS → SYSTEM)
When accounts clicks **Mark as Paid**:
- System checks each PO line item that has a product link
- For unlinked items: attempts fuzzy-match on name; creates new product record if no match
- Upserts `ProductVendorPrice` — if this vendor/product combination is new or the price changed, updates the catalogue
- Sets `Product.lastPrice` and `Product.lastVendorId`
- Future POs using that product name will see this vendor's latest price as a hint
### 7.10 Import a PO from Excel (MANAGER)
1. Navigate to **Import PO**
2. Upload an Excel file in Pelagia's standard template format
3. System extracts: line items (name, description, qty, unit, price, GST), vendor details, quotation number/date
4. User selects vessel and account from dropdowns
5. Review and optionally edit extracted line items
6. Save as Draft → PO created; submitter can then edit and submit
### 7.11 Export PO History (AUDITOR / MANAGER)
1. Navigate to **History**
2. Apply filters: date range, vessel, status
3. Click **Export PDF** or **Export CSV**
4. File downloaded with all matching POs; up to 200 results per export
---
## 8. Data Entities
### Purchase Order
Fields: PO number (auto-generated), title, status, total amount, currency, date required, project code, manager note, payment reference, quotation number/date, requisition number/date, place of delivery, all T&C text fields, timestamps.
### PO Line Item
Fields: name, description, quantity, unit, size, unit price, GST rate (default 18%), total price (computed), sort order, optional product link.
### Vendor
Fields: name, vendor ID (optional, unique), address, pincode, GSTIN, contact name/mobile/email, latitude/longitude (geocoded silently from pincode), verified flag, active flag.
### Product (Item)
Fields: code (auto-generated or manual), name, description, last price, last vendor, active flag. Prices tracked per vendor via `ProductVendorPrice` (one record per productvendor pair).
### Vessel
Fields: name, IMO number (optional), active flag, assigned site (optional).
### Site
Fields: name, code, address, pincode, latitude/longitude, active flag.
### Account (Cost Centre)
Fields: code, name, description, active flag.
### User
Fields: employee ID, email, name, role, active flag, password hash.
### Inventory & Consumption
- `ItemInventory`: quantity of a product at a site (one row per productsite pair)
- `ItemConsumption`: daily draw-down record (one row per productsitedate)
---
## 9. Key UI Patterns
### Status Badges
Each PO status has a distinct colour:
- DRAFT — neutral grey
- SUBMITTED / MGR_REVIEW — blue (in-progress)
- VENDOR_ID_PENDING — orange/warning
- EDITS_REQUESTED — yellow/warning
- MGR_APPROVED — teal/success-adjacent
- SENT_FOR_PAYMENT — purple
- PAID_DELIVERED — blue-green
- CLOSED — green/success
- REJECTED — red/danger
### Confirmation before Destructive Actions
Delete buttons use a two-step inline confirm: "Delete [name]? Confirm / Cancel". No modal dialog — the confirm state replaces the button in-place.
### Inline Editing in Tables
Manager line-item editing in the approval flow happens in an inline form on the same page, not in a modal, so the manager can reference the rest of the PO while editing.
### GST Calculation (always visible in PO forms)
Below the line-items table, a live summary shows:
- Taxable amount (sum of qty × unit price)
- GST amount (sum of qty × unit price × GST rate)
- Grand Total (taxable + GST)
### Product Autocomplete
In the PO line-item name field, typing triggers a fuzzy search of the item catalogue. Dropdown shows:
- Product name and code
- Price hints per vendor: "Vendor A: ₹1,200 · Vendor B: ₹1,050"
### Cart Persistence
Cart is stored in browser `localStorage` under a fixed key. It survives navigation but is local to the device and user. A `cart-updated` custom event allows components to react to changes in real time.
### Notifications / Emails
Every PO status transition triggers an email to relevant parties:
- Submit → manager
- Approve → submitter + accounts
- Reject → submitter
- Request Edits → submitter
- Request Vendor ID → submitter
- Payment sent → submitter + manager
- Mark paid → submitter + manager
- Receipt confirmed → manager + accounts
---
## 10. Non-Goals (Out of Scope)
- Mobile app (web-only, desktop-first)
- Public-facing pages (entirely internal)
- Self-registration / OAuth login
- Vendor portal (vendors do not log in)
- Automated bank/payment-gateway integration (payment is marked manually)

View file

@ -0,0 +1,341 @@
# Playwright Test Design — Pelagia Portal
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 |

264
Docs/TEST_PLAN.md Normal file
View file

@ -0,0 +1,264 @@
# Pelagia Portal — Test Plan
**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`.

BIN
Docs/Untitled.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

309
Docs/e2e-test-framework.md Normal file
View file

@ -0,0 +1,309 @@
# PPMS — E2E Test Framework Reference
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 GitHub Actions 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.

240
Docs/e2e-test-plan.md Normal file
View file

@ -0,0 +1,240 @@
# PPMS — E2E Test Plan
**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 (GitHub Actions), the following configuration applies:
```yaml
# .github/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 }}
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