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
Owner

Review follow-ups and hardening for the Crewing module, raised against the stack tip feat/crewing-epfo. These resolve the substantive findings from the cross-PR review of the crewing stack (each was a tracked follow-up), with tests for every change.

Commits

  • fix(crewing): mask Aadhaar/PAN document numbers server-side — closes a PII leak where SeafarerDocument.number was sent to the client unmasked for all roles/doc types. Aadhaar/PAN now masked server-side via the existing bank/EPF gate (full only for Accounts/SuperUser).
  • test(crewing): lock in R2/R8/R11 gates + R6 clash overlap boundaries — regression tests for the reconciliation rulings (waiver-never-auto, Manager-only returns, MPO-no-attendance, clash cover-subtraction).
  • fix(crewing): harden onboardCandidate (D1/D3) — block onboarding without a Manager-approved salary; record created IDs in the CREW_ONBOARDED audit metadata; contract letter written inside the transaction (atomic).
  • feat(crewing): enforce recruitment vetting gates C5 + partial C3 — ≥1 reference before leaving competency; expired mandatory rank-doc blocks doc verification. C4 (experience) deferred with a tracked note (wiki Tech-Debt TD-4).
  • refactor(crewing): correct audit action types + atomic auto-raise backfillsSALARY_RETURNED/SELECTION_RETURNED/WAIVER_DECLINED/RECORD_DELETED (drops unused GATE_FAILED, enum migration); deletions audited; leave-clash / sign-off backfill requisitions now created inside the approval/sign-off transaction.
  • feat(crewing): ex-hand recognition (B3) — intake matches a returning hand (email, else name) and reuses their existing EX_HAND row instead of duplicating; ex-hands sort first; the detail callout renders real tour history + documents.
  • feat(crewing): complete requisition list A3 — candidate-count column + rank/reason filters.
  • test(crewing): cover EPFO stub contract + /api/epfo permission gate — deterministic stub contract and the verify_bank_epf gate on the proxy routes (logic extracted into a dependency-free module production uses).

Verification

  • pnpm test — 249 unit passed (13 pre-existing skips)
  • pnpm test:integration — 242 passed (29 files)
  • pnpm type-check — clean
  • One additive migration (20260622180000_crewing_audit_action_types).

🤖 Generated with Claude Code

Review follow-ups and hardening for the Crewing module, raised against the stack tip `feat/crewing-epfo`. These resolve the substantive findings from the cross-PR review of the crewing stack (each was a tracked follow-up), with tests for every change. ### Commits - **fix(crewing): mask Aadhaar/PAN document numbers server-side** — closes a PII leak where `SeafarerDocument.number` was sent to the client unmasked for all roles/doc types. Aadhaar/PAN now masked server-side via the existing bank/EPF gate (full only for Accounts/SuperUser). - **test(crewing): lock in R2/R8/R11 gates + R6 clash overlap boundaries** — regression tests for the reconciliation rulings (waiver-never-auto, Manager-only returns, MPO-no-attendance, clash cover-subtraction). - **fix(crewing): harden onboardCandidate (D1/D3)** — block onboarding without a Manager-approved salary; record created IDs in the `CREW_ONBOARDED` audit metadata; contract letter written inside the transaction (atomic). - **feat(crewing): enforce recruitment vetting gates C5 + partial C3** — ≥1 reference before leaving competency; expired mandatory rank-doc blocks doc verification. C4 (experience) deferred with a tracked note (wiki Tech-Debt TD-4). - **refactor(crewing): correct audit action types + atomic auto-raise backfills** — `SALARY_RETURNED`/`SELECTION_RETURNED`/`WAIVER_DECLINED`/`RECORD_DELETED` (drops unused `GATE_FAILED`, enum migration); deletions audited; leave-clash / sign-off backfill requisitions now created inside the approval/sign-off transaction. - **feat(crewing): ex-hand recognition (B3)** — intake matches a returning hand (email, else name) and reuses their existing EX_HAND row instead of duplicating; ex-hands sort first; the detail callout renders real tour history + documents. - **feat(crewing): complete requisition list A3** — candidate-count column + rank/reason filters. - **test(crewing): cover EPFO stub contract + /api/epfo permission gate** — deterministic stub contract and the `verify_bank_epf` gate on the proxy routes (logic extracted into a dependency-free module production uses). ### Verification - `pnpm test` — 249 unit passed (13 pre-existing skips) - `pnpm test:integration` — 242 passed (29 files) - `pnpm type-check` — clean - One additive migration (`20260622180000_crewing_audit_action_types`). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
shad0w added 8 commits 2026-06-22 18:43:51 +00:00
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>
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>
- 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>
- 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>
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>
- 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>
- 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>
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
1ef0c53ff0
- 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>
shad0w merged commit 86fe23167c into feat/crewing-epfo 2026-06-22 18:54:59 +00:00
Sign in to join this conversation.
No description provided.