Class: ApprovalEngine::Approval

Inherits:
ApplicationRecord show all
Defined in:
app/models/approval_engine/approval.rb

Overview

The aggregate root of one approval run: a host record + the event that spawned it, fanning out into one or more parallel tracks that gather per ‘approvals_required` (`:all` by default, like a layer). Progression methods run while the approval row is locked by the acting step, so they don’t relock.

Constant Summary collapse

STATUSES =
%w[pending approved rejected quarantined cancelled].freeze
TERMINAL_STATUSES =
%w[approved rejected quarantined cancelled].freeze

Instance Method Summary collapse

Instance Method Details

#cancel!(reason: nil) ⇒ Object

Withdraw an in-flight approval — the third terminal outcome beside approved and rejected, for when the thing being approved is voided or retracted. Cancels any still-open tracks/steps and fires ‘after_cancelled`. A no-op once terminal. Unlike the gather, this is a host entry point, so it takes its own lock.



79
80
81
82
83
84
85
86
87
88
89
90
# File 'app/models/approval_engine/approval.rb', line 79

def cancel!(reason: nil)
  return self if terminal?

  with_lock do
    return self if terminal?

    update!(status: "cancelled")
    cancel_remaining_tracks!
    emit_outbox("approval.cancelled", reason)
  end
  self
end

#current_bottleneckObject

The step this approval is currently waiting on the longest — i.e. where it’s stuck right now. The oldest still-pending step across all tracks, or nil if nothing is pending. ‘step.waiting_for` gives the elapsed seconds; the host decides what counts as “late” and whether to nudge or escalate.



55
56
57
# File 'app/models/approval_engine/approval.rb', line 55

def current_bottleneck
  steps.pending.order(:activated_at).first
end

#gather!Object

Re-evaluate after any track resolves: approve once enough tracks have, fail once the count is unreachable, else wait. A layer’s logic, over tracks.



61
62
63
64
65
66
67
68
69
70
71
72
# File 'app/models/approval_engine/approval.rb', line 61

def gather!
  return if terminal?

  case track_outcome
  when :met
    update!(status: "approved")
    cancel_remaining_tracks!
    emit_outbox("approval.approved")
  when :failed
    fail_gather!(reason: "required track approvals are no longer reachable")
  end
end

#stepObject



47
48
49
# File 'app/models/approval_engine/approval.rb', line 47

def step
  steps.sole
end

#terminal?Boolean

Returns:

  • (Boolean)


34
35
36
# File 'app/models/approval_engine/approval.rb', line 34

def terminal?
  TERMINAL_STATUSES.include?(status)
end

#trackObject

Convenience readers for the common single-track case. An approval always has at least one track, so when there’s exactly one these read more naturally than ‘tracks.first`. They raise (ActiveRecord::SoleRecord exceeded) once the approval has fanned out, so a caller is never silently handed the wrong track — reach for `tracks` / `steps` then.



43
44
45
# File 'app/models/approval_engine/approval.rb', line 43

def track
  tracks.sole
end