Class: ApprovalEngine::Track

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

Overview

One track of an approval (e.g. “Finance” or “Legal”). A track holds ordered layers of steps; a layer resolves once its consensus policy is met, which activates the next layer or completes the track.

All progression happens synchronously inside the acting step’s lock, so a track is never observed in a half-advanced state.

Constant Summary collapse

STATUSES =
%w[pending approved rejected cancelled].freeze
OPEN_STEP_STATUSES =
%w[waiting pending].freeze

Instance Method Summary collapse

Instance Method Details

#advance!(step) ⇒ Object

A step was approved or rejected: re-evaluate its layer and short-circuit. The layer resolves the moment its consensus is met, fails the moment its consensus is unreachable, and otherwise waits for more votes. Both approve! and reject! funnel through here so rejection respects the layer’s consensus policy instead of being a blanket veto.



27
28
29
30
31
32
33
34
35
36
37
# File 'app/models/approval_engine/track.rb', line 27

def advance!(step)
  layer_steps = steps.for_iteration(step.iteration).for_layer(step.layer)

  case layer_outcome(layer_steps)
  when :met
    cancel_steps(layer_steps.pending) # remaining votes are no longer needed
    activate_next_layer(step) || complete!
  when :failed
    fail!
  end
end

#advance_after_changes_requested!(step) ⇒ Object

An approver requested changes: cancel this iteration’s open work and append a fresh iteration. The track stays pending.



41
42
43
44
# File 'app/models/approval_engine/track.rb', line 41

def advance_after_changes_requested!(step)
  cancel_open_steps!
  IterationBuilder.build_next_iteration!(step)
end

#layer_tally(layer, iteration: steps.maximum(:iteration)) ⇒ Object

The live consensus tally for one layer (within an iteration) — the same facts ‘advance!` decides on, exposed as a read so a host UI can show “N of M approved” and why a layer is met/failed/undecided without re-deriving the consensus math (which only the engine should own).

track.layer_tally(1)
# => { required: 2, approved: 1, rejected: 0, pending: 2, waiting: 0,
#      group_size: 3, outcome: :undecided }

Defaults to the track’s latest iteration. A layer that hasn’t opened yet (all steps still ‘waiting`) reads as `:undecided`, not `:failed`. Returns a zeroed `:undecided` tally for a layer that has no steps.



58
59
60
# File 'app/models/approval_engine/track.rb', line 58

def layer_tally(layer, iteration: steps.maximum(:iteration))
  tally_for(steps.for_iteration(iteration).for_layer(layer))
end