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,50 +0,0 @@
1
- module DoubleEntry
2
- module Reporting
3
- RSpec.describe Aggregate 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
- subject(:transfer) { Transfer.transfer(amount, options) }
11
-
12
- context '200 transfers in a single day, half with metadata' do
13
- # Surprisingly, the number of transfers makes no difference to the time taken to aggregate them. Some sample results:
14
- # 20,000 => 524ms
15
- # 10,000 => 573ms
16
- # 1,000 => 486ms
17
- # 100 => 608ms
18
- # 10 => 509ms
19
- # 1 => 473ms
20
- before do
21
- Timecop.freeze Time.local(2015, 06, 30) do
22
- 100.times { Transfer.transfer(amount, :from => test, :to => savings, :code => :bonus) }
23
- 100.times { Transfer.transfer(amount, :from => test, :to => savings, :code => :bonus, :metadata => { :country => 'AU', :tax => 'GST' }) }
24
- end
25
- end
26
-
27
- it 'calculates monthly all_time ranges quickly without a filter' do
28
- profile_aggregation_with_filter(nil)
29
- # local results: 517ms, 484ms, 505ms, 482ms, 525ms
30
- end
31
-
32
- it 'calculates monthly all_time ranges quickly with a filter' do
33
- profile_aggregation_with_filter([:metadata => { :country => 'AU' }])
34
- # local results when run independently (caching improves performance when run consecutively):
35
- # 655ms, 613ms, 597ms, 607ms, 627ms
36
- end
37
- end
38
-
39
- def profile_aggregation_with_filter(filter)
40
- start_profiling
41
- range = TimeRange.make(:year => 2015, :month => 06, :range_type => :all_time)
42
- options = {}
43
- options[:filter] = filter if filter
44
- Reporting.aggregate(:sum, :savings, :bonus, range, options)
45
- profile_name = filter ? 'aggregate-with-metadata' : 'aggregate'
46
- stop_profiling(profile_name)
47
- end
48
- end
49
- end
50
- end
@@ -1,123 +0,0 @@
1
- module DoubleEntry
2
- module Reporting
3
- RSpec.describe AggregateArray do
4
- let(:user) { User.make! }
5
- let(:start) { nil }
6
- let(:finish) { nil }
7
- let(:range_type) { 'year' }
8
- let(:function) { 'sum' }
9
- let(:account) { :savings }
10
- let(:transfer_code) { :bonus }
11
- subject(:aggregate_array) do
12
- AggregateArray.new(
13
- function,
14
- account,
15
- transfer_code,
16
- :range_type => range_type,
17
- :start => start,
18
- :finish => finish,
19
- )
20
- end
21
-
22
- context 'given a deposit was made in 2007 and 2008' do
23
- before do
24
- Timecop.travel(Time.local(2007)) do
25
- perform_deposit user, 10_00
26
- end
27
- Timecop.travel(Time.local(2008)) do
28
- perform_deposit user, 20_00
29
- end
30
- end
31
-
32
- context 'given the date is 2009-03-19' do
33
- before { Timecop.travel(Time.local(2009, 3, 19)) }
34
-
35
- context 'when called with range type of "year"' do
36
- let(:range_type) { 'year' }
37
- let(:start) { '2006-08-03' }
38
- it { should eq [Money.zero, Money.new(10_00), Money.new(20_00), Money.zero] }
39
-
40
- describe 'reuse of aggregates' do
41
- let(:years) { TimeRangeArray.make(range_type, start, finish) }
42
-
43
- context 'and some aggregates were created previously' do
44
- before do
45
- Aggregate.formatted_amount(function, account, transfer_code, years[0])
46
- Aggregate.formatted_amount(function, account, transfer_code, years[1])
47
- allow(Aggregate).to receive(:formatted_amount)
48
- end
49
-
50
- it 'only asks Aggregate for the non-existent ones' do
51
- expect(Aggregate).not_to receive(:formatted_amount).with(function, account, transfer_code, years[0], :filter => nil)
52
- expect(Aggregate).not_to receive(:formatted_amount).with(function, account, transfer_code, years[1], :filter => nil)
53
-
54
- expect(Aggregate).to receive(:formatted_amount).with(function, account, transfer_code, years[2], :filter => nil)
55
- expect(Aggregate).to receive(:formatted_amount).with(function, account, transfer_code, years[3], :filter => nil)
56
- aggregate_array
57
- end
58
- end
59
- end
60
- end
61
- end
62
- end
63
-
64
- context 'given a deposit was made in October and December 2006' do
65
- before do
66
- Timecop.travel(Time.local(2006, 10)) do
67
- perform_deposit user, 10_00
68
- end
69
- Timecop.travel(Time.local(2006, 12)) do
70
- perform_deposit user, 20_00
71
- end
72
- end
73
-
74
- context 'when called with range type of "month", a start of "2006-09-01", and finish of "2007-01-02"' do
75
- let(:range_type) { 'month' }
76
- let(:start) { '2006-09-01' }
77
- let(:finish) { '2007-01-02' }
78
- it { should eq [Money.zero, Money.new(10_00), Money.zero, Money.new(20_00), Money.zero] }
79
- end
80
-
81
- context 'given the date is 2007-02-02' do
82
- before { Timecop.travel(Time.local(2007, 2, 2)) }
83
-
84
- context 'when called with range type of "month"' do
85
- let(:range_type) { 'month' }
86
- let(:start) { '2006-08-03' }
87
- it { should eq [Money.zero, Money.zero, Money.new(10_00), Money.zero, Money.new(20_00), Money.zero, Money.zero] }
88
- end
89
- end
90
- end
91
-
92
- context 'when account is in BTC currency' do
93
- let(:account) { :btc_savings }
94
- let(:range_type) { 'year' }
95
- let(:start) { "#{Time.now.year}-01-01" }
96
- let(:transfer_code) { :btc_test_transfer }
97
-
98
- before do
99
- perform_btc_deposit(user, 100_000_000)
100
- perform_btc_deposit(user, 100_000_000)
101
- end
102
-
103
- it { should eq [Money.new(200_000_000, :btc)] }
104
- end
105
-
106
- context 'when called with range type of "invalid_and_should_not_work"' do
107
- let(:range_type) { 'invalid_and_should_not_work' }
108
- it 'raises an argument error' do
109
- expect { aggregate_array }.to raise_error ArgumentError, "Invalid range type 'invalid_and_should_not_work'"
110
- end
111
- end
112
-
113
- context 'when an invalid function is provided' do
114
- let(:range_type) { 'month' }
115
- let(:start) { '2006-08-03' }
116
- let(:function) { :invalid_function }
117
- it 'raises an AggregateFunctionNotSupported error' do
118
- expect { aggregate_array }.to raise_error AggregateFunctionNotSupported
119
- end
120
- end
121
- end
122
- end
123
- end
@@ -1,205 +0,0 @@
1
- # encoding: utf-8
2
- module DoubleEntry
3
- module Reporting
4
- RSpec.describe Aggregate do
5
- let(:user) { User.make! }
6
- let(:expected_weekly_average) do
7
- (Money.new(20_00) + Money.new(40_00) + Money.new(50_00)) / 3
8
- end
9
- let(:expected_monthly_average) do
10
- (Money.new(20_00) + Money.new(40_00) + Money.new(50_00) + Money.new(40_00) + Money.new(50_00)) / 5
11
- end
12
-
13
- before do
14
- # Thursday
15
- Timecop.freeze Time.local(2009, 10, 1) do
16
- perform_deposit user, 20_00
17
- end
18
-
19
- # Saturday
20
- Timecop.freeze Time.local(2009, 10, 3) do
21
- perform_deposit user, 40_00
22
- end
23
-
24
- Timecop.freeze Time.local(2009, 10, 10) do
25
- perform_deposit user, 50_00
26
- end
27
- Timecop.freeze Time.local(2009, 11, 1, 0, 59, 0) do
28
- perform_deposit user, 40_00
29
- end
30
- Timecop.freeze Time.local(2009, 11, 1, 1, 00, 0) do
31
- perform_deposit user, 50_00
32
- end
33
- end
34
-
35
- it 'should store the aggregate for quick retrieval' do
36
- Aggregate.new(:sum, :savings, :bonus, TimeRange.make(:year => 2009, :month => 10)).amount
37
- expect(LineAggregate.count).to eq 1
38
- end
39
-
40
- it 'should only store the aggregate once if it is requested more than once' do
41
- Aggregate.new(:sum, :savings, :bonus, TimeRange.make(:year => 2009, :month => 9)).amount
42
- Aggregate.new(:sum, :savings, :bonus, TimeRange.make(:year => 2009, :month => 9)).amount
43
- Aggregate.new(:sum, :savings, :bonus, TimeRange.make(:year => 2009, :month => 10)).amount
44
- expect(LineAggregate.count).to eq 2
45
- end
46
-
47
- it 'should calculate the complete year correctly' do
48
- amount = Aggregate.new(:sum, :savings, :bonus, TimeRange.make(:year => 2009)).formatted_amount
49
- expect(amount).to eq Money.new(200_00)
50
- end
51
-
52
- it 'should calculate seperate months correctly' do
53
- amount = Aggregate.new(:sum, :savings, :bonus, TimeRange.make(:year => 2009, :month => 10)).formatted_amount
54
- expect(amount).to eq Money.new(110_00)
55
-
56
- amount = Aggregate.new(:sum, :savings, :bonus, TimeRange.make(:year => 2009, :month => 11)).formatted_amount
57
- expect(amount).to eq Money.new(90_00)
58
- end
59
-
60
- it 'should calculate seperate weeks correctly' do
61
- # Week 40 - Mon Sep 28, 2009 to Sun Oct 4 2009
62
- amount = Aggregate.new(:sum, :savings, :bonus, TimeRange.make(:year => 2009, :week => 40)).formatted_amount
63
- expect(amount).to eq Money.new(60_00)
64
- end
65
-
66
- it 'should calculate seperate days correctly' do
67
- # 1 Nov 2009
68
- amount = Aggregate.new(:sum, :savings, :bonus, TimeRange.make(:year => 2009, :week => 44, :day => 7)).formatted_amount
69
- expect(amount).to eq Money.new(90_00)
70
- end
71
-
72
- it 'should calculate seperate hours correctly' do
73
- # 1 Nov 2009
74
- amount = Aggregate.new(:sum, :savings, :bonus, TimeRange.make(:year => 2009, :week => 44, :day => 7, :hour => 0)).formatted_amount
75
- expect(amount).to eq Money.new(40_00)
76
- amount = Aggregate.new(:sum, :savings, :bonus, TimeRange.make(:year => 2009, :week => 44, :day => 7, :hour => 1)).formatted_amount
77
- expect(amount).to eq Money.new(50_00)
78
- end
79
-
80
- it 'should calculate, but not store aggregates when the time range is still current' do
81
- Timecop.freeze Time.local(2009, 11, 21) do
82
- amount = Aggregate.new(:sum, :savings, :bonus, TimeRange.make(:year => 2009, :month => 11)).formatted_amount
83
- expect(amount).to eq Money.new(90_00)
84
- expect(LineAggregate.count).to eq 0
85
- end
86
- end
87
-
88
- it 'should calculate, but not store aggregates when the time range is in the future' do
89
- Timecop.freeze Time.local(2009, 11, 21) do
90
- amount = Aggregate.new(:sum, :savings, :bonus, TimeRange.make(:year => 2009, :month => 12)).formatted_amount
91
- expect(amount).to eq Money.new(0)
92
- expect(LineAggregate.count).to eq 0
93
- end
94
- end
95
-
96
- it 'should calculate monthly all_time ranges correctly' do
97
- amount = Aggregate.new(:sum, :savings, :bonus, TimeRange.make(:year => 2009, :month => 12, :range_type => :all_time)).formatted_amount
98
- expect(amount).to eq Money.new(200_00)
99
- end
100
-
101
- it 'calculates the average monthly all_time ranges correctly' do
102
- amount = Aggregate.new(:average, :savings, :bonus, TimeRange.make(:year => 2009, :month => 12, :range_type => :all_time)).formatted_amount
103
- expect(amount).to eq expected_monthly_average
104
- end
105
-
106
- it 'returns the correct count for weekly all_time ranges correctly' do
107
- amount = Aggregate.new(:count, :savings, :bonus, TimeRange.make(:year => 2009, :month => 12, :range_type => :all_time)).formatted_amount
108
- expect(amount).to eq 5
109
- end
110
-
111
- it 'should calculate weekly all_time ranges correctly' do
112
- amount = Aggregate.new(:sum, :savings, :bonus, TimeRange.make(:year => 2009, :week => 43, :range_type => :all_time)).formatted_amount
113
- expect(amount).to eq Money.new(110_00)
114
- end
115
-
116
- it 'calculates the average weekly all_time ranges correctly' do
117
- amount = Aggregate.new(:average, :savings, :bonus, TimeRange.make(:year => 2009, :week => 43, :range_type => :all_time)).formatted_amount
118
- expect(amount).to eq expected_weekly_average
119
- end
120
-
121
- it 'returns the correct count for weekly all_time ranges correctly' do
122
- amount = Aggregate.new(:count, :savings, :bonus, TimeRange.make(:year => 2009, :week => 43, :range_type => :all_time)).formatted_amount
123
- expect(amount).to eq 3
124
- end
125
-
126
- it 'raises an AggregateFunctionNotSupported exception' do
127
- expect do
128
- Aggregate.new(
129
- :not_supported_calculation, :savings, :bonus, TimeRange.make(:year => 2009, :week => 43, :range_type => :all_time)
130
- ).amount
131
- end.to raise_error(AggregateFunctionNotSupported)
132
- end
133
-
134
- context 'filters' do
135
- let(:range) { TimeRange.make(:year => 2011, :month => 10) }
136
- let(:filter) do
137
- [
138
- :scope => {
139
- :name => :test_filter,
140
- },
141
- ]
142
- end
143
-
144
- DoubleEntry::Line.class_eval do
145
- scope :test_filter, -> { where(:amount => 10_00) }
146
- end
147
-
148
- before do
149
- Timecop.freeze Time.local(2011, 10, 10) do
150
- perform_deposit user, 10_00
151
- end
152
-
153
- Timecop.freeze Time.local(2011, 10, 10) do
154
- perform_deposit user, 9_00
155
- end
156
- end
157
-
158
- it 'saves filtered aggregations' do
159
- expect do
160
- Aggregate.new(:sum, :savings, :bonus, range, :filter => filter).amount
161
- end.to change { LineAggregate.count }.by 1
162
- end
163
-
164
- it 'saves filtered aggregation only once for a range' do
165
- expect do
166
- Aggregate.new(:sum, :savings, :bonus, range, :filter => filter).amount
167
- Aggregate.new(:sum, :savings, :bonus, range, :filter => filter).amount
168
- end.to change { LineAggregate.count }.by 1
169
- end
170
-
171
- it 'saves filtered aggregations and non filtered aggregations separately' do
172
- expect do
173
- Aggregate.new(:sum, :savings, :bonus, range, :filter => filter).amount
174
- Aggregate.new(:sum, :savings, :bonus, range).amount
175
- end.to change { LineAggregate.count }.by 2
176
- end
177
-
178
- it 'loads the correct saved aggregation' do
179
- # cache the results for filtered and unfiltered aggregations
180
- Aggregate.new(:sum, :savings, :bonus, range, :filter => filter).amount
181
- Aggregate.new(:sum, :savings, :bonus, range).amount
182
-
183
- # ensure a second call loads the correct cached value
184
- amount = Aggregate.new(:sum, :savings, :bonus, range, :filter => filter).formatted_amount
185
- expect(amount).to eq Money.new(10_00)
186
-
187
- amount = Aggregate.new(:sum, :savings, :bonus, range).formatted_amount
188
- expect(amount).to eq Money.new(19_00)
189
- end
190
- end
191
- end
192
- RSpec.describe Aggregate, 'currencies' do
193
- let(:user) { User.make! }
194
- before do
195
- perform_btc_deposit(user, 100_000_000)
196
- perform_btc_deposit(user, 200_000_000)
197
- end
198
-
199
- it 'should calculate the sum in the correct currency' do
200
- amount = Aggregate.new(:sum, :btc_savings, :btc_test_transfer, TimeRange.make(:year => Time.now.year)).formatted_amount
201
- expect(amount).to eq(Money.new(300_000_000, :btc))
202
- end
203
- end
204
- end
205
- end
@@ -1,90 +0,0 @@
1
- # encoding: utf-8
2
- RSpec.describe DoubleEntry::Reporting::LineAggregateFilter do
3
- describe '.filter' do
4
- let(:function) { :sum }
5
- let(:account) { :account }
6
- let(:code) { :transfer_code }
7
- let(:filter_criteria) { nil }
8
- let(:start) { Time.parse('2014-07-27 10:55:44 +1000') }
9
- let(:finish) { Time.parse('2015-07-27 10:55:44 +1000') }
10
- let(:range) do
11
- instance_double(DoubleEntry::Reporting::MonthRange, :start => start, :finish => finish)
12
- end
13
-
14
- let(:lines_scope) { spy(DoubleEntry::Line) }
15
-
16
- subject(:filter) do
17
- DoubleEntry::Reporting::LineAggregateFilter.new(account, code, range, filter_criteria)
18
- end
19
-
20
- before do
21
- stub_const('DoubleEntry::Line', lines_scope)
22
-
23
- allow(DoubleEntry::LineMetadata).to receive(:table_name).and_return('double_entry_line_metadata')
24
-
25
- allow(lines_scope).to receive(:where).and_return(lines_scope)
26
- allow(lines_scope).to receive(:joins).and_return(lines_scope)
27
- allow(lines_scope).to receive(:ten_dollar_purchases).and_return(lines_scope)
28
- allow(lines_scope).to receive(:ten_dollar_purchases_by_category).and_return(lines_scope)
29
-
30
- filter.filter
31
- end
32
-
33
- context 'with named scopes specified' do
34
- let(:filter_criteria) do
35
- [
36
- # an example of calling a named scope called with arguments
37
- {
38
- :scope => {
39
- :name => :ten_dollar_purchases_by_category,
40
- :arguments => [:cat_videos, :cat_pictures],
41
- },
42
- },
43
- # an example of calling a named scope with no arguments
44
- {
45
- :scope => {
46
- :name => :ten_dollar_purchases,
47
- },
48
- },
49
- # an example of providing a single metadatum criteria to filter on
50
- {
51
- :metadata => {
52
- :meme => 'business_cat',
53
- },
54
- },
55
- ]
56
- end
57
-
58
- it 'filters by all the scopes provided' do
59
- expect(DoubleEntry::Line).to have_received(:ten_dollar_purchases)
60
- expect(DoubleEntry::Line).to have_received(:ten_dollar_purchases_by_category).
61
- with(:cat_videos, :cat_pictures)
62
- end
63
-
64
- it 'filters by all the metadata provided' do
65
- expect(DoubleEntry::Line).to have_received(:joins).with(:metadata)
66
- expect(DoubleEntry::Line).to have_received(:where).
67
- with(:double_entry_line_metadata => { :key => :meme, :value => 'business_cat' })
68
- end
69
- end
70
-
71
- context 'with a code specified' do
72
- let(:code) { :transfer_code }
73
-
74
- it 'retrieves the appropriate lines for aggregation' do
75
- expect(DoubleEntry::Line).to have_received(:where).with(:account => account)
76
- expect(DoubleEntry::Line).to have_received(:where).with(:created_at => start..finish)
77
- expect(DoubleEntry::Line).to have_received(:where).with(:code => code)
78
- end
79
- end
80
-
81
- context 'with no code specified' do
82
- let(:code) { nil }
83
-
84
- it 'retrieves the appropriate lines for aggregation' do
85
- expect(DoubleEntry::Line).to have_received(:where).with(:account => account)
86
- expect(DoubleEntry::Line).to have_received(:where).with(:created_at => start..finish)
87
- end
88
- end
89
- end
90
- end