Pembubaran PP on the ReviewData Engine (Tenant #2) — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Migrate the PEMBUBARAN_PP review READ+RENDER path onto the generic ReviewData engine as its second tenant — projector + the two missing section renderers (registry-entity, attestation) + a V2 page at /pp/pembubaran/:id/review-v2.
Architecture: Strangler, same as tenant #1: the projector serves GET /api/submissions/:id/review; ALL write actions keep hitting the existing per-flow endpoints (/api/pp/pembubaran/{finalize,konfirmasi,alasan,document,sections/:key/approve}). Legacy page + legacy /review-data endpoint stay untouched until Efran smoke-tests V2 (Phase B convention). Alasan selection and KTP upload have no engine kind — they remain bespoke cards on the V2 page beside <ReviewEngine>.
Tech Stack: Bun + Hono + Prisma (backend), React 19 + TanStack Query + shadcn (frontend), zod contract at contract/review.ts (STRICT — unknown key = violation).
Global Constraints
- TDD: failing test first for every behavior. Frequent commits.
- Do NOT touch:
flow-engine/**, akuisisi/peleburan/laporan-rups/pembubaran-PT files, the legacyPpPembubaranReviewPage.tsx, the legacyGET /api/pp/pembubaran/:id/review-datahandler and its tests (they stay until the post-smoke flip),pp-konfirmasi.tssemantics (shared with Perbaikan PP — consume, don't fork). contract/review.tsalready has every kind needed (registry-entity L153, attestation L226, declarations L241) — NO contract changes in this plan.- Design conventions: emerald done-glow
border-emerald-200 dark:border-emerald-900computed LIVE from local/optimistic state (memory: reference-review-done-glow); icon-button actions; confirm rightmost; validation boxes in the extraction-step design language. - Backend tests:
cd backend && bun run test(test DB via bunfig preload; NEVER barebun test). Frontend:npx vitest run. Bothbunx tsc --noEmitclean. - ValidationStatus spellings
PASS/WARNING/FAIL/SKIPPED; VerifikasiStatus quirks per memory.
Locked design decisions
requiredSections = ["pernyataan"]— matches the legacy finalize 4-gate (READY + alasan non-empty + pernyataan approved + no un-overridden FAIL). The contract-test fixture'sidentitas_pemilik: required:truepredates the 2026-07-03 per-field decision; the fixture stays (it's just a valid shape), the projector follows the real gate.- KTP fields become confirmable (
identitas_pemilikfield-list: nik, nama, tempat_lahir, tanggal_lahir;editable:true) — deliberate delta from the legacy read-only grid, consistent with Efran's per-field-approval ruling. The FE submit-gate therefore requires their confirmation; the backend finalize gate stays legacy (FE stricter than BE is fine). reMatchAndValidate dispatch for PEMBUBARAN_PP is already wired (submission-processor.ts L755), so generic field edit/confirm endpoints revalidate correctly out of the box. - Attestation is an engine section (kind
attestation, keypernyataan) rendered by a NEW genericAttestationSectionrenderer — unlike tenant #1, where pernyataan is a page-local card. All-or-nothing checkbox semantics: checking every item →actions.approveSection("pernyataan", true); unchecking any →approved:false. Live emerald glow from local checkbox state. - Registry entity: projector emits
registryEntities.pp_terdaftar(source:"PTP",matchedBy:"auto", label"PP Terdaftar") with two groups — "Badan Usaha" (Nama, NPWP, Email, Telepon, Modal fromoldData.perseroan) and "Pemilik" (Nama, NIK fromoldData.owner).candidates: null. New genericRegistryEntitySectionrenderer (read-only card, DetailGrid-style 2-col groups). - Lazy expiry lives in
prepare(): the projector'sprepare(id)callslazyExpireIfNeeded(id)so the KEDALUWARSA flip happens on the engine read path exactly as it does on the legacy one. meta.expiresAt = konfirmasiExpiresAt(ISO string or null) — pembubaran is the first tenant to populate it.- V2 page terminal semantics:
readOnly(the task-16 ReviewEngine prop) when status ∈ {MENUNGGU_KONFIRMASI, COMPLETED, KEDALUWARSA}; finalize button only in READY; KonfirmasiBanner + konfirmasi mutation preserved; status-aware polling per Finding F3 (1500ms transient, 60_000 for MENUNGGU_KONFIRMASI, false for READY/COMPLETED/KEDALUWARSA/ERROR) applied to the ENGINE query. - Declarations: 3 items (
PP_PEMBUBARAN_DECLARATIONS), keys D1–D3,checked = sectionApprovals["pernyataan"] ?? false(section-level approval is the only persistence — same as tenant #1).
Task 1: Backend projector + registration
Files:
- Create: backend/src/review/projectors/pp-pembubaran-review.ts
- Create: backend/src/review/__tests__/pp-pembubaran-projection.test.ts
- Modify: backend/src/review/review-registry.ts (PEMBUBARAN_PP: null → projection)
Interfaces:
- Consumes: ReviewProjection (review-registry.ts), lazyExpireIfNeeded (services/pp-konfirmasi.ts), PpRegistrySnapshot (services/pp-company-lookup.ts), PP_PEMBUBARAN_DECLARATIONS (routes/pp-pembubaran.ts L31 — export it), normalizeConfidence/PROVENANCE_MAP pattern from pp-pendirian-review.ts (extract to a shared projector-utils.ts if copying >20 lines).
- Produces: ppPembubaranReviewProjection: ReviewProjection with sections (SectionSchema[]): pp_terdaftar (registry-entity, order 1), alasan — NO (bespoke, not a section), identitas_pemilik (field-list, order 2, docTypes ["KTP"]), pernyataan (attestation, order 3).
Steps:
- [ ] Failing tests first (pp-pembubaran-projection.test.ts), seeded via the same helpers pp-pembubaran.test.ts uses (READY submission with oldData snapshot + KTP doc with 4 fields + validations + sectionApprovals):
1. projectReview(id) output parses with ReviewDataSchema (contract STRICT parse — the real gate).
2. registryEntities.pp_terdaftar groups carry oldData values (nama, npwp, owner nama/nik).
3. fields has 4 KTP refs "{ktpDocId}:nik" etc., confidence normalized to 0–100, editable:true.
4. sections = [registry-entity pp_terdaftar, field-list identitas_pemilik, attestation pernyataan]; requiredSections=["pernyataan"].
5. meta.expiresAt null in READY; equals konfirmasiExpiresAt ISO in MENUNGGU_KONFIRMASI.
6. declarations = 3 items D1–D3, checked mirrors sectionApprovals["pernyataan"]; attestation VM items identical.
7. prepare() on a past-due MENUNGGU_KONFIRMASI flips status to KEDALUWARSA (assert via DB re-read) — and the follow-up projectReview reflects it.
8. Unknown id → null (route 404s).
9. GET /api/submissions/:id/review end-to-end for a PEMBUBARAN_PP submission returns 200 + parses (registration wired; route test app mounts reviewData route like pp-pendirian-parity did).
- [ ] Implement projector (copy tenant-1 structure; no rosters — rosters: {}; validationResults mapped with overridable:true; attestation VM {items, choices: [], publications: null}).
- [ ] Register in REVIEW_REGISTRY. Export PP_PEMBUBARAN_DECLARATIONS from routes/pp-pembubaran.ts (import into projector — no duplication).
- [ ] Gates: bunx tsc --noEmit && bun run test. Commit: feat(review): PEMBUBARAN_PP projector — tenant #2 on the ReviewData engine.
Task 2: Generic renderers — RegistryEntitySection + AttestationSection
Files:
- Create: frontend/src/components/review-engine/RegistryEntitySection.tsx, AttestationSection.tsx
- Create tests: frontend/src/components/review-engine/__tests__/RegistryEntitySection.test.tsx, AttestationSection.test.tsx
- Modify: frontend/src/components/review-engine/ReviewEngine.tsx (DEFAULT_RENDERERS += "registry-entity", "attestation")
Interfaces:
- Consumes: SectionRenderer signature (incl. the readOnly prop task 16 adds), DetailGrid, ReviewActions.approveSection.
- Produces: renderers registered by kind; AttestationSection drives approveSection(section.key, bool) all-or-nothing.
Steps:
- [ ] Failing tests first:
- RegistryEntitySection: renders group labels + field label/value pairs from data.registryEntities[section.entityKey]; renders "—" for null values; no interactive controls; source badge ("Data PTP").
- AttestationSection: renders N checkboxes from the VM items; checking all fires approveSection(key, true); unchecking one after all-checked fires approveSection(key, false); live emerald done-glow class on its container when all checked (convention test, mirror the pendirian pernyataan test); readOnly disables all checkboxes; server-checked items seed local state.
- [ ] Implement (visual language: same card idiom as engine sections; AttestationSection mirrors the V2-pendirian pernyataan card look — checkbox rows, progress n/N badge, "Disetujui" tag).
- [ ] Add both to DEFAULT_RENDERERS. ReviewEngine.test.tsx: the "renderer belum tersedia" fallback test must now use a still-unrendered kind (e.g. diff).
- [ ] Gates + commit: feat(review-engine): registry-entity + attestation section renderers.
Task 3: V2 page + route + hooks
Files:
- Create: frontend/src/pages/pp/PpPembubaranReviewPageV2.tsx
- Create: frontend/src/pages/pp/__tests__/PpPembubaranReviewPageV2.test.tsx
- Modify: frontend/src/routes.tsx (add pp/pembubaran/:submissionId/review-v2)
- Modify (if needed): frontend/src/hooks/use-review-engine-data.ts — add optional refetchInterval override param (status-aware poll; default behavior unchanged for tenant #1)
Interfaces:
- Consumes: <ReviewEngine readOnly>, useReviewEngineData, existing pembubaran mutations from use-pp-pembubaran.ts (usePpPembubaranAlasan, KTP upload, usePpPembubaranFinalize, usePpPembubaranKonfirmasi, section approval), KonfirmasiBanner, UploadToAutofill, submitGate, ReviewProgressHeader, StepIndicator.
- Produces: the smoke surface /pp/pembubaran/:id/review-v2.
Steps:
- [ ] Failing page tests first (fixture = contract-shaped ReviewData for PEMBUBARAN_PP + legacy alasan catalogue mock):
1. Renders PP Terdaftar card (registry values), KTP field-list, attestation checkboxes via the engine; bespoke Alasan card + KTP upload card render beside them.
2. Finalize disabled until: alasan chosen + all KTP fields confirmed + attestation approved; enabled when all true and no FAIL.
3. Status MENUNGGU_KONFIRMASI → KonfirmasiBanner + engine readOnly (controls disabled) + no finalize; KEDALUWARSA → expired banner, readOnly; COMPLETED → readOnly.
4. F3 poll contract: engine query refetchInterval 60_000 when MENUNGGU_KONFIRMASI, false when READY/COMPLETED/KEDALUWARSA/ERROR (unit-test the interval fn — export it).
5. Last-alasan-uncheck guard preserved (F5) in the bespoke card.
6. Optimistic confirm on KTP fields (reuse engine mutation hooks pattern — generic document field endpoints).
- [ ] Implement page (65/35 split + ReviewPdfViewer for the KTP doc; header progress; humanized finalize errors like tenant #1).
- [ ] Add route (V2 alongside legacy; NO flip).
- [ ] Gates + commit: feat(pembubaran-pp): V2 review page on the ReviewData engine at /review-v2.
Task 4: Docs + ship
- [ ]
tidyup/tasks.mdPhase B: mark PEMBUBARAN_PP line — engine + V2 page done (2026-07-04), AWAITING Efran smoke → then flip. - [ ] Ledger entry in
.superpowers/sdd/progress.md. - [ ]
infra/deploy/build-and-ship.sh(full gates) → verifyhttps://x056.ahu-demo.ocr.val.idhealth +/pp/pembubaran/<id>/review-v2reachable. - [ ] Commit docs:
docs(tidyup): pembubaran PP tenant #2 shipped to staging.
Self-review notes
- Type consistency:
SectionRendererreadOnly prop comes from Task 16 (route-flip wave) — this plan MUST NOT start until that wave is merged; Task 2/3 build on its signature. - The fixture-vs-gate requiredSections divergence is deliberate (decision #1).
- No placeholder steps; exact shapes live in contract/review.ts and the recon quotes above.