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:
- Peralihan saham is FORCED ON (not user-optional). In
_formAkuisisiList.phpthe "Peralihan saham" checkbox is pre-checked +display:none— every akuisisi is definitionally a share transfer that shifts control. - 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. - 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_PT→akuisisi-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 akuisisi→AKTA_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)
backend/prisma/schema.prisma— addAKUISISI_PTtoenum SubmissionType(doc comment: "Akuisisi/Pengambilalihan PT: control-changing share transfer");bunx prisma generate+db push.backend/src/services/akta-txn-classifier.ts:32— add'akuisisi'toBUILT_AKTA_TXN_TYPES. Do NOT ship alone (see trap below).backend/src/routes/submissions.ts:82inferSubmissionType— widen return union + addif (files.some(f => eff(f)==='AKTA_AKUISISI')) return 'AKUISISI_PT'before thePENDIRIAN_PTdefault (line 96).mapToDocumentType/order already handleAKTA_*.backend/src/services/submission-dispatch.ts— add'akuisisi-pt'toProcessorKind;case 'AKUISISI_PT': return 'akuisisi-pt'inprocessorKindForType(clearsnever); matchingcase 'akuisisi-pt'importingprocessAkuisisiSubmissionindispatchProcessor.backend/src/services/submission-processor.ts:709reMatchAndValidate— addif (submission?.type==='AKUISISI_PT') { const {reMatchAndValidateAkuisisi}=await import('./akuisisi-processor'); return reMatchAndValidateAkuisisi(submissionId); }BEFORE the fallthroughdeleteMany/AKTA_PENDIRIAN block. (the memory gotcha)- Build
akuisisi-processor.ts—processAkuisisiSubmission+reMatchAndValidateAkuisisi(owns its own upsert/delete-stale discipline). This is the substantive workBUILT_AKTA_TXN_TYPESmembership asserts. backend/src/routes/klasifikasi.ts— addAKUISISI_DOC_TYPES(requiredAKTA_AKUISISI"Akta Pengambilalihan" maxCount 1 + KTP/NPWP/BERITA_ACARA_RUPS/AKTA_PEMINDAHAN_HAK + koran); spread intoLABEL_MAP(L122); add?type=akuisisibranch in GET/labels(L265).frontend/src/lib/classifier-labels.ts—AKTA_AKUISISI → 'Akta Pengambilalihan'inCLASSIFIER_LABEL_MAP.frontend/src/components/klasifikasi/detect-banner.tsx— add'akuisisi'toDetectedFlow+FLOW_LABELS+FLOW_DESCRIPTIONS.frontend/src/pages/KlasifikasiPage.tsx— extendKlasifikasiLabelType/computedLabelType/detectedFlow; addelse if (data.type==='AKUISISI_PT') navigate('/akuisisi/${id}/extraction')before the PENDIRIAN default (L468); add React Router route + extraction page.- Perubahan-scoped routes/services — extend every
type !== 'PERUBAHAN_PT'guard (perubahan.tsreview-data L265, submit L1144, selected-jenis L672/L821;submissions.tsGET status L470, /summarize L2243/L2256, retry re-kick L2190) or fork akuisisi analogues. - Tests (against
ahu_ocr_testonly) —detectUnsupportedAktaTypeno longer flags akuisisi;inferSubmissionType('AKTA_AKUISISI')==='AKUISISI_PT';processorKindForType('AKUISISI_PT'); regression guard proving a field edit on anAKUISISI_PTsubmission does NOT wipe itsValidationResultrows.
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, …])thenif (!aktaSubDoc) return(the akta isAKTA_AKUISISI, notAKTA_PENDIRIAN) — so all validations are silently wiped and never rebuilt. Callers already use the serializedqueueReMatchAndValidate; only the internal dispatch is missing. BUILT_AKTA_TXN_TYPEShalf-ship is dangerous. Adding step 2 without step 3 removes the gate butinferSubmissionTypehas no akuisisi branch → akuisisi silently mis-routes toPENDIRIAN_PT. Ship 2+3 together.- Two exhaustiveness switches are tripwires.
processorKindForType'sconst _exhaustive: never = typewill fail to compile the moment the enum lands — intended, forces the resume//startpath to route correctly. Same discipline needed at everytype !== 'PERUBAHAN_PT'guard (these do NOT auto-cover a new type). - Section-approval gate. The submit gate reads explicit persisted
PerubahanSectionApprovalrows viaappearingSectionKeys(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==10only) — 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.createAkuisisiTransaksiomits guards (NPWP-KSWP, voucher-reuse, PPD service-time) vscreatePerubahanTransaksi. 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
kepadaname 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.