PR Review — dev/1 → master
Branch: dev/1 (merged from dev/2 + master)
Scope: 722 files changed, 64 k insertions, 13 k deletions
Review method: 7 parallel finder angles × candidate dedup × 1-vote verification
Result: 3 confirmed bugs, safe to fix before merging
All other candidates (17 total) were REFUTED by code inspection.
Bug 1 — HIGH · fast-track payment verification broken for all '2'-scheme vouchers
File: backend/src/lib/simpadhu-db.ts:49
// Current (broken):
/^(2\d{3})(\d{2})(\d{2})\d{4,}$/.exec(code)
// Should mirror the '8' scheme:
/^2(\d{4})(\d{2})(\d{2})\d{4,}$/.exec(code)
The '8'-scheme regex correctly anchors the sentinel outside the capture group: ^8(\d{4}) — literal 8, then 4 captured year digits. The '2'-scheme regex instead writes ^(2\d{3}), pulling the leading 2 inside the capture group, yielding m[1]='2YYY' (e.g. '2202' for year 2026), m[2]='60' (month), m[3]='70' (day). Month 60 and day 70 both fail the range guards, so every valid '2'-prefix voucher returns null — lookupBillingCode reports them as not found, and fast-track payment verification always fails for this scheme.
Fix: Change ^(2\d{3}) → ^2(\d{4}).
Bug 2 — HIGH · BA RUPS authoritative branch silently bypassed when share percentages are null
File: backend/src/flow-engine/rules/pt-akta/rups-quorum.ts:67
if (c.rupsQuorum.hasBaRupsDoc && total > 0) {
ratio = ... // BA RUPS (authoritative — stricter ¾ threshold for ps89)
} else if (c.rupsQuorum.aktaKehadiranRatio != null) {
ratio = c.rupsQuorum.aktaKehadiranRatio; // weaker fallback
}
total is the sum of pemegangSaham[].persentase ?? 0. When a BA RUPS doc is uploaded (hasBaRupsDoc=true) but share-percentage extraction hasn't finished yet (all persentase are null → total===0), the compound condition is false. The rule falls through to the weaker aktaKehadiranRatio branch and emits a PASS at the lower akta threshold instead of waiting for the authoritative BA RUPS data.
For PENGGABUNGAN/AKUISISI/PELEBURAN (ps89 category) the legal first-meeting threshold is ¾; the akta fallback may be lower. A submission with an incomplete BA RUPS can silently receive a PASS verdict it doesn't deserve.
Fix: When hasBaRupsDoc && total === 0, treat as incomplete data (SKIPPED or WARNING) rather than falling through to the weaker source. One approach:
if (c.rupsQuorum.hasBaRupsDoc) {
if (total === 0) {
// BA RUPS present but percentages not yet resolved — don't fall through
return { status: "SKIPPED", reason: "ba_rups_percentages_pending" };
}
ratio = /* BA RUPS ratio */;
} else if (c.rupsQuorum.aktaKehadiranRatio != null) {
ratio = c.rupsQuorum.aktaKehadiranRatio;
}
Bug 3 — MEDIUM · dedupeByNomorSk collapses all null-SK companies onto one key
File: backend/src/services/company-lookup.ts:252 (called from lines 216 and 236)
function dedupeByNomorSk(matches: LookupMatch[]): LookupMatch[] {
// ...
const key = m.nomor_sk.replace(/\s+/g, "").toUpperCase();
rowToMatch (line 271) constructs nomor_sk: String(row.nomor_sk ?? ""), so every DB row with a NULL nomor_sk produces the string "". All such rows share the same dedup key "", and only the first survives the seen-Set filter. In the medium-confidence name-search path (LIMIT 10), if multiple distinct companies all have no recorded nomor_sk, all but the first are silently dropped — the user cannot find the correct company in AWAITING_COMPANY_SELECTION and the submission stays stuck.
Fix: Skip dedup when the key is empty (let nulls through as unique):
const key = m.nomor_sk.replace(/\s+/g, "").toUpperCase();
if (!key) return true; // don't dedup null-SK entries against each other
if (seen.has(key)) return false;
seen.add(key);
return true;
What was checked and cleared
| Area | Angles run | Candidates | Confirmed |
|---|---|---|---|
| Flow-engine flows/rules/processor | A, B | 9 | 1 (rups-quorum) |
| Backend lib / OCR / persistence | A, C | 8 | 1 (simpadhu billing) |
| Frontend store / lib / routes | A, B | 7 | 0 |
| Frontend pages | A, B | 6 | 0 |
| company-lookup.ts | A | 1 | 1 (dedupeByNomorSk) |
Notable refutations worth knowing:
- processor.ts timeout race — closed: the catch block also writes doc.status=ERROR; processOneDocument reads that before deciding to write DONE.
- isPerubahanFamilyAkta + AKTA_PENGGABUNGAN — intentional; all callsites are gated on routeFamily="perubahan" and handle penggabungan correctly.
- RUPS_TAHUNAN quorum rule removed — documented in-code as deliberate exclusion (laporan tahunan is administrative, not a voting decision).
- apostille decidedAt null crash — decidedAt is DateTime @default(now()) in the schema, non-nullable.
- rematch.ts skipping beforeValidation — pendirian derive logic is covered via standardPtMatchers in the rematch path.
- jenisPerubahanSelectionAtom stale — atom is reset in all three exit paths from the klasifikasi page.