29 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Commands
# Development
pnpm dev # Next.js + Turbopack at localhost:3000
pnpm lint # ESLint
pnpm type-check # tsc --noEmit
# Tests
pnpm test # Unit tests (Vitest, jsdom)
pnpm test:watch # Unit tests in watch mode
pnpm test:integration # Integration tests (Vitest, node + real DB)
pnpm test:e2e # E2E tests (Playwright, headless)
pnpm test:e2e:ui # E2E tests with interactive UI
pnpm test:all # All test suites
# Run a single test file
pnpm test -- tests/unit/po-line-items-editor.test.tsx
pnpm test:integration -- tests/integration/create-po.test.ts
# Database
pnpm db:migrate # Create + apply migration (dev)
pnpm db:migrate:deploy # Apply migrations (CI/prod)
pnpm db:seed # Populate sample data
pnpm db:studio # Prisma GUI at localhost:5555
pnpm db:reset # Drop + recreate + seed (dev)
Architecture
Overview
Internal purchase order management system for a maritime company. Full-stack Next.js 15 App Router app with Prisma + PostgreSQL, NextAuth v5 credentials auth, and Tailwind CSS v4.
Key design decisions:
- Server Components for all data-fetching pages; Client Components only where interactivity is needed
- Server Actions for all mutations (form submissions, approvals, etc.)
- Prisma
Decimalfields cannot be passed directly to Client Components — convert withNumber()in the Server Component before passing as props (seepo-detail.tsx→lineItemsForEditorpattern) - File storage toggles automatically: Cloudflare R2 in production,
.dev-uploads/directory in development - Email toggles automatically: Resend in production, console log in development
PO Lifecycle (State Machine)
lib/po-state-machine.ts enforces all status transitions. The canonical flow:
DRAFT → SUBMITTED → MGR_REVIEW → MGR_APPROVED → SENT_FOR_PAYMENT → PAID_DELIVERED → CLOSED
↓↑ ↕ ↕
EDITS_REQUESTED / REJECTED PARTIALLY_PAID PARTIALLY_CLOSED
/ VENDOR_ID_PENDING
Partial payments (PARTIALLY_PAID) and partial receipts (PARTIALLY_CLOSED) loop until the full amount/quantity is settled. Imported POs are created directly in CLOSED. Every status change is validated against the state machine and recorded as a POAction row (audit trail).
Role-Based Permissions
lib/permissions.ts defines hasPermission(role, permission) and requirePermission(role, permission). Roles: TECHNICAL, MANNING, ACCOUNTS, MANAGER, SUPERUSER, AUDITOR, ADMIN. Permissions include (non-exhaustive): create_po, approve_po, process_payment, confirm_receipt, create_vendor, manage_vendors, manage_products, manage_sites, manage_vessels_accounts, manage_users. create_vendor is held by submitters too; manage_* by Manager/Admin.
Pattern: Server Actions call requirePermission() (or hasPermission()) at the top before any DB write.
Auth: NextAuth v5 with a Microsoft Entra SSO provider and a credentials provider. SSO-only users have no passwordHash (it is nullable) — the profile page lets them optionally set one, and is reachable by every role. Only approvers (approve_po) can upload a signature.
Key Directories
app/(portal)/— All authenticated pages (portal layout with sidebar)app/api/po/[id]/export/— PDF and XLSX export endpointlib/validations/po.ts— Zod schemas for PO forms; exportsTC_FIXED_LINEandTC_DEFAULTSlib/po-state-machine.ts— All valid status transitions with required roleslib/notifier.ts— Email dispatch (Resend in prod, console in dev)lib/storage.ts— File upload/download (R2 in prod, local in dev)components/po/— PO-specific components (line items editor, status badge, etc.)tests/integration/helpers.ts—makeSession(),makePoForm(),fd()for integration test setup
Cost Centre Model
A PO's cost centre is a Vessel (the Vessel model). PurchaseOrder.vesselId is required. POs no longer reference a Site as a cost centre — that earlier dual Vessel-or-Site design was removed.
Form field: PO create/edit/import forms use a plain vesselId select (no more costCentreRef encoding).
Display pattern: po.vessel?.name ?? "—".
URL pre-select: /po/new?vesselId=<id>.
Terminology: "Vessel" is surfaced as "Cost Centre" everywhere in the UI, including the admin page (/admin/vessels → "Cost Centre Management"). Site still exists as a separate construct (used for vendor-distance and inventory), but is not a PO cost centre. Budget heads are labelled "Accounting Code" (not "Account").
Accounting Code Hierarchy
Account is a self-referential 3-level tree via parentId (AccountHierarchy relation): Top Category (6-digit, e.g. 100000) → Sub-Category (100100) → Leaf Item (100101). Codes are 6-digit numeric strings. Seed data lives in prisma/accounting-codes-data.ts.
- Only leaf items (accounts with no children) are selectable on a PO.
- PO forms group leaf codes by their sub-category in a searchable dropdown (
components/ui/searchable-select.tsx, a portal-rendered combobox used in the line-items editor and the main accounting-code field).
Companies (multi-company invoicing)
Company represents the sister company a PO is billed under (PurchaseOrder.companyId, optional). Fields: name, code (unique short code, e.g. PMS), gstNumber, address, telephone, mobile, email, invoiceEmail, invoiceAddress. Managed at /admin/companies. The selected company's details populate the exported PO header / invoice block (falling back to hardcoded Pelagia defaults when no company is linked).
Delivery Locations (issue #19)
DeliveryLocation (a Company FK + free-text address + isActive) is an admin-managed list that backs the PO Place of Delivery dropdown. Managed at /admin/delivery-locations, gated by the manage_delivery_locations permission (Manager + SuperUser + Admin — explicitly not admin-only, per the issue). The CRUD mirrors /admin/sites (table + Add/Edit dialogs + activate/deactivate + delete).
The three PO forms (new-po-form, edit-po-form, manager-edit-po-form) render a shared <DeliveryLocationField> — a native <select name="placeOfDelivery"> populated from the active locations, each formatted by lib/delivery-location.ts formatDeliveryLocation(company, address) → "Company — address". PurchaseOrder.placeOfDelivery stays a free-text snapshot (no FK): the dropdown only changes how the value is picked, so the export, import, and historical/imported POs are unchanged. On edit, a current value not in the active list is preserved as a leading "(current)" option so it's never silently dropped. Deleting a location is therefore always safe (no PO references it).
PO Numbering (lib/po-number.ts)
Structured format: COMPANY/VESSEL/PO_ID/FY — e.g. PMS/HNR1/9000/2024-25. The financial year is Indian (Apr–Mar) rendered YYYY-YY. System-generated PO_ID starts at 9000 to avoid clashing with historical numbers. Imported POs keep their original PO number verbatim; parsePoNumber() extracts the company/vessel/id parts on import.
Payments
When Accounts records a payment, a compulsory payment date is captured (PurchaseOrder.paymentDate) — the input defaults to today and rejects future dates (validated in processPaymentSchema and markPaid). There is also an editable poDate field; the exported PO "Date" shows poDate ?? approvedAt ?? createdAt (i.e. the approval date once approved, not creation).
Advance payment (issue #92): the approving Manager sets how much of the PO is paid first via a 0–100% slider on the approval card (approval-actions.tsx, default 100%). The slider is convenience only — the resolved absolute amount is stored on PurchaseOrder.suggestedAdvancePayment (Decimal(12,2), nullable; null = no explicit advance ⇒ full payment). approvePo() clamps it to [0, totalAmount] and records it on the APPROVED audit row; it is set once at approval and never edited after. Accounts sees it on the payment queue + PO detail, and it prefills the first payment's amount (payment-actions.tsx, only when nothing is paid yet and the advance is a true partial); the balance then runs through the normal PARTIALLY_PAID loop. It does not appear on the exported PO/invoice.
Vendors
Vendor carries isVerified, gstin, pincode + latitude/longitude (geocoded for vendor-distance sorting from a Site), and a VendorContact[] list. Submitters can create vendors (permission create_vendor) but they are created unverified; a vendor becomes verified when a PO is closed/paid with it, on import, or when a Manager/Accounts/Admin runs verifyVendor. Only manage_vendors holders may assign a vendorId (the formal verified code).
Inventory (feature-flagged)
Inventory (ItemInventory, keyed by productId + siteId) is incremented at PO approval — not on close — for the ordered quantities, when the PO has a siteId. The whole inventory surface (site stock, consumption) is gated by NEXT_PUBLIC_INVENTORY_ENABLED (see lib/feature-flags.ts); the vendor/product catalogue used for PO creation stays available regardless.
Import → Closed
/po/import parses a Pelagia-format Excel PO and saves it directly as CLOSED (historical record, bypasses approval). It auto-detects the company (by header/code), auto-matches the vessel by code, auto-creates the vendor and any unknown products, and upserts per-vendor prices.
Crewing (feature-flagged)
A crew-management module built incrementally per the wiki Crewing-Implementation-Spec (the authoritative spec), behind NEXT_PUBLIC_CREWING_ENABLED (off unless "true"). It is delivered in phases (spec §12). Foundations and Requisitions ship so far:
- Role:
SITE_STAFF(the newRoleenum member) — PM / Assistant PM / Site In-charge log in as site staff and act on behalf of crew. MPO isMANNING. - Permissions:
lib/permissions.tsholds the full crewing grant matrix (spec §6) as the source of truth —PO_ROLE_PERMISSIONS+CREWING_ROLE_PERMISSIONSare merged intoROLE_PERMISSIONS. Notable rules: MPO has no attendance/leave;decide_leave/approve_*/select_candidateare Manager-only;manage_ranksis Manager + Admin. - Reference data:
Rankis a self-referential org-chart hierarchy (likeAccount), seeded fromprisma/rank-data.ts;RankDocRequirement(seeded fromprisma/rank-doc-data.ts) lists the documents each rank must hold. Both seed via the sharedprisma/seed-ranks.tsin dev and prod seeds.Rank.grantsLoginis true only for the three management ranks. - Admin screen:
/admin/ranks("Ranks & documents", gated bymanage_ranks+ the flag) — the rank hierarchy card + per-rank required-documents card.
Phase 2 — Requisitions + relief (spec §5.2/§8.2–8.3):
- Models:
Requisition(lifecycleOPEN → SHORTLISTING → PROPOSING → INTERVIEWING → SELECTED → FILLED,→ CANCELLED),ReliefRequest(site-flagged gap the office converts), andCrewAction(the crewing audit trail — thePOActionmirror).Requisition.autoRaisedmarks system-raised vacancies. - State machine:
lib/requisition-state-machine.tsmirrorspo-state-machine.ts(TRANSITIONS,canPerformAction,getAvailableActions; orthogonalCANCEL_ROLES/canCancel). Final selection is Manager-only; withdraw is allowed from OPEN/SHORTLISTING bycancel_requisitionholders (MPO + Manager, per §6). Codes (REQ-9000…) come fromlib/requisition-number.ts. - Actions (
app/(portal)/crewing/requisitions/actions.ts):raiseRequisition,cancelRequisition,transitionRequisition,requestReliefCover,convertReliefToRequisition— each guards flag + permission + state, writes aCrewAction, and notifies vianotifyCrew. The sharedautoRaiseRequisition()inlib/requisition-service.tsis the backfill entry point sign-off / leave-clash (later phases) will call. - Screens:
/crewing/requisitions(list + Raise modal + "Relief requests from sites" convert) and/crewing/requisitions/[id](detail; the recruitment pipeline is a later phase). Requisitions is in the flag-gated sidebar Crewing section (CREWING_ITEMS, Manager + MPO). The Ranks link stays under Administration. - Notifications:
lib/notifier.tsnotifyCrew()is the PO-independent path (writesNotificationrows with a nullpoId);CrewNotificationEventcoversREQUISITION_RAISED/RELIEF_REQUESTED/RELIEF_CONVERTED. - Deferred: sign-off / experience-record (Epic K) is part of spec §12 item 2 but depends on the crew/assignment models from Phase 3/4, so it lands with those.
autoRaiseRequisition()is already in place for it.
Phase 3a — Candidates (Epic B; spec §8.6): Phase 3 (candidate intake + 7-stage pipeline + onboarding) ships as stacked sub-PRs — 3a candidates, 3b pipeline, 3c onboarding.
- Model:
CrewMemberis the talent-pool spine — one row per person, created on first contact and kept throughCANDIDATE → EMPLOYEE → EX_HAND(CrewStatus).employeeIdis assigned only at onboarding (3c).CandidateType(NEW/EX_HAND) andCandidateSourcederive from the chosen source;currentRankId(rank held) +appliedRankId(rank applied for).CrewActiongained a nullablecrewMemberId(it now references at most one entity). - Actions (
app/(portal)/crewing/candidates/actions.ts):addCandidate/updateCandidate— guard flag +manage_candidates, write aCrewAction, optional CV upload viabuildStorageKey("cv", …)+uploadBuffer. An EX_HAND source maps totype/status = EX_HAND; an edit never downgrades anEMPLOYEE. - Screens:
/crewing/candidates(master list with search / source / rank-applied / min-experience filters rendered as removable chips + match count + Clear all; Add-candidate modal) and/crewing/candidates/[id](profile; the 7-stage pipeline/stepper is 3b). Candidates added to the flag-gated Crewing nav (Manager + MPO). - Deferred: the public careers intake API (A2, §13 open question) — 3a uses the internal Add-candidate modal only; CVs are stored but not parsed.
Phase 3b — Recruitment pipeline (Epic C; spec §5.1/§8.4–8.5/§8.13):
- Models:
Application(one per requisition+candidate) drives the 7-stageApplicationStage(SHORTLISTED → COMPETENCY_AND_REFERENCES → DOC_VERIFICATION → SALARY_AGREEMENT → PROPOSED → INTERVIEW → SELECTED;→ REJECTED;ONBOARDEDis 3c).ApplicationGaterecords each vetting gate —SALARY/SELECTION/WAIVERgates withresult=PENDINGare the Manager's queue items.ReferenceCheck, effective-datedSalaryStructure(attached to the Application in 3b; bound to the assignment in 3c), and minimalBankDetail/EpfDetailcaptured at DOC_VERIFICATION (PII encryption deferred to Phase 4).CrewActiongainedapplicationId. - State machine:
lib/application-pipeline.ts(mirrors po/requisition machines) — sourcing advances are MPO/Manager;approve_salaryandselectare Manager-only;canRejectis orthogonal.BOARD_STAGESis the 7 columns. - Actions (
app/(portal)/crewing/applications/actions.ts):addApplication(first candidate moves the requisition OPEN→SHORTLISTING),advanceStage,recordReferenceCheck,verifyDocuments(captures bank/EPF),agreeSalary→approveSalary/returnSalary,recordInterviewResult,requestInterviewWaiver→approveInterviewWaiver/declineInterviewWaiver,selectCandidate/returnSelection(sets requisition→SELECTED),rejectApplication. Waiver is never automatic (R2). Notifications:SALARY_FOR_APPROVAL/SELECTION_FOR_APPROVAL/WAIVER_REQUESTED(+CANDIDATE_PROPOSED). - Screens: pipeline board per requisition (
/crewing/requisitions/[id]/pipeline, 7 columns + Add-candidate), the application workhorse (/crewing/applications/[id]— 7-step stepper + adaptive per-stage action card), and an "Open pipeline" action on the requisition detail. - Central approvals (§8.13 R8):
/approvalsnow also lists pending crewing gates (Salary / Selection / Waiver) with inline Approve/Return, alongside POs — one unified Manager queue.
Phase 3c — Onboarding (Epic D; spec §8.5/§9/§11):
- Models:
CrewAssignment(a tour of duty,AssignmentStatusACTIVE/ON_LEAVE/SIGNED_OFF — leave/sign-off are Phase 4) andContractLetter(salaryRestricted).SalaryStructuregainedassignmentId(bound at onboarding).CrewActionType += CREW_ONBOARDED. Employee numbersCRW-xxxxvialib/employee-number.ts. - Action (
onboardCandidate,onboard_crew): one transaction off aSELECTEDapplication — assignemployeeId, createCrewAssignment(ACTIVE, signOnDate), bind the approvedSalaryStructure(assignmentId+effectiveFrom),Application → ONBOARDED,Requisition → FILLED,CrewMember → EMPLOYEE(+currentRank); contract letter stored after. Onboarded crew leave the Candidates pool (the Crew directory is Phase 4). - Screen: the SELECTED action card's Onboard to crew modal (joining date, contract upload, starts-automatically chips); the assigned
CRW-number shows on the ONBOARDED card. - Deferred: SITE_STAFF login creation for management ranks (grantsLogin) is a follow-up; attendance/experience/PPE records (the "starts automatically" chips) begin in Phase 4.
Phase 4a — Crew records & profile + PPE (Epics E + F; spec §8.7–8.8): Phase 4 (crew records, PPE, leave/attendance + sign-off) ships as stacked sub-PRs — 4a records/profile/PPE, 4b leave/attendance, 4c sign-off/experience.
- Models:
SeafarerDocument,NextOfKin(isEmergency),ExperienceRecord,PpeIssue(PpeItemenum) — all onCrewMember.CrewActionType += DOCUMENT_UPLOADED / RECORD_UPDATED / PPE_ISSUED / PPE_RETURNED / EXPERIENCE_ADDED. (BankDetail/EpfDetailalready exist from 3b.) - PII masking (
lib/crew-pii.ts, spec §6/§8.8): bank account number + Aadhaar are full only for Accounts/SuperUser, masked (•••• 1234) otherwise; salary hidden from site staff. Masking is applied server-side before data crosses to the client. - Actions (
app/(portal)/crewing/crew/actions.ts):uploadDocument/deleteDocument,saveBankEpf,addNextOfKin/deleteNextOfKin,issuePpe/returnPpe,addExperience— guarded byupload_crew_records/issue_ppe, each writes aCrewAction. Document/contract files viabuildStorageKey("crew-document", …). - Screens:
/crewing/crew(directory — activeEMPLOYEEcrew, search + vessel filter; ex-hands excluded) and/crewing/crew/[id](tabbed profile: Documents · Bank & EPF · Next of kin · PPE · Experience · Pay status). Crew added to the flag-gated nav (MGR/MPO/Site/Accounts). - Deferred: site-staff own-site scoping (needs a User↔Site link, not modelled — all crew show for now); the records verify queue (§8.11, Phase 5); the Pay-status tab shows the salary structure only until wage reports (Phase 6).
Phase 4b — Leave & attendance (Epic G; spec §5.3/§8.9–8.10):
- Models:
LeaveRequest(LeaveType,LeaveStatus) andAttendance(AttendanceStatus,@@unique([assignmentId, date])) hang offCrewAssignment.CrewActionType += LEAVE_APPLIED / LEAVE_DECIDED / ATTENDANCE_RECORDED. - Leave (R1): Site staff apply on behalf (
apply_leave); the Manager decides (decide_leave) — the MPO has no leave role. On approval the assignment goesON_LEAVE. Leave approvals also surface in the central/approvalsqueue (§8.13 "Leave" kind, inline Approve/Decline). NotificationLEAVE_FOR_APPROVAL. - Clash auto-backfill (R6, Option A):
VesselRankRequirement{vesselId, rankId, minStrength}configures required crew strength per rank per vessel.lib/leave-clash.tsflags a clash when approving a leave would drop the active same-rank cover over the window belowminStrength(default 1 when unconfigured) → auto-raises aLEAVErequisition via the Phase-2autoRaiseRequisition. The requirement is managed by the office (manage_crew). - Attendance (R5): daily month calendar, site staff record (
record_attendance), Manager views (view_attendance) but cannot edit, MPO has neither.saveAttendance(assignmentId, marks)bulk-upserts the dirty cells. - Screens:
/crewing/leave(apply-on-behalf modal + requests list with Manager Approve/Decline) and/crewing/attendance(crew dropdown + month grid, tap-to-cycle Present/Absent/Leave/Half-day, Save). Leave + Attendance added to the flag-gated nav (Manager + Site staff only). - Deferred: the 6-month leave-planner timeline with clash bars (§8.9) is a lightweight list for now; hours/overtime attendance (A7) stays deferred.
Crewing admin (office/admin management): a new manage_crew permission (Manager + SuperUser + Admin) gates a small Administration surface:
- Crew management (
/admin/crew): full CRUD overCrewMember(any status), and direct placement —placeCrewassigns a crew member to a vessel/site without a requisition (creates anACTIVECrewAssignment; promotes a candidate toEMPLOYEEwith aCRW-number; blocked if they already have an active assignment). - Crew strength (
/admin/crew-strength): CRUD overVesselRankRequirement(theminStrengththat drives R6 leave-clash detection). - Both links sit under Administration (flag-gated, Manager/Admin/SuperUser).
Phase 4c — Sign-off & experience (Epic K; spec §5.3): completes Phase 4 (and the Epic K piece deferred from Phase 2).
signOffCrew(assignmentId, date, remarks)(crewing/crew/actions.ts,sign_off_crew): one transaction — assignment →SIGNED_OFF(+signOffDate), append an internalExperienceRecord(rank, on/off dates, computeddurationMonths), flip the sameCrewMemberEMPLOYEE → EX_HAND(so they return to the Candidates pool as a returning hand),CrewAction CREW_SIGNED_OFF; then auto-raise aSIGN_OFFbackfill requisition viaautoRaiseRequisition. (CrewActionType += CREW_SIGNED_OFF.)- Screen: a Sign off button on the crew-profile header (
/crewing/crew/[id],sign_off_crewholders — Site staff / MPO / Manager); on success it redirects to the Crew directory (the member is no longerEMPLOYEE). - This closes Phase 4 (E/F/G + K). Remaining roadmap: Phase 5 (verification + appraisal), Phase 6 (payroll, dashboards, notifications).
Phase 5a — Verification (Epic I; spec §8.11/R11): the office queue for site-entered records (Phase 5 ships as 5a verification → 5b appraisal).
- Actions (
crewing/verification/actions.ts):verifyDocument(id, approve, remarks)(verify_site_records— MPO/Manager) sets aSeafarerDocument'sverificationStatus+verifiedById;verifyBankEpf(crewMemberId, "bank"|"epf", approve, remarks)(verify_bank_epf— Accounts) does the same forBankDetail/EpfDetail. Rejection requires remarks; both write aCrewAction(RECORD_VERIFIED/RECORD_REJECTED). No new models — the verification fields already existed (3b/4a). - Screen:
/crewing/verification— role-aware (MPO sees pending documents with expiry flags; Accounts sees pending bank/EPF), Verify / Reject-with-remarks. Leave is not here (it's a Manager approval, R11). Added to nav (MPO + Accounts + SuperUser, §7). - Deferred (per decision): PPE / next-of-kin verification gates (low-risk; no
verificationStatuson those models).
Phase 5b — Appraisal (Epic H; spec §5.4/§8.14): completes Phase 5.
- Model:
Appraisal(onCrewAssignment) +AppraisalStatus(DRAFT → SUBMITTED → MPO_VERIFIED → MANAGER_APPROVED;→ REJECTED).ratingsis a small JSON (competence/conduct/safety).CrewActionType += APPRAISAL_SUBMITTED/VERIFIED/APPROVED/REJECTED. - State machine
lib/appraisal-state-machine.ts:verify(SUBMITTED→MPO_VERIFIED, MPO/Manager) andapprove(MPO_VERIFIED→MANAGER_APPROVED, Manager); orthogonal reject. - Actions (
crewing/appraisals/actions.ts):raiseAppraisal(raise_appraisal— PM/site staff; createsSUBMITTED),verifyAppraisal(verify_appraisal— MPO),approveAppraisal(approve_appraisal— Manager); reject paths require remarks; notificationsAPPRAISAL_FOR_VERIFICATION/APPRAISAL_FOR_APPROVAL. - Three surfaces (§8.14): the PM raises + sees status on the crew profile Appraisals tab; the MPO verifies in the Verification queue (Appraisals section); the Manager approves in the central /approvals queue (Appraisal kind).
- This completes Phase 5 (I + H). Remaining roadmap: Phase 6 — payroll (Pay-status tab + Approvals "Wage"), dashboards, notifications (J, M).
Crewing follow-ups (resolved deferrals): the self-contained deferrals from earlier phases are now done:
- SITE_STAFF login on onboard/placement —
lib/crew-login.tsmaybeCreateSiteStaffLogincreates a passwordlessSITE_STAFFUser(sharing theCRW-employee no.) when agrantsLoginrank is onboarded (onboardCandidate) or placed (placeCrew) and the crew member has an email; the login'ssiteIdis set to the assignment's site. - Own-site scoping (§8.7) —
User.siteIdadded; the Crew directory filters aSITE_STAFFuser with a home site to crew whose active assignment is at that site (graceful: nositeId→ unscoped). The link is set at login creation above. - PPE / next-of-kin verify gates —
PpeIssue/NextOfKingainedverificationStatus+verifiedById;verifyPpe/verifyNextOfKin(verify_site_records— MPO) and queue sections in/crewing/verification. - EPFO / UAN assisted verification (A3):
EpfoService/is a standalone Express + Playwright proxy (the GstService pattern) that does an OTP-handshake UAN lookup against the EPFO member portal —POST /otpthenPOST /verify. The app proxies via/api/epfo/otp+/api/epfo(gated byverify_bank_epf), and the EPFO check affordance in the verification queue records the returned member name ontoEpfDetail.epfoMemberName(recordEpfoCheck). The live portal navigation is stubbed behindEPFO_LIVE(deterministic in dev/CI: OTP000000→ matched) until the real selectors/OTP are validated. Aadhaar is intentionally not handled (UIDAI-restricted — stays assisted-manual; onlyaadhaarLast4stored, masked). - Still deferred (not self-contained): the public careers intake API (A2, external) and the Pay-status pay rows (Phase 6 payroll).
GST Calculation
totalAmount = sum(quantity × unitPrice × (1 + gstRate)) for each line item. The gstRate is stored as a decimal on POLineItem (e.g., 0.18 = 18%). This applies in Server Actions when computing totalPrice per line and the PO totalAmount.
Environment Variables
NEXTAUTH_SECRET # Required always
NEXTAUTH_URL # Required always (e.g., http://localhost:3000)
DATABASE_URL # PostgreSQL connection string
AZURE_AD_CLIENT_ID, AZURE_AD_CLIENT_SECRET, AZURE_AD_TENANT_ID
# Microsoft Entra SSO (prod). auth.ts reads them at module
# load — set placeholders in non-SSO/dev envs so it boots.
# Optional in dev (defaults to local storage + console email):
R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET_NAME, R2_PUBLIC_URL
RESEND_API_KEY, EMAIL_FROM, EMAIL_FROM_NAME
# Report Issue button (lib/forgejo.ts); token needs write:issue:
FORGEJO_URL, FORGEJO_REPO, FORGEJO_TOKEN
GST_SERVICE_URL # GstService microservice (defaults to localhost:3003)
EPFO_SERVICE_URL # EpfoService microservice for UAN lookup (defaults to localhost:3004)
NEXT_PUBLIC_INVENTORY_ENABLED # Inventory feature flag
NEXT_PUBLIC_SUBMITTER_VIEW_ALL_ENABLED # Opt-in ("true"): submitters (TECHNICAL/MANNING) read & export every PO + History (read-only)
NEXT_PUBLIC_CREWING_ENABLED # Crewing module feature flag (opt-in "true"; off by default)
NEXT_PUBLIC_ENV_LABEL # When set, shows a non-prod banner (EnvBanner). Leave unset in prod.
Operations & automation
This repo runs a self-hosted issue-to-deploy pipeline on the pms1 server (Forgejo +
headless Claude Code). See ../automation/README.md. Relevant
when working in this codebase:
- The Report Issue button (portal header) files a Forgejo issue; a watcher triages it
and, for auto-fixable ones, implements a fix and opens a PR. Deploys are gated on a
human merging the PR and pushing a
vX.Y.Ztag. - Automated fixes and the staging instance run against
pelagia_test, a daily mirror of the production database, in dev mode (console email, local storage). Migrations are applied to it, so its schema tracksmaster. Never assume an empty DB — it holds prod-like data.