approval_engine 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +138 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +249 -0
  5. data/app/controllers/approval_engine/application_controller.rb +10 -0
  6. data/app/controllers/approval_engine/approvals_controller.rb +29 -0
  7. data/app/helpers/approval_engine/application_helper.rb +27 -0
  8. data/app/jobs/approval_engine/application_job.rb +4 -0
  9. data/app/jobs/approval_engine/process_outbox_job.rb +100 -0
  10. data/app/jobs/approval_engine/timeout_sweep_job.rb +19 -0
  11. data/app/models/approval_engine/application_record.rb +5 -0
  12. data/app/models/approval_engine/approval.rb +144 -0
  13. data/app/models/approval_engine/approval_plan.rb +60 -0
  14. data/app/models/approval_engine/audit_log.rb +28 -0
  15. data/app/models/approval_engine/consensus.rb +37 -0
  16. data/app/models/approval_engine/delegation.rb +34 -0
  17. data/app/models/approval_engine/history.rb +62 -0
  18. data/app/models/approval_engine/outbox_event.rb +47 -0
  19. data/app/models/approval_engine/step.rb +271 -0
  20. data/app/models/approval_engine/template_step.rb +22 -0
  21. data/app/models/approval_engine/track.rb +127 -0
  22. data/app/models/approval_engine/track_template.rb +23 -0
  23. data/app/models/approval_engine/trigger_rule.rb +17 -0
  24. data/app/models/concerns/approval_engine/approvable.rb +183 -0
  25. data/app/services/approval_engine/approval_builder.rb +172 -0
  26. data/app/services/approval_engine/iteration_builder.rb +44 -0
  27. data/app/services/approval_engine/rule_evaluator.rb +119 -0
  28. data/app/views/approval_engine/approvals/index.html.erb +38 -0
  29. data/app/views/approval_engine/approvals/show.html.erb +55 -0
  30. data/app/views/layouts/approval_engine/dashboard.html.erb +49 -0
  31. data/config/routes.rb +8 -0
  32. data/db/migrate/20260614103434_create_approval_engine_core_tables.rb +107 -0
  33. data/db/migrate/20260614103435_create_approval_engine_infrastructure_tables.rb +35 -0
  34. data/db/migrate/20260614104809_create_approval_engine_blueprint_tables.rb +60 -0
  35. data/db/migrate/20260616000001_add_trigger_rule_to_approvals.rb +14 -0
  36. data/db/migrate/20260617000001_add_approvals_required_to_approvals.rb +11 -0
  37. data/db/migrate/20260617000002_add_delivery_tracking_to_outbox_events.rb +11 -0
  38. data/lib/approval_engine/approval_exposure.rb +66 -0
  39. data/lib/approval_engine/configuration.rb +67 -0
  40. data/lib/approval_engine/engine.rb +17 -0
  41. data/lib/approval_engine/model_extensions.rb +20 -0
  42. data/lib/approval_engine/version.rb +3 -0
  43. data/lib/approval_engine.rb +12 -0
  44. data/lib/generators/approval_engine/install/install_generator.rb +34 -0
  45. data/lib/generators/approval_engine/install/templates/POST_INSTALL +55 -0
  46. data/lib/generators/approval_engine/install/templates/approval_engine.rb +21 -0
  47. data/lib/generators/approval_engine/views/templates/approvals/index.html.erb +17 -0
  48. data/lib/generators/approval_engine/views/templates/approvals_controller.rb +32 -0
  49. data/lib/generators/approval_engine/views/views_generator.rb +33 -0
  50. metadata +129 -0
