ledger_accountable 0.0.10.pre → 0.1.4.pre
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.
- checksums.yaml +4 -4
- data/README.md +56 -8
- data/lib/generators/ledger_accountable/templates/ledger_entry.rb +1 -0
- data/lib/generators/ledger_accountable/templates/migration.rb +1 -1
- data/lib/ledger_accountable/ledger_item/entry_creator.rb +21 -0
- data/lib/ledger_accountable/ledger_item/state_transition.rb +209 -0
- data/lib/ledger_accountable/ledger_item.rb +274 -0
- data/lib/ledger_accountable/ledger_owner.rb +33 -0
- data/lib/ledger_accountable/version.rb +1 -1
- data/lib/ledger_accountable.rb +4 -381
- metadata +22 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f94845e66b1f81f8b7897908a9c88fe5c12d63d91882045e721ade66851dca5b
|
4
|
+
data.tar.gz: 32851e373387e7da3ad970b810cd4b445f9a033e72bf77883698939e1c2caeea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c652558bbafd396ed31cf47c2f354c16d986b9f6a087f09c8601e7d93e12772feb850f103b2e265abf51184bba80e5c6af19089f71b431ea438cea1054883da2
|
7
|
+
data.tar.gz: 2b9395f031cddb37a6392608f77c038382b8f6d45d625edebec41fb7248302a7d5d2ef1a6b8919b5bc58ea3708fcdda6111337fa9f0596b734bacaa445f4abf0
|
data/README.md
CHANGED
@@ -26,17 +26,37 @@ $ rails generate ledger_accountable
|
|
26
26
|
|
27
27
|
## Usage
|
28
28
|
|
29
|
-
|
29
|
+
### `LedgerAccountable::LedgerOwner`
|
30
|
+
|
31
|
+
Include **`LedgerAccountable::LedgerOwner`** in any model which maintains a ledger - that is, a model whose instances have a sum balance of debits and credits based on the values of Ledger Item associations.
|
30
32
|
|
31
33
|
```ruby
|
32
|
-
class Order <
|
33
|
-
|
34
|
+
class Order < ActiveRecord::Base
|
35
|
+
include LedgerAccountable::LedgerOwner
|
36
|
+
|
34
37
|
has_many :order_items, dependent: :destroy
|
35
38
|
has_many :payments, dependent: :destroy
|
36
39
|
end
|
40
|
+
```
|
41
|
+
|
42
|
+
This provides methods for tracking the ledger:
|
43
|
+
|
44
|
+
- **`balance`** - Net sum of all ledger entries
|
45
|
+
- **`credit_total`** - Sum of all credit entries (e.g., payments)
|
46
|
+
- **`debit_total`** - Sum of all debit entries (e.g., charges)
|
47
|
+
|
48
|
+
### `LedgerAccountable::LedgerItem`
|
37
49
|
|
38
|
-
|
39
|
-
|
50
|
+
Include `LedgerAccountable::LedgerItem` in any associated model of a `LedgerAccountable::LedgerOwner` to trigger ledger entries when:
|
51
|
+
|
52
|
+
- Instances of that model are associated to the Ledger Owner
|
53
|
+
- Instances of that model are unassociated from the Ledger Owner
|
54
|
+
- Associated instances of that model are changed
|
55
|
+
- Associated instances of that model are destroyed
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
class OrderItem < ActiveRecord::Base
|
59
|
+
include LedgerAccountable::LedgerItem
|
40
60
|
|
41
61
|
belongs_to :order
|
42
62
|
|
@@ -46,6 +66,7 @@ class OrderItem < ApplicationRecord
|
|
46
66
|
amount: :cost,
|
47
67
|
net_amount: :net_cost_change,
|
48
68
|
type: :debit
|
69
|
+
|
49
70
|
def cost
|
50
71
|
quantity * unit_price
|
51
72
|
end
|
@@ -57,13 +78,40 @@ class OrderItem < ApplicationRecord
|
|
57
78
|
end
|
58
79
|
end
|
59
80
|
|
60
|
-
|
61
|
-
|
62
|
-
include LedgerAccountable
|
81
|
+
class Payment < ActiveRecord::Base
|
82
|
+
include LedgerAccountable::LedgerItem
|
63
83
|
|
64
84
|
belongs_to :order
|
65
85
|
|
66
86
|
# Track ledger changes on order with the amount attribute and mark it as a credit
|
67
87
|
track_ledger :order, amount: :amount, type: :credit
|
68
88
|
end
|
89
|
+
|
90
|
+
class Refund < ActiveRecord::Base
|
91
|
+
include LedgerAccountable::LedgerItem
|
92
|
+
|
93
|
+
belongs_to :order
|
94
|
+
|
95
|
+
# Track ledger changes on order with the amount attribute and mark it as a debit
|
96
|
+
track_ledger :order, amount: :amount, type: :debit
|
97
|
+
end
|
69
98
|
```
|
99
|
+
|
100
|
+
The track_ledger method accepts the following options:
|
101
|
+
|
102
|
+
- **`amount`**: Method or attribute that determines the ledger amount (required)
|
103
|
+
- **`type`**: :debit or :credit (defaults to :credit)
|
104
|
+
- **`net_amount`**: Method to calculate amount changes for updates (optional)
|
105
|
+
- **`ledger_attributes`**: Additional attributes that should trigger ledger entries when changed (optional)
|
106
|
+
|
107
|
+
### Ledger Entry Types
|
108
|
+
|
109
|
+
Ledger entries are automatically created in the following scenarios:
|
110
|
+
|
111
|
+
- **`:addition`** - When a new item is added to a ledger
|
112
|
+
- **`:deletion`** - When an item is removed from a ledger
|
113
|
+
- **`:modification`** - When an item's amount changes
|
114
|
+
|
115
|
+
When a LedgerItem changes owners, both a `deletion` entry for the old owner and an `addition` entry for the new owner are created in the same transaction
|
116
|
+
|
117
|
+
<!-- TODO: documentation for alternate object destruction libraries: callbacks to trigger ledger removal for objects that aren't destroyed -->
|
@@ -16,6 +16,7 @@ class LedgerEntry < ActiveRecord::Base
|
|
16
16
|
|
17
17
|
scope :debits, -> { where(transaction_type: :debit) }
|
18
18
|
scope :credits, -> { where(transaction_type: :credit) }
|
19
|
+
scope :with_ledger_item_type, -> (type) { where(ledger_item_type: type) }
|
19
20
|
|
20
21
|
def to_itemized_s(line_type = :line)
|
21
22
|
I18n.t!("#{TRANSLATION_PREFIX}.#{ledger_item_type.constantize.model_name.param_key}.#{line_type}",
|
@@ -2,7 +2,7 @@ class CreateLedgerEntries < ActiveRecord::Migration[6.1]
|
|
2
2
|
def change
|
3
3
|
create_table :ledger_entries do |t|
|
4
4
|
t.references :owner, polymorphic: true, null: false
|
5
|
-
t.references :ledger_item, polymorphic: true, null: false
|
5
|
+
t.references :ledger_item, polymorphic: true, type: :string, null: false
|
6
6
|
t.integer :transaction_type, null: false
|
7
7
|
t.integer :entry_type, null: false
|
8
8
|
t.integer :amount_cents, default: 0, null: false
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LedgerAccountable
|
4
|
+
module LedgerItem
|
5
|
+
# Creates a single LedgerEntry.
|
6
|
+
class EntryCreator
|
7
|
+
def self.create(item:, owner:, amount:, type:, entry_type:, metadata: {})
|
8
|
+
LedgerEntry.create!(
|
9
|
+
# create! will raise if the ledger entry fails to be created,
|
10
|
+
# which will rollback the attempt to save the LedgerAccountable object
|
11
|
+
owner: owner,
|
12
|
+
ledger_item: item,
|
13
|
+
transaction_type: type,
|
14
|
+
entry_type: entry_type,
|
15
|
+
amount_cents: amount,
|
16
|
+
metadata: metadata
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LedgerAccountable
|
4
|
+
module LedgerItem
|
5
|
+
# Manages ledger entry creation during model lifecycle changes.
|
6
|
+
# Determines which entries are needed (addition, deletion, modification) and
|
7
|
+
# ensures all entries are created within the same transaction as the model changes.
|
8
|
+
#
|
9
|
+
# Simplified control flow:
|
10
|
+
# Am I already in a ledger?
|
11
|
+
# YES
|
12
|
+
# am I still in that ledger?
|
13
|
+
# YES
|
14
|
+
# has my amount changed?
|
15
|
+
# YES
|
16
|
+
# => create entry with the net change
|
17
|
+
# NO
|
18
|
+
# => no update unless overridden
|
19
|
+
# NO
|
20
|
+
# => create a deletion ledger entry
|
21
|
+
# do I have a new ledger?
|
22
|
+
# YES
|
23
|
+
# => create a new ledger entry
|
24
|
+
# NO
|
25
|
+
# => create a new ledger entry
|
26
|
+
class StateTransition
|
27
|
+
def self.execute(item, &block)
|
28
|
+
new(item).execute(&block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize(item)
|
32
|
+
@item = item
|
33
|
+
end
|
34
|
+
|
35
|
+
def execute(&block)
|
36
|
+
return yield unless should_record_entry?
|
37
|
+
|
38
|
+
# Compute all values that depend on object state BEFORE the save
|
39
|
+
ledger_entries_to_create = determine_required_entries
|
40
|
+
metadata = @item.build_ledger_metadata
|
41
|
+
|
42
|
+
ActiveRecord::Base.transaction do
|
43
|
+
with_required_save(&block)
|
44
|
+
# Create all necessary entries after the save
|
45
|
+
ledger_entries_to_create.each do |entry_params|
|
46
|
+
record_entry(
|
47
|
+
owner: entry_params[:owner],
|
48
|
+
amount: entry_params[:amount],
|
49
|
+
entry_type: entry_params[:entry_type],
|
50
|
+
metadata: metadata
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def with_required_save
|
59
|
+
# attempt to save or destroy the object
|
60
|
+
commit_successful = yield
|
61
|
+
|
62
|
+
# Proceed only if the save or destroy operation was successful
|
63
|
+
raise ActiveRecord::Rollback if !commit_successful && LedgerItem.require_successful_entries
|
64
|
+
end
|
65
|
+
|
66
|
+
def should_record_entry?
|
67
|
+
return false unless @item.should_persist_in_ledger?
|
68
|
+
|
69
|
+
if @item.to_be_destroyed?
|
70
|
+
true
|
71
|
+
else
|
72
|
+
should_record_addition? || should_record_update? || should_record_removal?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def determine_required_entries
|
77
|
+
entries = []
|
78
|
+
|
79
|
+
if @item.to_be_destroyed?
|
80
|
+
entries << build_destruction_entry
|
81
|
+
else
|
82
|
+
entries << build_removal_entry if should_record_removal?
|
83
|
+
entries << build_addition_entry if should_record_addition?
|
84
|
+
entries << build_modification_entry if should_record_update?
|
85
|
+
end
|
86
|
+
|
87
|
+
entries
|
88
|
+
end
|
89
|
+
|
90
|
+
def build_destruction_entry
|
91
|
+
{
|
92
|
+
type: @item.transaction_type,
|
93
|
+
entry_type: :deletion,
|
94
|
+
owner: @item.current_owner,
|
95
|
+
amount: -@item.ledger_amount
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
def build_removal_entry
|
100
|
+
{
|
101
|
+
type: @item.transaction_type,
|
102
|
+
entry_type: :deletion,
|
103
|
+
owner: @item.previous_owner,
|
104
|
+
amount: -@item.ledger_amount
|
105
|
+
}
|
106
|
+
end
|
107
|
+
|
108
|
+
def build_addition_entry
|
109
|
+
{
|
110
|
+
type: @item.transaction_type,
|
111
|
+
entry_type: :addition,
|
112
|
+
owner: @item.current_owner,
|
113
|
+
amount: @item.ledger_amount
|
114
|
+
}
|
115
|
+
end
|
116
|
+
|
117
|
+
def build_modification_entry
|
118
|
+
{
|
119
|
+
type: @item.transaction_type,
|
120
|
+
entry_type: :modification,
|
121
|
+
owner: @item.current_owner,
|
122
|
+
amount: @item.net_ledger_amount
|
123
|
+
}
|
124
|
+
end
|
125
|
+
|
126
|
+
def record_entry(owner:, amount:, entry_type:, metadata:)
|
127
|
+
EntryCreator.create(
|
128
|
+
item: @item,
|
129
|
+
owner: owner,
|
130
|
+
amount: amount,
|
131
|
+
type: @item.transaction_type,
|
132
|
+
entry_type: entry_type,
|
133
|
+
metadata: metadata
|
134
|
+
)
|
135
|
+
rescue ActiveRecord::RecordInvalid => e
|
136
|
+
raise e if LedgerItem.require_successful_entries
|
137
|
+
end
|
138
|
+
|
139
|
+
def determine_owner
|
140
|
+
case determine_entry_type
|
141
|
+
when :deletion
|
142
|
+
if @item.to_be_destroyed?
|
143
|
+
# the item is being destroyed, so use its current owner
|
144
|
+
@item.current_owner
|
145
|
+
else
|
146
|
+
# the item is removed from the ledger, so use its previous owner
|
147
|
+
@item.previous_owner
|
148
|
+
end
|
149
|
+
else
|
150
|
+
@item.current_owner
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def calculate_amount
|
155
|
+
case determine_entry_type
|
156
|
+
when :modification
|
157
|
+
@item.net_ledger_amount
|
158
|
+
when :deletion
|
159
|
+
if @item.current_owner.present?
|
160
|
+
-@item.ledger_amount
|
161
|
+
else
|
162
|
+
@item.net_ledger_amount
|
163
|
+
end
|
164
|
+
else
|
165
|
+
@item.ledger_amount
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def determine_entry_type
|
170
|
+
if @item.to_be_destroyed?
|
171
|
+
:deletion
|
172
|
+
elsif should_record_removal?
|
173
|
+
:deletion
|
174
|
+
elsif should_record_addition?
|
175
|
+
:addition
|
176
|
+
else
|
177
|
+
:modification
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def should_record_addition?
|
182
|
+
return false unless @item.current_owner.present?
|
183
|
+
|
184
|
+
!@item.entries_for_current_owner? || @item.last_entry_was?('deletion')
|
185
|
+
end
|
186
|
+
|
187
|
+
def should_record_removal?
|
188
|
+
if @item.current_owner.nil?
|
189
|
+
@item.entries_for_previous_owner? && !@item.last_entry_was?('deletion')
|
190
|
+
elsif @item.previous_owner.present? && @item.current_owner.present?
|
191
|
+
@item.entries_for_previous_owner? &&
|
192
|
+
@item.ledger_entries.where(owner: @item.previous_owner).last&.entry_type != 'deletion'
|
193
|
+
else
|
194
|
+
false
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def should_record_update?
|
199
|
+
return false unless @item.should_persist_in_ledger?
|
200
|
+
|
201
|
+
if @item.entries_for_current_owner?
|
202
|
+
@item.ledger_attributes_changed? || @item.would_change_ledger_balance?
|
203
|
+
else
|
204
|
+
false
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
@@ -0,0 +1,274 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# LedgerAccountable::LedgerItem adds ledger functionality to any model that acts as an item in a ledger.
|
4
|
+
#
|
5
|
+
# It supports tracking two types of ledger entries:
|
6
|
+
# 1. Debits: outgoing items which decrease the ledger balance (for example, an item being sold)
|
7
|
+
# 2. Credits: incoming items which increase the ledger balance (for example, a payment taken)
|
8
|
+
#
|
9
|
+
# Usage:
|
10
|
+
# Include LedgerAccountable in a model to have it generate ledger entries based on its
|
11
|
+
# addition/deletion/update events relative to its ledger owner.
|
12
|
+
#
|
13
|
+
# Specify `ledger_owner`, the owner of the ledger entries, and optionally `ledger_attributes`
|
14
|
+
# to track specific attributes changes.
|
15
|
+
#
|
16
|
+
# Example:
|
17
|
+
# ```ruby
|
18
|
+
# class Order < ApplicationRecord
|
19
|
+
# has_many :ledger_entries, as: :owner
|
20
|
+
# has_many :order_items, dependent: :destroy
|
21
|
+
# has_many :payments, dependent: :destroy
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# class OrderItem < ApplicationRecord
|
25
|
+
# include LedgerAccountable::LedgerItem
|
26
|
+
#
|
27
|
+
# belongs_to :order
|
28
|
+
#
|
29
|
+
# # Track ledger changes on order with the cost method and provide a net_amount
|
30
|
+
# # to dynamically compute net changes to its cost
|
31
|
+
# track_ledger :order,
|
32
|
+
# amount: :cost,
|
33
|
+
# net_amount: :net_cost_change,
|
34
|
+
# type: :debit
|
35
|
+
# def cost
|
36
|
+
# quantity * unit_price
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# private
|
40
|
+
#
|
41
|
+
# def net_cost_change
|
42
|
+
# cost - (quantity_was * unit_price_was)
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
#
|
47
|
+
# class Payment < ApplicationRecord
|
48
|
+
# include LedgerAccountable::LedgerItem
|
49
|
+
#
|
50
|
+
# belongs_to :order
|
51
|
+
#
|
52
|
+
# # Track ledger changes on order with the amount attribute and mark it as a credit
|
53
|
+
# track_ledger :order, amount: :amount, type: :credit
|
54
|
+
# end
|
55
|
+
# ```
|
56
|
+
#
|
57
|
+
# Note:
|
58
|
+
# - The `ledger_owner` association must exist in the LedgerAccountable::LedgerItem model.
|
59
|
+
#
|
60
|
+
module LedgerAccountable
|
61
|
+
module LedgerItem
|
62
|
+
autoload :EntryCreator, 'ledger_accountable/ledger_item/entry_creator'
|
63
|
+
autoload :StateTransition, 'ledger_accountable/ledger_item/state_transition'
|
64
|
+
|
65
|
+
extend ActiveSupport::Concern
|
66
|
+
|
67
|
+
class InitializationError < StandardError
|
68
|
+
end
|
69
|
+
|
70
|
+
class InvalidLedgerOwnerError < InitializationError
|
71
|
+
end
|
72
|
+
|
73
|
+
# Toggle for whether ledger entry creation should be required for updates to
|
74
|
+
# LedgerAccountable objects.
|
75
|
+
# If set to `true`, failed ledger entries will roll back all changes.
|
76
|
+
# Defaults to all non-production environments.
|
77
|
+
mattr_accessor :require_successful_entries
|
78
|
+
@@require_successful_entries = !Rails.env.production?
|
79
|
+
|
80
|
+
# The time at which ledger entries began.
|
81
|
+
# Defaults to Unix Epoch (1970-01-01 00:00:00 +0000)
|
82
|
+
mattr_accessor :epoch
|
83
|
+
@@epoch = Time.at(0)
|
84
|
+
|
85
|
+
included do
|
86
|
+
has_many :ledger_entries, as: :ledger_item
|
87
|
+
|
88
|
+
# around_[action] are used below to ensure that these callbacks run after model callbacks
|
89
|
+
# which may modify object values in a commit lifecycle
|
90
|
+
around_create :process_ledger_transition
|
91
|
+
around_update :process_ledger_transition
|
92
|
+
around_destroy :process_ledger_transition
|
93
|
+
# the owner of the ledger entries
|
94
|
+
class_attribute :ledger_owner
|
95
|
+
# the name of the attribute or method which determines the ledger amount
|
96
|
+
class_attribute :ledger_amount_attribute
|
97
|
+
# the name of the attribute or method which determines the ledger amount
|
98
|
+
class_attribute :ledger_net_amount_method
|
99
|
+
# the type of ledger entry to create - debit or credit
|
100
|
+
class_attribute :transaction_type
|
101
|
+
# attributes of the LedgerAccountable that should trigger a ledger entry when changed
|
102
|
+
class_attribute :ledger_attributes
|
103
|
+
end
|
104
|
+
|
105
|
+
class_methods do
|
106
|
+
# registers the model as a ledger item for ledger_owner,
|
107
|
+
# to be updated when the provided attributes (or ledger owner) are changed
|
108
|
+
def track_ledger(ledger_owner, options = {})
|
109
|
+
validate_and_assign_ledger_owner(ledger_owner)
|
110
|
+
validate_and_assign_transaction_type(options)
|
111
|
+
validate_and_assign_ledger_amount_attribute(options)
|
112
|
+
validate_net_amount_method(options)
|
113
|
+
validate_and_assign_ledger_attributes(options)
|
114
|
+
end
|
115
|
+
|
116
|
+
def validate_and_assign_ledger_owner(ledger_owner)
|
117
|
+
# verify that an instance of the LedgerAccountable model can respond to ledger_owner
|
118
|
+
unless instance_methods.include?(ledger_owner)
|
119
|
+
raise InvalidLedgerOwnerError, "LedgerAccountable::LedgerItem model #{model_name} must respond to the provided value for ledger_owner (#{ledger_owner})."
|
120
|
+
end
|
121
|
+
|
122
|
+
self.ledger_owner = ledger_owner
|
123
|
+
end
|
124
|
+
|
125
|
+
def validate_and_assign_transaction_type(options)
|
126
|
+
if options[:type].present?
|
127
|
+
raise 'LedgerAccountable type must be :debit or :credit' unless %i[debit credit].include?(options[:type])
|
128
|
+
|
129
|
+
self.transaction_type = options[:type]
|
130
|
+
else
|
131
|
+
self.transaction_type = :credit
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def validate_and_assign_ledger_amount_attribute(options)
|
136
|
+
raise "track_ledger :amount is required in #{model_name}" unless options[:amount].present?
|
137
|
+
raise 'track_ledger :amount must be a symbol' unless options[:amount].is_a?(Symbol)
|
138
|
+
|
139
|
+
self.ledger_amount_attribute = options[:amount]
|
140
|
+
end
|
141
|
+
|
142
|
+
def validate_net_amount_method(options)
|
143
|
+
return unless options[:net_amount].present?
|
144
|
+
raise 'track_ledger :net_amount must be a symbol' unless options[:net_amount].is_a?(Symbol)
|
145
|
+
|
146
|
+
self.ledger_net_amount_method = options[:net_amount]
|
147
|
+
end
|
148
|
+
|
149
|
+
def validate_and_assign_ledger_attributes(options)
|
150
|
+
# verify that provided ledger attributes are correctly formatted
|
151
|
+
if options[:ledger_attributes].present? && options[:ledger_attributes].any? { |attr| !attr.is_a?(Symbol) }
|
152
|
+
raise "LedgerAccountable attributes must be symbols. Did you mean #{options[:ledger_attributes].map do |attr|
|
153
|
+
":#{attr}"
|
154
|
+
end.join(', ')}?"
|
155
|
+
end
|
156
|
+
|
157
|
+
self.ledger_attributes = options[:ledger_attributes]
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# the amount to be recorded in the ledger entry on creation or deletion; typically the full amount on the
|
162
|
+
# LedgerAccountable object
|
163
|
+
def ledger_amount
|
164
|
+
unless respond_to?(self.class.ledger_amount_attribute)
|
165
|
+
raise NotImplementedError,
|
166
|
+
"LedgerAccountable::LedgerItem model '#{model_name}' specified #{self.class.ledger_amount_attribute} for track_ledger :amount, but does not implement #{self.class.ledger_amount_attribute}"
|
167
|
+
end
|
168
|
+
|
169
|
+
ledger_amount_multiplier * (send(self.class.ledger_amount_attribute) || 0)
|
170
|
+
end
|
171
|
+
|
172
|
+
# the amount to be recorded in the ledger entry on update; typically a net change to the dollar amount
|
173
|
+
# stored on the LedgerAccountable object
|
174
|
+
def net_ledger_amount
|
175
|
+
if self.class.ledger_net_amount_method
|
176
|
+
net_amount_result = send(self.class.ledger_net_amount_method)
|
177
|
+
ledger_amount_multiplier * net_amount_result
|
178
|
+
else
|
179
|
+
unless attribute_method?(self.class.ledger_amount_attribute.to_s)
|
180
|
+
# if a method is provided to compute ledger_amount,
|
181
|
+
logger.warn "
|
182
|
+
LedgerAccountable::LedgerItem model '#{model_name}' appears to use a method for track_ledger :amount, \
|
183
|
+
but did not provide an option for :net_amount. This can lead to unexpected ledger entry amounts when modifying #{model_name}.
|
184
|
+
"
|
185
|
+
end
|
186
|
+
|
187
|
+
previous_ledger_amount = ledger_amount_multiplier * attribute_was(self.class.ledger_amount_attribute)
|
188
|
+
# p 'prev:', previous_ledger_amount, 'cur:', ledger_amount
|
189
|
+
ledger_amount - (previous_ledger_amount || 0)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def ledger_attributes_changed?
|
194
|
+
# if no ledger attributes are specified, only trigger if the balance changed
|
195
|
+
return false if ledger_attributes.blank?
|
196
|
+
|
197
|
+
changed_attributes.keys.any? { |key| ledger_attributes.include?(key.to_sym) }
|
198
|
+
end
|
199
|
+
|
200
|
+
def would_change_ledger_balance?
|
201
|
+
net_ledger_amount != 0
|
202
|
+
end
|
203
|
+
|
204
|
+
def current_owner
|
205
|
+
send(self.class.ledger_owner)
|
206
|
+
end
|
207
|
+
|
208
|
+
def previous_owner
|
209
|
+
if current_owner.nil?
|
210
|
+
# if the owner was removed, get the owner of its last ledger entry
|
211
|
+
ledger_entries.reload.last&.owner
|
212
|
+
else
|
213
|
+
# otherwise get the owner from the last entry NOT for its current owner
|
214
|
+
ledger_entries.reload.where.not(owner: current_owner).last&.owner
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def build_ledger_metadata
|
219
|
+
# can be overridden to return a hash of metadata values
|
220
|
+
{}
|
221
|
+
end
|
222
|
+
|
223
|
+
def last_entry_was?(entry_type)
|
224
|
+
if current_owner.present?
|
225
|
+
ledger_entries.reload.where(owner: current_owner).last&.entry_type == entry_type
|
226
|
+
elsif previous_owner.present?
|
227
|
+
ledger_entries.reload.where(owner: previous_owner).last&.entry_type == entry_type
|
228
|
+
else
|
229
|
+
false
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# An overrideable method to determine if the object should be persisted in the ledger
|
234
|
+
#
|
235
|
+
# For example, payments should not be recorded in a ledger until they're finalized
|
236
|
+
# by default, a LedgerAccountable should persist in a ledger if has a ledger_owner via
|
237
|
+
# track_ledger
|
238
|
+
def should_persist_in_ledger?
|
239
|
+
# warning log if the ledger owner is not set - the LedgerAccountable model must
|
240
|
+
# include track_ledger
|
241
|
+
if self.class.ledger_owner.blank?
|
242
|
+
Rails.logger.warn "LedgerAccountable::LedgerItem model #{model_name} must include track_ledger to use ledger functionality"
|
243
|
+
end
|
244
|
+
|
245
|
+
self.class.ledger_owner.present? && has_current_or_previous_ledger?
|
246
|
+
end
|
247
|
+
|
248
|
+
def entries_for_current_owner?
|
249
|
+
ledger_entries.reload.where(owner: current_owner).any?
|
250
|
+
end
|
251
|
+
|
252
|
+
def entries_for_previous_owner?
|
253
|
+
ledger_entries.reload.where(owner: previous_owner).any?
|
254
|
+
end
|
255
|
+
|
256
|
+
def to_be_destroyed?
|
257
|
+
@_destroy_callback_already_called ||= false
|
258
|
+
end
|
259
|
+
|
260
|
+
private
|
261
|
+
|
262
|
+
def process_ledger_transition(&block)
|
263
|
+
StateTransition.execute(self, &block)
|
264
|
+
end
|
265
|
+
|
266
|
+
def ledger_amount_multiplier
|
267
|
+
self.class.transaction_type == :credit ? 1 : -1
|
268
|
+
end
|
269
|
+
|
270
|
+
def has_current_or_previous_ledger?
|
271
|
+
previous_owner.present? || current_owner.present?
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LedgerAccountable
|
4
|
+
#
|
5
|
+
# LedgerAccountable::LedgerOwner is intended to be included in models
|
6
|
+
# which act as the owner of a ledger.
|
7
|
+
#
|
8
|
+
module LedgerOwner
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
included do
|
12
|
+
has_many :ledger_entries, as: :owner
|
13
|
+
end
|
14
|
+
|
15
|
+
def balance
|
16
|
+
ledger_entries.sum(:amount_cents)
|
17
|
+
end
|
18
|
+
|
19
|
+
# The absolute value of the sum of the LedgerOwner's credit entries.
|
20
|
+
# This can be used to determine the total amount credited to the ledger -
|
21
|
+
# for example, its total revenue.
|
22
|
+
def credit_total
|
23
|
+
ledger_entries.credits.sum(:amount_cents).abs
|
24
|
+
end
|
25
|
+
|
26
|
+
# The absolute value of the sum of the LedgerOwner's debit entries.
|
27
|
+
# This can be used to determine the total amount owed to the ledger -
|
28
|
+
# for example, the aggregate value of the items sold.
|
29
|
+
def debit_total
|
30
|
+
ledger_entries.debits.sum(:amount_cents).abs
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/ledger_accountable.rb
CHANGED
@@ -1,387 +1,10 @@
|
|
1
|
-
# LedgerAccountable adds ledger functionality to any model that acts as an item in a ledger.
|
2
|
-
#
|
3
|
-
# It supports tracking two types of ledger entries:
|
4
|
-
# 1. Debits: outgoing items which decrease the ledger balance (for example, an item being sold)
|
5
|
-
# 2. Credits: incoming items which increase the ledger balance (for example, a payment taken)
|
6
|
-
#
|
7
|
-
# Usage:
|
8
|
-
# Include LedgerAccountable in a model to have it generate ledger entries based on its
|
9
|
-
# addition/deletion/update events relative to its ledger owner.
|
10
|
-
#
|
11
|
-
# Specify `ledger_owner`, the owner of the ledger entries, and optionally `ledger_attributes`
|
12
|
-
# to track specific attributes changes.
|
13
|
-
#
|
14
|
-
# Example:
|
15
|
-
# ```ruby
|
16
|
-
# class Order < ApplicationRecord
|
17
|
-
# has_many :ledger_entries, as: :owner
|
18
|
-
# has_many :order_items, dependent: :destroy
|
19
|
-
# has_many :payments, dependent: :destroy
|
20
|
-
# end
|
21
|
-
#
|
22
|
-
# class OrderItem < ApplicationRecord
|
23
|
-
# include LedgerAccountable
|
24
|
-
#
|
25
|
-
# belongs_to :order
|
26
|
-
#
|
27
|
-
# # Track ledger changes on order with the cost method and provide a net_amount
|
28
|
-
# # to dynamically compute net changes to its cost
|
29
|
-
# track_ledger :order,
|
30
|
-
# amount: :cost,
|
31
|
-
# net_amount: :net_cost_change,
|
32
|
-
# type: :debit
|
33
|
-
# def cost
|
34
|
-
# quantity * unit_price
|
35
|
-
# end
|
36
|
-
#
|
37
|
-
# private
|
38
|
-
#
|
39
|
-
# def net_cost_change
|
40
|
-
# cost - (quantity_was * unit_price_was)
|
41
|
-
# end
|
42
|
-
# end
|
43
|
-
#
|
44
|
-
#
|
45
|
-
# class Payment < ApplicationRecord
|
46
|
-
# include LedgerAccountable
|
47
|
-
#
|
48
|
-
# belongs_to :order
|
49
|
-
#
|
50
|
-
# # Track ledger changes on order with the amount attribute and mark it as a credit
|
51
|
-
# track_ledger :order, amount: :amount, type: :credit
|
52
|
-
# end
|
53
|
-
# ```
|
54
|
-
#
|
55
|
-
# Note:
|
56
|
-
# - The `ledger_owner` association must exist in the LedgerAccountable model.
|
57
|
-
#
|
58
1
|
module LedgerAccountable
|
59
|
-
|
2
|
+
autoload :LedgerItem, 'ledger_accountable/ledger_item'
|
3
|
+
autoload :LedgerOwner, 'ledger_accountable/ledger_owner'
|
60
4
|
|
61
|
-
#
|
62
|
-
#
|
63
|
-
# If set to `true`, failed ledger entries will roll back all changes.
|
64
|
-
# Defaults to all non-production environments.
|
65
|
-
mattr_accessor :require_successful_entries
|
66
|
-
@@require_successful_entries = !Rails.env.production?
|
67
|
-
|
68
|
-
# The time at which ledger entries began.
|
69
|
-
# Defaults to Unix Epoch (1970-01-01 00:00:00 +0000)
|
70
|
-
mattr_accessor :epoch
|
71
|
-
@@epoch = Time.at(0)
|
72
|
-
|
73
|
-
included do
|
74
|
-
has_many :ledger_entries, as: :ledger_item
|
75
|
-
|
76
|
-
# around_[action] are used below to ensure that these callbacks run after model callbacks
|
77
|
-
# which may modify object values in a commit lifecycle
|
78
|
-
around_create :record_ledger_addition, if: :should_record_ledger_addition? # record a ledger entry when the LedgerAccountable is created or updated
|
79
|
-
around_update :record_ledger_addition, if: :should_record_ledger_addition? # record a ledger entry when the LedgerAccountable is created or updated
|
80
|
-
around_update :record_ledger_removal, if: :should_record_ledger_removal? # record a ledger entry when the LedgerAccountable is created or updated
|
81
|
-
around_update :record_ledger_update, if: :should_record_ledger_update? # record a ledger entry when the LedgerAccountable is updated
|
82
|
-
around_destroy :record_ledger_destruction, if: :should_persist_in_ledger? # record a ledger entry when the LedgerAccountable is destroyed
|
83
|
-
# the owner of the ledger entries
|
84
|
-
class_attribute :ledger_owner
|
85
|
-
# the name of the attribute or method which determines the ledger amount
|
86
|
-
class_attribute :ledger_amount_attribute
|
87
|
-
# the name of the attribute or method which determines the ledger amount
|
88
|
-
class_attribute :ledger_net_amount_method
|
89
|
-
# the type of ledger entry to create - debit or credit
|
90
|
-
class_attribute :transaction_type
|
91
|
-
# attributes of the LedgerAccountable that should trigger a ledger entry when changed
|
92
|
-
class_attribute :ledger_attributes
|
93
|
-
end
|
94
|
-
|
95
|
-
class_methods do
|
96
|
-
# registers the model as a ledger item for ledger_owner,
|
97
|
-
# to be updated when the provided attributes (or ledger owner) are changed
|
98
|
-
def track_ledger(ledger_owner, options = {})
|
99
|
-
validate_and_assign_ledger_owner(ledger_owner)
|
100
|
-
validate_and_assign_transaction_type(options)
|
101
|
-
validate_and_assign_ledger_amount_attribute(options)
|
102
|
-
validate_net_amount_method(options)
|
103
|
-
validate_and_assign_ledger_attributes(options)
|
104
|
-
end
|
105
|
-
|
106
|
-
def validate_and_assign_ledger_owner(ledger_owner)
|
107
|
-
# verify that an instance of the LedgerAccountable model can respond to ledger_owner
|
108
|
-
unless instance_methods.include?(ledger_owner)
|
109
|
-
raise "LedgerAccountable model #{model_name} must respond to the provided value for ledger_owner (#{ledger_owner})."
|
110
|
-
end
|
111
|
-
|
112
|
-
self.ledger_owner = ledger_owner
|
113
|
-
end
|
114
|
-
|
115
|
-
def validate_and_assign_transaction_type(options)
|
116
|
-
if options[:type].present?
|
117
|
-
raise 'LedgerAccountable type must be :debit or :credit' unless %i[debit credit].include?(options[:type])
|
118
|
-
|
119
|
-
self.transaction_type = options[:type]
|
120
|
-
else
|
121
|
-
self.transaction_type = :credit
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
def validate_and_assign_ledger_amount_attribute(options)
|
126
|
-
raise "track_ledger :amount is required in #{model_name}" unless options[:amount].present?
|
127
|
-
raise 'track_ledger :amount must be a symbol' unless options[:amount].is_a?(Symbol)
|
128
|
-
|
129
|
-
self.ledger_amount_attribute = options[:amount]
|
130
|
-
end
|
131
|
-
|
132
|
-
def validate_net_amount_method(options)
|
133
|
-
return unless options[:net_amount].present?
|
134
|
-
raise 'track_ledger :net_amount must be a symbol' unless options[:net_amount].is_a?(Symbol)
|
135
|
-
|
136
|
-
self.ledger_net_amount_method = options[:net_amount]
|
137
|
-
end
|
138
|
-
|
139
|
-
def validate_and_assign_ledger_attributes(options)
|
140
|
-
# verify that provided ledger attributes are correctly formatted
|
141
|
-
if options[:ledger_attributes].present? && options[:ledger_attributes].any? { |attr| !attr.is_a?(Symbol) }
|
142
|
-
raise "LedgerAccountable attributes must be symbols. Did you mean #{options[:ledger_attributes].map do |attr|
|
143
|
-
":#{attr}"
|
144
|
-
end.join(', ')}?"
|
145
|
-
end
|
146
|
-
|
147
|
-
self.ledger_attributes = options[:ledger_attributes]
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
# Default way to set up LedgerAccountable. Run rails generate ledger_accountable to create
|
152
|
-
# a fresh initializer with all configuration values.
|
5
|
+
# Default way to configure LedgerAccountable. Run rails generate ledger_accountable to create
|
6
|
+
# or update an initializer with default configuration values.
|
153
7
|
def self.setup
|
154
8
|
yield self
|
155
9
|
end
|
156
|
-
|
157
|
-
# the amount to be recorded in the ledger entry on creation or deletion; typically the full amount on the
|
158
|
-
# LedgerAccountable object
|
159
|
-
def ledger_amount
|
160
|
-
unless respond_to?(self.class.ledger_amount_attribute)
|
161
|
-
raise NotImplementedError,
|
162
|
-
"LedgerAccountable model '#{model_name}' specified #{self.class.ledger_amount_attribute} for track_ledger :amount, but does not implement #{self.class.ledger_amount_attribute}"
|
163
|
-
end
|
164
|
-
|
165
|
-
ledger_amount_multiplier * (send(self.class.ledger_amount_attribute) || 0)
|
166
|
-
end
|
167
|
-
|
168
|
-
# the amount to be recorded in the ledger entry on update; typically a net change to the dollar amount
|
169
|
-
# stored on the LedgerAccountable object
|
170
|
-
def net_ledger_amount
|
171
|
-
if self.class.ledger_net_amount_method
|
172
|
-
net_amount_result = send(self.class.ledger_net_amount_method)
|
173
|
-
ledger_amount_multiplier * net_amount_result
|
174
|
-
else
|
175
|
-
unless attribute_method?(self.class.ledger_amount_attribute.to_s)
|
176
|
-
# if a method is provided to compute ledger_amount,
|
177
|
-
logger.warn "
|
178
|
-
LedgerAccountable model '#{model_name}' appears to use a method for track_ledger :amount, \
|
179
|
-
but did not provide an option for :net_amount. This can lead to unexpected ledger entry amounts when modifying #{model_name}.
|
180
|
-
"
|
181
|
-
end
|
182
|
-
|
183
|
-
previous_ledger_amount = ledger_amount_multiplier * attribute_was(self.class.ledger_amount_attribute)
|
184
|
-
ledger_amount - (previous_ledger_amount || 0)
|
185
|
-
end
|
186
|
-
end
|
187
|
-
|
188
|
-
private
|
189
|
-
|
190
|
-
# Am I already in a ledger?
|
191
|
-
# YES
|
192
|
-
# am I still in that ledger?
|
193
|
-
# YES
|
194
|
-
# has my amount changed?
|
195
|
-
# YES
|
196
|
-
# => create entry with the net change
|
197
|
-
# NO
|
198
|
-
# => no update unless overriden
|
199
|
-
# NO
|
200
|
-
# => create a deletion ledger entry
|
201
|
-
# do I have a new ledger?
|
202
|
-
# YES
|
203
|
-
# => create a new ledger entry
|
204
|
-
# NO
|
205
|
-
# => create a new ledger entry
|
206
|
-
|
207
|
-
# by default, a LedgerAccountable should record a ledger entry when the ledger_attributes are changed
|
208
|
-
# or if the update would otherwise change the ledger balance
|
209
|
-
def should_record_ledger_update?
|
210
|
-
return false unless should_persist_in_ledger?
|
211
|
-
|
212
|
-
if ledger_entries.where(owner: current_owner).any?
|
213
|
-
ledger_attributes_changed? || would_change_ledger_balance?
|
214
|
-
else
|
215
|
-
false
|
216
|
-
end
|
217
|
-
end
|
218
|
-
|
219
|
-
def should_record_ledger_addition?
|
220
|
-
should_persist_in_ledger? && added_to_ledger?
|
221
|
-
end
|
222
|
-
|
223
|
-
def should_record_ledger_removal?
|
224
|
-
should_persist_in_ledger? && removed_from_ledger?
|
225
|
-
end
|
226
|
-
|
227
|
-
def ledger_attributes_changed?
|
228
|
-
# if no ledger attributes are specified, only trigger if the balance changed
|
229
|
-
return false if ledger_attributes.blank?
|
230
|
-
|
231
|
-
changed_attributes.keys.any? { |key| ledger_attributes.include?(key.to_sym) }
|
232
|
-
end
|
233
|
-
|
234
|
-
def removed_from_ledger?
|
235
|
-
if current_owner.nil?
|
236
|
-
# if the owner was removed, check if its has non-deletion entries for its
|
237
|
-
# previous owner
|
238
|
-
entries_for_previous_owner? && !last_entry_was?('deletion')
|
239
|
-
elsif previous_owner.present? && current_owner.present?
|
240
|
-
# if the owner has changed, check if it was removed from its last owner's ledger
|
241
|
-
entries_for_previous_owner? && ledger_entries.where(owner: previous_owner).last&.entry_type != 'deletion'
|
242
|
-
else
|
243
|
-
false
|
244
|
-
end
|
245
|
-
end
|
246
|
-
|
247
|
-
def added_to_ledger?
|
248
|
-
if current_owner.present?
|
249
|
-
!entries_for_current_owner? || last_entry_was?('deletion')
|
250
|
-
else
|
251
|
-
false
|
252
|
-
end
|
253
|
-
end
|
254
|
-
|
255
|
-
def would_change_ledger_balance?
|
256
|
-
net_ledger_amount != 0
|
257
|
-
end
|
258
|
-
|
259
|
-
# create an :addition ledger entry with the full ledger amount
|
260
|
-
def record_ledger_addition(&block)
|
261
|
-
metadata = build_ledger_metadata
|
262
|
-
owner = send(self.class.ledger_owner)
|
263
|
-
|
264
|
-
record_ledger_entry(owner, ledger_amount, :addition, metadata, &block)
|
265
|
-
end
|
266
|
-
|
267
|
-
# create a :deletion ledger entry with an amount dependent on its owner's presence
|
268
|
-
def record_ledger_removal(&block)
|
269
|
-
metadata = build_ledger_metadata
|
270
|
-
owner = previous_owner
|
271
|
-
# if the owner is no longer present, remove the Accountable's full amount
|
272
|
-
# from the ledger; otherwise, remove the net ledger amount
|
273
|
-
amount_to_remove = current_owner.present? ? -ledger_amount : net_ledger_amount
|
274
|
-
|
275
|
-
record_ledger_entry(owner, amount_to_remove, :deletion, metadata, &block)
|
276
|
-
end
|
277
|
-
|
278
|
-
# create a :deletion ledger entry with the full ledger amount
|
279
|
-
# varies slightly from record_ledger_removal in that:
|
280
|
-
# - it always records its full negative ledger amount
|
281
|
-
# - it records an entry on its current ledger owner rather than its previous owner
|
282
|
-
def record_ledger_destruction(&block)
|
283
|
-
metadata = build_ledger_metadata
|
284
|
-
owner = current_owner
|
285
|
-
|
286
|
-
record_ledger_entry(owner, -ledger_amount, :deletion, metadata, &block)
|
287
|
-
end
|
288
|
-
|
289
|
-
# create a :modification ledger entry with the net change in net_ledger_amount
|
290
|
-
def record_ledger_update(&block)
|
291
|
-
metadata = build_ledger_metadata
|
292
|
-
owner = current_owner
|
293
|
-
|
294
|
-
record_ledger_entry(owner, net_ledger_amount, :modification, metadata, &block)
|
295
|
-
end
|
296
|
-
|
297
|
-
def record_ledger_entry(owner, amount, entry_type, metadata = nil, &block)
|
298
|
-
ActiveRecord::Base.transaction do
|
299
|
-
with_required_save(&block)
|
300
|
-
|
301
|
-
if owner.present? && amount.present?
|
302
|
-
begin
|
303
|
-
LedgerEntry.create!(
|
304
|
-
# create! will raise if the ledger entry fails to be created,
|
305
|
-
# which will rollback the attempt to save the LedgerAccountable object
|
306
|
-
owner: owner,
|
307
|
-
ledger_item: self,
|
308
|
-
transaction_type: self.transaction_type,
|
309
|
-
entry_type: entry_type,
|
310
|
-
amount_cents: amount,
|
311
|
-
metadata: metadata
|
312
|
-
)
|
313
|
-
rescue ActiveRecord::RecordInvalid => e
|
314
|
-
raise e if @@require_successful_entries
|
315
|
-
end
|
316
|
-
end
|
317
|
-
end
|
318
|
-
end
|
319
|
-
|
320
|
-
def with_required_save
|
321
|
-
commit_successful = yield # attempt to save or destroy the object
|
322
|
-
|
323
|
-
# Proceed only if the save or destroy operation was successful
|
324
|
-
raise ActiveRecord::Rollback if !commit_successful && @@require_successful_entries
|
325
|
-
end
|
326
|
-
|
327
|
-
def current_owner
|
328
|
-
send(self.class.ledger_owner)
|
329
|
-
end
|
330
|
-
|
331
|
-
def previous_owner
|
332
|
-
if current_owner.nil?
|
333
|
-
# if the owner was removed, get the owner of its last ledger entry
|
334
|
-
ledger_entries.last&.owner
|
335
|
-
else
|
336
|
-
# otherwise get the owner from the last entry NOT for its current owner
|
337
|
-
ledger_entries.where.not(owner: current_owner).last&.owner
|
338
|
-
end
|
339
|
-
end
|
340
|
-
|
341
|
-
def build_ledger_metadata
|
342
|
-
# can be overridden to return a hash of metadata values
|
343
|
-
{}
|
344
|
-
end
|
345
|
-
|
346
|
-
def last_entry_was?(entry_type)
|
347
|
-
if current_owner.present?
|
348
|
-
ledger_entries.where(owner: current_owner).last&.entry_type == entry_type
|
349
|
-
elsif previous_owner.present?
|
350
|
-
ledger_entries.where(owner: previous_owner).last&.entry_type == entry_type
|
351
|
-
else
|
352
|
-
false
|
353
|
-
end
|
354
|
-
end
|
355
|
-
|
356
|
-
def ledger_amount_multiplier
|
357
|
-
self.class.transaction_type == :credit ? 1 : -1
|
358
|
-
end
|
359
|
-
|
360
|
-
# An overrideable method to determine if the object should be persisted in the ledger
|
361
|
-
#
|
362
|
-
# For example, payments should not be recorded in a ledger until they're finalized
|
363
|
-
# by default, a LedgerAccountable should persist in a ledger if has a ledger_owner via
|
364
|
-
# track_ledger
|
365
|
-
def should_persist_in_ledger?
|
366
|
-
# warning log if the ledger owner is not set - the LedgerAccountable model must
|
367
|
-
# include track_ledger
|
368
|
-
if self.class.ledger_owner.blank?
|
369
|
-
Rails.logger.warn "LedgerAccountable model #{model_name} must include track_ledger to use ledger functionality"
|
370
|
-
end
|
371
|
-
|
372
|
-
self.class.ledger_owner.present?
|
373
|
-
end
|
374
|
-
|
375
|
-
def entries_for_current_owner?
|
376
|
-
ledger_entries.reload.where(owner: current_owner).any?
|
377
|
-
end
|
378
|
-
|
379
|
-
def entries_for_previous_owner?
|
380
|
-
ledger_entries.reload.where(owner: previous_owner).any?
|
381
|
-
end
|
382
|
-
|
383
|
-
def to_be_destroyed?
|
384
|
-
@_destroy_callback_already_called ||= false
|
385
|
-
end
|
386
10
|
end
|
387
|
-
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ledger_accountable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.1.4.pre
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brendan Maclean
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2024-
|
12
|
+
date: 2024-12-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -52,47 +52,47 @@ dependencies:
|
|
52
52
|
- !ruby/object:Gem::Version
|
53
53
|
version: '8'
|
54
54
|
- !ruby/object:Gem::Dependency
|
55
|
-
name:
|
55
|
+
name: database_cleaner-active_record
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
57
57
|
requirements:
|
58
|
-
- -
|
58
|
+
- - '='
|
59
59
|
- !ruby/object:Gem::Version
|
60
|
-
version:
|
60
|
+
version: 2.1.0
|
61
61
|
type: :development
|
62
62
|
prerelease: false
|
63
63
|
version_requirements: !ruby/object:Gem::Requirement
|
64
64
|
requirements:
|
65
|
-
- -
|
65
|
+
- - '='
|
66
66
|
- !ruby/object:Gem::Version
|
67
|
-
version:
|
67
|
+
version: 2.1.0
|
68
68
|
- !ruby/object:Gem::Dependency
|
69
|
-
name:
|
69
|
+
name: rake
|
70
70
|
requirement: !ruby/object:Gem::Requirement
|
71
71
|
requirements:
|
72
|
-
- -
|
72
|
+
- - "~>"
|
73
73
|
- !ruby/object:Gem::Version
|
74
|
-
version:
|
74
|
+
version: '12.0'
|
75
75
|
type: :development
|
76
76
|
prerelease: false
|
77
77
|
version_requirements: !ruby/object:Gem::Requirement
|
78
78
|
requirements:
|
79
|
-
- -
|
79
|
+
- - "~>"
|
80
80
|
- !ruby/object:Gem::Version
|
81
|
-
version:
|
81
|
+
version: '12.0'
|
82
82
|
- !ruby/object:Gem::Dependency
|
83
|
-
name:
|
83
|
+
name: sqlite3
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
85
85
|
requirements:
|
86
86
|
- - '='
|
87
87
|
- !ruby/object:Gem::Version
|
88
|
-
version:
|
88
|
+
version: 1.6.9
|
89
89
|
type: :development
|
90
90
|
prerelease: false
|
91
91
|
version_requirements: !ruby/object:Gem::Requirement
|
92
92
|
requirements:
|
93
93
|
- - '='
|
94
94
|
- !ruby/object:Gem::Version
|
95
|
-
version:
|
95
|
+
version: 1.6.9
|
96
96
|
description: LedgerAccountable is a gem for recording ledger entries to store an accounting
|
97
97
|
history in your Rails models.
|
98
98
|
email:
|
@@ -109,6 +109,10 @@ files:
|
|
109
109
|
- lib/generators/ledger_accountable/templates/ledger_entry.rb
|
110
110
|
- lib/generators/ledger_accountable/templates/migration.rb
|
111
111
|
- lib/ledger_accountable.rb
|
112
|
+
- lib/ledger_accountable/ledger_item.rb
|
113
|
+
- lib/ledger_accountable/ledger_item/entry_creator.rb
|
114
|
+
- lib/ledger_accountable/ledger_item/state_transition.rb
|
115
|
+
- lib/ledger_accountable/ledger_owner.rb
|
112
116
|
- lib/ledger_accountable/version.rb
|
113
117
|
- lib/locale/en.yml
|
114
118
|
homepage: https://github.com/bmaclean/ledger-accountable
|
@@ -117,6 +121,7 @@ metadata:
|
|
117
121
|
homepage_uri: https://github.com/bmaclean/ledger-accountable
|
118
122
|
source_code_uri: https://github.com/bmaclean/ledger-accountable
|
119
123
|
changelog_uri: https://github.com/bmaclean/ledger-accountable/blob/main/CHANGELOG.md
|
124
|
+
rubygems_mfa_required: 'false'
|
120
125
|
post_install_message:
|
121
126
|
rdoc_options: []
|
122
127
|
require_paths:
|
@@ -125,14 +130,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
125
130
|
requirements:
|
126
131
|
- - ">="
|
127
132
|
- !ruby/object:Gem::Version
|
128
|
-
version:
|
133
|
+
version: 3.0.6
|
129
134
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
130
135
|
requirements:
|
131
136
|
- - ">"
|
132
137
|
- !ruby/object:Gem::Version
|
133
138
|
version: 1.3.1
|
134
139
|
requirements: []
|
135
|
-
rubygems_version: 3.
|
140
|
+
rubygems_version: 3.2.33
|
136
141
|
signing_key:
|
137
142
|
specification_version: 4
|
138
143
|
summary: Ledger accounting for Rails models
|