think
16px
820px

Peleburan PT — whole-flow review fixes

Branch: feat/pt-flows-registry. All items applied, verified once, committed.

Fixes

C1 — verifikator could never APPROVE a peleburan (empty roster → 422)

  • Roster source chosen: the AKTA_PELEBURAN deed's AktaPemegangSaham rows (the NEW PT's post-transaction roster). Peleburan has NO oldData.pemegang_saham (null by design), and the deed is extracted with the pendirian schema, so it always carries AktaPemegangSaham (proven: review-data already maps akta.pemegangSaham from that relation, and PtAktaCtx reads it). No product fork — deed roster rows exist in practice.
  • Widening kept roster-LOCAL: PERUBAHAN_FAMILY_AKTA is also consumed by the dispatch-context finder + isPerubahanFamilyAkta, so I did NOT widen the shared const. resolveVoterRoster uses a local ROSTER_AKTA_TYPES = [...PERUBAHAN_FAMILY_AKTA, "AKTA_PELEBURAN"] with a comment.
  • "PublicVotingPage guard": there is NO frontend type guard (verified — PublicVotingPage.tsx renders unconditionally). The real place that resolved placeholders for peleburan was the backend vote.ts akta-doc finder (and resolveAktaDispatchContext for the invitation email). Widened both to accept AKTA_PELEBURAN so the public voting page + email show the real NEW-PT name/date/notaris instead of placeholders.
  • Tests: BE decide-APPROVE (verifikator-peleburan.test.ts) seeds a peleburan submission + verifikasi + deed AktaPemegangSaham → APPROVE → MENUNGGU_VOTING + one ShareholderVote per deed pemegang saham; roster-resolver unit case in shareholder-voting.test.ts.

C2 — sources panel unreachable + zero-source hole

  • (a) FE: "Kelola PT Sumber" button on PeleburanReviewPage's sources section (visible only when status READY) opens the existing PeleburanSourcesPanel. Added an optional onClose prop to the panel (dismissible in READY; still blocking/no-close in AWAITING). onResolved → invalidate peleburan-review. Test asserts the button renders + opens the panel.
  • (b) Gate semantics chosen: PELEBURAN_MIN_SOURCES now FAILs at 0 when the deed is extracted; SKIP kept ONLY while the deed hasn't extracted yet. Distinguisher = ctx.aktaFields.length > 0 (the ctx knows the akta status; validation only ever runs post-extraction — Phase 5 / re-validate — so in practice an approved empty set FAILs). Ps.122 citation in the rule + tests updated (0→FAIL).

I1 — SABH hang timeout

  • peleburan-sources.ts: hard Promise.race timeout (apostille-registry-hang pattern) around all three SABH primitives (lookupCompany, loadOldData, loadPerseroanBlokirState) via a withTimeout helper. Budget = SourceResolveDeps.timeoutMs, default PELEBURAN_SABH_TIMEOUT_MS || 10000, injectable for tests. Hang→throw→existing per-row catch degrades the row to AWAITING_SELECTION. New test: never-resolving injected lookup + 100ms budget → row degrades within timeout.

I2 — partial-verification WARNING

  • PELEBURAN_SOURCE_BLOKIR + PELEBURAN_SOURCE_STATUS: when SOME rows verified and SOME unknown (MANUAL / no registry state) → WARNING naming the unchecked rows (was a clean PASS). All-unknown stays SKIPPED; all-verified-clean stays PASS. Tests add a mixed case → WARNING.

I3 — submit 409 envelope

  • routes/peleburan.ts submit wrong-status 409 now carries code: "WRONG_STATUS". Routes test asserts the code.

I4 — loud comment

  • flow-engine/processor.ts multi-source branch: comment warning the import("../services/peleburan-sources") is peleburan-coupled; a second multi-source tenant must add a per-flow resolve hook on the companyLookup config, not copy this wiring.

Minor folds

  • runPeleburanDeedPasses per-row loop → single createMany (atomic count for MIN_SOURCES).
  • Stale comments refreshed: focused-passes.ts ("later flows" — bukti-pengumuman is live), registry.ts ("until Task 6 registers" stub note above two live tenants), and the laporan-rups-routing.test.ts ladder describe-title (now includes Peleburan).

Verification

  • bunx tsc --noEmit — backend + frontend: clean.
  • Backend: rules-peleburan + shareholder-voting + peleburan-sources = 26 pass; src/flow-engine + peleburan-routes + verifikator-peleburan + verifikator-decide + laporan-rups-routing = 143 pass; peleburan-integration (E2E) + vote-public.integration = 8 pass.
  • Frontend: PeleburanReviewPage + PeleburanSourcesPanel = 13 pass.

Concerns

  • The C1 email dispatch is fire-and-forget (background); in demo mode (SMTP unset, as in tests) it no-ops. Verified the APPROVE transition + vote rows are committed inside the tx before the response.
  • PELEBURAN_SOURCE_STATUS partial-WARNING keys off "no registry state" which is also the SABH-unavailable signal — acceptable (all-unavailable still SKIPs; only a genuine mix warns).