Last active 4 days ago

Revision 7fdb32c1e7c81c963474eeef80cf7b34685a6322

FLOW_BOD_REJECT_ROLLBACK.md Raw

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:

// 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 APPROVEDPENDING (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 rollbackPausedNOT_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 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 APPROVEDPENDING + 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-leafWAITING_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 APPROVEDPENDING (Need Approval, notif Owner/Admin); leaf ber-prereq belum terpenuhi: tetap pause
X leaf / non-leaf restart penuh: leaf di-flip APPROVEDPENDING, 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)

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_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.