think
16px
820px

Akuisisi PT — Build Map

1. What Akuisisi is (and the single crux)

Akuisisi (Pengambilalihan) is, legally and in SABH, a member of the Perubahan family — not a new species. Under UU 40/2007 Pasal 1 angka 11 it is "an act to acquire shares of a company that RESULTS IN THE CHANGE OF CONTROL (beralihnya pengendalian)." In SABH it is stamped id_aksi_transaksi = 19 (vs Perubahan = 10, Merger = 11), and downstream getListLink groups 10 || 19 || 11 through the same linkPerubahanPerseroan path. The share-register / modal / direksi-komisaris / KBLI payload is byte-identical to FormDataPerseroanPerubahanDetail.

The single crux: an Akuisisi is a Perubahan PT whose akta approves a control-changing share transfer, plus a mandatory newspaper announcement (pengumuman). Everything else about it — the diff, the roster mechanics, the SABH lookup, the review/verify/vote machinery — is Perubahan. Concretely, three things distinguish it from a plain Perubahan:

  1. Peralihan saham is FORCED ON (not user-optional). In _formAkuisisiList.php the "Peralihan saham" checkbox is pre-checked + display:none — every akuisisi is definitionally a share transfer that shifts control.
  2. Pengumuman surat kabar is REQUIRED (rule on='akuisisi'), with a deterministic date window: tanggal_koran <= tanggal_rups - 30 days (UUPT Pasal 127 ayat 2). Plain Perubahan only needs koran on a modal reduction.
  3. A legal-path classification tree the plain Perubahan does not model: Perseroan Tertutup vs Terbuka, "Langsung ke pemegang saham vs Melalui Direksi", Rancangan Pengambilalihan, and (Terbuka only) Persetujuan OJK.

Note the SABH rups_utama field-name collision: in step 2 it means meeting-type (bar circular vs rups full RUPS); in step 3 the same POST key is reused for Tertutup vs Terbuka. A rebuild must keep these two concepts separate.

2. Reuse map (PoC perubahan → Akuisisi)

Component Path Reuse for Akuisisi
Dispatch / processor-kind switch backend/src/services/submission-dispatch.ts 1:1 pattern. Add AKUISISI_PTakuisisi-pt case + import processAkuisisiSubmission. never-check forces it.
Perubahan processor (Phase 1–5 + reMatchAndValidate) backend/src/services/perubahan-processor.ts Fork → akuisisi-processor.ts. Phase 2 SABH lookup + loadOldData of the target PT is identical.
Company lookup / oldData backend/src/services/company-lookup.ts Reuse as-is. Akuisisi loads the target PT's SABH snapshot the same way.
PT snapshot (cap-table split) backend/src/services/sabh-pt-snapshot.ts Reusable for pre-filling the target's current cap table.
Diff primitives backend/src/services/perubahan-diff.ts Reuse as-is (diffPeople fuzzy, diffTable, diffScalar, diffCurrency, isFullReplacementDeed). Only the gating wrapper is akuisisi-scoped.
Post-change roster backend/src/services/post-perubahan-roster.ts HIGHEST-VALUE REUSE. composePostRoster/applyPeralihan (subtract from dari, add to kepada, recompute persentase) is exactly the acquisition-of-shares primitive.
Change-type detector backend/src/services/change-type-detector.ts Reuse the pure-function shape; author a small acquisition jenis map (control-change) + required-docs builder.
Cross-validator backend/src/services/cross-validator.ts Reuse identity/modal/KBLI/OLD_DATA_CONSISTENCY/validateSharesTransferBalance (V5). Add akuisisi rules (pengumuman 30-day window, control threshold).
Jenis selection / section keys backend/src/services/jenis-selection.ts Reuse getEffectiveJenis + appearingSectionKeys for the finalize gate.
Shareholder voting backend/src/services/shareholder-voting.ts Reuse roster+token-email dispatch if target-PT RUPS consent is required.
Section approval / attestation / verifikasi backend/prisma/schema.prisma + backend/src/routes/verifikator.ts Reuse the persisted-section-approval + MENUNGGU_VERIFIKATOR→VOTING→SELESAI state machine (shared or akuisisi-scoped tables).
review-data composite backend/src/services/load-diff-input.ts + backend/src/routes/perubahan.ts (GET review-data) Fork the assembler for akuisisi.
Akta schema (peralihan core) backend/src/schema/akta-perubahan.ts Reuse PeralihanSaham[], pemegang_saham_lama/baru, detail_pemegang_saham. Cap-table mechanics carry over.
Frontend flow KlasifikasiPage.tsx, PerubahanExtractionPage.tsx, CompanySelectionModal.tsx, PerubahanDeltaReviewPage.tsx, VerifikatorReviewPage.tsx, PublicVotingPage.tsx, components/perubahan/review/*, use-perubahan*.ts, flow-steps.ts, destination-for.ts, routes.tsx, sidebar.tsx Mirror/fork per-file; add akuisisi labels/routes.

Classifier is already done: akta-txn-classifier.ts defines txnType akuisisiAKTA_AKUISISI with a title rule (/^\s*AKTA\s+PENGAMBILALIHAN/im); klasifikasi.ts refineAktaLabel already overrides to it. It is recognized today only to be blocked (not in BUILT_AKTA_TXN_TYPES).

3. Phase-B wiring checklist (new-SubmissionType plumbing)

  1. backend/prisma/schema.prisma — add AKUISISI_PT to enum SubmissionType (doc comment: "Akuisisi/Pengambilalihan PT: control-changing share transfer"); bunx prisma generate + db push.
  2. backend/src/services/akta-txn-classifier.ts:32 — add 'akuisisi' to BUILT_AKTA_TXN_TYPES. Do NOT ship alone (see trap below).
  3. backend/src/routes/submissions.ts:82 inferSubmissionType — widen return union + add if (files.some(f => eff(f)==='AKTA_AKUISISI')) return 'AKUISISI_PT' before the PENDIRIAN_PT default (line 96). mapToDocumentType/order already handle AKTA_*.
  4. backend/src/services/submission-dispatch.ts — add 'akuisisi-pt' to ProcessorKind; case 'AKUISISI_PT': return 'akuisisi-pt' in processorKindForType (clears never); matching case 'akuisisi-pt' importing processAkuisisiSubmission in dispatchProcessor.
  5. backend/src/services/submission-processor.ts:709 reMatchAndValidate — add if (submission?.type==='AKUISISI_PT') { const {reMatchAndValidateAkuisisi}=await import('./akuisisi-processor'); return reMatchAndValidateAkuisisi(submissionId); } BEFORE the fallthrough deleteMany/AKTA_PENDIRIAN block. (the memory gotcha)
  6. Build akuisisi-processor.tsprocessAkuisisiSubmission + reMatchAndValidateAkuisisi (owns its own upsert/delete-stale discipline). This is the substantive work BUILT_AKTA_TXN_TYPES membership asserts.
  7. backend/src/routes/klasifikasi.ts — add AKUISISI_DOC_TYPES (required AKTA_AKUISISI "Akta Pengambilalihan" maxCount 1 + KTP/NPWP/BERITA_ACARA_RUPS/AKTA_PEMINDAHAN_HAK + koran); spread into LABEL_MAP (L122); add ?type=akuisisi branch in GET /labels (L265).
  8. frontend/src/lib/classifier-labels.tsAKTA_AKUISISI → 'Akta Pengambilalihan' in CLASSIFIER_LABEL_MAP.
  9. frontend/src/components/klasifikasi/detect-banner.tsx — add 'akuisisi' to DetectedFlow + FLOW_LABELS + FLOW_DESCRIPTIONS.
  10. frontend/src/pages/KlasifikasiPage.tsx — extend KlasifikasiLabelType/computedLabelType/detectedFlow; add else if (data.type==='AKUISISI_PT') navigate('/akuisisi/${id}/extraction') before the PENDIRIAN default (L468); add React Router route + extraction page.
  11. Perubahan-scoped routes/services — extend every type !== 'PERUBAHAN_PT' guard (perubahan.ts review-data L265, submit L1144, selected-jenis L672/L821; submissions.ts GET status L470, /summarize L2243/L2256, retry re-kick L2190) or fork akuisisi analogues.
  12. Tests (against ahu_ocr_test only)detectUnsupportedAktaType no longer flags akuisisi; inferSubmissionType('AKTA_AKUISISI')==='AKUISISI_PT'; processorKindForType('AKUISISI_PT'); regression guard proving a field edit on an AKUISISI_PT submission does NOT wipe its ValidationResult rows.

4. Risks / gotchas

  • reMatchAndValidate trap (highest risk). Without step 5, every field edit/confirm on an akuisisi submission falls through to the PENDIRIAN path: db.$transaction([identityMatch.deleteMany, validationResult.deleteMany, …]) then if (!aktaSubDoc) return (the akta is AKTA_AKUISISI, not AKTA_PENDIRIAN) — so all validations are silently wiped and never rebuilt. Callers already use the serialized queueReMatchAndValidate; only the internal dispatch is missing.
  • BUILT_AKTA_TXN_TYPES half-ship is dangerous. Adding step 2 without step 3 removes the gate but inferSubmissionType has no akuisisi branch → akuisisi silently mis-routes to PENDIRIAN_PT. Ship 2+3 together.
  • Two exhaustiveness switches are tripwires. processorKindForType's const _exhaustive: never = type will fail to compile the moment the enum lands — intended, forces the resume//start path to route correctly. Same discipline needed at every type !== 'PERUBAHAN_PT' guard (these do NOT auto-cover a new type).
  • Section-approval gate. The submit gate reads explicit persisted PerubahanSectionApproval rows via appearingSectionKeys(effectiveJenis) — never derived from child confirmed state, and must use collapsed effective-jenis keys (regression C1) not raw/detected keys (H1). Akuisisi needs analogous (or shared) approval tables or the finalize gate breaks.
  • Finalize gate parity. Perubahan's soft gates include PEMEGANG_SAHAM_CONTACT (every post-PS must fuzzy-match a Data Kontak ≥0.7), CONTRADICTORY_JENIS, RUPS syaratUtama when NONPAD. Akuisisi adds the pengumuman 30-day window as a deterministic gate; the 14-day creditor-objection window is advisory/lifecycle (SABH does not enforce it client-side).
  • isTransactionVerifiable() diverges in SABH (id_aksi==10 only) — akuisisi (19) is NOT eligible for the same PS-notification/PERBAIKAN path there. The PoC can choose its own verify path, but don't assume SABH treats 19 like 10 for verification.
  • createAkuisisiTransaksi omits guards (NPWP-KSWP, voucher-reuse, PPD service-time) vs createPerubahanTransaksi. Not a PoC concern (no SABH submit-back yet), but note if strict parity is later wanted.
  • Two-party gap. SABH akuisisi is single-subject data-entry (one PT amends itself; acquirer is just a kepada name string). It does not model a two-party acquirer/target framing with the acquirer's own cap table — so the PoC does NOT need to either for parity. Resist scope-creep into dual-entity resolution.