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.
@@ -0,0 +1,90 @@
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
@@ -1,11 +1,39 @@
1
1
  # encoding: utf-8
2
- module DoubleEntry
3
- module Reporting
4
- RSpec.describe LineAggregate do
5
- describe '.table_name' do
6
- subject { LineAggregate.table_name }
7
- it { should eq('double_entry_line_aggregates') }
8
- end
2
+ RSpec.describe DoubleEntry::Reporting::LineAggregate do
3
+ describe '.table_name' do
4
+ subject { DoubleEntry::Reporting::LineAggregate.table_name }
5
+ it { should eq('double_entry_line_aggregates') }
6
+ end
7
+
8
+ describe '#aggregate' do
9
+ let(:line_relation) { spy }
10
+ let(:filter) do
11
+ instance_double(DoubleEntry::Reporting::LineAggregateFilter, :filter => line_relation)
12
+ end
13
+
14
+ let(:function) { :sum }
15
+ let(:account) { spy }
16
+ let(:code) { spy }
17
+ let(:named_scopes) { spy }
18
+ let(:range) { spy }
19
+
20
+ subject(:aggregate) do
21
+ DoubleEntry::Reporting::LineAggregate.aggregate(function, account, code, range, named_scopes)
22
+ end
23
+
24
+ before do
25
+ allow(DoubleEntry::Reporting::LineAggregateFilter).to receive(:new).and_return(filter)
26
+ aggregate
27
+ end
28
+
29
+ it 'applies the specified filters' do
30
+ expect(DoubleEntry::Reporting::LineAggregateFilter).to have_received(:new).
31
+ with(account, code, range, named_scopes)
32
+ expect(filter).to have_received(:filter)
33
+ end
34
+
35
+ it 'performs the aggregation on the filtered lines' do
36
+ expect(line_relation).to have_received(:sum).with(:amount)
9
37
  end
10
38
  end
11
39
  end
@@ -42,4 +42,140 @@ RSpec.describe DoubleEntry::Reporting do
42
42
  end
43
43
  end
44
44
  end
