double_entry 0.10.3 → 1.0.0

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.
@@ -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: