double_entry 1.0.1 → 2.0.0.beta5

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 (79) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +497 -0
  3. data/README.md +107 -44
  4. data/double_entry.gemspec +22 -49
  5. data/lib/active_record/locking_extensions.rb +3 -3
  6. data/lib/active_record/locking_extensions/log_subscriber.rb +1 -1
  7. data/lib/double_entry.rb +29 -21
  8. data/lib/double_entry/account.rb +39 -46
  9. data/lib/double_entry/account_balance.rb +20 -3
  10. data/lib/double_entry/balance_calculator.rb +5 -5
  11. data/lib/double_entry/configurable.rb +1 -0
  12. data/lib/double_entry/configuration.rb +8 -2
  13. data/lib/double_entry/errors.rb +13 -13
  14. data/lib/double_entry/line.rb +7 -6
  15. data/lib/double_entry/locking.rb +5 -5
  16. data/lib/double_entry/transfer.rb +37 -30
  17. data/lib/double_entry/validation.rb +1 -0
  18. data/lib/double_entry/validation/account_fixer.rb +36 -0
  19. data/lib/double_entry/validation/line_check.rb +25 -43
  20. data/lib/double_entry/version.rb +1 -1
  21. data/lib/generators/double_entry/install/install_generator.rb +22 -1
  22. data/lib/generators/double_entry/install/templates/initializer.rb +20 -0
  23. data/lib/generators/double_entry/install/templates/migration.rb +45 -55
  24. metadata +35 -256
  25. data/.gitignore +0 -32
  26. data/.rspec +0 -2
  27. data/.travis.yml +0 -29
  28. data/.yardopts +0 -2
  29. data/Gemfile +0 -2
  30. data/Rakefile +0 -15
  31. data/lib/double_entry/reporting.rb +0 -181
  32. data/lib/double_entry/reporting/aggregate.rb +0 -110
  33. data/lib/double_entry/reporting/aggregate_array.rb +0 -76
  34. data/lib/double_entry/reporting/day_range.rb +0 -42
  35. data/lib/double_entry/reporting/hour_range.rb +0 -45
  36. data/lib/double_entry/reporting/line_aggregate.rb +0 -16
  37. data/lib/double_entry/reporting/line_aggregate_filter.rb +0 -79
  38. data/lib/double_entry/reporting/month_range.rb +0 -94
  39. data/lib/double_entry/reporting/time_range.rb +0 -59
  40. data/lib/double_entry/reporting/time_range_array.rb +0 -49
  41. data/lib/double_entry/reporting/week_range.rb +0 -107
  42. data/lib/double_entry/reporting/year_range.rb +0 -40
  43. data/script/jack_hammer +0 -210
  44. data/script/setup.sh +0 -8
  45. data/spec/active_record/locking_extensions_spec.rb +0 -110
  46. data/spec/double_entry/account_balance_spec.rb +0 -7
  47. data/spec/double_entry/account_spec.rb +0 -130
  48. data/spec/double_entry/balance_calculator_spec.rb +0 -88
  49. data/spec/double_entry/configuration_spec.rb +0 -50
  50. data/spec/double_entry/line_spec.rb +0 -80
  51. data/spec/double_entry/locking_spec.rb +0 -214
  52. data/spec/double_entry/performance/double_entry_performance_spec.rb +0 -32
  53. data/spec/double_entry/performance/reporting/aggregate_performance_spec.rb +0 -50
  54. data/spec/double_entry/reporting/aggregate_array_spec.rb +0 -123
  55. data/spec/double_entry/reporting/aggregate_spec.rb +0 -205
  56. data/spec/double_entry/reporting/line_aggregate_filter_spec.rb +0 -90
  57. data/spec/double_entry/reporting/line_aggregate_spec.rb +0 -39
  58. data/spec/double_entry/reporting/month_range_spec.rb +0 -139
  59. data/spec/double_entry/reporting/time_range_array_spec.rb +0 -169
  60. data/spec/double_entry/reporting/time_range_spec.rb +0 -45
  61. data/spec/double_entry/reporting/week_range_spec.rb +0 -103
  62. data/spec/double_entry/reporting_spec.rb +0 -181
  63. data/spec/double_entry/transfer_spec.rb +0 -93
  64. data/spec/double_entry/validation/line_check_spec.rb +0 -99
  65. data/spec/double_entry_spec.rb +0 -428
  66. data/spec/generators/double_entry/install/install_generator_spec.rb +0 -30
  67. data/spec/spec_helper.rb +0 -118
  68. data/spec/support/accounts.rb +0 -21
  69. data/spec/support/blueprints.rb +0 -43
  70. data/spec/support/database.example.yml +0 -21
  71. data/spec/support/database.travis.yml +0 -24
  72. data/spec/support/double_entry_spec_helper.rb +0 -27
  73. data/spec/support/gemfiles/Gemfile.rails-3.2.x +0 -8
  74. data/spec/support/gemfiles/Gemfile.rails-4.1.x +0 -6
  75. data/spec/support/gemfiles/Gemfile.rails-4.2.x +0 -5
  76. data/spec/support/gemfiles/Gemfile.rails-5.0.x +0 -5
  77. data/spec/support/performance_helper.rb +0 -26
  78. data/spec/support/reporting_configuration.rb +0 -6
  79. data/spec/support/schema.rb +0 -74