45
+
46
+ describe '::aggregate' do
47
+ before do
48
+ # get rid of "helpful" predefined config
49
+ @config_accounts = DoubleEntry.configuration.accounts
50
+ @config_transfers = DoubleEntry.configuration.transfers
51
+ DoubleEntry.configuration.accounts = DoubleEntry::Account::Set.new
52
+ DoubleEntry.configuration.transfers = DoubleEntry::Transfer::Set.new
53
+
54
+ DoubleEntry.configure do |config|
55
+ config.define_accounts do |accounts|
56
+ accounts.define(:identifier => :savings)
57
+ accounts.define(:identifier => :cash)
58
+ accounts.define(:identifier => :credit)
59
+ end
60
+
61
+ config.define_transfers do |transfers|
62
+ transfers.define(:from => :savings, :to => :cash, :code => :spend)
63
+ transfers.define(:from => :cash, :to => :savings, :code => :save)
64
+ transfers.define(:from => :cash, :to => :credit, :code => :bill)
65
+ end
66
+ end
67
+
68
+ cash = DoubleEntry.account(:cash)
69
+ savings = DoubleEntry.account(:savings)
70
+ credit = DoubleEntry.account(:credit)
71
+ DoubleEntry.transfer(Money.new(10_00), :from => cash, :to => savings, :code => :save, :metadata => { :reason => 'payday' })
72
+ DoubleEntry.transfer(Money.new(10_00), :from => cash, :to => savings, :code => :save, :metadata => { :reason => 'payday' })
73
+ DoubleEntry.transfer(Money.new(20_00), :from => cash, :to => savings, :code => :save)
74
+ DoubleEntry.transfer(Money.new(20_00), :from => cash, :to => savings, :code => :save)
75
+ DoubleEntry.transfer(Money.new(30_00), :from => cash, :to => credit, :code => :bill)
76
+ DoubleEntry.transfer(Money.new(40_00), :from => cash, :to => credit, :code => :bill)
77
+ DoubleEntry.transfer(Money.new(50_00), :from => savings, :to => cash, :code => :spend)
78
+ DoubleEntry.transfer(Money.new(60_00), :from => savings, :to => cash, :code => :spend, :metadata => { :category => 'entertainment' })
79
+ end
80
+
81
+ after do
82
+ # restore "helpful" predefined config
83
+ DoubleEntry.configuration.accounts = @config_accounts
84
+ DoubleEntry.configuration.transfers = @config_transfers
85
+ end
86
+
87
+ describe 'filter solely on transaction identifiers and time' do
88
+ let(:function) { :sum }
89
+ let(:account) { :savings }
90
+ let(:code) { :save }
91
+ let(:range) { DoubleEntry::Reporting::MonthRange.current }
92
+
93
+ subject(:aggregate) do
94
+ DoubleEntry::Reporting.aggregate(function, account, code, range)
95
+ end
96
+
97
+ specify 'Total attempted to save' do
98
+ expect(aggregate).to eq(Money.new(60_00))
99
+ end
100
+ end
101
+
102
+ describe 'filter by named scope that does not take arguments' do
103
+ let(:function) { :sum }
104
+ let(:account) { :savings }
105
+ let(:code) { :save }
106
+ let(:range) { DoubleEntry::Reporting::MonthRange.current }
107
+
108
+ subject(:aggregate) do
109
+ DoubleEntry::Reporting.aggregate(
110
+ function, account, code, range,
111
+ :filter => [
112
+ :scope => {
113
+ :name => :ten_dollar_transfers,
114
+ },
115
+ ]
116
+ )
117
+ end
118
+
119
+ before do
120
+ DoubleEntry::Line.class_eval do
121
+ scope :ten_dollar_transfers, -> { where(:amount => Money.new(10_00).fractional) }
122
+ end
123
+ end
124
+
125
+ specify 'Total amount of $10 transfers attempted to save' do
126
+ expect(aggregate).to eq(Money.new(20_00))
127
+ end
128
+ end
129
+
130
+ describe 'filter by named scope that takes arguments' do
131
+ let(:function) { :sum }
132
+ let(:account) { :savings }
133
+ let(:code) { :save }
134
+ let(:range) { DoubleEntry::Reporting::MonthRange.current }
135
+
136
+ subject(:aggregate) do
137
+ DoubleEntry::Reporting.aggregate(
138
+ function, account, code, range,
139
+ :filter => [
140
+ :scope => {
141
+ :name => :specific_transfer_amount,
142
+ :arguments => [Money.new(20_00)],
143
+ },
144
+ ]
145
+ )
146
+ end
147
+
148
+ before do
149
+ DoubleEntry::Line.class_eval do
150
+ scope :specific_transfer_amount, ->(amount) { where(:amount => amount.fractional) }
151
+ end
152
+ end
153
+
154
+ specify 'Total amount of transfers of $20 attempted to save' do
155
+ expect(aggregate).to eq(Money.new(40_00))
156
+ end
157
+ end
158
+
159
+ describe 'filter by metadata' do
160
+ let(:function) { :sum }
161
+ let(:account) { :savings }
162
+ let(:code) { :save }
163
+ let(:range) { DoubleEntry::Reporting::MonthRange.current }
164
+
165
+ subject(:aggregate) do
166
+ DoubleEntry::Reporting.aggregate(
167
+ function, account, code, range,
168
+ :filter => [
169
+ :metadata => {
170
+ :reason => 'payday',
171
+ },
172
+ ]
173
+ )
174
+ end
175
+
176
+ specify 'Total amount of transfers saved because payday' do
177
+ expect(aggregate).to eq(Money.new(20_00))
178
+ end
179
+ end
180
+ end
45
181
  end
@@ -17,13 +17,70 @@ module DoubleEntry
17
17
  end
18
18
  end
19
19
 
