135dika / action-plan.docs.md

Last active 2 months ago

Like 0

135dika revised this gist 2 months ago · 30fdb25

1 file changed, 272 insertions, 186 deletions

action-plan.docs.md

Diff is too large to be shown

135dika revised this gist 2 months ago · 9725535

1 file changed, 186 insertions, 272 deletions

action-plan.docs.md

Diff is too large to be shown

135dika revised this gist 2 months ago · 6ce3e5b

1 file changed, 28 insertions

action-plan.docs.md
@@ -11,6 +11,7 @@ Action plan module: **Activity → Task → Subtask** hierarchy with progress en
11 11 - `INCOMPLETE_DATA` — not accessible.
12 12 - `NOT_STARTED` / `IN_PROGRESS` — full access.
13 13 - `NEED_APPROVAL` / `COMPLETED` / `REJECTED` — read-only for management writes; approval & BOD routes **stay writable**.
14 + - **VP/BOD read visibility** — a member assigned as VP (any item `approverId`) or as BOD (`ACTIVE` round approver) gets **full read** of tree/detail regardless of project tier or `ActionPlanMembers` membership. `MEMBER`-only users without any VP/BOD assignment see only their `ActionPlanMembers` scopes (others pruned).
14 15 - **Progress input** (submit/respond/feedback) requires project `IN_PROGRESS` (`ProjectInProgressGuard`).
15 16
16 17 ## Status enums
@@ -329,3 +330,30 @@ List rows include `requestedData`, `oldData`, `reason`, `status`, requester/resp
329 330 - REVISE → VP: **Action Plan Rejected by BOD** — `{ScopeLabel} {scopeName} in {projectName} was rejected by {bodName}. You must make decision` (+ ` Note: {reason}`).
330 331 - ROLLBACK → VP: **Action Plan Rolled Back by BOD** — `…was rolled back by {bodName}. Its prerequisite {prereqName} was re-opened for re-approval.` (+ note).
331 332 - All-APPROVE → VP + admins + members: **Action Plan Approved by BOD** — `…was approved by BOD. Handover complete.` (+ note).
333 + - Downstream consumer released (prerequisite re-closed) → VP: **Action Plan Ready for Approval** — `{ScopeLabel} {scopeName} in {projectName} is now ready for your approval. Its prerequisite {prereqName} was rolled back and has been re-approved and closed.` (+ ` Note: {reason}`). Sent per item as the prerequisite chain re-closes (A → then P/Q → then I…).
334 +
335 + ## Downstream rollback consumers
336 +
337 + When a rollback resolves (B → target X), **all same-level items whose direct prerequisite is X** (and their full subtrees) are affected:
338 +
339 + | State | Effect |
340 + |---|---|
341 + | Item **not yet closed** | `rollbackPaused = true` marker only — data/progress/approvalStatus **unchanged**; display forced `NOT_STARTED`; **on hold** while X re-opens. All writes blocked 400: approval respond/re-request/BOD → `400 <Scope> is on hold: waiting for its prerequisite to be approved and closed`; also update, progress submit/respond, plans, members. |
342 + | Item **CLOSED** | node → `WAITING_APPROVAL` (no progress flip, `approvalReason` nulled); its children display `NOT_STARTED` (marker) until the chain resumes. |
343 + | Release | when X is re-approved & `CLOSED`, each held consumer releases in chain order (A after X closes; A's children after A closes — no progress flip, just `WAITING_APPROVAL`) + *Action Plan Ready for Approval* per item. |
344 +
345 + ## Activity log (`namespace: action_plan`)
346 +
347 + Every BOD/approval interaction writes an activity-log row (`GET /api/projects/:projectId/activity`, `recordId` = the affected item id):
348 +
349 + | Category | When |
350 + |---|---|
351 + | `ACTION_PLAN_APPROVAL_APPROVED` / `_REJECTED` | VP approve / reject |
352 + | `ACTION_PLAN_BOD_ESCALATED` | VP escalates to BOD (`approverCount`, `roundId`) |
353 + | `ACTION_PLAN_BOD_DECISION` | **each** BOD vote — `{decision, actor, reason, roundId}`, incl. votes that leave the round pending/conflicted |
354 + | `ACTION_PLAN_BOD_APPROVED` / `_REVISED` / `_ROLLBACK` | terminal round resolution (revise copy: *must be re-approved by VP*; rollback: `prerequisiteName`) |
355 + | `ACTION_PLAN_ROLLBACK_REOPENED` | **one row per node** restarted by rollback (B subtree + target X subtree): *re-opened for re-approval due to rollback by BOD* |
356 + | `ACTION_PLAN_ROLLBACK_PAUSED` | **one row per held downstream consumer**: *on hold — waiting for prerequisite … (rollback effect)* |
357 + | `ACTION_PLAN_ROLLBACK_RELEASED` | **one row per consumer** released after the prerequisite re-closes: *ready for re-approval — prerequisite … re-approved and closed (rollback effect)* |
358 +
359 + The per-node rows make the multi-item state changes explainable: why each item restarted, paused, or resumed is visible in its own history.

135dika revised this gist 2 months ago · a02a26c

1 file changed, 49 insertions, 2 deletions

action-plan.docs.md
@@ -23,6 +23,8 @@ Action plan module: **Activity → Task → Subtask** hierarchy with progress en
23 23
24 24 `WAITING_APPROVAL` · `CLOSED` · `REJECTED` · `BOD_APPROVAL` (escalated)
25 25
26 + > Item without `approverId` (approval optional) never enters this enum — `approvalStatus` stays `null` (§ Approval optional).
27 +
26 28 ### `bodResult`
27 29
28 30 `REJECTED_REVISE` · `REJECTED_ROLLBACK`
@@ -53,6 +55,34 @@ Action plan module: **Activity → Task → Subtask** hierarchy with progress en
53 55 5. **VP REJECT**: walks descendants level by level; startable leaves reopen (`NEED_APPROVAL`, last `APPROVED`→`PENDING`), leaves blocked by unmet live prerequisite pause (`rollbackPaused`) and auto-resume when the prerequisite closes (`releaseRollbackPausedDependents`).
54 56 6. **Delete guard**: progress ≥ 100% (derived `COMPLETED`) or `approvalStatus = CLOSED` → delete blocked 400 (also blocks cascade delete of a complete/closed descendant).
55 57
58 + ## Approval optional (derived from `approverId`)
59 +
60 + VP approval is **optional per item**, derived from `approverId` itself (no separate flag):
61 +
62 + - Item **WITH** `approverId` → approval **mandatory**: the full VP/BOD cycle applies — auto `WAITING_APPROVAL` on completion, VP respond, BOD escalation, Closed chip.
63 + - Item **WITHOUT** `approverId` (`null`) → approval **optional**: effective progress ≥ 100 completes the item with `approvalStatus` staying `null` — no `WAITING_APPROVAL`, no *approval-requested* notification, no Closed chip, no BOD possible.
64 +
65 + **Assign-VP rule** (PATCH `approverId`) once the item's effective progress has reached ≥ 100 %:
66 +
67 + | Change | Result |
68 + |---|---|
69 + | assign (`null` → person) | **400** |
70 + | remove (person → `null`) | **400** |
71 + | replace (person → person) | **allowed — pure swap**: only `approverId` changes; `approvalStatus` / `approvalReason` / `approvedAt` / `bodResult` / `bodReason` / `bodRoundId` / `rollbackPaused` all untouched, no reopen, no late-assignment notification |
72 +
73 + Below 100 % the pre-existing assignment semantics are unchanged (assignment never mutates `approvalStatus` on an incomplete item).
74 +
75 + **Prerequisite rule**:
76 +
77 + - VP-less prerequisite (`approverId = null`) → satisfied by effective progress ≥ 100 **alone** (no `CLOSED` needed).
78 + - VP-held prerequisite → still requires `CLOSED` **and** effective progress ≥ 100.
79 +
80 + **Parent completeness** — a child counts toward the parent's `WAITING_APPROVAL` trigger ("done") when `child.approvalStatus = CLOSED` **or** (child has no VP **and** child's approved rollup ≥ 100).
81 +
82 + **All approval endpoints on a VP-less item return 400**: `…/approval/respond` (incl. `ESCALATE`), `…/approval/re-request`, `…/approval/bod`, `…/approval/bod/re-request`.
83 +
84 + **Existing-data migration** (applied mid-feature): prior rows with non-null `approvalStatus` were set to `CLOSED`, and ACTIVE BOD rounds were closed.
85 +
56 86 ---
57 87
58 88 # Endpoint catalog
@@ -110,6 +140,8 @@ Detail — item payload (§ Item payload).
110 140 ### `PATCH /:activityId`
111 141 Body (`UpdateActivityDto`) — all ✳️: `name`, `activityType`, `approverId`, `workWeight`, `workVolume`, `workVolumeUnit` (*requires workVolume pair rule*), `startDate`, `endDate`, `prerequisiteIds[]`.
112 142
143 + > `approverId`: assign / remove blocked 400 once complete (≥ 100 %); replace = pure swap (§ Approval optional).
144 +
113 145 ### `DELETE /:activityId`
114 146 No body. **400** if item or live descendant complete (`≥100%`) or `CLOSED`.
115 147
@@ -151,6 +183,21 @@ Guards: item `BOD_APPROVAL`, caller is assigned BOD with no vote yet, round not
151 183 ### `POST …/approval/bod/re-request` — VP resumes BOD cycle after REVISE
152 184 No body.
153 185
186 + ### `GET /approval-obligations` — pre-member-removal check (project level)
187 + Query:
188 + | Param | Req | Notes |
189 + |---|---|---|
190 + | `userId` | ✅ | uuid — the member whose action-plan responsibilities are checked |
191 +
192 + Purpose: list the user's outstanding approval responsibilities in the project before member removal (removal itself lives outside this module). → `{ "hasObligation": bool, "obligations": […] }`:
193 +
194 + - **VP obligations** — every item where the user is the VP, **all states including `CLOSED`**:
195 + `{ "type": "VP", "scope": "ACTIVITY" | "TASK" | "SUBTASK", "itemId", "itemName" }`
196 + - **BOD obligations** — every `ACTIVE` BOD round the user is an assigned approver, **including rounds they already voted on**:
197 + `{ "type": "BOD", "scope": "ACTIVITY" | "TASK" | "SUBTASK", "itemId", "itemName", "roundId", "responded" }` — `responded` = whether the user cast a decision.
198 +
199 + Empty result → `hasObligation: false`, empty `obligations`.
200 +
154 201 ## 3. Item payload (detail/list/tree)
155 202
156 203 BOD-related fields on every item: `approvalStatus`, `approvalReason`, `bodRoundId`, `bodResult`, `bodReason`, `bodDisplayStatus`, `rollbackPaused`, plus `approverId/approverName/approverEmail`, `prerequisites[]`, `prerequisiteIds[]`.
@@ -169,7 +216,7 @@ Detail-only (`GET /A`, `GET A/tasks/T`, `GET A/tasks/T/subtasks/S`):
169 216 | `POST /` | `CreateTaskDto` — `name` ✅; `workWeight/workVolume/workVolumeUnit/startDate/endDate` ✳️ (volume pair rule) |
170 217 | `GET /` | list under activity |
171 218 | `GET /:taskId` | detail (§ Item payload) |
172 - | `PATCH /:taskId` | `UpdateTaskDto` — `name/approverId/workWeight/workVolume/workVolumeUnit/startDate/endDate/prerequisiteIds[]` all ✳️ |
219 + | `PATCH /:taskId` | `UpdateTaskDto` — `name/approverId/workWeight/workVolume/workVolumeUnit/startDate/endDate/prerequisiteIds[]` all ✳️ — `approverId`: assign/remove blocked 400 once complete, replace = pure swap (§ Approval optional) |
173 220 | `DELETE /:taskId` | complete/CLOSED guard 400 |
174 221
175 222 Plus per task: progress (`POST/GET /:taskId/progress`, `GET /:taskId/progress/:progressId`, `POST …/respond`), feedbacks, `GET/PUT /:taskId/plans`, members (`POST/DELETE/GET /:taskId/members`), edit-requests (`POST/GET /:taskId/edit-requests`), approval routes (§ 2).
@@ -181,7 +228,7 @@ Plus per task: progress (`POST/GET /:taskId/progress`, `GET /:taskId/progress/:p
181 228 | `POST /` | `CreateSubtaskDto` — `name` ✅; `workVolume/workVolumeUnit/workWeight/startDate/endDate` ✳️ (volume pair rule) |
182 229 | `GET /` | list under task |
183 230 | `GET /:subtaskId` | detail |
184 - | `PATCH /:subtaskId` | `UpdateSubtaskDto` — all ✳️ incl `approverId`, `prerequisiteIds[]` |
231 + | `PATCH /:subtaskId` | `UpdateSubtaskDto` — all ✳️ incl `approverId`, `prerequisiteIds[]` — `approverId`: assign/remove blocked 400 once complete, replace = pure swap (§ Approval optional) |
185 232 | `DELETE /:subtaskId` | complete/CLOSED guard 400 |
186 233
187 234 Plus: members (`POST/DELETE/GET /:subtaskId/members`), plans (`GET/PUT /:subtaskId/plans`), edit-requests (`POST/GET /:subtaskId/edit-requests`), progress (`POST/GET /:subtaskId/progress`, `GET /:subtaskId/progress/:progressId`, `POST …/respond`), feedbacks (`POST/GET …/feedbacks`, `PATCH/DELETE …/feedbacks/:feedbackId`), approval routes (§ 2).

135dika revised this gist 2 months ago · a5ab435

1 file changed, 199 insertions, 192 deletions

action-plan.docs.md

Diff is too large to be shown

135dika revised this gist 2 months ago · 62e03eb

1 file changed, 5 insertions, 4 deletions

action-plan.docs.md
@@ -60,10 +60,11 @@ Derived from progress rows + approval state. Display overrides applied (see BOD
60 60 1. **VP** (designated approver) responds to an item's approval: `APPROVE` | `REJECT` | `ESCALATE`. `ESCALATE` requires `bodUserIds` (≥1, max 50; each must be a user in the project's program agenda) → approvalStatus `BOD_APPROVAL`, a new `ActionPlanBodRounds` round created, notification sent to each BOD.
61 61 2. **Each assigned BOD** votes on the round: `APPROVE` | `REVISE` | `ROLLBACK` (reason required, 1–500 chars).
62 62 - One **rollback target lock** applies: once a BOD votes `ROLLBACK` to target T, all other BODs may only vote `ROLLBACK` to the same T (a different `rollbackId` → 400). `rollbackId` must be a **direct prerequisite** of the item.
63 - 3. **Resolution** when all assigned BODs have voted:
64 - - Any `REVISE` → REVISE wins (precedence: `REVISE > ROLLBACK > APPROVE`). Item → `WAITING_APPROVAL`, `bodResult = REJECTED_REVISE`, `bodReason` = earliest voter's reason; data & progress rows untouched; VP notified ("Action Plan Rejected by BOD").
65 - - All `ROLLBACK` to same target T → item → `WAITING_APPROVAL`, `bodResult = REJECTED_ROLLBACK`; T's and item's subtrees restart (leaves: last `APPROVED` progress → `PENDING`; each node → `WAITING_APPROVAL`; nodes whose prerequisites aren't met → `rollbackPaused` until the prerequisite closes). VP notified ("Action Plan Rolled Back by BOD").
66 - - All `APPROVE` → item `approvalStatus = CLOSED` (handover); VP + admins + action plan members notified ("Action Plan Approved by BOD").
63 + 3. **Resolution** when all assigned BODs have voted — precedence `ROLLBACK > REVISE > APPROVE`:
64 + - Any/all `ROLLBACK` votes to the same target T → ROLLBACK wins. Item → `WAITING_APPROVAL`, `bodResult = REJECTED_ROLLBACK`, `bodReason` = earliest voter's reason; T's and item's subtrees restart (leaves: last `APPROVED` progress → `PENDING`; each node → `WAITING_APPROVAL`; nodes whose prerequisites aren't met → `rollbackPaused` until the prerequisite closes). VP notified ("Action Plan Rolled Back by BOD").
65 + - Else any `REVISE` → REVISE wins. Item → `WAITING_APPROVAL`, `bodResult = REJECTED_REVISE`, `bodReason` = earliest voter's reason; data & progress rows untouched; VP notified ("Action Plan Rejected by BOD").
66 + - Else (all `APPROVE`) → item `approvalStatus = CLOSED` (handover); VP + admins + action plan members notified ("Action Plan Approved by BOD").
67 + - Different `ROLLBACK` targets across voters → round stays `ACTIVE`, item stays `BOD_APPROVAL`, no writes (defensive; the single-target lock normally prevents this).
67 68 4. **Re-escalation**: VP `ESCALATE` again after a BOD REVISE carries the previous BOD list over; may *add* members, may not remove/replace.
68 69 5. **VP `REJECT`** (non-escalated flow) does not reopen closed children wholesale: it walks descendants level by level (Activity → Tasks → Subtasks). Leaves that are startable (no unmet live prerequisite) reopen → `NEED_APPROVAL`/`PENDING`; leaves blocked by an unmet prerequisite pause with `rollbackPaused = true` and resume automatically when the prerequisite reaches `CLOSED` (`.releaseRollbackPausedDependents`).
69 70 6. **Delete guard**: an item whose effective progress ≥ 100% (derived `COMPLETED`) or `approvalStatus = CLOSED` (VP- or BOD-approved) cannot be deleted — including as a descendant of a parent delete (`400` naming the offending item).

135dika revised this gist 2 months ago · 1fd34e1

1 file changed, 204 insertions, 281 deletions

action-plan.docs.md

Diff is too large to be shown

135dika revised this gist 3 months ago · d0c38dd

1 file changed, 17 insertions, 15 deletions

action-plan.docs.md
@@ -146,20 +146,24 @@ DELETE /.../feedbacks/:fId
146 146
147 147 ## Authorization
148 148
149 - Two-layer authorization at the controller level via guards.
149 + Two-layer authorization at the controller level.
150 150
151 151 ### Guards
152 152
153 153 | Guard | Applies to | Check |
154 154 |---|---|---|
155 + | `AccessControlGuard` via `@Authorize({ membership: true })` | All routes | Token validation + workspace membership via `resolveAccess()` gRPC |
155 156 | `ProjectInProgressGuard` | All routes | Project status must be `IN_PROGRESS` |
156 - | `WorkspaceAccessGuard` | All routes | User must have active workspace membership for this project |
157 157
158 - Guards run before any route handler. Authentication is handled upstream by `AccessControlGuard` (validates Bearer token).
158 + `@Authorize({ membership: true })` runs `AccessControlGuard` which:
159 + 1. Validates bearer token via AuthService gRPC
160 + 2. Resolves scope from `:projectId` route param
161 + 3. Calls `workspaceAccess.resolveAccess()` — checks user has any membership (MEMBER/OWNER/ADMIN) in the workspace/project hierarchy
162 + 4. Stashes validated session so `@CurrentUser()` reuses it without extra gRPC
159 163
160 - ### WorkspaceAccessGuard — How It Works
164 + ### Hierarchy Scope
161 165
162 - Every action-plan request requires the user to have an `active` row in `workspace_member_scopes` for the project's hierarchy. The guard resolves the hierarchy from `:projectId`:
166 + `resolveAccess()` checks membership across the full hierarchy:
163 167
164 168 ```
165 169 AGENDA
@@ -167,25 +171,22 @@ AGENDA
167 171 └─ PROJECT ← projectId from route param
168 172 ```
169 173
170 - User passes if they have at least one matching row with:
171 - - `status = 'active'`
172 - - `role IN (MEMBER, OWNER, ADMIN)`
173 - - One of: `scope_type=PROJECT, scope_id=projectId` OR `scope_type=WORKSPACE` OR `scope_type=PROGRAM` OR `scope_type=AGENDA`
174 + A user with membership at any ancestor level (WORKSPACE/PROGRAM/AGENDA) can access child projects.
174 175
175 - Rejection response — `403 Forbidden`:
176 + ### Rejection Response — `403 Forbidden`:
176 177 ```json
177 - { "statusCode": 403, "message": "You do not have access to this project" }
178 + { "statusCode": 403, "message": "Access denied", "denial": { "type": "...", "details": {...} } }
178 179 ```
179 180
180 181 ### Write Operation — Role Check
181 182
182 - Write operations have an additional inline role check via `validateWorkspaceRole` helper in `action-plans.helpers.ts`.
183 + Write operations have an additional inline role check via `validateWorkspaceRole` helper in `action-plans.helpers.ts`. This queries `workspace_member_scopes` directly to enforce exact role requirements (OWNER/ADMIN for admin ops, MEMBER+ for member ops).
183 184
184 185 ### Operation → Role Mapping
185 186
186 187 | Operation | Required Role |
187 188 |---|---|
188 - | Read (GET any route) | `MEMBER` / `OWNER` / `ADMIN` (via guard) |
189 + | Read (GET any route) | Any workspace member (via guard) |
189 190 | Create activity/task/subtask | `OWNER` / `ADMIN` |
190 191 | Update activity/task/subtask | `OWNER` / `ADMIN` |
191 192 | Delete activity/task/subtask | `OWNER` / `ADMIN` |
@@ -200,7 +201,8 @@ Write operations have an additional inline role check via `validateWorkspaceRole
200 201
201 202 | Status | Message | Cause |
202 203 |---|---|---|
203 - | `403` | `You do not have access to this project` | Not a workspace member (guard) |
204 + | `401` | `Authorization token required` | Missing/invalid bearer token |
205 + | `403` | `Access denied` (with `denial` payload) | Not a workspace member (AccessControlGuard) |
204 206 | `403` | `Action plan is only available for projects with IN_PROGRESS status` | Project status not IN_PROGRESS |
205 207 | `403` | `You do not have the required role to perform this action` | Insufficient role for write op |
206 208 | `400` | `You are not assigned to this scope...` | Not in `actionPlanMembers` (member ops) |
@@ -208,7 +210,7 @@ Write operations have an additional inline role check via `validateWorkspaceRole
208 210 ### Double-Check for Member Operations
209 211
210 212 Member operations (`createProgress`, `createEditRequest`) have two-tier write validation:
211 - 1. **Tier 1** — `validateWorkspaceRole`: user must have active membership in workspace
213 + 1. **Tier 1** — `validateWorkspaceRole`: user must have active workspace membership with required role
212 214 2. **Tier 2** — `validateScopeMember`: user must be assigned to the specific scope via `actionPlanMembers`
213 215
214 216 ---

135dika revised this gist 3 months ago · b10f2aa

1 file changed, 38 insertions, 10 deletions

action-plan.docs.md
@@ -146,27 +146,46 @@ DELETE /.../feedbacks/:fId
146 146
147 147 ## Authorization
148 148
149 - All write operations require the actor to have an active role in `workspace_member_scopes`. Validation via shared helper `validateWorkspaceRole` in `action-plans.helpers.ts`.
149 + Two-layer authorization at the controller level via guards.
150 150
151 - ### Role Hierarchy Scope
151 + ### Guards
152 152
153 - Authorization checks across 4 scope levels — owner/admin at any level can manage child projects:
153 + | Guard | Applies to | Check |
154 + |---|---|---|
155 + | `ProjectInProgressGuard` | All routes | Project status must be `IN_PROGRESS` |
156 + | `WorkspaceAccessGuard` | All routes | User must have active workspace membership for this project |
157 +
158 + Guards run before any route handler. Authentication is handled upstream by `AccessControlGuard` (validates Bearer token).
159 +
160 + ### WorkspaceAccessGuard — How It Works
161 +
162 + Every action-plan request requires the user to have an `active` row in `workspace_member_scopes` for the project's hierarchy. The guard resolves the hierarchy from `:projectId`:
154 163
155 164 ```
156 165 AGENDA
157 166 └─ PROGRAM
158 - └─ PROJECT ← projectId from request
167 + └─ PROJECT ← projectId from route param
168 + ```
169 +
170 + User passes if they have at least one matching row with:
171 + - `status = 'active'`
172 + - `role IN (MEMBER, OWNER, ADMIN)`
173 + - One of: `scope_type=PROJECT, scope_id=projectId` OR `scope_type=WORKSPACE` OR `scope_type=PROGRAM` OR `scope_type=AGENDA`
174 +
175 + Rejection response — `403 Forbidden`:
176 + ```json
177 + { "statusCode": 403, "message": "You do not have access to this project" }
159 178 ```
160 179
161 - A user must have an `active` row in `workspace_member_scopes` with:
162 - - `role` matching the required role set
163 - - `user_id` = actor
164 - - One of: `scope_type=PROJECT, scope_id=projectId` OR `scope_type=WORKSPACE, workspace_id=project.workspace_id` OR `scope_type=PROGRAM, scope_id=project.program_id` OR `scope_type=AGENDA, scope_id=project.program.agenda_id`
180 + ### Write Operation — Role Check
181 +
182 + Write operations have an additional inline role check via `validateWorkspaceRole` helper in `action-plans.helpers.ts`.
165 183
166 184 ### Operation → Role Mapping
167 185
168 186 | Operation | Required Role |
169 187 |---|---|
188 + | Read (GET any route) | `MEMBER` / `OWNER` / `ADMIN` (via guard) |
170 189 | Create activity/task/subtask | `OWNER` / `ADMIN` |
171 190 | Update activity/task/subtask | `OWNER` / `ADMIN` |
172 191 | Delete activity/task/subtask | `OWNER` / `ADMIN` |
@@ -177,9 +196,18 @@ A user must have an `active` row in `workspace_member_scopes` with:
177 196 | Submit progress | `MEMBER` / `OWNER` / `ADMIN` + `actionPlanMembers` check |
178 197 | Create edit request | `MEMBER` / `OWNER` / `ADMIN` + `actionPlanMembers` check |
179 198
199 + ### Rejection Responses
200 +
201 + | Status | Message | Cause |
202 + |---|---|---|
203 + | `403` | `You do not have access to this project` | Not a workspace member (guard) |
204 + | `403` | `Action plan is only available for projects with IN_PROGRESS status` | Project status not IN_PROGRESS |
205 + | `403` | `You do not have the required role to perform this action` | Insufficient role for write op |
206 + | `400` | `You are not assigned to this scope...` | Not in `actionPlanMembers` (member ops) |
207 +
180 208 ### Double-Check for Member Operations
181 209
182 - Member operations (`createProgress`, `createEditRequest`) have two-tier validation:
210 + Member operations (`createProgress`, `createEditRequest`) have two-tier write validation:
183 211 1. **Tier 1** — `validateWorkspaceRole`: user must have active membership in workspace
184 212 2. **Tier 2** — `validateScopeMember`: user must be assigned to the specific scope via `actionPlanMembers`
185 213
@@ -320,4 +348,4 @@ Response shape (all scopes):
320 348 - Subtask in progress → `workWeight`/`workVolume` locked
321 349
322 350 ### Soft Delete
323 - All deletes are soft (`deletedAt`). Cascades: deleting activity → all tasks + subtasks. Deleting task → all subtasks.
351 + All deletes are soft (`deletedAt`). Cascades: deleting activity → all tasks + subtasks. Deleting task → all subtasks.

135dika revised this gist 3 months ago · 771c2a7

1 file changed, 72 insertions, 17 deletions

action-plan.docs.md
@@ -144,26 +144,81 @@ DELETE /.../feedbacks/:fId
144 144
145 145 ---
146 146
147 + ## Authorization
148 +
149 + All write operations require the actor to have an active role in `workspace_member_scopes`. Validation via shared helper `validateWorkspaceRole` in `action-plans.helpers.ts`.
150 +
151 + ### Role Hierarchy Scope
152 +
153 + Authorization checks across 4 scope levels — owner/admin at any level can manage child projects:
154 +
155 + ```
156 + AGENDA
157 + └─ PROGRAM
158 + └─ PROJECT ← projectId from request
159 + ```
160 +
161 + A user must have an `active` row in `workspace_member_scopes` with:
162 + - `role` matching the required role set
163 + - `user_id` = actor
164 + - One of: `scope_type=PROJECT, scope_id=projectId` OR `scope_type=WORKSPACE, workspace_id=project.workspace_id` OR `scope_type=PROGRAM, scope_id=project.program_id` OR `scope_type=AGENDA, scope_id=project.program.agenda_id`
165 +
166 + ### Operation → Role Mapping
167 +
168 + | Operation | Required Role |
169 + |---|---|
170 + | Create activity/task/subtask | `OWNER` / `ADMIN` |
171 + | Update activity/task/subtask | `OWNER` / `ADMIN` |
172 + | Delete activity/task/subtask | `OWNER` / `ADMIN` |
173 + | Set plans | `OWNER` / `ADMIN` |
174 + | Add/remove member (any scope) | `OWNER` / `ADMIN` |
175 + | Respond progress (approve/reject) | `OWNER` / `ADMIN` |
176 + | Respond edit request | `OWNER` / `ADMIN` |
177 + | Submit progress | `MEMBER` / `OWNER` / `ADMIN` + `actionPlanMembers` check |
178 + | Create edit request | `MEMBER` / `OWNER` / `ADMIN` + `actionPlanMembers` check |
179 +
180 + ### Double-Check for Member Operations
181 +
182 + Member operations (`createProgress`, `createEditRequest`) have two-tier validation:
183 + 1. **Tier 1** — `validateWorkspaceRole`: user must have active membership in workspace
184 + 2. **Tier 2** — `validateScopeMember`: user must be assigned to the specific scope via `actionPlanMembers`
185 +
186 + ---
187 +
147 188 ## Notifications
148 189
149 - Fire-and-forget. Does not block operation on failure.
190 + Fire-and-forget. Does not block operation on failure. All events are **bidirectional** — both actor (pelaku) and target (korban) receive the notification for audit trail.
150 191
151 - ### Member Notifications
152 - | Trigger | Type | Recipient |
153 - |---|---|---|
154 - | Add to subtask | `action_plan_member_added` | Added user |
155 - | Remove from subtask | `action_plan_member_removed` | Removed user |
156 - | Add to task | `action_plan_member_added` | Added user |
157 - | Remove from task | `action_plan_member_removed` | Removed user |
158 - | Add to activity | `action_plan_member_added` | Added user |
159 - | Remove from activity | `action_plan_member_removed` | Removed user |
160 -
161 - ### Progress Respond Notifications
162 - | Verb | Type | Recipient |
192 + ### Event Keys
193 +
194 + | Event Key | Category | Description |
163 195 |---|---|---|
164 - | APPROVE | `action_plan_progress_approve` | Submitter |
165 - | APPROVE_WITH_NOTES | `action_plan_progress_approve_with_notes` | Submitter |
166 - | REJECT | `action_plan_progress_reject` | Submitter |
196 + | `action_plan.member.added` | Targeted (actor + target) | Member assigned to scope |
197 + | `action_plan.member.removed` | Targeted (actor + target) | Member removed from scope |
198 + | `action_plan.progress.submitted` | Broadcast (all approvers) | Member submits progress |
199 + | `action_plan.progress.approved` | Targeted (actor + target) | Admin approves progress |
200 + | `action_plan.progress.rejected` | Targeted (actor + target) | Admin rejects progress |
201 + | `action_plan.edit_request.requested` | Broadcast (all approvers) | Member requests edit |
202 + | `action_plan.edit_request.responded` | Targeted (actor + target) | Admin responds to edit request |
203 +
204 + ### Recipient Resolution
205 +
206 + **Broadcast events** (`progress.submitted`, `edit_request.requested`):
207 + Resolve all `OWNER`/`ADMIN` users from `workspace_member_scopes` across 4-level hierarchy (PROJECT → WORKSPACE → PROGRAM → AGENDA). Plus the submitter for audit trail.
208 +
209 + **Targeted events** (all others):
210 + Notify the specific actor + the specific target user only. No broadcast to hierarchy owners.
211 +
212 + ### Notification Recipients by Trigger
213 +
214 + | Trigger | Recipients |
215 + |---|---|
216 + | Submit progress | Submitter + all OWNER/ADMIN in hierarchy |
217 + | Approve/reject progress | Submitter + approving admin |
218 + | Submit edit request | All OWNER/ADMIN in hierarchy |
219 + | Respond edit request | Requester + responding admin |
220 + | Add member | Added user + admin who added |
221 + | Remove member | Removed user + admin who removed |
167 222
168 223 ---
169 224
@@ -265,4 +320,4 @@ Response shape (all scopes):
265 320 - Subtask in progress → `workWeight`/`workVolume` locked
266 321
267 322 ### Soft Delete
268 - All deletes are soft (`deletedAt`). Cascades: deleting activity → all tasks + subtasks. Deleting task → all subtasks.
323 + All deletes are soft (`deletedAt`). Cascades: deleting activity → all tasks + subtasks. Deleting task → all subtasks.