Module: PlanMyStuff::IssueExtractions::Approvals
- Included in:
- PlanMyStuff::Issue
- Defined in:
- lib/plan_my_stuff/issue_extractions/approvals.rb
Instance Method Summary collapse
-
#approvals_required? ⇒ Boolean
True when at least one approver is required on this issue.
-
#approve!(user:) ⇒ PlanMyStuff::Approval
Flips the caller’s approval to
approvedfrom any other state (pendingorrejected). -
#approvers ⇒ Array<PlanMyStuff::Approval>
All required approvers (pending + approved + rejected).
-
#fully_approved? ⇒ Boolean
True when approvers are required AND every approver has approved.
-
#pending_approvals ⇒ Array<PlanMyStuff::Approval>
Approvers who have not yet acted (pending only; rejections are NOT pending – the approver has responded).
-
#reject!(user:) ⇒ PlanMyStuff::Approval
Flips the caller’s approval to
rejectedfrom any other state (pendingorapproved). -
#rejected_approvals ⇒ Array<PlanMyStuff::Approval>
Approvers who have rejected.
-
#remove_approvers!(user_ids:, user: nil) ⇒ Array<PlanMyStuff::Approval>
Removes approvers from this issue’s required-approvals list.
-
#request_approvals!(user_ids:, user: nil) ⇒ Array<PlanMyStuff::Approval>
Adds approvers to this issue’s required-approvals list.
-
#revoke_approval!(user:, target_user_id: nil) ⇒ PlanMyStuff::Approval
Flips an approved or rejected record back to
pending.
Instance Method Details
#approvals_required? ⇒ Boolean
Returns true when at least one approver is required on this issue.
23 24 25 |
# File 'lib/plan_my_stuff/issue_extractions/approvals.rb', line 23 def approvals_required? approvers.present? end |
#approve!(user:) ⇒ PlanMyStuff::Approval
Flips the caller’s approval to approved from any other state (pending or rejected). Only the approver themselves may call this. Fires plan_my_stuff.issue.approval_granted and, when this flip completes the approval set, plan_my_stuff.issue.all_approved.
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/plan_my_stuff/issue_extractions/approvals.rb', line 93 def approve!(user:) actor_id = resolve_actor_id!(user) just_approved, was_fully_approved = modify_approvals! do |current| approval = current.find { |a| a.user_id == actor_id } raise(PlanMyStuff::ValidationError, "User #{actor_id} is not in the approvers list") if approval.nil? raise(PlanMyStuff::ValidationError, "User #{actor_id} has already approved") if approval.approved? approval.status = 'approved' approval.approved_at = Time.current approval.rejected_at = nil [current, approval] end finish_state_change(:approval_granted, just_approved, user: user, was_fully_approved: was_fully_approved) just_approved end |
#approvers ⇒ Array<PlanMyStuff::Approval>
Returns all required approvers (pending + approved + rejected).
7 8 9 |
# File 'lib/plan_my_stuff/issue_extractions/approvals.rb', line 7 def approvers .approvals end |
#fully_approved? ⇒ Boolean
Returns true when approvers are required AND every approver has approved. A single rejection blocks this gate until the approver revokes.
29 30 31 |
# File 'lib/plan_my_stuff/issue_extractions/approvals.rb', line 29 def fully_approved? approvals_required? && approvers.all?(&:approved?) end |
#pending_approvals ⇒ Array<PlanMyStuff::Approval>
Returns approvers who have not yet acted (pending only; rejections are NOT pending – the approver has responded).
13 14 15 |
# File 'lib/plan_my_stuff/issue_extractions/approvals.rb', line 13 def pending_approvals approvers.select(&:pending?) end |
#reject!(user:) ⇒ PlanMyStuff::Approval
Flips the caller’s approval to rejected from any other state (pending or approved). Only the approver themselves may call this. Fires plan_my_stuff.issue.approval_rejected and, when this flip drops the issue out of fully_approved? (i.e. the caller was the last approved approver), plan_my_stuff.issue.approvals_invalidated (+trigger: :rejected+).
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/plan_my_stuff/issue_extractions/approvals.rb', line 122 def reject!(user:) actor_id = resolve_actor_id!(user) just_rejected, was_fully_approved = modify_approvals! do |current| approval = current.find { |a| a.user_id == actor_id } raise(PlanMyStuff::ValidationError, "User #{actor_id} is not in the approvers list") if approval.nil? raise(PlanMyStuff::ValidationError, "User #{actor_id} has already rejected") if approval.rejected? approval.status = 'rejected' approval.rejected_at = Time.current approval.approved_at = nil [current, approval] end finish_state_change( :approval_rejected, just_rejected, user: user, was_fully_approved: was_fully_approved, trigger: :rejected, ) just_rejected end |
#rejected_approvals ⇒ Array<PlanMyStuff::Approval>
Returns approvers who have rejected.
18 19 20 |
# File 'lib/plan_my_stuff/issue_extractions/approvals.rb', line 18 def rejected_approvals approvers.select(&:rejected?) end |
#remove_approvers!(user_ids:, user: nil) ⇒ Array<PlanMyStuff::Approval>
Removes approvers from this issue’s required-approvals list. Only support users may call this. Removing a pending approver may flip the issue into fully_approved? (fires all_approved). Removing an approved approver fires no events (state does not flip). Removing the last approver never fires aggregate events (issue no longer has approvals_required?).
70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/plan_my_stuff/issue_extractions/approvals.rb', line 70 def remove_approvers!(user_ids:, user: nil) guard_support!(user) ids = Array.wrap(user_ids).map(&:to_i) just_removed, was_fully_approved = modify_approvals! do |current| removed = current.select { |a| ids.include?(a.user_id) } [current - removed, removed] end emit_aggregate_events(was_fully_approved: was_fully_approved, trigger: nil, user: user) just_removed end |
#request_approvals!(user_ids:, user: nil) ⇒ Array<PlanMyStuff::Approval>
Adds approvers to this issue’s required-approvals list. Idempotent: users already present are no-ops. Only support users may call this.
Fires plan_my_stuff.issue.approval_requested when any user is newly added. Also fires plan_my_stuff.issue.approvals_invalidated (+trigger: :approver_added+) when the new approvers flip the issue out of a fully-approved state.
45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/plan_my_stuff/issue_extractions/approvals.rb', line 45 def request_approvals!(user_ids:, user: nil) guard_support!(user) ids = Array.wrap(user_ids).map(&:to_i) just_added, was_fully_approved = modify_approvals! do |current| existing_ids = current.map(&:user_id) new_ids = ids - existing_ids added = new_ids.map { |id| PlanMyStuff::Approval.new(user_id: id, status: 'pending') } [current + added, added] end finish_request_approvals(just_added, user: user, was_fully_approved: was_fully_approved) just_added end |
#revoke_approval!(user:, target_user_id: nil) ⇒ PlanMyStuff::Approval
Flips an approved or rejected record back to pending. Approvers may revoke their own response; support users may revoke any approver’s response by passing target_user_id:. Non-support callers passing a target_user_id: that is not their own raise AuthorizationError.
Emits the granular event keyed off the source state: plan_my_stuff.issue.approval_revoked from approved, or plan_my_stuff.issue.rejection_revoked from rejected. When revoking an approval drops the issue out of fully_approved?, also fires plan_my_stuff.issue.approvals_invalidated (+trigger: :revoked+). Revoking a rejection cannot change fully_approved? (the issue was already gated), so no aggregate event fires.
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/plan_my_stuff/issue_extractions/approvals.rb', line 163 def revoke_approval!(user:, target_user_id: nil) actor_id = resolve_actor_id!(user) caller_is_support = PlanMyStuff::UserResolver.support?(PlanMyStuff::UserResolver.resolve(user)) target_id = target_user_id&.to_i || actor_id if !caller_is_support && target_id != actor_id raise(PlanMyStuff::AuthorizationError, "Only support users may revoke another user's response") end revoked_from = nil just_revoked, was_fully_approved = modify_approvals! do |current| approval = current.find { |a| a.user_id == target_id } raise(PlanMyStuff::ValidationError, "User #{target_id} is not in the approvers list") if approval.nil? if approval.pending? raise(PlanMyStuff::ValidationError, "User #{target_id} has not responded -- nothing to revoke") end revoked_from = approval.status approval.status = 'pending' approval.approved_at = nil approval.rejected_at = nil [current, approval] end event = (revoked_from == 'approved') ? :approval_revoked : :rejection_revoked finish_state_change( event, just_revoked, user: user, was_fully_approved: was_fully_approved, trigger: (event == :approval_revoked) ? :revoked : nil, ) just_revoked end |