20
+ describe '::transfer' do
21
+ let(:amount) { Money.new(10_00) }
22
+ let(:user) { User.make! }
23
+ let(:test) { DoubleEntry.account(:test, :scope => user) }
24
+ let(:savings) { DoubleEntry.account(:savings, :scope => user) }
25
+ let(:new_lines) { Line.all[-2..-1] }
26
+
27
+ subject(:transfer) { Transfer.transfer(amount, options) }
28
+
29
+ context 'without metadata' do
30
+ let(:options) { { :from => test, :to => savings, :code => :bonus } }
31
+
32
+ it 'creates lines' do
33
+ expect { transfer }.to change { Line.count }.by 2
34
+ end
35
+
36
+ it 'does not create metadata lines' do
37
+ expect { transfer }.not_to change { LineMetadata.count }
38
+ end
39
+ end
40
+
41
+ context 'with metadata' do
42
+ let(:options) { { :from => test, :to => savings, :code => :bonus, :metadata => { :country => 'AU', :tax => 'GST' } } }
43
+ let(:new_metadata) { LineMetadata.all[-4..-1] }
44
+
45
+ it 'creates metadata lines' do
46
+ expect { transfer }.to change { LineMetadata.count }.by 4
47
+ end
48
+
49
+ it 'associates the metadata lines with the transfer lines' do
50
+ transfer
51
+ expect(new_metadata.count { |meta| meta.line == new_lines.first }).to be 2
52
+ expect(new_metadata.count { |meta| meta.line == new_lines.last }).to be 2
53
+ end
54
+
55
+ it 'stores the first key/value pair' do
56
+ transfer
57
+ countries = new_metadata.select { |meta| meta.key == :country }
58
+ expect(countries.size).to be 2
59
+ expect(countries.count { |meta| meta.value == 'AU' }).to be 2
60
+ end
61
+
62
+ it 'associates the first key/value pair with both lines' do
63
+ transfer
64
+ countries = new_metadata.select { |meta| meta.key == :country }
65
+ expect(countries.map(&:line).uniq.size).to be 2
66
+ end
67
+
68
+ it 'stores another key/value pair' do
69
+ transfer
70
+ taxes = new_metadata.select { |meta| meta.key == :tax }
71
+ expect(taxes.size).to be 2
72
+ expect(taxes.count { |meta| meta.value == 'GST' }).to be 2
73
+ end
74
+ end
75
+ end
76
+
20
77
  describe Transfer::Set do
21
78
  describe '#define' do
22
79
  before do
23
80
  subject.define(
24
81
  :code => 'code',
25
82
  :from => double(:identifier => 'from'),
26
- :to => double(:identifier => 'to'),
83
+ :to => double(:identifier => 'to'),
27
84
  )
28
85
  end
29
86
  its(:first) { should be_a Transfer }
@@ -0,0 +1,26 @@
1
+ module PerformanceHelper
2
+ require 'ruby-prof'
3
+
4
+ def start_profiling(measure_mode = RubyProf::PROCESS_TIME)
5
+ RubyProf.measure_mode = measure_mode
6
+ RubyProf.start
7
+ end
8
+
9
+ def stop_profiling(profile_name = nil)
10
+ result = RubyProf.stop
11
+ puts "#{profile_name} Time: #{format('%#.3g', total_time(result))}s"
12
+ unless ENV.fetch('CI', false)
13
+ if profile_name
14
+ outdir = './profiles'
15
+ Dir.mkdir(outdir) unless Dir.exist?(outdir)
16
+ printer = RubyProf::MultiPrinter.new(result)
17
+ printer.print(:path => outdir, :profile => profile_name)
18
+ end
19
+ end
20
+ result
21
+ end
22
+
23
+ def total_time(result)
24
+ result.threads.inject(0) { |time, thread| time + thread.total_time }
25
+ end
26
+ end
@@ -55,6 +55,15 @@ ActiveRecord::Schema.define do
55
55
  t.timestamps :null => false
56
56
  end
57
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"
66
+
58
67
  # test table only
59
68
  create_table "users", :force => true do |t|
60
69
  t.string "username", :null => false
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: double_entry
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.3
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anthony Sellitti
@@ -15,7 +15,7 @@ authors:
15
15
  autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
- date: 2015-07-15 00:00:00.000000000 Z
18
+ date: 2015-08-04 00:00:00.000000000 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: money
@@ -241,6 +241,90 @@ dependencies:
241
241
  - - "~>"
242
242
  - !ruby/object:Gem::Version
243
243
  version: 0.32.0
