double_entry 0.10.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module DoubleEntry
4
- VERSION = '0.10.3'
4
+ VERSION = '1.0.0'
5
5
  end
@@ -54,9 +54,19 @@ class CreateDoubleEntryTables < ActiveRecord::Migration
54
54
  t.text "log"
55
55
  t.timestamps :null => false
56
56
  end
57
+
58
+ create_table "double_entry_line_metadata", :force => true do |t|
59
+ t.integer "line_id", :null => false
60
+ t.string "key", :limit => 48, :null => false
61
+ t.string "value", :limit => 64, :null => false
62
+ t.timestamps :null => false
63
+ end
64
+
65
+ add_index "double_entry_line_metadata", ["line_id", "key", "value"], :name => "lines_meta_line_id_key_value_idx"
57
66
  end
58
67
 
59
68
  def self.down
69
+ drop_table "double_entry_line_metadata"
60
70
  drop_table "double_entry_line_checks"
61
71
  drop_table "double_entry_line_aggregates"
62
72
  drop_table "double_entry_lines"
@@ -148,6 +148,35 @@ RSpec.describe DoubleEntry::Locking do
148
148
  end.to_not raise_error
149
149
  end
150
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
+
151
180
  # sqlite cannot handle these cases so they don't run when DB=sqlite
152
181
  describe 'concurrent locking', :unless => ENV['DB'] == 'sqlite' do
153
182
  it 'allows multiple threads to lock at the same time' do
@@ -0,0 +1,32 @@
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
@@ -0,0 +1,50 @@
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
@@ -5,11 +5,11 @@ module DoubleEntry
5
5
  let(:start) { nil }
6
6
  let(:finish) { nil }
7
7
  let(:range_type) { 'year' }
8
- let(:function) { :sum }
8
+ let(:function) { 'sum' }
9
9
  let(:account) { :savings }
10
10
  let(:transfer_code) { :bonus }
11
11
  subject(:aggregate_array) do
12
- Reporting.aggregate_array(
12
+ AggregateArray.new(
13
13
  function,
14
14
  account,
15
15
  transfer_code,
@@ -42,17 +42,17 @@ module DoubleEntry
42
42
 
43
43
  context 'and some aggregates were created previously' do
44
44
  before do
45
- Reporting.aggregate(function.to_s, account, transfer_code, :filter => nil, :range => years[0])
46
- Reporting.aggregate(function.to_s, account, transfer_code, :filter => nil, :range => years[1])
47
- allow(Reporting).to receive(:aggregate)
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
48
  end
49
49
 
50
- it 'only asks Reporting for the non-existent ones' do
51
- expect(Reporting).not_to receive(:aggregate).with(function.to_s, account, transfer_code, :filter => nil, :range => years[0])
52
- expect(Reporting).not_to receive(:aggregate).with(function.to_s, account, transfer_code, :filter => nil, :range => years[1])
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
53
 
54
- expect(Reporting).to receive(:aggregate).with(function.to_s, account, transfer_code, :filter => nil, :range => years[2])
55
- expect(Reporting).to receive(:aggregate).with(function.to_s, account, transfer_code, :filter => nil, :range => years[3])
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
56
  aggregate_array
57
57
  end
58
58
  end
@@ -33,166 +33,113 @@ module DoubleEntry
33
33
  end
34
34
 
35
35
  it 'should store the aggregate for quick retrieval' do
36
- Aggregate.new(:sum, :savings, :bonus, :range => TimeRange.make(:year => 2009, :month => 10)).amount
36
+ Aggregate.new(:sum, :savings, :bonus, TimeRange.make(:year => 2009, :month => 10)).amount
37
37
  expect(LineAggregate.count).to eq 1
38
38
  end
39
39
 
40
40
  it 'should only store the aggregate once if it is requested more than once' do
41
- Aggregate.new(:sum, :savings, :bonus, :range => TimeRange.make(:year => 2009, :month => 9)).amount
42
- Aggregate.new(:sum, :savings, :bonus, :range => TimeRange.make(:year => 2009, :month => 9)).amount
43
- Aggregate.new(:sum, :savings, :bonus, :range => TimeRange.make(:year => 2009, :month => 10)).amount
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
44
  expect(LineAggregate.count).to eq 2
45
45
  end
46
46
 
47
47
  it 'should calculate the complete year correctly' do
48
- expect(
49
- Aggregate.new(
50
- :sum, :savings, :bonus,
51
- :range => TimeRange.make(:year => 2009)
52
- ).formatted_amount,
53
- ).to eq Money.new(200_00)
48
+ amount = Aggregate.new(:sum, :savings, :bonus, TimeRange.make(:year => 2009)).formatted_amount
49
+ expect(amount).to eq Money.new(200_00)
54
50
  end
55
51
 
56
52
  it 'should calculate seperate months correctly' do
57
- expect(
58
- Aggregate.new(
59
- :sum, :savings, :bonus,
60
- :range => TimeRange.make(:year => 2009, :month => 10)
61
- ).formatted_amount,
62
- ).to eq Money.new(110_00)
63
- expect(
64
- Aggregate.new(
65
- :sum, :savings, :bonus,
66
- :range => TimeRange.make(:year => 2009, :month => 11)
67
- ).formatted_amount,
68
- ).to eq Money.new(90_00)
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)
69
58
  end
70
59
 
71
60
  it 'should calculate seperate weeks correctly' do
72
61
  # Week 40 - Mon Sep 28, 2009 to Sun Oct 4 2009
73
- expect(
74
- Aggregate.new(
75
- :sum, :savings, :bonus,
76
- :range => TimeRange.make(:year => 2009, :week => 40)
77
- ).formatted_amount,
78
- ).to eq Money.new(60_00)
62
+ amount = Aggregate.new(:sum, :savings, :bonus, TimeRange.make(:year => 2009, :week => 40)).formatted_amount
63
+ expect(amount).to eq Money.new(60_00)
79
64
  end
80
65
 
81
66
  it 'should calculate seperate days correctly' do
82
67
  # 1 Nov 2009
83
- expect(
84
- Aggregate.new(
85
- :sum, :savings, :bonus,
86
- :range => TimeRange.make(:year => 2009, :week => 44, :day => 7)
87
- ).formatted_amount,
88
- ).to eq Money.new(90_00)
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)
89
70
  end
