think
16px
820px

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_rups di 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 = falseSKIPPED ("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

flowchart TD A[rupsQuorumRule.run] --> B{hasBaRupsDoc DAN roster total > 0?} B -- ya --> C[ratio = Σ hadir% / Σ total%
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

flowchart LR subgraph A[Bagian A — fallback kuorum] A1[parser baru] --> A2[context +2 field] --> A3[rule: prioritas BA RUPS → akta → SKIPPED] end subgraph B[Bagian B — hide section] B1[config flow: buang section] --> B2[required-list FE] --> B3[hapus UI + verifikator] --> B4[update test] end
  • 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 required agar submit tetap jalan.

Urutan implementasi yang disarankan

  1. Bagian A: parser → context → rule → test (paling terisolasi).
  2. Bagian B: config flow + required-list → UI → verifikator → perbaiki test.
  3. bunx tsc --noEmit di backend & frontend, jalankan test terkait.