244
+ - !ruby/object:Gem::Dependency
245
+ name: pry
246
+ requirement: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - ">="
249
+ - !ruby/object:Gem::Version
250
+ version: '0'
251
+ type: :development
252
+ prerelease: false
253
+ version_requirements: !ruby/object:Gem::Requirement
254
+ requirements:
255
+ - - ">="
256
+ - !ruby/object:Gem::Version
257
+ version: '0'
258
+ - !ruby/object:Gem::Dependency
259
+ name: pry-doc
260
+ requirement: !ruby/object:Gem::Requirement
261
+ requirements:
262
+ - - ">="
263
+ - !ruby/object:Gem::Version
264
+ version: '0'
265
+ type: :development
266
+ prerelease: false
267
+ version_requirements: !ruby/object:Gem::Requirement
268
+ requirements:
269
+ - - ">="
270
+ - !ruby/object:Gem::Version
271
+ version: '0'
272
+ - !ruby/object:Gem::Dependency
273
+ name: pry-byebug
274
+ requirement: !ruby/object:Gem::Requirement
275
+ requirements:
276
+ - - ">="
277
+ - !ruby/object:Gem::Version
278
+ version: '0'
279
+ type: :development
280
+ prerelease: false
281
+ version_requirements: !ruby/object:Gem::Requirement
282
+ requirements:
283
+ - - ">="
284
+ - !ruby/object:Gem::Version
285
+ version: '0'
286
+ - !ruby/object:Gem::Dependency
287
+ name: pry-stack_explorer
288
+ requirement: !ruby/object:Gem::Requirement
289
+ requirements:
290
+ - - ">="
291
+ - !ruby/object:Gem::Version
292
+ version: '0'
293
+ type: :development
294
+ prerelease: false
295
+ version_requirements: !ruby/object:Gem::Requirement
296
+ requirements:
297
+ - - ">="
298
+ - !ruby/object:Gem::Version
299
+ version: '0'
300
+ - !ruby/object:Gem::Dependency
301
+ name: awesome_print
302
+ requirement: !ruby/object:Gem::Requirement
303
+ requirements:
304
+ - - ">="
305
+ - !ruby/object:Gem::Version
306
+ version: '0'
307
+ type: :development
308
+ prerelease: false
309
+ version_requirements: !ruby/object:Gem::Requirement
310
+ requirements:
311
+ - - ">="
312
+ - !ruby/object:Gem::Version
313
+ version: '0'
314
+ - !ruby/object:Gem::Dependency
315
+ name: ruby-prof
316
+ requirement: !ruby/object:Gem::Requirement
317
+ requirements:
318
+ - - ">="
319
+ - !ruby/object:Gem::Version
320
+ version: '0'
321
+ type: :development
322
+ prerelease: false
323
+ version_requirements: !ruby/object:Gem::Requirement
324
+ requirements:
325
+ - - ">="
326
+ - !ruby/object:Gem::Version
327
+ version: '0'
244
328
  description:
245
329
  email:
246
330
  - anthony.sellitti@envato.com
@@ -275,6 +359,7 @@ files:
275
359
  - lib/double_entry/configuration.rb
276
360
  - lib/double_entry/errors.rb
277
361
  - lib/double_entry/line.rb
362
+ - lib/double_entry/line_metadata.rb
278
363
  - lib/double_entry/locking.rb
279
364
  - lib/double_entry/reporting.rb
280
365
  - lib/double_entry/reporting/aggregate.rb
@@ -282,6 +367,7 @@ files:
282
367
  - lib/double_entry/reporting/day_range.rb
283
368
  - lib/double_entry/reporting/hour_range.rb
284
369
  - lib/double_entry/reporting/line_aggregate.rb
370
+ - lib/double_entry/reporting/line_aggregate_filter.rb
285
371
  - lib/double_entry/reporting/month_range.rb
286
372
  - lib/double_entry/reporting/time_range.rb
287
373
  - lib/double_entry/reporting/time_range_array.rb
@@ -302,8 +388,11 @@ files:
302
388
  - spec/double_entry/configuration_spec.rb
303
389
  - spec/double_entry/line_spec.rb
