Last active 4 days ago

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