PERALIHAN_PP_KE_PT — Task 1 (backend projector) report
Status: DONE
Commit: add69e0d762ac26882c3d429fc74c380a66ed9d7 — feat(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— theppPeralihanPtReviewProjection(ReviewProjection), noprepare()hook (legacyGET /:id/review-datahas 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.ts—PERALIHAN_PP_KE_PT: ppPeralihanPtReviewProjection(wasnull).
Section order (legacy page order, survey §2)
pp_asal (registry-entity) → data_perseroan → akta_notaris → notaris_pengganti (hidden when empty) → peralihan_recital (new, the 8 camelCase recital keys) → modal_saham → pemegang_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
- PADNN invariant (survey findings 8-9): this projector's
addPlainFieldnever callsstripPtPrefix, onvalueordisplayValue— unlike PT's version, which strips only for display to paper over pre-fix legacy rows. Test assertsnama_perseroanseeded as"PT MAJU BERSAMA"round-trips withvalue: "PT MAJU BERSAMA",displayValue: null; same fornamaBaru. - Contract-gap resolution (survey finding 4):
PpAsalCandidate.nameScorehas no slot inRegistryEntityVM.candidates({id,label}) — resolved by embedding the score into the label ("PP MAJU JAYA — 87%"), did not touchcontract/review.ts, per the brief's explicit instruction. - 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
identitascells read the akta row's ownnik/nikAtauRegistrasi/npwpcolumn directly rather than porting PT'sresolveNikForName(which depends onIdentityMatch, always empty for this flow). pp_asalregistry-entity is always emitted (never conditionally hidden), mirroring the legacyPpAsalCard, which always renders (matched display or not-found+picker state).- Reused pt-shape.ts's
DATA_PERSEROAN_KEYS/AKTA_NOTARIS_KEYS/NOTARIS_PENGGANTI_KEYS/MODAL_FIELD_KEYS/FIELD_LABELSverbatim; portedmodal_saham(per-field confirm, not atomic) and the three rosters near-verbatim frompt-pendirian-review.ts, including the pt-smoke4 nama-only cellFieldRef rule for pemegang saham rows. readPpAsalOldData/PpAsalCandidateimported fromservices/pp-peralihan-pt-match.ts(read-only wiring, no logic ported) to interpretSubmission.oldDatainto 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_penggantisection is wired (reusingpt-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
/finalizeroute (3 bespoke gates: asal-match, live dissolution re-check, validation FAIL) was intentionally left untouched — this projector only servesGET /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.