think
16px
820px

PERALIHAN_PP_KE_PT — Task 1 (backend projector) report

Status: DONE

Commit: add69e0d762ac26882c3d429fc74c380a66ed9d7feat(review): PERALIHAN_PP_KE_PT projector — tenant #4 on the ReviewData engine (branch refactor/tidyup-foundation, not pushed).

What was built

  • backend/src/review/projectors/pp-peralihan-pt-review.ts — the ppPeralihanPtReviewProjection (ReviewProjection), no prepare() hook (legacy GET /:id/review-data has zero side effects, confirmed in survey §1).
  • backend/src/review/__tests__/pp-peralihan-pt-review.test.ts — 9 tests (TDD: written first, watched fail on missing module, then made to pass with no further iteration needed).
  • backend/src/review/review-registry.tsPERALIHAN_PP_KE_PT: ppPeralihanPtReviewProjection (was null).

Section order (legacy page order, survey §2)

pp_asal (registry-entity) → data_perseroanakta_notarisnotaris_pengganti (hidden when empty) → peralihan_recital (new, the 8 camelCase recital keys) → modal_sahampemegang_saham (roster) → pengurus (roster, direksi+komisaris) → kbli (roster). All approvable:false/required:false; requiredSections: []; declarations: []; no identitas section (survey finding 7 — unreachable in this flow's UI); no bukti_setor (no such documents in this flow).

Key decisions

  1. PADNN invariant (survey findings 8-9): this projector's addPlainField never calls stripPtPrefix, on value or displayValue — unlike PT's version, which strips only for display to paper over pre-fix legacy rows. Test asserts nama_perseroan seeded as "PT MAJU BERSAMA" round-trips with value: "PT MAJU BERSAMA", displayValue: null; same for namaBaru.
  2. Contract-gap resolution (survey finding 4): PpAsalCandidate.nameScore has no slot in RegistryEntityVM.candidates ({id,label}) — resolved by embedding the score into the label ("PP MAJU JAYA — 87%"), did not touch contract/review.ts, per the brief's explicit instruction.
  3. No identity section / no fuzzy KTP lookup: this flow never runs identity matching (no secondary KTP/NPWP upload step — survey finding 7). Pengurus/Pemegang-Saham identitas cells read the akta row's own nik/nikAtauRegistrasi/npwp column directly rather than porting PT's resolveNikForName (which depends on IdentityMatch, always empty for this flow).
  4. pp_asal registry-entity is always emitted (never conditionally hidden), mirroring the legacy PpAsalCard, which always renders (matched display or not-found+picker state).
  5. Reused pt-shape.ts's DATA_PERSEROAN_KEYS/AKTA_NOTARIS_KEYS/NOTARIS_PENGGANTI_KEYS/MODAL_FIELD_KEYS/FIELD_LABELS verbatim; ported modal_saham (per-field confirm, not atomic) and the three rosters near-verbatim from pt-pendirian-review.ts, including the pt-smoke4 nama-only cellFieldRef rule for pemegang saham rows.
  6. readPpAsalOldData/PpAsalCandidate imported from services/pp-peralihan-pt-match.ts (read-only wiring, no logic ported) to interpret Submission.oldData into the matched-snapshot vs. candidates-only shape.

Gates

  • tsc --noEmit: clean.
  • Full backend suite: 2113 pass / 1 skip / 0 fail (baseline 2104/1 + 9 new tests, no regressions). Architecture fitness tests (file-size cap, registry totality) green.
  • New file is 434 lines, well under the 800-line new-file cap.

Concerns / follow-ups for later tasks

  • notaris_pengganti section is wired (reusing pt-shape.ts's key list) but not exercised with real data in the test fixtures — it's the same hidden-when-empty pattern as PT, low risk, flagged for whoever builds the frontend page to confirm the akta extraction actually ever populates those keys for a conversion deed.
  • The legacy page's own /finalize route (3 bespoke gates: asal-match, live dissolution re-check, validation FAIL) was intentionally left untouched — this projector only serves GET /api/submissions/:id/review; a later task should decide whether/how the V2 review page wires to that bespoke finalize route (out of scope for Task 1, per the survey's explicit "stays a bespoke route" call).
  • No frontend/route/contract files were touched, per the brief's constraint.