# Flow BOD Reject — Rollback (Action Plans) Dokumen ini menjelaskan **secara rinci** apa yang terjadi pada seluruh hierarki Action Plan ketika hasil vote BOD adalah **ROLLBACK** (reject + rollback ke prerequisite). Bahasa Indonesia; contoh payload request/response tetap dalam bahasa Inggris sesuai konvensi API. Referensi implementasi: `src/features/version/1/action-plans/` — helper utama di `action-plans.helpers.ts` (`restartRollbackSubtree` L173, `pauseSubtreeForRollback` L500, `reprocessReleasedChildren` L992, `markRollbackDownstreamConsumers` L371, `releaseRollbackPausedDependents` L259, `assertNotRollbackPaused` L465, `aggregateBodRound` L2200, `resolveApprovalNotifyTargets` L2659, `resolveActionPlanOwnerAdminUserIds` L2590). --- ## 1. Ikhtisar Rollback dipicu saat **semua BOD yang ditugaskan sudah memberikan vote** dan agregasi menghasilkan **ROLLBACK** (lihat §2). Kronologi: 1. Item yang di-escalate (sebut **B**) di-rollback ke salah satu **prerequisite langsung** yang dipilih BOD (sebut **X**). 2. **Subtree penuh X** (target) di-restart: progress terakhir leaf dijadikan `PENDING` dan status approval dikembalikan ke `WAITING_APPROVAL` — sesuai posisi masing-masing node (leaf / non-leaf / terhalang prerequisite). 3. **Subtree penuh B** (item yang di-rollback) **TIDAK di-restart saat itu juga**: semua node live di-set `WAITING_APPROVAL` + `rollbackPaused = true` + `bodResult = REJECTED_ROLLBACK`, **data progress TIDAK diubah** (tetap 100% `APPROVED`), tampil `NOT_STARTED`. B baru "diproses ulang" ketika X di-approve & `CLOSED` lagi (lihat §6c) — node leaf rantai awal di-flip ke `PENDING` saat itu. 4. **Konsumen downstream** (item same-level lain yang prerequisite-nya = X) ikut di-hold — lihat §6. 5. Semua perubahan ditulis **dalam satu transaksi database**; setiap item terdampak mendapat baris **activity log** dan (bila aktif) **notifikasi**. 6. Riwayat selesai ketika rantai prerequisite di-approve ulang berurutan sampai B di-approve/di-close lagi oleh VP & BOD. > Perbedaan kunci dengan **REJECT oleh VP**: VP REJECT hanya membuka **anak langsung** yang tidak memblokir (*non-blocker chain*), root yang direject tetap `REJECTED`. **BOD ROLLBACK me-restart subtree target X secara penuh**; **item B ditahan (hold) tanpa flip** sampai X selesai, baru subtree B dibuka ulang bertahap. --- ## 2. Agregasi vote & prerequisitenya BOD vote lewat `POST …/approval/bod`: ```json // Request body (English, sesuai API) { "decision": "ROLLBACK", // "APPROVE" | "REVISE" | "ROLLBACK" "reason": "pekerjaan tidak sesuai spesifikasi", // wajib, min 1 karakter "rollbackId": "uuid-x" // wajib saat ROLLBACK; harus prereq LANGSUNG item & di-mark } ``` Aturan: - **Precedence hasil akhir** (frozen): `ROLLBACK > REVISE > APPROVE`. - **One-target lock**: BOD pertama yang vote `ROLLBACK` ke target T "mengunci" target. BOD berikutnya hanya boleh ROLLBACK ke T; target lain → `400 BOD rollback target is locked: all ROLLBACK votes must name the same prerequisite`. - **ROLLBACK_CONFLICT** (defensif, bila target berbeda tetap lolos): round tetap `ACTIVE`, item tetap `BOD_APPROVAL`, **tidak ada satu pun write** — menunggu desain. Hanya activity log `ACTION_PLAN_BOD_DECISION` yang muncul. - Semua vote `REVISE` → item `WAITING_APPROVAL` + `bodResult = REJECTED_REVISE`, data/progress **tidak disentuh**; VP harus meng-approve ulang (untuk merevisi, pendekatan re-approve VP). - Semua `APPROVE` (tanpa ROLLBACK/REVISE) → item `CLOSED` (handover) + map rapikan. Hasil agregasi ROLLBACK single-target: | Nilai item B | Nilai round | |---|---| | `approvalStatus = WAITING_APPROVAL` | `status = CLOSED` | | `bodResult = REJECTED_ROLLBACK` | `result = REJECTED_ROLLBACK` | | `bodReason = reason` (vote ROLLBACK pertama) | `reason` dari voter pertama | --- ## 3. Apa yang terjadi pada **B** (item yang di-rollback / direject) Subtree B **tidak di-flip saat resolve**. Semua node live dalam subtree B (root + descendants) di-set lewat `pauseSubtreeForRollback(tx, scope, B)`: - `approvalStatus = WAITING_APPROVAL` - `rollbackPaused = true` → tampil `NOT_STARTED` (override display) - `bodResult = REJECTED_ROLLBACK` - `approvalReason = null` - **Data progress TIDAK diubah** — baris progress tetap 100% `APPROVED` (nilai/approvedBy/approvedAt utuh) - **Tidak ada notifikasi** pada tahap ini (NOT_STARTED + WAITING ⇒ tanpa notif; VP "tidak bisa ngapa-ngapain" sebelum X selesai) Node B baru "diproses ulang" ketika X di-approve & `CLOSED` lagi (release, §6c): saat itu seluruh subtree B di-restart — leaf rantai-awal di-flip `APPROVED`→`PENDING` (Need Approval), node lain jadi `WAITING_APPROVAL`, chain yang masih menunggu prereq tetap pause. Kronologi B dalam dua tahap (leaf): | Tahap | Status B | |---|---| | T1 resolve rollback | `NOT_STARTED` + `WAITING_APPROVAL`, progress tetap 100% `APPROVED` | | T2 setelah X `CLOSED` | leaf rantai-awal → progress terakhir di-flip `PENDING` (Need Approval) + notif ke Admin/Owner; leaf akar B → `WAITING_APPROVAL` tanpa flip (Completed & Waiting) + notif ke VP — VP approve → CLOSED; VP reject → flip progress akhir (`PENDING`) | > Catatan: rollback BOD hanya men-flip baris progress `APPROVED` (strict). Baris `APPROVED_WITH_NOTES` tidak di-flip — berbeda dengan VP REJECT yang juga membuka `APPROVED_WITH_NOTES`. --- ## 4. Apa yang terjadi pada **X** (prereq yang dipilih sebagai opsi rollback) Target X juga di-restart penuh, dengan fungsi yang sama: `restartRollbackSubtree(tx, scope, X)`. - X non-leaf → `WAITING_APPROVAL`, progress miliknya tidak diubah; subtree X di-traversal sama seperti §3b. - X leaf & semua prereq X terpenuhi → progress terakhir → `PENDING`, X → `WAITING_APPROVAL`. - X leaf & prereq X belum terpenuhi → `rollbackPaused = true` saja. Jadi **X bukan "di-undo ke nol"** — X dibuka ulang untuk approval ulang dengan data progress tetap utuh. Begitu X di-approve & `CLOSED` lagi oleh VP (dan BOD bila wajib), release cascade berjalan (§6). --- ## 5. Posisi X dalam rantai prerequisite (awal / tengah / akhir) Rantai contoh: `… → W → X → A → P → Q → I` (masing-masing `→` = prerequisite langsung; item di kiri adalah prereq item di kanan). ### 5a. X berada di **paling akhir rantai** (X tidak punya prereq lagi; X adalah "akar" dependensi) - Restart X + subtree X sesuai §4; subtree B **ditahan** (pause marker) sesuai §3. - X bisa langsung dibuka (`WAITING_APPROVAL`/flip) karena tidak menunggu prereq apapun. - Barisan konsumen A→P→Q→I **dan** subtree B baru ikut berjalan ketika X `CLOSED` lagi (release cascade, §6). Ini skenario paling "besar": seluruh rantai di belakang X menunggu, lalu terbuka berurutan. ### 5b. X berada di **tengah rantai** (X punya prereq sendiri W, dan punya konsumen A di belakangnya) - X dibuka ulang **tergantung W**: approb X hanya bisa dibuka penuh jika W `CLOSED`/terpenuhi; jika tidak, X jadi `rollbackPaused = true` (hold) sampai W closes. - Konsumen A (prereq langsung X) ikut di-hold (marker), P/Q/I menyusul sesuai release cascade; subtree B juga di-hold menunggu X. - Efek berantai: status ulang dimulai dari W (atau lebih dalam), lalu X, lalu A→P→Q→I (dan B). ### 5c. X berada di **paling awal rantai** (X punya konsumen, tapi X sendiri adalah prereq paling ujung milik item lain, misal X adalah konsumennya W) Anggap rantai: `W → X → A` dengan X punya prereq W. - Sama seperti §5b: pembukaan X menunggu W. Konsumen A & rantainya di-hold. - Karena X bukan akar dependensi, **efeknya tidak bisa "instan"** — semuanya menunggu W di-approve & close dulu, baru X, baru A, dst. ### Prinsip umum Rollback **hanya secara langsung** me-restart subtree **X** (target); **subtree B ditahan** (`rollbackPaused`, tanpa flip) sampai X `CLOSED` lagi, lalu di-restart bertahap (leaf rantai-awal di-flip). Item lain di belakang X (A, P, Q, I) **tidak di-restart** saat itu juga — mereka hanya di-*hold* (marker `rollbackPaused`, display `NOT_STARTED`) dan **release bertahap** satu-per-satu setiap kali prereqnya ditutup lagi (lihat §6). Data/progress mereka tidak pernah diubah sampai release. --- ## 6. Konsumen downstream & release bertahap ### 6a. Marker saat rollback resolve (`markRollbackDownstreamConsumers`) Untuk setiap item same-level **yang prereq LANGSUNG-nya = X** (selain B): - Seluruh subtree live konsumen itu di-set `rollbackPaused = true` (**marker only**): **tidak ada** perubahan `approvalStatus` (konsumen yang sudah `CLOSED` tetap `CLOSED`), **tidak ada** flip progress, **tidak ada** notice, **tidak ada** recompute. - Eksklusi (defensif): item B, item X, dan subtree live keduanya. - Display: item & subtree itu tampil `NOT_STARTED` (override `rollbackPaused` → `NOT_STARTED`), walau data sebenarnya utuh. - Setiap node yang di-mark masuk activity log `ACTION_PLAN_ROLLBACK_PAUSED`. ### 6b. Hold (read-only) Item yang sedang `rollbackPaused` (di dirinya atau ancestor) **tidak bisa diubah** oleh endpoint write: ``` 400 is on hold: waiting for its prerequisite to be approved and closed ``` Diblokir: update item, approval respond `/approval/respond`, `/approval/re-request`, `/approval/bod`, submit progress (`…/progress`), respond progress, `PUT …/plans`, member add/remove (per level sesuai guard; subtask progress melalui controller `assertSubtaskNotRollbackPaused`). > Catatan implementasi: `request*BodReApproval` (langsung), `subtask …/plans PUT`, dan subtask member add/remove belum ter-guard langsung (inner plain re-request tetap ter-guard). ### 6c. Release (`releaseRollbackPausedDependents`) — dipicu saat item `CLOSED` Dipanggil **di dalam transaksi** tepat setelah item di-approve → `CLOSED`, pada 6 titik: VP `respond…Approval` (APPROVE) dan BOD all-approve (APPROVED). - Cari semua dependents **yang prereq langsung-nya = item yang baru close** DAN sedang `rollbackPaused` DAN semua prereq-nya terpenuhi. - Untuk **konsumen downstream** (tanpa `bodResult = REJECTED_ROLLBACK`): release = `approvalStatus = WAITING_APPROVAL` + `rollbackPaused = false` + `approvalReason = null`. **Tanpa flip progress** — VP tinggal meng-approve ulang pekerjaan yang sudah ada. - Untuk **subtree B** (dependent dengan `bodResult = REJECTED_ROLLBACK` — artinya item ini adalah bagian dari item yang di-rollback): release dilakukan **bersamaan dengan restart penuh subtree B**: - Node akar B leaf → `WAITING_APPROVAL` + unpause tanpa flip (Completed & Waiting; notif ke **VP**; flip progress baru terjadi bila VP REJECT). - Node non-leaf → `WAITING_APPROVAL` + unpause tanpa flip (notif ke **VP**), lalu anaknya di-reprocess: - **Leaf rantai-awal** (tidak punya prereq live / prereq rantai paling awal) → flip `APPROVED`→`PENDING` + `WAITING_APPROVAL` + unpause (Need Approval; notif ke **Owner/Admin**). - **Leaf yang masih punya prereq belum terpenuhi** → tetap pause (`rollbackPaused = true`), tanpa flip, tanpa notif — terbuka nanti saat prereqnya selesai. - **Non-leaf** → `WAITING_APPROVAL` + unpause, lalu turun ke anaknya (notif ke VP). - **Blocker child** (dipakai sebagai prereq oleh sibling live) → tetap pause tanpa flip/notif. - Mirip restart X non-leaf, tetapi **dimulai belakangan**: baru berlangsung setelah X `CLOSED`, bukan saat resolve. - **Cascade bertahap**: anak-anak konsumen tidak di-release dalam gelombang yang sama — mereka release ketika **konsumen itu sendiri** (atau prerequisite terkait) di-approve & close lagi. Dengan begitu: X close → **B**/A dibuka; node lanjutan terbuka berurutan ketika prereqnya ditutup. Ini perilaku "Waiting Approval dari A lalu ke P dst" yang disepakati tim desain. - Setiap node yang release masuk activity log `ACTION_PLAN_ROLLBACK_RELEASED`; node B yang di-flip masuk notifikasi *Ready for Approval* / *progress reopened* (routing lihat §8). Urutan release pada rantai `X → A → P → Q → I` (dan B di sisi lain): ``` [X closed] → A → WAITING_APPROVAL + subtree B mulai di-restart (leaf rantai-awal → PENDING) [A approved]→ A → CLOSED → P → WAITING_APPROVAL [P approved]→ P → CLOSED → Q → WAITING_APPROVAL [Q approved]→ Q → CLOSED → I → WAITING_APPROVAL [I approved]→ I → CLOSED (rantai pulih penuh) ``` --- ## 7. Leaf vs children — ringkas | Bentuk item | Saat rollback resolve | Saat X re-`CLOSED` | |---|---|---| | **B leaf** | `NOT_STARTED` + `WAITING_APPROVAL` + `rollbackPaused`, progress TIDAK di-flip (tetap 100%) | `WAITING_APPROVAL` + unpause tanpa flip (Completed & Waiting, notif VP); flip progress hanya bila VP REJECT | | **B non-leaf** | root + seluruh subtree live: `WAITING_APPROVAL` + pause (marker), tanpa flip | root & non-leaf: unpause (notif VP); leaf rantai-awal: flip `APPROVED`→`PENDING` (Need Approval, notif Owner/Admin); leaf ber-prereq belum terpenuhi: tetap pause | | **X leaf / non-leaf** | restart penuh: leaf di-flip `APPROVED`→`PENDING`, non-leaf `WAITING_APPROVAL`, blocker-subtree di-pause | — (release cascade konsumen) | | **Konsumen A (prereq langsung = X)** | seluruh subtree `rollbackPaused = true` (marker only; A yang `CLOSED` tetap `CLOSED`); display `NOT_STARTED` | `WAITING_APPROVAL` tanpa flip (notif VP); release bertahap | | **Item rantai lebih dalam (P, Q, I)** | tidak di-restart; ikut setelah release cascade ketika prereq masing-masing close | — | Data yang **tidak pernah hilang**: seluruh baris progress (termasuk yang `APPROVED`/`APPROVED_WITH_NOTES`), file/lampiran, feedback, plans, dan history activity log. Yang berubah hanya **status** (`status` derived, `approvalStatus`) dan **pointer `rollbackPaused`/`bodResult`** — flip progress memakai mekanisme amends (`amendsId`) sehingga riwayat progress tetap terlihat. --- ## 8. Notifikasi & activity log yang dihasilkan (sekali rollback resolve) **Routing notifikasi (aturan baku, dipakai semua transisi approval):** tujuan penerima ditentukan dari status item setelah transisi (`resolveApprovalNotifyTargets`): | Status item setelah transisi | Penerima | |---|---| | Derived `NEED_APPROVAL` (ada progress `PENDING`, mis. leaf baru di-flip) | **Owner/Admin project** (`resolveActionPlanOwnerAdminUserIds`) | | Derived `COMPLETED` (≥100%) & `approvalStatus = WAITING_APPROVAL` | **VP** (`approverId`) | | Derived `COMPLETED` & `approvalStatus = BOD_APPROVAL` | **BOD** (approvers round `bodRoundId`) | | `rollbackPaused` / `NOT_STARTED` (walau `WAITING_APPROVAL`) | **tidak ada notifikasi** | Satu resolusi ROLLBACK menghasilkan (via outbox in-transaksi; fallback legacy fire-and-forget bila writer tidak ada): | Notifikasi | Penerima (menurut routing di atas) | Isi (ringkas) | |---|---|---| | `sendBodRolledBack` | Owner/Admin + members + VP | *Action Plan Rolled Back by BOD* — `…was rolled back by {bodName}. Its prerequisite {prereqName} was re-opened for re-approval.` | | `sendApprovalReopened` | per node non-paused (X-side: flipped→Owner/Admin, WAITING→VP) | approval dibuka ulang (`WAITING_APPROVAL`) | | `sendProgressReopened` | per leaf yang di-flip (X-side → Owner/Admin) | progress terakhir dibuka ulang (`PENDING`) | | `sendApprovalRequestedReleased` | per release (B-side: flipped/NEED_APPROVAL→Owner/Admin, WAITING→VP; konsumen→VP) | *Action Plan Ready for Approval* — `…Its prerequisite {prereqName} was rolled back and has been re-approved and closed.` | > B-side **tidak menimbulkan notifikasi apa pun saat resolve** — seluruh subtree B pause (NOT_STARTED). Notifikasi untuk B baru muncul ketika X `CLOSED` lagi (tahap release, §6c). Activity log (namespace `action_plan`, `GET /api/projects/:projectId/activity`) — **satu baris per aksi, per item terdampak**: | Kategori | Saat | |---|---| | `ACTION_PLAN_BOD_DECISION` | setiap vote BOD (termasuk yang masih pending/conflict) | | `ACTION_PLAN_BOD_ROLLBACK` | resolusi rollback (item B; `prerequisiteName`) | | `ACTION_PLAN_ROLLBACK_REOPENED` | **per node** yang di-restart (subtree X, termasuk paused) | | `ACTION_PLAN_ROLLBACK_PAUSED` | **per node** yang di-hold (subtree B saat resolve + konsumen) | | `ACTION_PLAN_ROLLBACK_RELEASED` | **per node** yang release saat prereq closes (termasuk flip leaf B) | --- ## 9. Contoh payload (English, sesuai API) ### BOD vote (request) ```json POST /api/v1/projects/:projectId/action-plans/:activityId/approval/bod { "decision": "ROLLBACK", "reason": "Volume pekerjaan tidak sesuai progres approved", "rollbackId": "activity-x-id" } ``` ### Item detail (response `data` — potongan field BOD) ```json { "id": "activity-b-id", "approvalStatus": "WAITING_APPROVAL", "approvalReason": null, "bodRoundId": "round-1", "bodResult": "REJECTED_ROLLBACK", "bodReason": "Volume pekerjaan tidak sesuai progres approved", "bodDisplayStatus": "NOT_STARTED", "rollbackPaused": true, "approverId": "user-vp-id", "bodApprovals": [ { "userId": "bod-1", "name": "Hendra", "decision": "ROLLBACK", "reason": "Volume pekerjaan tidak sesuai progres approved", "rollbackItemId": "activity-x-id", "rollbackTargetName": "Perizinan", "respondedAt": "2026-09-10T03:00:00.000Z" } ], "rollbackOptions": [{ "id": "activity-x-id", "name": "Perizinan" }] } ``` > Keterangan: `bodDisplayStatus` = `NOT_STARTED` bila `bodResult = REJECTED_ROLLBACK` **atau** `rollbackPaused = true`; `REJECTED_BY_BOD` saat REVISE; `BOD_APPROVAL` saat masih ditunggu vote. `rollbackOptions` terkunci ke 1 target setelah BOD pertama vote ROLLBACK. --- ## 10. Batas & kasus yang masih menunggu desain 1. **ROLLBACK_CONFLICT** (target berbeda dari beberapa BOD): implementasi defensif — round tetap `ACTIVE`, tidak ada write. Bukan jalur normal karena one-target lock. 2. Konsumen yang **belum `CLOSED`** pada saat rollback: di-hold marker, lanjut dari status terakhir ketika prereq close (`m0349` — "ke tahan aja gitu? Iya mas. Jadi lanjutin status terakhir"). 3. `request*BodReApproval`, subtask `PUT …/plans`, subtask member add/remove belum dirangkul guard hold (lihat §6b). --- *Dokumen ini menggambarkan perilaku implementasi terkini (rework B-side no-flip + routing notifikasi — pasca commit `489ce94`, belum di-commit). Endpoint & payload tetap bahasa Inggris; ulasan naratif bahasa Indonesia.*