Module: ApprovalEngine::Approvable
- Extended by:
- ActiveSupport::Concern
- Defined in:
- app/models/concerns/approval_engine/approvable.rb
Overview
Mixed into a host model by the ‘has_approvals` macro. Gives the model its approval association, the `exposes_for_approval` anti-corruption DSL, and the trigger that spawns an approval for a domain event.
class Invoice < ApplicationRecord
has_approvals
exposes_for_approval do
attribute :amount, type: :decimal
attribute :department, type: :string, source: ->(i) { i.department.name }
end
def after_approved
PaymentService.disburse_funds!(self)
end
end
By default, creating the record evaluates the tenant’s rules and spawns the matching approval. Override ‘trigger_approval?` to gate that, or pass `has_approvals(on: [])` to opt out and trigger manually with `record.run_approval!(event:)`.
Constant Summary collapse
- LIFECYCLE_EVENTS =
The ActiveRecord lifecycle events the ‘on:` option understands, mapped to the conventional suffix of the event they route (create -> “<model>.created”). Add a lifecycle here and it’s wired automatically — no new methods.
{ create: "created", update: "updated", destroy: "destroyed" }.freeze
Instance Method Summary collapse
-
#approval_candidates(event:, tenant_id: approval_tenant_id) ⇒ Object
Every approval that would match ‘event` for this record, in priority order — so you can let a user choose which to trigger rather than letting the engine auto-pick the top one.
-
#approval_history ⇒ Object
A read-only view of everything this record has gone through — approvals, the step tree, and a chronological timeline of actions + comments.
- #approval_in_flight? ⇒ Boolean
- #approval_status ⇒ Object
- #latest_approval ⇒ Object
-
#preview_approval(event:, tenant_id: approval_tenant_id) ⇒ Object
Preview what ‘event` would trigger for this record, without writing anything — handy for showing a user “this will go to Manager, then CFO” before they commit an action.
-
#run_approval!(event: nil, templates: nil, approvals_required: "all", tenant_id: approval_tenant_id) ⇒ Object
Start an approval, two ways (pass exactly one):.
-
#serialize_for_approval ⇒ Object
The flat, string-keyed payload derived from ‘exposes_for_approval`.
-
#trigger_approval?(_lifecycle = nil) ⇒ Boolean
Host override hook: return false to skip an automatic trigger.
Instance Method Details
#approval_candidates(event:, tenant_id: approval_tenant_id) ⇒ Object
Every approval that would match ‘event` for this record, in priority order — so you can let a user choose which to trigger rather than letting the engine auto-pick the top one. Returns an array of ApprovalPlan; writes nothing.
142 143 144 145 146 147 148 149 |
# File 'app/models/concerns/approval_engine/approvable.rb', line 142 def approval_candidates(event:, tenant_id: approval_tenant_id) ApprovalEngine::RuleEvaluator.candidates( event_name: event, tenant_id: tenant_id, target: self, payload: serialize_for_approval ) end |
#approval_history ⇒ Object
A read-only view of everything this record has gone through — approvals, the step tree, and a chronological timeline of actions + comments. The gem assembles it; you decide who may see it and how to render it.
154 155 156 |
# File 'app/models/concerns/approval_engine/approvable.rb', line 154 def approval_history ApprovalEngine::History.for(self) end |
#approval_in_flight? ⇒ Boolean
162 163 164 |
# File 'app/models/concerns/approval_engine/approvable.rb', line 162 def approval_in_flight? approvals.pending.exists? end |
#approval_status ⇒ Object
166 167 168 |
# File 'app/models/concerns/approval_engine/approvable.rb', line 166 def approval_status latest_approval&.status end |
#latest_approval ⇒ Object
158 159 160 |
# File 'app/models/concerns/approval_engine/approvable.rb', line 158 def latest_approval approvals.order(created_at: :desc).first end |
#preview_approval(event:, tenant_id: approval_tenant_id) ⇒ Object
Preview what ‘event` would trigger for this record, without writing anything — handy for showing a user “this will go to Manager, then CFO” before they commit an action. Works against the in-memory record, so you can preview an unsaved change (`invoice.amount = 20_000; invoice.preview_…`). Returns an ApprovalEngine::ApprovalPlan.
129 130 131 132 133 134 135 136 |
# File 'app/models/concerns/approval_engine/approvable.rb', line 129 def preview_approval(event:, tenant_id: approval_tenant_id) ApprovalEngine::RuleEvaluator.preview( event_name: event, tenant_id: tenant_id, target: self, payload: serialize_for_approval ) end |
#run_approval!(event: nil, templates: nil, approvals_required: "all", tenant_id: approval_tenant_id) ⇒ Object
Start an approval, two ways (pass exactly one):
run_approval!(event: "invoice.created") # engine routes by rules
run_approval!(templates: [finance, legal]) # you choose explicitly
With ‘event:`, the tenant’s rules are evaluated and the highest-priority match is spawned — returns the approval, a quarantine approval on a rule failure, or nil when nothing matched (or the tenant can’t be resolved).
With ‘templates:`, rule evaluation is skipped and exactly those templates are started (several become parallel tracks of one approval); always returns the spawned approval. Pair with `approval_candidates` to let a user choose instead of the engine. `approvals_required` is the gather consensus across those tracks (default `:all`).
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'app/models/concerns/approval_engine/approvable.rb', line 94 def run_approval!(event: nil, templates: nil, approvals_required: "all", tenant_id: approval_tenant_id) raise ArgumentError, "pass either event: or templates:, not both" if event && templates if templates ApprovalEngine::ApprovalBuilder.build_parallel!(templates: Array(templates), target: self, approvals_required: approvals_required) elsif event return if tenant_id.nil? ApprovalEngine::RuleEvaluator.call( event_name: event, tenant_id: tenant_id, target: self, payload: serialize_for_approval ) else raise ArgumentError, "pass either event: or templates:" end end |
#serialize_for_approval ⇒ Object
The flat, string-keyed payload derived from ‘exposes_for_approval`.
76 77 78 |
# File 'app/models/concerns/approval_engine/approvable.rb', line 76 def serialize_for_approval approval_exposure.serialize(self) end |
#trigger_approval?(_lifecycle = nil) ⇒ Boolean
Host override hook: return false to skip an automatic trigger. Receives the lifecycle (:create or :update), so you can gate per-event — e.g. only auto-route an update when a specific transition happened:
def trigger_approval?(lifecycle)
lifecycle == :update ? saved_change_to_status? : true
end
120 121 122 |
# File 'app/models/concerns/approval_engine/approvable.rb', line 120 def trigger_approval?(_lifecycle = nil) true end |