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:
- Item yang di-escalate (sebut B) di-rollback ke salah satu prerequisite langsung yang dipilih BOD (sebut X).
- Subtree penuh X (target) di-restart: progress terakhir leaf dijadikan
PENDINGdan status approval dikembalikan keWAITING_APPROVAL— sesuai posisi masing-masing node (leaf / non-leaf / terhalang prerequisite). - 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), tampilNOT_STARTED. B baru "diproses ulang" ketika X di-approve &CLOSEDlagi (lihat §6c) — node leaf rantai awal di-flip kePENDINGsaat itu. - Konsumen downstream (item same-level lain yang prerequisite-nya = X) ikut di-hold — lihat §6.
- Semua perubahan ditulis dalam satu transaksi database; setiap item terdampak mendapat baris activity log dan (bila aktif) notifikasi.
- 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:
// 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
ROLLBACKke 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 tetapBOD_APPROVAL, tidak ada satu pun write — menunggu desain. Hanya activity logACTION_PLAN_BOD_DECISIONyang muncul. - Semua vote
REVISE→ itemWAITING_APPROVAL+bodResult = REJECTED_REVISE, data/progress tidak disentuh; VP harus meng-approve ulang (untuk merevisi, pendekatan re-approve VP). - Semua
APPROVE(tanpa ROLLBACK/REVISE) → itemCLOSED(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_APPROVALrollbackPaused = true→ tampilNOT_STARTED(override display)bodResult = REJECTED_ROLLBACKapprovalReason = 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). BarisAPPROVED_WITH_NOTEStidak di-flip — berbeda dengan VP REJECT yang juga membukaAPPROVED_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 = truesaja.
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
CLOSEDlagi (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 jadirollbackPaused = 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 perubahanapprovalStatus(konsumen yang sudahCLOSEDtetapCLOSED), 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(overriderollbackPaused→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 <Activity|Task|Subtask> 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
rollbackPausedDAN 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.
- Leaf rantai-awal (tidak punya prereq live / prereq rantai paling awal) → flip
- Mirip restart X non-leaf, tetapi dimulai belakangan: baru berlangsung setelah X
CLOSED, bukan saat resolve.
- Node akar B leaf →
- 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
CLOSEDlagi (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)
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)
{
"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_STARTEDbilabodResult = REJECTED_ROLLBACKataurollbackPaused = true;REJECTED_BY_BODsaat REVISE;BOD_APPROVALsaat masih ditunggu vote.rollbackOptionsterkunci ke 1 target setelah BOD pertama vote ROLLBACK.
10. Batas & kasus yang masih menunggu desain
- ROLLBACK_CONFLICT (target berbeda dari beberapa BOD): implementasi defensif — round tetap
ACTIVE, tidak ada write. Bukan jalur normal karena one-target lock. - Konsumen yang belum
CLOSEDpada saat rollback: di-hold marker, lanjut dari status terakhir ketika prereq close (m0349— "ke tahan aja gitu? Iya mas. Jadi lanjutin status terakhir"). request*BodReApproval, subtaskPUT …/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.