90
71
 
91
72
  it 'should calculate seperate hours correctly' do
92
73
  # 1 Nov 2009
93
- expect(
94
- Aggregate.new(
95
- :sum, :savings, :bonus,
96
- :range => TimeRange.make(:year => 2009, :week => 44, :day => 7, :hour => 0)
97
- ).formatted_amount,
98
- ).to eq Money.new(40_00)
99
- expect(
100
- Aggregate.new(
101
- :sum, :savings, :bonus,
102
- :range => TimeRange.make(:year => 2009, :week => 44, :day => 7, :hour => 1)
103
- ).formatted_amount,
104
- ).to eq Money.new(50_00)
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)
105
78
  end
106
79
 
107
80
  it 'should calculate, but not store aggregates when the time range is still current' do
108
81
  Timecop.freeze Time.local(2009, 11, 21) do
109
- expect(
110
- Aggregate.new(
111
- :sum, :savings, :bonus,
112
- :range => TimeRange.make(:year => 2009, :month => 11)
113
- ).formatted_amount,
114
- ).to eq Money.new(90_00)
82
+ amount = Aggregate.new(:sum, :savings, :bonus, TimeRange.make(:year => 2009, :month => 11)).formatted_amount
83
+ expect(amount).to eq Money.new(90_00)
115
84
  expect(LineAggregate.count).to eq 0
116
85
  end
117
86
  end
118
87
 
119
88
  it 'should calculate, but not store aggregates when the time range is in the future' do
120
89
  Timecop.freeze Time.local(2009, 11, 21) do
121
- expect(
122
- Aggregate.new(
123
- :sum, :savings, :bonus,
124
- :range => TimeRange.make(:year => 2009, :month => 12)
125
- ).formatted_amount,
126
- ).to eq Money.new(0)
90
+ amount = Aggregate.new(:sum, :savings, :bonus, TimeRange.make(:year => 2009, :month => 12)).formatted_amount
91
+ expect(amount).to eq Money.new(0)
127
92
  expect(LineAggregate.count).to eq 0
128
93
  end
129
94
  end
130
95
 
131
96
  it 'should calculate monthly all_time ranges correctly' do
132
- expect(
133
- Aggregate.new(
134
- :sum, :savings, :bonus,
135
- :range => TimeRange.make(:year => 2009, :month => 12, :range_type => :all_time)
136
- ).formatted_amount,
137
- ).to eq Money.new(200_00)
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)
138
99
  end
139
100
 
140
101
  it 'calculates the average monthly all_time ranges correctly' do
141
- expect(
142
- Aggregate.new(
143
- :average, :savings, :bonus,
144
- :range => TimeRange.make(:year => 2009, :month => 12, :range_type => :all_time)
145
- ).formatted_amount,
146
- ).to eq expected_monthly_average
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
147
104
  end
148
105
 
149
106
  it 'returns the correct count for weekly all_time ranges correctly' do
150
- expect(
151
- Aggregate.new(
152
- :count, :savings, :bonus,
153
- :range => TimeRange.make(:year => 2009, :month => 12, :range_type => :all_time)
154
- ).formatted_amount,
155
- ).to eq 5
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
156
109
  end
157
110
 
158
111
  it 'should calculate weekly all_time ranges correctly' do
