feat(crewing): review hardening — PII mask, vetting gates, audit/atomicity, ex-hand, A3, EPFO tests #90

Merged
shad0w merged 8 commits from feat/crewing-review-hardening into feat/crewing-epfo 2026-06-22 18:54:59 +00:00

8 commits

Author SHA1 Message Date
1ef0c53ff0 test(crewing): cover EPFO stub contract + /api/epfo permission gate
All checks were successful
PR checks / checks (pull_request) Successful in 45s
PR checks / integration (pull_request) Successful in 30s
- Extract EpfoService's pure stub + validation logic into a dependency-free
  module (EpfoService/src/stub.ts); index.ts now uses it in its stub branches so
  the tested logic IS the production stub behaviour.
- epfo.test.ts (App integration): the deterministic stub contract
  (OTP 000000 → matched, UAN/OTP validation, session expiry) and the Next proxy
  routes' verify_bank_epf gate — 401 unauthenticated, 403 for the MPO, Accounts
  passes through to a mocked upstream, body validated before the upstream call.
  No EPFO_LIVE, no running service.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 23:56:14 +05:30
93d13a415c feat(crewing): complete requisition list A3 (candidate count + filters)
- A3 AC2: each requisition row shows its candidate count (sourced via
  _count.applications in the list query) alongside the existing days-open age.
- A3 AC1: add rank and reason filters (derived from the visible data, like the
  existing vessel/site filter) on top of search + status + location.

requisitions.test.ts asserts the per-row candidateCount (2 vs 0) the page exposes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 23:52:05 +05:30
df950c7253 feat(crewing): ex-hand recognition (B3)
- AC1: addCandidate recognizes a returning hand re-entered as a fresh candidate
  — matched to their existing EX_HAND pool record by email (preferred) or exact
  name — and reuses that row instead of creating a duplicate, preserving tour
  history/documents/bank. Audited CANDIDATE_UPDATED { exHandRecognized: true }.
- AC2: the Candidates list sorts ex-hands above new candidates by default
  (stable, preserving createdAt order within each group).
- AC3: the candidate detail "Returning crew" callout now renders the matched
  member's actual tour history (ExperienceRecord) and documents on file.

candidates.test.ts covers email/name recognition, the no-match path, and the
ex-hand-first page ordering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 23:49:43 +05:30
0679883273 refactor(crewing): correct audit action types + atomic auto-raise backfills
Audit-trail & transaction consistency (spec §11 "one transition, one row"):

- Action types: returnSalary/returnSelection/declineInterviewWaiver no longer
  mislabel a backward decision as its forward action. New CrewActionType members
  SALARY_RETURNED / SELECTION_RETURNED / WAIVER_DECLINED; added RECORD_DELETED;
  dropped the unused GATE_FAILED (migration recreates the enum).
- Deletions are audited: deleteDocument / deleteNextOfKin now write a
  RECORD_DELETED CrewAction (PII removals are traceable).
- Atomicity: autoRaiseRequisition takes an optional tx so the leave-clash and
  sign-off backfills are created INSIDE the approval/sign-off transaction; the
  office notification (notifyAutoRaised) fires after commit. An approved leave or
  a sign-off can no longer commit without its backfill requisition.

Tests assert the corrected action types (crewing-gates, crew-records) and the
existing clash/sign-off suites still pass with the in-transaction backfill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 23:46:23 +05:30
184250f903 feat(crewing): enforce recruitment vetting gates C5 + partial C3
- C5 (Epic C5 AC1): advanceStage("verify_competency") now requires ≥1
  ReferenceCheck before leaving COMPETENCY_AND_REFERENCES.
- C3 (Epic C3 AC1): verifyDocuments blocks advancement when a mandatory document
  for the seat's rank that the candidate holds is expired. Missing-document
  presence stays enforced post-onboarding in the verification queue (seafarer
  docs aren't collected pre-onboarding) — documented inline + in wiki Tech-Debt.
- C4 (experience): deferred with an inline note (Requisition has no
  min-experience field yet — Epic A2 AC1).

applications.test.ts: reference-gate block/pass and expired-required-doc
block/renew-pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 23:40:08 +05:30
d796e81efc fix(crewing): harden onboardCandidate (D1 guard, D3 metadata, atomic contract)
- D1: require a Manager-approved SalaryStructure before onboarding; a SELECTED
  application with none is now blocked instead of silently binding zero salary
  rows.
- D3 AC2: the CREW_ONBOARDED CrewAction records the created IDs
  (assignmentId, employeeId, salaryStructureId) in metadata.
- Atomicity: the contract letter is uploaded before the transaction and its row
  is created INSIDE it, so onboarding is one atomic write (no half-onboarded
  crew member without a contract on failure).

onboarding.test.ts asserts the metadata and the new D1 block (no assignment, the
candidate stays a CANDIDATE).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 23:32:30 +05:30
06ff587024 fix(crewing): mask Aadhaar/PAN document numbers server-side
The crew profile page passed SeafarerDocument.number to the client unmasked for
all roles and all doc types, exposing full Aadhaar/PAN identity numbers to MPO /
Manager / Site staff — contradicting the field's PII annotation and §6 /
Roles-and-Permissions §3 (Aadhaar/PAN are gated to Accounts/SuperUser, same as
the bank account number).

- crew-pii.ts: add documentNumberValue(number, docType, role) — masks AADHAAR /
  PAN for non-privileged roles via the existing canViewFullBankEpf gate +
  maskTail; non-identity docs (passport, CDC, STCW…) pass through; preserves the
  string|null contract.
- crew/[id]/page.tsx: mask the number server-side before it crosses to the client.
- Tests: unit cases for the helper; an integration test that invokes the server
  component and asserts the documents prop is masked for MANAGER/SITE_STAFF/MPO
  and full for ACCOUNTS/SUPERUSER.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 23:29:11 +05:30
53fbdb5c53 test(crewing): lock in R2/R8/R11 gates + R6 clash overlap boundaries
Adds two integration suites covering reconciliation rulings that the existing
crewing tests left on the happy path only:

- leave-clash.test.ts (R6/A5, §5.3): the cover-subtraction and date-overlap
  paths in leaveCausesClash — a same-rank crew already on an *overlapping*
  approved leave is not available cover (auto-raises), a non-overlapping leave
  still counts (no raise), different-rank crew never count, and a configured
  minStrength still met after the leave does not raise.
- crewing-gates.test.ts: salary/selection *returns* are Manager-only and
  audited (R8); an interview waiver can never reach a NEW candidate by any path,
  incl. the Manager (R2); bank reject requires remarks; PPE / next-of-kin verify
  gates are MPO-only with remarks on reject (R11/§8.11); and a SUBMITTED
  appraisal cannot be Manager-approved without MPO verification (H3).

Full suite: 245 unit + 225 integration green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:58:48 +05:30