double_entry 1.0.1 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +432 -0
  3. data/README.md +36 -9
  4. data/double_entry.gemspec +20 -48
  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/account.rb +38 -45
  8. data/lib/double_entry/account_balance.rb +18 -1
  9. data/lib/double_entry/errors.rb +13 -13
  10. data/lib/double_entry/line.rb +3 -2
  11. data/lib/double_entry/reporting.rb +26 -38
  12. data/lib/double_entry/reporting/aggregate.rb +43 -23
  13. data/lib/double_entry/reporting/aggregate_array.rb +16 -13
  14. data/lib/double_entry/reporting/line_aggregate.rb +3 -2
  15. data/lib/double_entry/reporting/line_aggregate_filter.rb +8 -10
  16. data/lib/double_entry/reporting/line_metadata_filter.rb +33 -0
  17. data/lib/double_entry/transfer.rb +33 -27
  18. data/lib/double_entry/validation.rb +1 -0
  19. data/lib/double_entry/validation/account_fixer.rb +36 -0
  20. data/lib/double_entry/validation/line_check.rb +22 -40
  21. data/lib/double_entry/version.rb +1 -1
  22. data/lib/generators/double_entry/install/install_generator.rb +7 -1
  23. data/lib/generators/double_entry/install/templates/migration.rb +27 -25
  24. metadata +33 -243
  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/script/jack_hammer +0 -210
  32. data/script/setup.sh +0 -8
  33. data/spec/active_record/locking_extensions_spec.rb +0 -110
  34. data/spec/double_entry/account_balance_spec.rb +0 -7
  35. data/spec/double_entry/account_spec.rb +0 -130
  36. data/spec/double_entry/balance_calculator_spec.rb +0 -88
  37. data/spec/double_entry/configuration_spec.rb +0 -50
  38. data/spec/double_entry/line_spec.rb +0 -80
  39. data/spec/double_entry/locking_spec.rb +0 -214
  40. data/spec/double_entry/performance/double_entry_performance_spec.rb +0 -32
  41. data/spec/double_entry/performance/reporting/aggregate_performance_spec.rb +0 -50
  42. data/spec/double_entry/reporting/aggregate_array_spec.rb +0 -123
  43. data/spec/double_entry/reporting/aggregate_spec.rb +0 -205
  44. data/spec/double_entry/reporting/line_aggregate_filter_spec.rb +0 -90
  45. data/spec/double_entry/reporting/line_aggregate_spec.rb +0 -39
  46. data/spec/double_entry/reporting/month_range_spec.rb +0 -139
  47. data/spec/double_entry/reporting/time_range_array_spec.rb +0 -169
  48. data/spec/double_entry/reporting/time_range_spec.rb +0 -45
  49. data/spec/double_entry/reporting/week_range_spec.rb +0 -103
  50. data/spec/double_entry/reporting_spec.rb +0 -181
  51. data/spec/double_entry/transfer_spec.rb +0 -93
  52. data/spec/double_entry/validation/line_check_spec.rb +0 -99
  53. data/spec/double_entry_spec.rb +0 -428
  54. data/spec/generators/double_entry/install/install_generator_spec.rb +0 -30
  55. data/spec/spec_helper.rb +0 -118
  56. data/spec/support/accounts.rb +0 -21
  57. data/spec/support/blueprints.rb +0 -43
  58. data/spec/support/database.example.yml +0 -21
  59. data/spec/support/database.travis.yml +0 -24
  60. data/spec/support/double_entry_spec_helper.rb +0 -27
  61. data/spec/support/gemfiles/Gemfile.rails-3.2.x +0 -8
  62. data/spec/support/gemfiles/Gemfile.rails-4.1.x +0 -6
  63. data/spec/support/gemfiles/Gemfile.rails-4.2.x +0 -5
  64. data/spec/support/gemfiles/Gemfile.rails-5.0.x +0 -5
  65. data/spec/support/performance_helper.rb +0 -26
  66. data/spec/support/reporting_configuration.rb +0 -6
  67. 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