159
- expect(
160
- Aggregate.new(
161
- :sum, :savings, :bonus,
162
- :range => TimeRange.make(:year => 2009, :week => 43, :range_type => :all_time)
163
- ).formatted_amount,
164
- ).to eq Money.new(110_00)
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)
165
114
  end
166
115
 
167
116
  it 'calculates the average weekly all_time ranges correctly' do
168
- expect(
169
- Aggregate.new(
170
- :average, :savings, :bonus,
171
- :range => TimeRange.make(:year => 2009, :week => 43, :range_type => :all_time)
172
- ).formatted_amount,
173
- ).to eq expected_weekly_average
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
174
119
  end
175
120
 
176
121
  it 'returns the correct count for weekly all_time ranges correctly' do
177
- expect(
178
- Aggregate.new(
179
- :count, :savings, :bonus,
180
- :range => TimeRange.make(:year => 2009, :week => 43, :range_type => :all_time)
181
- ).formatted_amount,
182
- ).to eq 3
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
183
124
  end
184
125
 
185
126
  it 'raises an AggregateFunctionNotSupported exception' do
186
127
  expect do
187
128
  Aggregate.new(
188
- :not_supported_calculation, :savings, :bonus,
189
- :range => TimeRange.make(:year => 2009, :week => 43, :range_type => :all_time)
129
+ :not_supported_calculation, :savings, :bonus, TimeRange.make(:year => 2009, :week => 43, :range_type => :all_time)
190
130
  ).amount
191
131
  end.to raise_error(AggregateFunctionNotSupported)
192
132
  end
193
133
 
194
134
  context 'filters' do
195
135
  let(:range) { TimeRange.make(:year => 2011, :month => 10) }
136
+ let(:filter) do
137
+ [
138
+ :scope => {
139
+ :name => :test_filter,
140
+ },
141
+ ]
142
+ end
196
143
 
197
144
  DoubleEntry::Line.class_eval do
198
145
  scope :test_filter, -> { where(:amount => 10_00) }
@@ -210,36 +157,35 @@ module DoubleEntry
210
157
 
211
158
  it 'saves filtered aggregations' do
212
159
  expect do
213
- Aggregate.new(:sum, :savings, :bonus, :range => range, :filter => [:test_filter]).amount
160
+ Aggregate.new(:sum, :savings, :bonus, range, :filter => filter).amount
214
161
  end.to change { LineAggregate.count }.by 1
215
162
  end
216
163
 
217
164
  it 'saves filtered aggregation only once for a range' do
218
165
  expect do
219
- Aggregate.new(:sum, :savings, :bonus, :range => range, :filter => [:test_filter]).amount
220
- Aggregate.new(:sum, :savings, :bonus, :range => range, :filter => [:test_filter]).amount
166
+ Aggregate.new(:sum, :savings, :bonus, range, :filter => filter).amount
167
+ Aggregate.new(:sum, :savings, :bonus, range, :filter => filter).amount
221
168
  end.to change { LineAggregate.count }.by 1
222
169
  end
223
170
 
224
171
  it 'saves filtered aggregations and non filtered aggregations separately' do
225
172
  expect do
226
- Aggregate.new(:sum, :savings, :bonus, :range => range, :filter => [:test_filter]).amount
227
- Aggregate.new(:sum, :savings, :bonus, :range => range).amount
173
+ Aggregate.new(:sum, :savings, :bonus, range, :filter => filter).amount
174
+ Aggregate.new(:sum, :savings, :bonus, range).amount
228
175
  end.to change { LineAggregate.count }.by 2
229
176
  end
230
177
 
231
178
  it 'loads the correct saved aggregation' do
232
179
  # cache the results for filtered and unfiltered aggregations
233
- Aggregate.new(:sum, :savings, :bonus, :range => range, :filter => [:test_filter]).amount
234
- Aggregate.new(:sum, :savings, :bonus, :range => range).amount
180
+ Aggregate.new(:sum, :savings, :bonus, range, :filter => filter).amount
181
+ Aggregate.new(:sum, :savings, :bonus, range).amount
235
182
 
236
183
  # ensure a second call loads the correct cached value
237
- expect(
238
- Aggregate.new(:sum, :savings, :bonus, :range => range, :filter => [:test_filter]).formatted_amount,
239
- ).to eq Money.new(10_00)
240
- expect(
241
- Aggregate.new(:sum, :savings, :bonus, :range => range).formatted_amount,
242
- ).to eq Money.new(19_00)
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)
243
189
  end
244
190
  end
245
191
  end
@@ -251,11 +197,8 @@ module DoubleEntry
251
197
  end
252
198
 
253
199
  it 'should calculate the sum in the correct currency' do
254
- expect(
255
- Aggregate.new(
256
- :sum, :btc_savings, :btc_test_transfer, :range => TimeRange.make(:year => Time.now.year)
257
- ).formatted_amount,
258
- ).to eq(Money.new(300_000_000, :btc))
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))
259
202
  end
260
203
  end
261
204
  end