Rancangan: Fallback Kuorum via kehadiran_rups Akta + Hilangkan Section RUPS/Kehadiran (Berakhirnya & Pembubaran)
Status: SUDAH DIIMPLEMENTASIKAN · 2026-07-15 · backend + frontend tsc bersih · suite frontend 970/970 hijau · tes backend (bun:test) sudah diperbarui, jalankan di lingkungan Efran (bun tidak tersedia di sesi ini).
Ringkasan implementasi
Bagian A
- backend/src/lib/kehadiran-rups-parser.ts — utilitas baru + tes __tests__/kehadiran-rups-parser.test.ts.
- backend/src/flow-engine/context/pt-akta-context.ts — 2 field aktaKehadiranRatio/aktaKehadiranRaw (opsional) + import parser.
- backend/src/flow-engine/rules/pt-akta/rups-quorum.ts — prioritas BA RUPS → akta → SKIPPED; pesan bertanda "(dari akta)".
- Tes rule fallback ditambah di __tests__/rules-pt-akta.test.ts.
Bagian B
- Config flow: berakhirnya-status-pt.ts (buang risalah_rups), pembubaran-pt.ts (buang rups_kehadiran) — gate submit config-driven ikut lepas otomatis.
- Required-list FE: lib/berakhirnya-review.ts, lib/pembubaran-review.ts.
- UI dihapus: BerakhirnyaReviewPage, PembubaranReviewPage, VerifikatorBerakhirnyaReviewPage, VerifikatorPembubaranReviewPage (+ bersihkan import/var tak terpakai).
- Tes diperbarui: config-flow, integration, routes (backend) + page tests (frontend). Tes usang dibuang: "kehadiran toggle" (Pembubaran) dan assertion badge HADIR (Verifikator Pembubaran). Assertion label validasi "Kuorum RUPS" tetap (baris Hasil Validasi, bukan roster).
(Rancangan asli di bawah)
Status: draft desain · 2026-07-15
Dua perubahan terpisah dalam satu spec:
- Bagian A — kuorum kehadiran RUPS bisa dihitung dari field
kehadiran_rupsdi akta sebagai fallback saat tidak ada dokumen Berita Acara (BA) RUPS. Berlaku untuk flow yang memang memvalidasi kuorum (Perubahan, Akuisisi, Penggabungan, Peleburan). - Bagian B — untuk flow yang tidak memvalidasi kuorum (Berakhirnya, Pembubaran), hilangkan section roster "Risalah RUPS & Kehadiran" dari akta. Laporan RUPS tidak disentuh (RUPS adalah subjek utama flow-nya).
Bagian A — Fallback ke kehadiran_rups akta
Masalah sekarang
rupsQuorumRule menghitung kehadiran dari roster per-pemegang-saham (AktaPemegangSaham.persentase yang di-flag kehadiranRups === true). Flag itu hanya terisi oleh matchRupsAttendance() yang membaca dokumen BA RUPS. Jadi:
- Tidak ada BA RUPS →
hasBaRupsDoc = false→ SKIPPED ("Belum ada Berita Acara RUPS"). - SKIPPED disaring UI → baris kuorum tidak muncul sama sekali.
Padahal akta sendiri sudah merekam kuorum di field kehadiran_rups (contoh nilai: "5000 saham (100%)", "75%") — tapi field ini saat ini hanya dipakai untuk display (pt-akta-field-labels.ts:38), tidak masuk perhitungan.
Insight desain
BA RUPS memberi kehadiran per orang (perlu dijumlah). kehadiran_rups akta memberi rasio agregat langsung (present relatif terhadap total). Jadi fallback tidak perlu mengisi roster per orang — cukup pakai rasio agregat itu untuk dibandingkan ke ambang.
Alur prioritas
source = ba_rups] B -- tidak --> D{kehadiran_rups akta
ter-parse jadi persen?} D -- ya --> E[ratio = persen / 100
source = akta] D -- tidak --> F[SKIPPED] C --> G[bandingkan ke THRESHOLDS category × rupsKe] E --> G G --> H[PASS / WARNING + tandai source]
Prinsip: BA RUPS tetap otoritatif. Fallback akta hanya jalan saat BA RUPS tidak ada. Jika BA RUPS ada tapi pencocokan nama gagal (roster hadir 0), itu tidak ditutupi angka akta — biarkan tampil apa adanya sebagai sinyal kualitas data.
Perubahan file
1. Utilitas baru — backend/src/lib/kehadiran-rups-parser.ts
/** Parse the akta free-text kehadiran/kuorum string into an attendance ratio 0..1.
* "5000 saham (100%)" → 1.0 · "75%" → 0.75 · "hadir 66,7%" → 0.667
* Returns null when no explicit percent is present (→ SKIPPED, never a false PASS). */
export function parseKehadiranRupsPercent(raw: string | null): number | null;
Aturan parse:
- Prioritas token dalam kurung (NN%); jika tak ada, ambil NN% pertama.
- Terima koma desimal Indonesia (66,7% → 66.7).
- Guard: hasil > 100 atau <= 0 atau tak ada % → null.
- Tidak menebak dari "N saham" tanpa % (total lembar tak selalu diketahui di titik ini).
2. Context — backend/src/flow-engine/context/pt-akta-context.ts (~baris 385-402)
Tambah ke objek rupsQuorum:
const kehadiranRupsRaw = aktaFields.find((f) => f.fieldKey === "kehadiran_rups")?.value ?? null;
// ...
rupsQuorum: {
pemegangSaham: (aktaDoc?.aktaPemegangSaham ?? []).map((ps) => ({
persentase: ps.persentase, kehadiranRups: ps.kehadiranRups,
})),
hasBaRupsDoc,
rupsKe,
aktaKehadiranRatio: hasBaRupsDoc ? null : parseKehadiranRupsPercent(kehadiranRupsRaw), // NEW
aktaKehadiranRaw: kehadiranRupsRaw, // NEW (display/audit)
},
3. Rule — backend/src/flow-engine/rules/pt-akta/rups-quorum.ts
Perluas tipe input (c.rupsQuorum) dengan aktaKehadiranRatio: number | null dan aktaKehadiranRaw: string | null. Refactor run:
const total = c.rupsQuorum.pemegangSaham.reduce((s, p) => s + (p.persentase ?? 0), 0);
const present = c.rupsQuorum.pemegangSaham
.filter((p) => p.kehadiranRups === true).reduce((s, p) => s + (p.persentase ?? 0), 0);
let ratio: number | null = null;
let source: "ba_rups" | "akta" | null = null;
if (c.rupsQuorum.hasBaRupsDoc && total > 0) { ratio = present / total; source = "ba_rups"; }
else if (c.rupsQuorum.aktaKehadiranRatio != null) { ratio = c.rupsQuorum.aktaKehadiranRatio; source = "akta"; }
if (ratio == null) return { ...SKIPPED... }; // pesan: BA RUPS / data belum ada
const { threshold, comparator } = THRESHOLDS[category][rupsKe];
const belowQuorum = comparator === ">=" ? ratio < threshold : ratio <= threshold;
const src = source === "akta" ? " (dari akta)" : "";
// PASS/WARNING message menyertakan `src`, mis:
// "Kehadiran RUPS 100.00%${src} — memenuhi kuorum (≥75%)"
Catatan: saat source === "akta" kita hanya punya rasio agregat, jadi pesan tampilkan ratio*100 saja (bukan "X% dari Y%").
4. Frontend (opsional, ringan) — di penampil hasil validasi, karena pesan sudah menandai "(dari akta)", tidak perlu perubahan komponen. Bisa ditambah tooltip/badge "sumber: akta" kalau mau eksplisit.
Test (Bagian A)
- Unit
kehadiran-rups-parser.test.ts:"5000 saham (100%)"→1,"75%"→0.75,"66,7%"→0.667,"5000 saham"→null,"120%"→null,""/null→null. - Rule test: (a) ada BA RUPS → tetap pakai roster; (b) tanpa BA RUPS +
kehadiran_rups="100%"→ PASS ps89; (c) tanpa BA RUPS +"50%"+ ps89 ke-1 → WARNING; (d) tanpa BA RUPS + tak ada field → SKIPPED; (e) rupsKe=2 menurunkan ambang.
Bagian B — Hilangkan section "Risalah RUPS & Kehadiran" (Berakhirnya & Pembubaran)
Temuan penting: section ini adalah required approval gate
Kedua section didefinisikan required: true di config flow, dan masuk daftar required-section yang menggerbang submit. Jadi tidak cukup menghapus UI — harus melepas gate-nya juga, kalau tidak submit jadi mustahil.
| Flow | Section key | Deklarasi config | Required-list (FE) |
|---|---|---|---|
| Berakhirnya | risalah_rups |
berakhirnya-status-pt.ts:65 |
frontend/src/lib/berakhirnya-review.ts:8 |
| Pembubaran | rups_kehadiran |
pembubaran-pt.ts:55 |
frontend/src/lib/pembubaran-review.ts:7 |
Perubahan file
Backend — lepas section dari config flow
- backend/src/flow-engine/flows/berakhirnya-status-pt.ts:65 — hapus entri { key: "risalah_rups", ... }.
- backend/src/flow-engine/flows/pembubaran-pt.ts:55 — hapus entri { key: "rups_kehadiran", ... }.
Frontend — lepas dari required-list + hapus UI
- frontend/src/lib/berakhirnya-review.ts:8 — buang "risalah_rups" dari array.
- frontend/src/lib/pembubaran-review.ts:7 — buang "rups_kehadiran" dari array.
- frontend/src/pages/BerakhirnyaReviewPage.tsx:292-316 — hapus blok section "Risalah RUPS & Kehadiran".
- frontend/src/pages/PembubaranReviewPage.tsx:200-222 — hapus blok section "RUPS & Kehadiran Pemegang Saham".
- frontend/src/pages/VerifikatorBerakhirnyaReviewPage.tsx:93-104 — hapus blok "Kehadiran RUPS".
- frontend/src/pages/VerifikatorPembubaranReviewPage.tsx:79-90 — hapus blok "Kehadiran RUPS".
Data terkait (biarkan dormant / bersihkan bila mudah)
- Submission.rupsKehadiranRegistry & endpoint edit kehadiran untuk 2 flow ini jadi tak terpakai. Aman dibiarkan (dormant); pembersihan endpoint bersifat opsional.
Test (Bagian B) — perlu diperbarui
Referensi section-key ini muncul di banyak test; hilangkan dari ekspektasi:
- backend/src/flow-engine/__tests__/berakhirnya-flow.test.ts, berakhirnya-integration.test.ts
- backend/src/routes/__tests__/berakhirnya-routes.test.ts, pembubaran-routes.test.ts
- backend/src/flow-engine/__tests__/pembubaran-flow.test.ts, pembubaran-integration.test.ts
- frontend/src/lib/__tests__/berakhirnya-flow-config.test.ts, pembubaran-flow-config.test.ts
- frontend/src/pages/__tests__/BerakhirnyaReviewPage.test.tsx, PembubaranReviewPage.test.tsx
Di luar scope
- Laporan RUPS — tidak diubah; "Akta / Risalah RUPS Tahunan" adalah subjek utama flow, bukan artefak kuorum.
Ringkasan dampak
- Bagian A menambah cakupan (baris kuorum kini muncul walau tanpa BA RUPS, untuk flow ber-kuorum). Tidak mengubah perilaku saat BA RUPS ada.
- Bagian B menyederhanakan Berakhirnya & Pembubaran; wajib lepas gate
requiredagar submit tetap jalan.
Urutan implementasi yang disarankan
- Bagian A: parser → context → rule → test (paling terisolasi).
- Bagian B: config flow + required-list → UI → verifikator → perbaiki test.
bunx tsc --noEmitdi backend & frontend, jalankan test terkait.