@@ -0,0 +1,20 @@
1
+ module ApprovalEngine
2
+ # Extended onto ActiveRecord::Base so every model gains the class macro,
3
+ # the way `acts_as_*` gems do. Calling it mixes in the Approvable concern.
4
+ module ModelExtensions
5
+ # Arm a model with approvals.
6
+ #
7
+ # has_approvals # auto-routes on create (default)
8
+ # has_approvals(on: [:create, :update]) # also route on update
9
+ # has_approvals(on: []) # opt out; trigger manually
10
+ #
11
+ # `on:` accepts any of ApprovalEngine::Approvable::LIFECYCLE_EVENTS
12
+ # (:create, :update, :destroy), routing the conventional "<model>.created" etc.
13
+ # For domain transitions (e.g. "invoice.rejected"), the engine isn't lifecycle
14
+ # bound at all — just call `run_approval!(event:)` from your own action.
15
+ def has_approvals(on: [ :create ])
16
+ include ApprovalEngine::Approvable
17
+ self.approval_trigger_events = Array(on).freeze
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ module ApprovalEngine
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,12 @@
1
+ require "approval_engine/version"
2
+ require "approval_engine/configuration"
3
+ require "approval_engine/approval_exposure"
4
+ require "approval_engine/model_extensions"
5
+ require "approval_engine/engine"
6
+
7
+ module ApprovalEngine
8
+ # Base class for every error the engine raises itself, so a host can rescue
9
+ # ApprovalEngine::Error to catch them all. (Step transitions still raise the
10
+ # Rails-idiomatic ActiveRecord::RecordInvalid.)
11
+ class Error < StandardError; end
12
+ end
@@ -0,0 +1,34 @@
1
+ require "rails/generators/base"
2
+ require "rails/generators/active_record"
3
+
4
+ module ApprovalEngine
5
+ module Generators
6
+ # Copies the engine migrations into the host app and drops a configured
7
+ # initializer.
8
+ #
9
+ # rails generate approval_engine:install
10
+ class InstallGenerator < Rails::Generators::Base
11
+ include Rails::Generators::Migration
12
+
13
+ source_root File.expand_path("templates", __dir__)
14
+
15
+ desc "Installs ApprovalEngine: copies migrations and creates an initializer."
16
+
17
+ def self.next_migration_number(dirname)
18
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
19
+ end
20
+
21
+ def create_initializer
22
+ template "approval_engine.rb", "config/initializers/approval_engine.rb"
23
+ end
24
+
25
+ def copy_migrations
26
+ rake "approval_engine:install:migrations"
27
+ end
28
+
29
+ def show_readme
30
+ readme "POST_INSTALL" if behavior == :invoke
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,55 @@
1
+ ===============================================================================
2
+
3
+ ApprovalEngine installed.
4
+
5
+ Next steps:
6
+
7
+ 1. Run the migrations:
8
+
9
+ rails db:migrate
10
+
11
+ 2. Edit config/initializers/approval_engine.rb and set BOTH:
12
+ * actor_class — your approver class (e.g. "User")
13
+ * current_tenant_method — REQUIRED. Until it's set, auto-routing on
14
+ create silently does nothing. Single-tenant
15
+ apps can return a constant.
16
+
17
+ 3. Make your actor class resolve approval groups:
18
+
19
+ class User < ApplicationRecord
20
+ def self.resolve_approval_group(group_name, target)
21
+ where(role: group_name)
22
+ end
23
+ end
24
+
25
+ 4. Arm a model and declare its safe surface (a rule can only read attributes
26
+ you expose here):
27
+
28
+ class Invoice < ApplicationRecord
29
+ has_approvals
30
+
31
+ exposes_for_approval do
32
+ attribute :amount, type: :decimal
33
+ end
34
+
35
+ def after_approved
36
+ PaymentService.disburse_funds!(self)
37
+ end
38
+ end
39
+
40
+ 5. Create an ACTIVE template + a matching rule (see the README quickstart),
41
+ then CONFIRM the wiring before relying on it:
42
+
43
+ invoice.preview_approval(event: "invoice.created").triggered?
44
+ # false? -> check event_name match, the template's `active` status,
45
+ # the rule condition, and that its vars are exposed attributes.
46
+
47
+ 6. (Optional) Copy the approval views to customise them:
48
+
49
+ rails generate approval_engine:views
50
+
51
+ 7. (Optional) Mount the ops dashboard in config/routes.rb:
52
+
53
+ mount ApprovalEngine::Engine => "/approval_engine"
54
+
55
+ ===============================================================================
@@ -0,0 +1,21 @@
1
+ ApprovalEngine.configure do |config|
2
+ # ActiveJob queue used to relay the transactional outbox (host callbacks and
3
+ # notifications). Defaults to :default; a dedicated queue is recommended.
4
+ config.outbox_queue = :default
5
+
6
+ # How the engine resolves the current tenant for strict data isolation.
7
+ # Return anything that responds to #id, e.g. with acts_as_tenant:
8
+ # config.current_tenant_method = -> { Current.account }
9
+ # IMPORTANT: while this is nil, auto-routing on create silently no-ops (the
10
+ # engine can't scope the rules). Single-tenant apps can return a constant:
11
+ # config.current_tenant_method = -> { Struct.new(:id).new("default") }
12
+ config.current_tenant_method = nil
13
+
14
+ # Your application's actor class (who approves). It must define:
15
+ # def self.resolve_approval_group(group_name, target) -> [actors]
16
+ config.actor_class = "User"
17
+
18
+ # Fail closed by default: a malformed dynamic rule quarantines the approval
19
+ # instead of crashing the approval. Set to true in development/test to raise.
20
+ config.raise_on_rule_errors = false
21
+ end
@@ -0,0 +1,17 @@
1
+ <%# Copied by `rails generate approval_engine:views`. Style this however you like. %>
2
+ <h1>My approvals</h1>
3
+
4
+ <% if @pending_steps.empty? %>
5
+ <p>Nothing is awaiting your approval.</p>
6
+ <% else %>
7
+ <ul>
8
+ <% @pending_steps.each do |step| %>
9
+ <li>
10
+ <strong><%= step.target %></strong> —
11
+ <%= step.name || "Step" %> (layer <%= step.layer %>, iteration <%= step.iteration %>)
12
+ <%= button_to "Approve", approve_approval_path(step), method: :patch %>
13
+ <%= button_to "Reject", reject_approval_path(step), method: :patch %>
14
+ </li>
15
+ <% end %>
16
+ </ul>
17
+ <% end %>
@@ -0,0 +1,32 @@
1
+ # Copied by `rails generate approval_engine:views`.
2
+ #
3
+ # This file is yours now — restyle it, rename it, add authorization (Pundit,
4
+ # etc.). ApprovalEngine provides the mechanism (step.approve! / reject!); the
5
+ # user-facing experience is entirely up to you.
6
+ class ApprovalsController < ApplicationController
7
+ def index
8
+ # actionable_by includes steps delegated to you, and preloads each step's
9
+ # target so the view can show *what* is awaiting approval without N+1s.
10
+ @pending_steps = ApprovalEngine::Step.actionable_by(current_user)
11
+ .includes(track: { approval: :target })
12
+ .order(:created_at)
13
+ end
14
+
15
+ def approve
16
+ act(:approve!, notice: "Approval recorded.")
17
+ end
18
+
19
+ def reject
20
+ act(:reject!, notice: "Rejection recorded.")
21
+ end
22
+
23
+ private
24
+
25
+ def act(method, notice:)
26
+ step = ApprovalEngine::Step.pending.find(params[:id])
27
+ step.public_send(method, by: current_user, comment: params[:comment])
28
+ redirect_back fallback_location: approvals_path, notice: notice
29
+ rescue ActiveRecord::RecordInvalid => e
30
+ redirect_back fallback_location: approvals_path, alert: e.message
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ require "rails/generators/base"
2
+
3
+ module ApprovalEngine
4
+ module Generators
5
+ # Copies an example approvals controller and views into the host app. They
6
+ # are intentionally unstyled and minimal — you own them, theme them, rename
7
+ # them. The engine ships the mechanism; the customer-facing UI is yours.
8
+ #
9
+ # rails generate approval_engine:views
10
+ class ViewsGenerator < Rails::Generators::Base
11
+ source_root File.expand_path("templates", __dir__)
12
+
13
+ desc "Copies example approval controller and views into your app to customise."
14
+
15
+ def copy_controller
16
+ copy_file "approvals_controller.rb", "app/controllers/approvals_controller.rb"
17
+ end
18
+
19
+ def copy_views
20
+ directory "approvals", "app/views/approvals"
21
+ end
22
+
23
+ def routes_hint
24
+ say "\nAdd routes for the copied controller, for example:", :green
25
+ say <<~RUBY
26
+ resources :approvals, only: :index do
27
+ member { patch :approve; patch :reject }
28
+ end
29
+ RUBY
30
+ end
31
+ end
32
+ end
33
+ end
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: approval_engine
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Harry-kp
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 7.0.8
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '9.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: 7.0.8
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '9.0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: shiny_json_logic
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '0.3'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '0.3'
46
+ description: |-
47
+ A mountable Rails engine for human-in-the-loop approval flows: an
48
+ append-only ledger, dynamic JSON-Logic routing, consensus (any/all/majority),
49
+ delegation, and a transactional outbox — without forcing Redis or Sidekiq.
50
+ email:
51
+ - chaudharyharshit9@gmail.com
52
+ executables: []
53
+ extensions: []
54
+ extra_rdoc_files: []
55
+ files:
56
+ - CHANGELOG.md
57
+ - MIT-LICENSE
58
+ - README.md
59
+ - app/controllers/approval_engine/application_controller.rb
60
+ - app/controllers/approval_engine/approvals_controller.rb
61
+ - app/helpers/approval_engine/application_helper.rb
62
+ - app/jobs/approval_engine/application_job.rb
63
+ - app/jobs/approval_engine/process_outbox_job.rb
64
+ - app/jobs/approval_engine/timeout_sweep_job.rb
65
+ - app/models/approval_engine/application_record.rb
66
+ - app/models/approval_engine/approval.rb
67
+ - app/models/approval_engine/approval_plan.rb
68
+ - app/models/approval_engine/audit_log.rb
69
+ - app/models/approval_engine/consensus.rb
70
+ - app/models/approval_engine/delegation.rb
71
+ - app/models/approval_engine/history.rb
72
+ - app/models/approval_engine/outbox_event.rb
73
+ - app/models/approval_engine/step.rb
74
+ - app/models/approval_engine/template_step.rb
75
+ - app/models/approval_engine/track.rb
76
+ - app/models/approval_engine/track_template.rb
77
+ - app/models/approval_engine/trigger_rule.rb
78
+ - app/models/concerns/approval_engine/approvable.rb
79
+ - app/services/approval_engine/approval_builder.rb
80
+ - app/services/approval_engine/iteration_builder.rb
81
+ - app/services/approval_engine/rule_evaluator.rb
82
+ - app/views/approval_engine/approvals/index.html.erb
83
+ - app/views/approval_engine/approvals/show.html.erb
84
+ - app/views/layouts/approval_engine/dashboard.html.erb
85
+ - config/routes.rb
86
+ - db/migrate/20260614103434_create_approval_engine_core_tables.rb
87
+ - db/migrate/20260614103435_create_approval_engine_infrastructure_tables.rb
88
+ - db/migrate/20260614104809_create_approval_engine_blueprint_tables.rb
89
+ - db/migrate/20260616000001_add_trigger_rule_to_approvals.rb
90
+ - db/migrate/20260617000001_add_approvals_required_to_approvals.rb
91
+ - db/migrate/20260617000002_add_delivery_tracking_to_outbox_events.rb
92
+ - lib/approval_engine.rb
93
+ - lib/approval_engine/approval_exposure.rb
94
+ - lib/approval_engine/configuration.rb
95
+ - lib/approval_engine/engine.rb
96
+ - lib/approval_engine/model_extensions.rb
97
+ - lib/approval_engine/version.rb
98
+ - lib/generators/approval_engine/install/install_generator.rb
99
+ - lib/generators/approval_engine/install/templates/POST_INSTALL
100
+ - lib/generators/approval_engine/install/templates/approval_engine.rb
101
+ - lib/generators/approval_engine/views/templates/approvals/index.html.erb
102
+ - lib/generators/approval_engine/views/templates/approvals_controller.rb
103
+ - lib/generators/approval_engine/views/views_generator.rb
104
+ homepage: https://github.com/Harry-kp/approval_engine
105
+ licenses:
106
+ - MIT
107
+ metadata:
108
+ source_code_uri: https://github.com/Harry-kp/approval_engine
109
+ changelog_uri: https://github.com/Harry-kp/approval_engine/blob/main/CHANGELOG.md
110
+ bug_tracker_uri: https://github.com/Harry-kp/approval_engine/issues
111
+ rubygems_mfa_required: 'true'
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: 3.1.0
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubygems_version: 3.6.9
127
+ specification_version: 4
128
+ summary: Multi-tenant, immutable-ledger approval flows for Rails.
129
+ test_files: []