@@ -1,88 +0,0 @@
1
- # encoding: utf-8
2
-
3
- RSpec.describe DoubleEntry::BalanceCalculator do
4
- describe '#calculate' do
5
- let(:account) { DoubleEntry.account(:test, :scope => scope) }
6
- let(:scope) { User.make! }
7
- let(:from) { nil }
8
- let(:to) { nil }
9
- let(:at) { nil }
10
- let(:code) { nil }
11
- let(:codes) { nil }
12
- let(:relation) { double.as_null_object }
13
-
14
- before do
15
- allow(DoubleEntry::Line).to receive(:where).and_return(relation)
16
- DoubleEntry::BalanceCalculator.calculate(
17
- account,
18
- :scope => scope,
19
- :from => from,
20
- :to => to,
21
- :at => at,
22
- :code => code,
23
- :codes => codes,
24
- )
25
- end
26
-
27
- describe 'what happens with different times' do
28
- context 'when we want to sum the lines before a given created_at date' do
29
- let(:at) { Time.parse('2014-06-19 15:09:18 +1000') }
30
-
31
- it 'scopes the lines summed to times before (or at) the given time' do
32
- expect(relation).to have_received(:where).with(
33
- 'created_at <= ?', Time.parse('2014-06-19 15:09:18 +1000')
34
- )
35
- end
36
-
37
- context 'when a time range is also specified' do
38
- let(:from) { Time.parse('2014-06-19 10:09:18 +1000') }
39
- let(:to) { Time.parse('2014-06-19 20:09:18 +1000') }
40
-
41
- it 'ignores the time range when summing the lines' do
42
- expect(relation).to_not have_received(:where).with(
43
- :created_at => Time.parse('2014-06-19 10:09:18 +1000')..Time.parse('2014-06-19 20:09:18 +1000'),
44
- )
45
- expect(relation).to_not have_received(:sum)
46
- end
47
- end
48
- end
49
-
50
- context 'when we want to sum the lines between a given range' do
51
- let(:from) { Time.parse('2014-06-19 10:09:18 +1000') }
52
- let(:to) { Time.parse('2014-06-19 20:09:18 +1000') }
53
-
54
- it 'scopes the lines summed to times within the given range' do
55
- expect(relation).to have_received(:where).with(
56
- :created_at => Time.parse('2014-06-19 10:09:18 +1000')..Time.parse('2014-06-19 20:09:18 +1000'),
57
- )
58
- expect(relation).to have_received(:sum).with(:amount)
59
- end
60
- end
61
- end
62
-
63
- context 'when a single code is provided' do
64
- let(:code) { 'code1' }
65
-
66
- it 'scopes the lines summed by the given code' do
67
- expect(relation).to have_received(:where).with(:code => ['code1'])
68
- expect(relation).to have_received(:sum).with(:amount)
69
- end
70
- end
71
-
72
- context 'when a list of codes is provided' do
73
- let(:codes) { %w(code1 code2) }
74
-
75
- it 'scopes the lines summed by the given codes' do
76
- expect(relation).to have_received(:where).with(:code => %w(code1 code2))
77
- expect(relation).to have_received(:sum).with(:amount)
78
- end
79
- end
80
-
81
- context 'when no codes are provided' do
82
- it 'does not scope the lines summed by any code' do
83
- expect(relation).to_not have_received(:where).with(:code => anything)
84
- expect(relation).to_not have_received(:sum).with(:amount)
85
- end
86
- end
87
- end
88
- end
@@ -1,50 +0,0 @@
1
- # encoding: utf-8
2
- RSpec.describe DoubleEntry::Configuration do
3
- its(:accounts) { should be_a DoubleEntry::Account::Set }
4
- its(:transfers) { should be_a DoubleEntry::Transfer::Set }
5
-
6
- describe 'max lengths' do
7
- context 'given a max length has not been set' do
8
- its(:code_max_length) { should be 47 }
9
- its(:scope_identifier_max_length) { should be 23 }
10
- its(:account_identifier_max_length) { should be 31 }
11
- end
12
-
13
- context 'given a code max length of 10 has been set' do
14
- before { subject.code_max_length = 10 }
15
- its(:code_max_length) { should be 10 }
16
- end
17
-
18
- context 'given a scope identifier max length of 11 has been set' do
19
- before { subject.scope_identifier_max_length = 11 }
20
- its(:scope_identifier_max_length) { should be 11 }
21
- end
22
-
23
- context 'given an account identifier max length of 9 has been set' do
24
- before { subject.account_identifier_max_length = 9 }
25
- its(:account_identifier_max_length) { should be 9 }
26
- end
27
-
28
- after do
29
- subject.code_max_length = nil
30
- subject.scope_identifier_max_length = nil
31
- subject.account_identifier_max_length = nil
32
- end
33
- end
34
-
35
- describe '#define_accounts' do
36
- it 'yields the accounts set' do
37
- expect do |block|
38
- subject.define_accounts(&block)
39
- end.to yield_with_args(be_a DoubleEntry::Account::Set)
40
- end
41
- end
42
-
43
- describe '#define_transfers' do
44
- it 'yields the transfers set' do
45
- expect do |block|
46
- subject.define_transfers(&block)
47
- end.to yield_with_args(be_a DoubleEntry::Transfer::Set)
48
- end
49
- end
50
- end
@@ -1,80 +0,0 @@
1
- # encoding: utf-8
2
- RSpec.describe DoubleEntry::Line do
3
- it 'has a table name prefixed with double_entry_' do
4
- expect(DoubleEntry::Line.table_name).to eq 'double_entry_lines'
5
- end
6
-
7
- describe 'persistance' do
8
- let(:line_to_persist) do
9
- DoubleEntry::Line.new(
10
- :amount => Money.new(10_00),
11
- :balance => Money.zero,
12
- :account => account,
13
- :partner_account => partner_account,
14
- :code => code,
15
- )
16
- end
17
- let(:account) { DoubleEntry.account(:test, :scope => '17') }
18
- let(:partner_account) { DoubleEntry.account(:test, :scope => '72') }
19
- let(:code) { :test_code }
20
-
21
- subject(:persisted_line) do
22
- line_to_persist.save!
23
- line_to_persist.reload
24
- end
25
-
26
- describe 'attributes' do
27
- context 'given code = :the_code' do
28
- let(:code) { :the_code }
29
- its(:code) { should eq :the_code }
30
- end
31
-
32
- context 'given code = nil' do
33
- let(:code) { nil }
34
- specify { expect { line_to_persist.save! }.to raise_error }
35
- end
36
-
37
- context 'given account = :test, 54 ' do
38
- let(:account) { DoubleEntry.account(:test, :scope => '54') }
39
- its('account.account.identifier') { should eq :test }
40
- its('account.scope') { should eq '54' }
41
- end
42
-
43
- context 'given partner_account = :test, 91 ' do
44
- let(:partner_account) { DoubleEntry.account(:test, :scope => '91') }
45
- its('partner_account.account.identifier') { should eq :test }
46
- its('partner_account.scope') { should eq '91' }
47
- end
48
-
49
- context 'currency' do
50
- let(:account) { DoubleEntry.account(:btc_test, :scope => '17') }
51
- let(:partner_account) { DoubleEntry.account(:btc_test, :scope => '72') }
52
- its(:currency) { should eq 'BTC' }
53
- end
54
- end
55
-
56
- context 'when balance is sent negative' do
57
- before { DoubleEntry::Account.accounts.define(:identifier => :a_positive_only_acc, :positive_only => true) }
58
- let(:account) { DoubleEntry.account(:a_positive_only_acc) }
59
- let(:line) { DoubleEntry::Line.new(:balance => Money.new(-1), :account => account) }
60
-
61
- it 'raises AccountWouldBeSentNegative error' do
62
- expect { line.save }.to raise_error DoubleEntry::AccountWouldBeSentNegative
63
- end
64
- end
65
-
66
- context 'when balance is sent positive' do
67
- before { DoubleEntry::Account.accounts.define(:identifier => :a_negative_only_acc, :negative_only => true) }
68
- let(:account) { DoubleEntry.account(:a_negative_only_acc) }
69
- let(:line) { DoubleEntry::Line.new(:balance => Money.new(1), :account => account) }
70
-
71
- it 'raises AccountWouldBeSentPositiveError' do
72
- expect { line.save }.to raise_error DoubleEntry::AccountWouldBeSentPositiveError
73
- end
74
- end
75
-
76
- it 'has a table name prefixed with double_entry_' do
77
- expect(DoubleEntry::Line.table_name).to eq 'double_entry_lines'
78
- end
79
- end
80
- end
@@ -1,214 +0,0 @@
1
- # encoding: utf-8
2
-
3
- RSpec.describe DoubleEntry::Locking do
4
- before do
5
- @config_accounts = DoubleEntry.configuration.accounts
6
- @config_transfers = DoubleEntry.configuration.transfers
7
- DoubleEntry.configuration.accounts = DoubleEntry::Account::Set.new
8
- DoubleEntry.configuration.transfers = DoubleEntry::Transfer::Set.new
9
- end
10
-
11
- after do
12
- DoubleEntry.configuration.accounts = @config_accounts
13
- DoubleEntry.configuration.transfers = @config_transfers
14
- end
15
-
16
- before do
17
- scope = ->(x) { x }
18
-
19
- DoubleEntry.configure do |config|
20
- config.define_accounts do |accounts|
21
- accounts.define(:identifier => :account_a, :scope_identifier => scope)
22
- accounts.define(:identifier => :account_b, :scope_identifier => scope)
23
- accounts.define(:identifier => :account_c, :scope_identifier => scope)
24
- accounts.define(:identifier => :account_d, :scope_identifier => scope)
25
- accounts.define(:identifier => :account_e)
26
- end
27
-
28
- config.define_transfers do |transfers|
29
- transfers.define(:from => :account_a, :to => :account_b, :code => :test)
30
- transfers.define(:from => :account_c, :to => :account_d, :code => :test)
31
- end
32
- end
33
-
34
- @account_a = DoubleEntry.account(:account_a, :scope => '1')
35
- @account_b = DoubleEntry.account(:account_b, :scope => '2')
36
- @account_c = DoubleEntry.account(:account_c, :scope => '3')
37
- @account_d = DoubleEntry.account(:account_d, :scope => '4')
38
- @account_e = DoubleEntry.account(:account_e)
39
- end
40
-
41
- it 'creates missing account balance records' do
42
- expect do
43
- DoubleEntry::Locking.lock_accounts(@account_a) {}
44
- end.to change(DoubleEntry::AccountBalance, :count).by(1)
45
-
46
- account_balance = DoubleEntry::AccountBalance.find_by_account(@account_a)
47
- expect(account_balance).to_not be_nil
48
- expect(account_balance.balance).to eq Money.new(0)
49
- end
50
-
51
- it 'takes the balance for new account balance records from the lines table' do
52
- DoubleEntry::Line.create!(
53
- :account => @account_a,
54
- :partner_account => @account_b,
55
- :amount => Money.new(3_00),
56
- :balance => Money.new(3_00),
57
- :code => :test,
58
- )
59
- DoubleEntry::Line.create!(
60
- :account => @account_a,
61
- :partner_account => @account_b,
62
- :amount => Money.new(7_00),
63
- :balance => Money.new(10_00),
64
- :code => :test,
65
- )
66
-
67
- expect do
68
- DoubleEntry::Locking.lock_accounts(@account_a) {}
69
- end.to change(DoubleEntry::AccountBalance, :count).by(1)
70
-
71
- account_balance = DoubleEntry::AccountBalance.find_by_account(@account_a)
72
- expect(account_balance).to_not be_nil
73
- expect(account_balance.balance).to eq Money.new(10_00)
74
- end
75
-
76
- it 'prohibits locking inside a regular transaction' do
77
- expect do
78
- DoubleEntry::AccountBalance.transaction do
79
- DoubleEntry::Locking.lock_accounts(@account_a, @account_b) do
80
- end
81
- end
82
- end.to raise_error(DoubleEntry::Locking::LockMustBeOutermostTransaction)
83
- end
84
-
85
- it 'prohibits a transfer inside a regular transaction' do
86
- expect do
87
- DoubleEntry::AccountBalance.transaction do
88
- DoubleEntry.transfer(Money.new(10_00), :from => @account_a, :to => @account_b, :code => :test)
89
- end
90
- end.to raise_error(DoubleEntry::Locking::LockMustBeOutermostTransaction)
91
- end
92
-
93
- it "allows a transfer inside a lock if we've locked the transaction accounts" do
94
- expect do
95
- DoubleEntry::Locking.lock_accounts(@account_a, @account_b) do
96
- DoubleEntry.transfer(Money.new(10_00), :from => @account_a, :to => @account_b, :code => :test)
97
- end
98
- end.to_not raise_error
99
- end
100
-
101
- it "does not allow a transfer inside a lock if the right locks aren't held" do
102
- expect do
103
- DoubleEntry::Locking.lock_accounts(@account_a, @account_c) do
104
- DoubleEntry.transfer(Money.new(10_00), :from => @account_a, :to => @account_b, :code => :test)
105
- end
106
- end.to raise_error(DoubleEntry::Locking::LockNotHeld, 'No lock held for account: account_b, scope 2')
107
- end
108
-
109
- it 'allows nested locks if the outer lock locks all the accounts' do
110
- expect do
111
- DoubleEntry::Locking.lock_accounts(@account_a, @account_b) do
112
- DoubleEntry::Locking.lock_accounts(@account_a, @account_b) {}
113
- end
114
- end.to_not raise_error
115
- end
116
-
117
- it "prohibits nested locks if the out lock doesn't lock all the accounts" do
118
- expect do
119
- DoubleEntry::Locking.lock_accounts(@account_a) do
120
- DoubleEntry::Locking.lock_accounts(@account_a, @account_b) {}
121
- end
122
- end.to raise_error(DoubleEntry::Locking::LockNotHeld, 'No lock held for account: account_b, scope 2')
123
- end
124
-
125
- it 'rolls back a locking transaction' do
126
- DoubleEntry::Locking.lock_accounts(@account_a, @account_b) do
127
- DoubleEntry.transfer(Money.new(10_00), :from => @account_a, :to => @account_b, :code => :test)
128
- fail ActiveRecord::Rollback
129
- end
130
- expect(DoubleEntry.balance(@account_a)).to eq Money.new(0)
131
- expect(DoubleEntry.balance(@account_b)).to eq Money.new(0)
132
- end
133
-
134
- it "rolls back a locking transaction if there's an exception" do
135
- expect do
136
- DoubleEntry::Locking.lock_accounts(@account_a, @account_b) do
137
- DoubleEntry.transfer(Money.new(10_00), :from => @account_a, :to => @account_b, :code => :test)
138
- fail 'Yeah, right'
139
- end
140
- end.to raise_error('Yeah, right')
141
- expect(DoubleEntry.balance(@account_a)).to eq Money.new(0)
142
- expect(DoubleEntry.balance(@account_b)).to eq Money.new(0)
143
- end
144
-
145
- it 'allows locking a scoped account and a non scoped account' do
146
- expect do
147
- DoubleEntry::Locking.lock_accounts(@account_d, @account_e) {}
148
- end.to_not raise_error
149
- end
150
-
151
- context 'handling ActiveRecord::StatementInvalid errors' do
152
- context 'non lock wait timeout errors' do
153
- let(:error) { ActiveRecord::StatementInvalid.new('some other error') }
154
- before do
155
- allow(DoubleEntry::AccountBalance).to receive(:with_restart_on_deadlock).
156
- and_raise(error)
157
- end
158
-
159
- it 're-raises the ActiveRecord::StatementInvalid error' do
160
- expect do
161
- DoubleEntry::Locking.lock_accounts(@account_d, @account_e) {}
162
- end.to raise_error(error)
163
- end
164
- end
165
-
166
- context 'lock wait timeout errors' do
167
- before do
168
- allow(DoubleEntry::AccountBalance).to receive(:with_restart_on_deadlock).
169
- and_raise(ActiveRecord::StatementInvalid, 'lock wait timeout')
170
- end
171
-
172
- it 'raises a LockWaitTimeout error' do
173
- expect do
174
- DoubleEntry::Locking.lock_accounts(@account_d, @account_e) {}
175
- end.to raise_error(DoubleEntry::Locking::LockWaitTimeout)
176
- end
177
- end
178
- end
179
-
180
- # sqlite cannot handle these cases so they don't run when DB=sqlite
181
- describe 'concurrent locking', :unless => ENV['DB'] == 'sqlite' do
182
- it 'allows multiple threads to lock at the same time' do
183
- expect do
184
- threads = []
185
-
186
- threads << Thread.new do
187
- sleep 0.05
188
- DoubleEntry::Locking.lock_accounts(@account_a, @account_b) do
189
- DoubleEntry.transfer(Money.new(10_00), :from => @account_a, :to => @account_b, :code => :test)
190
- end
191
- end
192
-
193
- threads << Thread.new do
194
- DoubleEntry::Locking.lock_accounts(@account_c, @account_d) do
195
- sleep 0.1
196
- DoubleEntry.transfer(Money.new(10_00), :from => @account_c, :to => @account_d, :code => :test)
197
- end
198
- end
199
-
200
- threads.each(&:join)
201
- end.to_not raise_error
202
- end
203
-
204
- it 'allows multiple threads to lock accounts without balances at the same time' do
205
- threads = []
206
- expect do
207
- threads << Thread.new { DoubleEntry::Locking.lock_accounts(@account_a, @account_b) { sleep 0.1 } }
208
- threads << Thread.new { DoubleEntry::Locking.lock_accounts(@account_c, @account_d) { sleep 0.1 } }
209
-
210
- threads.each(&:join)
211
- end.to_not raise_error
212
- end
213
- end
214
- end
@@ -1,32 +0,0 @@
1
- module DoubleEntry
2
- RSpec.describe DoubleEntry do
3
- describe 'transfer performance' do
4
- include PerformanceHelper
5
- let(:user) { User.make! }
6
- let(:amount) { Money.new(10_00) }
7
- let(:test) { DoubleEntry.account(:test, :scope => user) }
8
- let(:savings) { DoubleEntry.account(:savings, :scope => user) }
9
-
10
- it 'creates a lot of transfers quickly without metadata' do
11
- profile_transfers_with_metadata(nil)
12
- # local results: 6.44, 5.93, 5.94
13
- end
14
-
15
- it 'creates a lot of transfers quickly with metadata' do
16
- big_metadata = {}
17
- 8.times { |i| big_metadata["key#{i}".to_sym] = "value#{i}" }
18
- profile_transfers_with_metadata(big_metadata)
19
- # local results: 21.2, 21.6, 20.9
20
- end
21
- end
22
-
23
- def profile_transfers_with_metadata(metadata)
24
- start_profiling
25
- options = { :from => test, :to => savings, :code => :bonus }
26
- options[:metadata] = metadata if metadata
27
- 100.times { Transfer.transfer(amount, options) }
28
- profile_name = metadata ? 'transfer-with-metadata' : 'transfer'
29
- stop_profiling(profile_name)
30
- end
31
- end
32
- end