304
390
  - spec/double_entry/locking_spec.rb
391
+ - spec/double_entry/performance/double_entry_performance_spec.rb
392
+ - spec/double_entry/performance/reporting/aggregate_performance_spec.rb
305
393
  - spec/double_entry/reporting/aggregate_array_spec.rb
306
394
  - spec/double_entry/reporting/aggregate_spec.rb
395
+ - spec/double_entry/reporting/line_aggregate_filter_spec.rb
307
396
  - spec/double_entry/reporting/line_aggregate_spec.rb
308
397
  - spec/double_entry/reporting/month_range_spec.rb
309
398
  - spec/double_entry/reporting/time_range_array_spec.rb
@@ -323,12 +412,38 @@ files:
323
412
  - spec/support/gemfiles/Gemfile.rails-3.2.x
324
413
  - spec/support/gemfiles/Gemfile.rails-4.1.x
325
414
  - spec/support/gemfiles/Gemfile.rails-4.2.x
415
+ - spec/support/performance_helper.rb
326
416
  - spec/support/reporting_configuration.rb
327
417
  - spec/support/schema.rb
328
418
  homepage: https://github.com/envato/double_entry
329
419
  licenses: []
330
420
  metadata: {}
331
- post_install_message:
421
+ post_install_message: |
422
+ Please note the following changes in DoubleEntry:
423
+ - New table `double_entry_line_metadata` has been introduced and is *required* for
424
+ aggregate reporting filtering to work. Existing applications must manually manage
425
+ this change via a migration similar to the following:
426
+
427
+ class CreateDoubleEntryLineMetadata < ActiveRecord::Migration
428
+ def self.up
429
+ create_table "#{DoubleEntry.table_name_prefix}line_metadata", :force => true do |t|
430
+ t.integer "line_id", :null => false
431
+ t.string "key", :limit => 48, :null => false
432
+ t.string "value", :limit => 64, :null => false
433
+ t.timestamps :null => false
434
+ end
435
+
436
+ add_index "#{DoubleEntry.table_name_prefix}line_metadata",
437
+ ["line_id", "key", "value"],
438
+ :name => "lines_meta_line_id_key_value_idx"
439
+ end
440
+
441
+ def self.down
442
+ drop_table "#{DoubleEntry.table_name_prefix}line_metadata"
443
+ end
444
+ end
445
+
446
+ Please ensure that you update your database accordingly.
332
447
  rdoc_options: []
333
448
  require_paths:
334
449
  - lib
@@ -344,7 +459,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
344
459
  version: '0'
345
460
  requirements: []
346
461
  rubyforge_project:
347
- rubygems_version: 2.4.5
462
+ rubygems_version: 2.2.2
348
463
  signing_key:
349
464
  specification_version: 4
350
465
  summary: Tools to build your double entry financial ledger
@@ -356,8 +471,11 @@ test_files:
356
471
  - spec/double_entry/configuration_spec.rb
357
472
  - spec/double_entry/line_spec.rb
358
473
  - spec/double_entry/locking_spec.rb
474
+ - spec/double_entry/performance/double_entry_performance_spec.rb
475
+ - spec/double_entry/performance/reporting/aggregate_performance_spec.rb
359
476
  - spec/double_entry/reporting/aggregate_array_spec.rb
360
477
  - spec/double_entry/reporting/aggregate_spec.rb
478
+ - spec/double_entry/reporting/line_aggregate_filter_spec.rb
361
479
  - spec/double_entry/reporting/line_aggregate_spec.rb
362
480
  - spec/double_entry/reporting/month_range_spec.rb
363
481
  - spec/double_entry/reporting/time_range_array_spec.rb
@@ -377,5 +495,7 @@ test_files:
377
495
  - spec/support/gemfiles/Gemfile.rails-3.2.x
378
496
  - spec/support/gemfiles/Gemfile.rails-4.1.x
379
497
  - spec/support/gemfiles/Gemfile.rails-4.2.x
498
+ - spec/support/performance_helper.rb
380
499
  - spec/support/reporting_configuration.rb
381
500
  - spec/support/schema.rb
501
+ has_rdoc: