135dika revised this gist 4 days ago. Go to revision
1 file changed, 273 insertions
FLOW_BOD_REJECT_ROLLBACK.md(file created)
| @@ -0,0 +1,273 @@ | |||
| 1 | + | # Flow BOD Reject — Rollback (Action Plans) | |
| 2 | + | ||
| 3 | + | 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. | |
| 4 | + | ||
| 5 | + | 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). | |
| 6 | + | ||
| 7 | + | --- | |
| 8 | + | ||
| 9 | + | ## 1. Ikhtisar | |
| 10 | + | ||
| 11 | + | Rollback dipicu saat **semua BOD yang ditugaskan sudah memberikan vote** dan agregasi menghasilkan **ROLLBACK** (lihat §2). Kronologi: | |
| 12 | + | ||
| 13 | + | 1. Item yang di-escalate (sebut **B**) di-rollback ke salah satu **prerequisite langsung** yang dipilih BOD (sebut **X**). | |
| 14 | + | 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). | |
| 15 | + | 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. | |
| 16 | + | 4. **Konsumen downstream** (item same-level lain yang prerequisite-nya = X) ikut di-hold — lihat §6. | |
| 17 | + | 5. Semua perubahan ditulis **dalam satu transaksi database**; setiap item terdampak mendapat baris **activity log** dan (bila aktif) **notifikasi**. | |
| 18 | + | 6. Riwayat selesai ketika rantai prerequisite di-approve ulang berurutan sampai B di-approve/di-close lagi oleh VP & BOD. | |
| 19 | + | ||
| 20 | + | > 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. | |
| 21 | + | ||
| 22 | + | --- | |
| 23 | + | ||
| 24 | + | ## 2. Agregasi vote & prerequisitenya | |
| 25 | + | ||
| 26 | + | BOD vote lewat `POST …/approval/bod`: | |
| 27 | + | ||
| 28 | + | ```json | |
| 29 | + | // Request body (English, sesuai API) | |
| 30 | + | { | |
| 31 | + | "decision": "ROLLBACK", // "APPROVE" | "REVISE" | "ROLLBACK" | |
| 32 | + | "reason": "pekerjaan tidak sesuai spesifikasi", // wajib, min 1 karakter | |
| 33 | + | "rollbackId": "uuid-x" // wajib saat ROLLBACK; harus prereq LANGSUNG item & di-mark | |
| 34 | + | } | |
| 35 | + | ``` | |
| 36 | + | ||
| 37 | + | Aturan: | |
| 38 | + | ||
| 39 | + | - **Precedence hasil akhir** (frozen): `ROLLBACK > REVISE > APPROVE`. | |
| 40 | + | - **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`. | |
| 41 | + | - **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. | |
| 42 | + | - Semua vote `REVISE` → item `WAITING_APPROVAL` + `bodResult = REJECTED_REVISE`, data/progress **tidak disentuh**; VP harus meng-approve ulang (untuk merevisi, pendekatan re-approve VP). | |
| 43 | + | - Semua `APPROVE` (tanpa ROLLBACK/REVISE) → item `CLOSED` (handover) + map rapikan. | |
| 44 | + | ||
| 45 | + | Hasil agregasi ROLLBACK single-target: | |
| 46 | + | ||
| 47 | + | | Nilai item B | Nilai round | | |
| 48 | + | |---|---| | |
| 49 | + | | `approvalStatus = WAITING_APPROVAL` | `status = CLOSED` | | |
| 50 | + | | `bodResult = REJECTED_ROLLBACK` | `result = REJECTED_ROLLBACK` | | |
| 51 | + | | `bodReason = reason` (vote ROLLBACK pertama) | `reason` dari voter pertama | | |
| 52 | + | ||
| 53 | + | --- | |
| 54 | + | ||
| 55 | + | ## 3. Apa yang terjadi pada **B** (item yang di-rollback / direject) | |
| 56 | + | ||
| 57 | + | Subtree B **tidak di-flip saat resolve**. Semua node live dalam subtree B (root + descendants) di-set lewat `pauseSubtreeForRollback(tx, scope, B)`: | |
| 58 | + | ||
| 59 | + | - `approvalStatus = WAITING_APPROVAL` | |
| 60 | + | - `rollbackPaused = true` → tampil `NOT_STARTED` (override display) | |
| 61 | + | - `bodResult = REJECTED_ROLLBACK` | |
| 62 | + | - `approvalReason = null` | |
| 63 | + | - **Data progress TIDAK diubah** — baris progress tetap 100% `APPROVED` (nilai/approvedBy/approvedAt utuh) | |
| 64 | + | - **Tidak ada notifikasi** pada tahap ini (NOT_STARTED + WAITING ⇒ tanpa notif; VP "tidak bisa ngapa-ngapain" sebelum X selesai) | |
| 65 | + | ||
| 66 | + | 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. | |
| 67 | + | ||
| 68 | + | Kronologi B dalam dua tahap (leaf): | |
| 69 | + | ||
| 70 | + | | Tahap | Status B | | |
| 71 | + | |---|---| | |
| 72 | + | | T1 resolve rollback | `NOT_STARTED` + `WAITING_APPROVAL`, progress tetap 100% `APPROVED` | | |
| 73 | + | | 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`) | | |
| 74 | + | ||
| 75 | + | > 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`. | |
| 76 | + | ||
| 77 | + | --- | |
| 78 | + | ||
| 79 | + | ## 4. Apa yang terjadi pada **X** (prereq yang dipilih sebagai opsi rollback) | |
| 80 | + | ||
| 81 | + | Target X juga di-restart penuh, dengan fungsi yang sama: `restartRollbackSubtree(tx, scope, X)`. | |
| 82 | + | ||
| 83 | + | - X non-leaf → `WAITING_APPROVAL`, progress miliknya tidak diubah; subtree X di-traversal sama seperti §3b. | |
| 84 | + | - X leaf & semua prereq X terpenuhi → progress terakhir → `PENDING`, X → `WAITING_APPROVAL`. | |
| 85 | + | - X leaf & prereq X belum terpenuhi → `rollbackPaused = true` saja. | |
| 86 | + | ||
| 87 | + | 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). | |
| 88 | + | ||
| 89 | + | --- | |
| 90 | + | ||
| 91 | + | ## 5. Posisi X dalam rantai prerequisite (awal / tengah / akhir) | |
| 92 | + | ||
| 93 | + | Rantai contoh: `… → W → X → A → P → Q → I` (masing-masing `→` = prerequisite langsung; item di kiri adalah prereq item di kanan). | |
| 94 | + | ||
| 95 | + | ### 5a. X berada di **paling akhir rantai** (X tidak punya prereq lagi; X adalah "akar" dependensi) | |
| 96 | + | ||
| 97 | + | - Restart X + subtree X sesuai §4; subtree B **ditahan** (pause marker) sesuai §3. | |
| 98 | + | - X bisa langsung dibuka (`WAITING_APPROVAL`/flip) karena tidak menunggu prereq apapun. | |
| 99 | + | - 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. | |
| 100 | + | ||
| 101 | + | ### 5b. X berada di **tengah rantai** (X punya prereq sendiri W, dan punya konsumen A di belakangnya) | |
| 102 | + | ||
| 103 | + | - 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. | |
| 104 | + | - Konsumen A (prereq langsung X) ikut di-hold (marker), P/Q/I menyusul sesuai release cascade; subtree B juga di-hold menunggu X. | |
| 105 | + | - Efek berantai: status ulang dimulai dari W (atau lebih dalam), lalu X, lalu A→P→Q→I (dan B). | |
| 106 | + | ||
| 107 | + | ### 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) | |
| 108 | + | ||
| 109 | + | Anggap rantai: `W → X → A` dengan X punya prereq W. | |
| 110 | + | ||
| 111 | + | - Sama seperti §5b: pembukaan X menunggu W. Konsumen A & rantainya di-hold. | |
| 112 | + | - Karena X bukan akar dependensi, **efeknya tidak bisa "instan"** — semuanya menunggu W di-approve & close dulu, baru X, baru A, dst. | |
| 113 | + | ||
| 114 | + | ### Prinsip umum | |
| 115 | + | ||
| 116 | + | 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. | |
| 117 | + | ||
| 118 | + | --- | |
| 119 | + | ||
| 120 | + | ## 6. Konsumen downstream & release bertahap | |
| 121 | + | ||
| 122 | + | ### 6a. Marker saat rollback resolve (`markRollbackDownstreamConsumers`) | |
| 123 | + | ||
| 124 | + | Untuk setiap item same-level **yang prereq LANGSUNG-nya = X** (selain B): | |
| 125 | + | ||
| 126 | + | - 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. | |
| 127 | + | - Eksklusi (defensif): item B, item X, dan subtree live keduanya. | |
| 128 | + | - Display: item & subtree itu tampil `NOT_STARTED` (override `rollbackPaused` → `NOT_STARTED`), walau data sebenarnya utuh. | |
| 129 | + | - Setiap node yang di-mark masuk activity log `ACTION_PLAN_ROLLBACK_PAUSED`. | |
| 130 | + | ||
| 131 | + | ### 6b. Hold (read-only) | |
| 132 | + | ||
| 133 | + | Item yang sedang `rollbackPaused` (di dirinya atau ancestor) **tidak bisa diubah** oleh endpoint write: | |
| 134 | + | ||
| 135 | + | ``` | |
| 136 | + | 400 <Activity|Task|Subtask> is on hold: waiting for its prerequisite to be approved and closed | |
| 137 | + | ``` | |
| 138 | + | ||
| 139 | + | 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`). | |
| 140 | + | ||
| 141 | + | > Catatan implementasi: `request*BodReApproval` (langsung), `subtask …/plans PUT`, dan subtask member add/remove belum ter-guard langsung (inner plain re-request tetap ter-guard). | |
| 142 | + | ||
| 143 | + | ### 6c. Release (`releaseRollbackPausedDependents`) — dipicu saat item `CLOSED` | |
| 144 | + | ||
| 145 | + | Dipanggil **di dalam transaksi** tepat setelah item di-approve → `CLOSED`, pada 6 titik: VP `respond…Approval` (APPROVE) dan BOD all-approve (APPROVED). | |
| 146 | + | ||
| 147 | + | - Cari semua dependents **yang prereq langsung-nya = item yang baru close** DAN sedang `rollbackPaused` DAN semua prereq-nya terpenuhi. | |
| 148 | + | - 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. | |
| 149 | + | - 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**: | |
| 150 | + | - Node akar B leaf → `WAITING_APPROVAL` + unpause tanpa flip (Completed & Waiting; notif ke **VP**; flip progress baru terjadi bila VP REJECT). | |
| 151 | + | - Node non-leaf → `WAITING_APPROVAL` + unpause tanpa flip (notif ke **VP**), lalu anaknya di-reprocess: | |
| 152 | + | - **Leaf rantai-awal** (tidak punya prereq live / prereq rantai paling awal) → flip `APPROVED`→`PENDING` + `WAITING_APPROVAL` + unpause (Need Approval; notif ke **Owner/Admin**). | |
| 153 | + | - **Leaf yang masih punya prereq belum terpenuhi** → tetap pause (`rollbackPaused = true`), tanpa flip, tanpa notif — terbuka nanti saat prereqnya selesai. | |
| 154 | + | - **Non-leaf** → `WAITING_APPROVAL` + unpause, lalu turun ke anaknya (notif ke VP). | |
| 155 | + | - **Blocker child** (dipakai sebagai prereq oleh sibling live) → tetap pause tanpa flip/notif. | |
| 156 | + | - Mirip restart X non-leaf, tetapi **dimulai belakangan**: baru berlangsung setelah X `CLOSED`, bukan saat resolve. | |
| 157 | + | - **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. | |
| 158 | + | - 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). | |
| 159 | + | ||
| 160 | + | Urutan release pada rantai `X → A → P → Q → I` (dan B di sisi lain): | |
| 161 | + | ||
| 162 | + | ``` | |
| 163 | + | [X closed] → A → WAITING_APPROVAL + subtree B mulai di-restart (leaf rantai-awal → PENDING) | |
| 164 | + | [A approved]→ A → CLOSED → P → WAITING_APPROVAL | |
| 165 | + | [P approved]→ P → CLOSED → Q → WAITING_APPROVAL | |
| 166 | + | [Q approved]→ Q → CLOSED → I → WAITING_APPROVAL | |
| 167 | + | [I approved]→ I → CLOSED (rantai pulih penuh) | |
| 168 | + | ``` | |
| 169 | + | ||
| 170 | + | --- | |
| 171 | + | ||
| 172 | + | ## 7. Leaf vs children — ringkas | |
| 173 | + | ||
| 174 | + | | Bentuk item | Saat rollback resolve | Saat X re-`CLOSED` | | |
| 175 | + | |---|---|---| | |
| 176 | + | | **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 | | |
| 177 | + | | **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 | | |
| 178 | + | | **X leaf / non-leaf** | restart penuh: leaf di-flip `APPROVED`→`PENDING`, non-leaf `WAITING_APPROVAL`, blocker-subtree di-pause | — (release cascade konsumen) | | |
| 179 | + | | **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 | | |
| 180 | + | | **Item rantai lebih dalam (P, Q, I)** | tidak di-restart; ikut setelah release cascade ketika prereq masing-masing close | — | | |
| 181 | + | ||
| 182 | + | 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. | |
| 183 | + | ||
| 184 | + | --- | |
| 185 | + | ||
| 186 | + | ## 8. Notifikasi & activity log yang dihasilkan (sekali rollback resolve) | |
| 187 | + | ||
| 188 | + | **Routing notifikasi (aturan baku, dipakai semua transisi approval):** tujuan penerima ditentukan dari status item setelah transisi (`resolveApprovalNotifyTargets`): | |
| 189 | + | ||
| 190 | + | | Status item setelah transisi | Penerima | | |
| 191 | + | |---|---| | |
| 192 | + | | Derived `NEED_APPROVAL` (ada progress `PENDING`, mis. leaf baru di-flip) | **Owner/Admin project** (`resolveActionPlanOwnerAdminUserIds`) | | |
| 193 | + | | Derived `COMPLETED` (≥100%) & `approvalStatus = WAITING_APPROVAL` | **VP** (`approverId`) | | |
| 194 | + | | Derived `COMPLETED` & `approvalStatus = BOD_APPROVAL` | **BOD** (approvers round `bodRoundId`) | | |
| 195 | + | | `rollbackPaused` / `NOT_STARTED` (walau `WAITING_APPROVAL`) | **tidak ada notifikasi** | | |
| 196 | + | ||
| 197 | + | Satu resolusi ROLLBACK menghasilkan (via outbox in-transaksi; fallback legacy fire-and-forget bila writer tidak ada): | |
| 198 | + | ||
| 199 | + | | Notifikasi | Penerima (menurut routing di atas) | Isi (ringkas) | | |
| 200 | + | |---|---|---| | |
| 201 | + | | `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.` | | |
| 202 | + | | `sendApprovalReopened` | per node non-paused (X-side: flipped→Owner/Admin, WAITING→VP) | approval dibuka ulang (`WAITING_APPROVAL`) | | |
| 203 | + | | `sendProgressReopened` | per leaf yang di-flip (X-side → Owner/Admin) | progress terakhir dibuka ulang (`PENDING`) | | |
| 204 | + | | `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.` | | |
| 205 | + | ||
| 206 | + | > 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). | |
| 207 | + | ||
| 208 | + | Activity log (namespace `action_plan`, `GET /api/projects/:projectId/activity`) — **satu baris per aksi, per item terdampak**: | |
| 209 | + | ||
| 210 | + | | Kategori | Saat | | |
| 211 | + | |---|---| | |
| 212 | + | | `ACTION_PLAN_BOD_DECISION` | setiap vote BOD (termasuk yang masih pending/conflict) | | |
| 213 | + | | `ACTION_PLAN_BOD_ROLLBACK` | resolusi rollback (item B; `prerequisiteName`) | | |
| 214 | + | | `ACTION_PLAN_ROLLBACK_REOPENED` | **per node** yang di-restart (subtree X, termasuk paused) | | |
| 215 | + | | `ACTION_PLAN_ROLLBACK_PAUSED` | **per node** yang di-hold (subtree B saat resolve + konsumen) | | |
| 216 | + | | `ACTION_PLAN_ROLLBACK_RELEASED` | **per node** yang release saat prereq closes (termasuk flip leaf B) | | |
| 217 | + | ||
| 218 | + | --- | |
| 219 | + | ||
| 220 | + | ## 9. Contoh payload (English, sesuai API) | |
| 221 | + | ||
| 222 | + | ### BOD vote (request) | |
| 223 | + | ||
| 224 | + | ```json | |
| 225 | + | POST /api/v1/projects/:projectId/action-plans/:activityId/approval/bod | |
| 226 | + | { | |
| 227 | + | "decision": "ROLLBACK", | |
| 228 | + | "reason": "Volume pekerjaan tidak sesuai progres approved", | |
| 229 | + | "rollbackId": "activity-x-id" | |
| 230 | + | } | |
| 231 | + | ``` | |
| 232 | + | ||
| 233 | + | ### Item detail (response `data` — potongan field BOD) | |
| 234 | + | ||
| 235 | + | ```json | |
| 236 | + | { | |
| 237 | + | "id": "activity-b-id", | |
| 238 | + | "approvalStatus": "WAITING_APPROVAL", | |
| 239 | + | "approvalReason": null, | |
| 240 | + | "bodRoundId": "round-1", | |
| 241 | + | "bodResult": "REJECTED_ROLLBACK", | |
| 242 | + | "bodReason": "Volume pekerjaan tidak sesuai progres approved", | |
| 243 | + | "bodDisplayStatus": "NOT_STARTED", | |
| 244 | + | "rollbackPaused": true, | |
| 245 | + | "approverId": "user-vp-id", | |
| 246 | + | "bodApprovals": [ | |
| 247 | + | { | |
| 248 | + | "userId": "bod-1", | |
| 249 | + | "name": "Hendra", | |
| 250 | + | "decision": "ROLLBACK", | |
| 251 | + | "reason": "Volume pekerjaan tidak sesuai progres approved", | |
| 252 | + | "rollbackItemId": "activity-x-id", | |
| 253 | + | "rollbackTargetName": "Perizinan", | |
| 254 | + | "respondedAt": "2026-09-10T03:00:00.000Z" | |
| 255 | + | } | |
| 256 | + | ], | |
| 257 | + | "rollbackOptions": [{ "id": "activity-x-id", "name": "Perizinan" }] | |
| 258 | + | } | |
| 259 | + | ``` | |
| 260 | + | ||
| 261 | + | > 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. | |
| 262 | + | ||
| 263 | + | --- | |
| 264 | + | ||
| 265 | + | ## 10. Batas & kasus yang masih menunggu desain | |
| 266 | + | ||
| 267 | + | 1. **ROLLBACK_CONFLICT** (target berbeda dari beberapa BOD): implementasi defensif — round tetap `ACTIVE`, tidak ada write. Bukan jalur normal karena one-target lock. | |
| 268 | + | 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"). | |
| 269 | + | 3. `request*BodReApproval`, subtask `PUT …/plans`, subtask member add/remove belum dirangkul guard hold (lihat §6b). | |
| 270 | + | ||
| 271 | + | --- | |
| 272 | + | ||
| 273 | + | *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.* | |
Newer
Older