double_entry 1.0.1 → 2.0.0.beta5

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.
Files changed (79) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +497 -0
  3. data/README.md +107 -44
  4. data/double_entry.gemspec +22 -49
  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.rb +29 -21
  8. data/lib/double_entry/account.rb +39 -46
  9. data/lib/double_entry/account_balance.rb +20 -3
  10. data/lib/double_entry/balance_calculator.rb +5 -5
  11. data/lib/double_entry/configurable.rb +1 -0
  12. data/lib/double_entry/configuration.rb +8 -2
  13. data/lib/double_entry/errors.rb +13 -13
  14. data/lib/double_entry/line.rb +7 -6
  15. data/lib/double_entry/locking.rb +5 -5
  16. data/lib/double_entry/transfer.rb +37 -30
  17. data/lib/double_entry/validation.rb +1 -0
  18. data/lib/double_entry/validation/account_fixer.rb +36 -0
  19. data/lib/double_entry/validation/line_check.rb +25 -43
  20. data/lib/double_entry/version.rb +1 -1
  21. data/lib/generators/double_entry/install/install_generator.rb +22 -1
  22. data/lib/generators/double_entry/install/templates/initializer.rb +20 -0
  23. data/lib/generators/double_entry/install/templates/migration.rb +45 -55
  24. metadata +35 -256
  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/lib/double_entry/reporting.rb +0 -181
  32. data/lib/double_entry/reporting/aggregate.rb +0 -110
  33. data/lib/double_entry/reporting/aggregate_array.rb +0 -76
  34. data/lib/double_entry/reporting/day_range.rb +0 -42
  35. data/lib/double_entry/reporting/hour_range.rb +0 -45
  36. data/lib/double_entry/reporting/line_aggregate.rb +0 -16
  37. data/lib/double_entry/reporting/line_aggregate_filter.rb +0 -79
  38. data/lib/double_entry/reporting/month_range.rb +0 -94
  39. data/lib/double_entry/reporting/time_range.rb +0 -59
  40. data/lib/double_entry/reporting/time_range_array.rb +0 -49
  41. data/lib/double_entry/reporting/week_range.rb +0 -107
  42. data/lib/double_entry/reporting/year_range.rb +0 -40
  43. data/script/jack_hammer +0 -210
  44. data/script/setup.sh +0 -8
  45. data/spec/active_record/locking_extensions_spec.rb +0 -110
  46. data/spec/double_entry/account_balance_spec.rb +0 -7
  47. data/spec/double_entry/account_spec.rb +0 -130
  48. data/spec/double_entry/balance_calculator_spec.rb +0 -88
  49. data/spec/double_entry/configuration_spec.rb +0 -50
  50. data/spec/double_entry/line_spec.rb +0 -80
  51. data/spec/double_entry/locking_spec.rb +0 -214
  52. data/spec/double_entry/performance/double_entry_performance_spec.rb +0 -32
  53. data/spec/double_entry/performance/reporting/aggregate_performance_spec.rb +0 -50
  54. data/spec/double_entry/reporting/aggregate_array_spec.rb +0 -123
  55. data/spec/double_entry/reporting/aggregate_spec.rb +0 -205
  56. data/spec/double_entry/reporting/line_aggregate_filter_spec.rb +0 -90
  57. data/spec/double_entry/reporting/line_aggregate_spec.rb +0 -39
  58. data/spec/double_entry/reporting/month_range_spec.rb +0 -139
  59. data/spec/double_entry/reporting/time_range_array_spec.rb +0 -169
  60. data/spec/double_entry/reporting/time_range_spec.rb +0 -45
  61. data/spec/double_entry/reporting/week_range_spec.rb +0 -103
  62. data/spec/double_entry/reporting_spec.rb +0 -181
  63. data/spec/double_entry/transfer_spec.rb +0 -93
  64. data/spec/double_entry/validation/line_check_spec.rb +0 -99
  65. data/spec/double_entry_spec.rb +0 -428
  66. data/spec/generators/double_entry/install/install_generator_spec.rb +0 -30
  67. data/spec/spec_helper.rb +0 -118
  68. data/spec/support/accounts.rb +0 -21
  69. data/spec/support/blueprints.rb +0 -43
  70. data/spec/support/database.example.yml +0 -21
  71. data/spec/support/database.travis.yml +0 -24
  72. data/spec/support/double_entry_spec_helper.rb +0 -27
  73. data/spec/support/gemfiles/Gemfile.rails-3.2.x +0 -8
  74. data/spec/support/gemfiles/Gemfile.rails-4.1.x +0 -6
  75. data/spec/support/gemfiles/Gemfile.rails-4.2.x +0 -5
  76. data/spec/support/gemfiles/Gemfile.rails-5.0.x +0 -5
  77. data/spec/support/performance_helper.rb +0 -26
  78. data/spec/support/reporting_configuration.rb +0 -6
  79. data/spec/support/schema.rb +0 -74
@@ -1,181 +0,0 @@
1
- # encoding: utf-8
2
- RSpec.describe DoubleEntry::Reporting do
3
- describe '::configure' do
4
- describe 'start_of_business' do
5
- subject(:start_of_business) { DoubleEntry::Reporting.configuration.start_of_business }
6
-
7
- context 'configured to 2011-03-12' do
8
- before do
9
- DoubleEntry::Reporting.configure do |config|
10
- config.start_of_business = Time.new(2011, 3, 12)
11
- end
12
- end
13
-
14
- it { should eq Time.new(2011, 3, 12) }
15
- end
16
-
17
- context 'not configured' do
18
- it { should eq Time.new(1970, 1, 1) }
19
- end
20
- end
21
- end
22
-
23
- describe '::scopes_with_minimum_balance_for_account' do
24
- subject(:scopes) { DoubleEntry::Reporting.scopes_with_minimum_balance_for_account(minimum_balance, :checking) }
25
-
26
- context "a 'checking' account with balance $100" do
27
- let!(:user) { User.make!(:checking_balance => Money.new(100_00)) }
28
-
29
- context 'when searching for balance $99' do
30
- let(:minimum_balance) { Money.new(99_00) }
31
- it { should include user.id }
32
- end
33
-
34
- context 'when searching for balance $100' do
35
- let(:minimum_balance) { Money.new(100_00) }
36
- it { should include user.id }
37
- end
38
-
39
- context 'when searching for balance $101' do
40
- let(:minimum_balance) { Money.new(101_00) }
41
- it { should_not include user.id }
42
- end
43
- end
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
181
- end
@@ -1,93 +0,0 @@
1
- # encoding: utf-8
2
- module DoubleEntry
3
- RSpec.describe Transfer do
4
- describe '::new' do
5
- context 'given a code 47 characters in length' do
6
- let(:code) { 'xxxxxxxxxxxxxxxx 47 characters xxxxxxxxxxxxxxxx' }
7
- specify do
8
- expect { Transfer.new(:code => code) }.to_not raise_error
9
- end
10
- end
11
-
12
- context 'given a code 48 characters in length' do
13
- let(:code) { 'xxxxxxxxxxxxxxxx 48 characters xxxxxxxxxxxxxxxxx' }
14
- specify do
15
- expect { Transfer.new(:code => code) }.to raise_error TransferCodeTooLongError, /'#{code}'/
16
- end
17
- end
18
- end
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
-
77
- describe Transfer::Set do
78
- describe '#define' do
79
- before do
80
- subject.define(
81
- :code => 'code',
82
- :from => double(:identifier => 'from'),
83
- :to => double(:identifier => 'to'),
84
- )
85
- end
86
- its(:first) { should be_a Transfer }
87
- its('first.code') { should eq 'code' }
88
- its('first.from.identifier') { should eq 'from' }
89
- its('first.to.identifier') { should eq 'to' }
90
- end
91
- end
92
- end
93
- end
@@ -1,99 +0,0 @@
1
- # encoding: utf-8
2
- module DoubleEntry
3
- module Validation
4
- RSpec.describe LineCheck do
5
- describe '#last' do
6
- context 'Given some checks have been created' do
7
- before do
8
- Timecop.freeze 3.minutes.ago do
9
- LineCheck.create! :last_line_id => 100, :errors_found => false, :log => ''
10
- end
11
- Timecop.freeze 1.minute.ago do
12
- LineCheck.create! :last_line_id => 300, :errors_found => false, :log => ''
13
- end
14
- Timecop.freeze 2.minutes.ago do
15
- LineCheck.create! :last_line_id => 200, :errors_found => false, :log => ''
16
- end
17
- end
18
-
19
- it 'should find the newest LineCheck created (by creation_date)' do
20
- expect(LineCheck.last.last_line_id).to eq 300
21
- end
22
- end
23
- end
24
-
25
- describe '#perform!' do
26
- subject(:performed_line_check) { LineCheck.perform! }
27
-
28
- context 'Given a user with 100 dollars' do
29
- before { User.make!(:savings_balance => Money.new(100_00)) }
30
-
31
- context 'And all is consistent' do
32
- context 'And all lines have been checked' do
33
- before { LineCheck.perform! }
34
-
35
- it { should be_nil }
36
-
37
- it 'should not persist a new LineCheck' do
38
- expect { LineCheck.perform! }.
39
- to_not change { LineCheck.count }
40
- end
41
- end
42
-
43
- it { should be_instance_of LineCheck }
44
- its(:errors_found) { should eq false }
45
-
46
- it 'should persist the LineCheck' do
47
- line_check = LineCheck.perform!
48
- expect(LineCheck.last).to eq line_check
49
- end
50
- end
51
-
52
- context 'And there is a consistency error in lines' do
53
- before { DoubleEntry::Line.order(:id).limit(1).update_all('balance = balance + 1') }
54
-
55
- its(:errors_found) { should be true }
56
- its(:log) { should match(/Error on line/) }
57
-
58
- it 'should correct the running balance' do
59
- expect { LineCheck.perform! }.
60
- to change { DoubleEntry::Line.order(:id).first.balance }.
61
- by Money.new(-1)
62
- end
63
- end
64
-
65
- context 'And there is a consistency error in account balance' do
66
- before { DoubleEntry::AccountBalance.order(:id).limit(1).update_all('balance = balance + 1') }
67
-
68
- its(:errors_found) { should be true }
69
-
70
- it 'should correct the account balance' do
71
- expect { LineCheck.perform! }.
72
- to change { DoubleEntry::AccountBalance.order(:id).first.balance }.
73
- by Money.new(-1)
74
- end
75
- end
76
- end
77
-
78
- context 'Given a user with a non default currency balance' do
79
- before { User.make!(:bitcoin_balance => Money.new(100_00, 'BTC')) }
80
- its(:errors_found) { should eq false }
81
- context 'And there is a consistency error in lines' do
82
- before { DoubleEntry::Line.order(:id).limit(1).update_all('balance = balance + 1') }
83
-
84
- its(:errors_found) { should eq true }
85
- it 'should correct the running balance' do
86
- expect { LineCheck.perform! }.
87
- to change { DoubleEntry::Line.order(:id).first.balance }.
88
- by Money.new(-1, 'BTC')
89
- end
90
- end
91
- end
92
-
93
- it 'has a table name prefixed with double_entry_' do
94
- expect(LineCheck.table_name).to eq 'double_entry_line_checks'
95
- end
96
- end
97
- end
98
- end
99
- end
@@ -1,428 +0,0 @@
1
- # encoding: utf-8
2
- RSpec.describe DoubleEntry do
3
- # these specs blat the DoubleEntry configuration, so take
4
- # a copy and clean up after ourselves
5
- before do
6
- @config_accounts = DoubleEntry.configuration.accounts
7
- @config_transfers = DoubleEntry.configuration.transfers
8
- DoubleEntry.configuration.accounts = DoubleEntry::Account::Set.new
9
- DoubleEntry.configuration.transfers = DoubleEntry::Transfer::Set.new
10
- end
11
-
12
- after do
13
- DoubleEntry.configuration.accounts = @config_accounts
14
- DoubleEntry.configuration.transfers = @config_transfers
15
- end
16
-
17
- describe 'configuration' do
18
- it 'checks for duplicates of accounts' do
19
- expect do
20
- DoubleEntry.configure do |config|
21
- config.define_accounts do |accounts|
22
- accounts.define(:identifier => :gah!)
23
- accounts.define(:identifier => :gah!)
24
- end
25
- end
26
- end.to raise_error DoubleEntry::DuplicateAccount
27
- end
28
-
29
- it 'checks for duplicates of transfers' do
30
- expect do
31
- DoubleEntry.configure do |config|
32
- config.define_transfers do |transfers|
33
- transfers.define(:from => :savings, :to => :cash, :code => :xfer)
34
- transfers.define(:from => :savings, :to => :cash, :code => :xfer)
35
- end
36
- end
37
- end.to raise_error DoubleEntry::DuplicateTransfer
38
- end
39
- end
40
-
41
- describe 'accounts' do
42
- before do
43
- DoubleEntry.configure do |config|
44
- config.define_accounts do |accounts|
45
- accounts.define(:identifier => :unscoped)
46
- accounts.define(:identifier => :scoped, :scope_identifier => ->(u) { u.id })
47
- end
48
- end
49
- end
50
-
51
- let(:scope) { double('a scope', :id => 1) }
52
-
53
- describe 'fetching' do
54
- it 'can find an unscoped account by identifier' do
55
- expect(DoubleEntry.account(:unscoped)).to_not be_nil
56
- end
57
-
58
- it 'can find a scoped account by identifier' do
59
- expect(DoubleEntry.account(:scoped, :scope => scope)).to_not be_nil
60
- end
61
-
62
- it 'raises an exception when it cannot find an account' do
63
- expect { DoubleEntry.account(:invalid) }.to raise_error(DoubleEntry::UnknownAccount)
64
- end
65
-
66
- it 'raises exception when you ask for an unscoped account w/ scope' do
67
- expect { DoubleEntry.account(:unscoped, :scope => scope) }.to raise_error(DoubleEntry::UnknownAccount)
68
- end
69
-
70
- it 'raises exception when you ask for a scoped account w/ out scope' do
71
- expect { DoubleEntry.account(:scoped) }.to raise_error(DoubleEntry::UnknownAccount)
72
- end
73
- end
74
-
75
- context 'an unscoped account' do
76
- subject(:unscoped) { DoubleEntry.account(:unscoped) }
77
-
78
- it 'has an identifier' do
79
- expect(unscoped.identifier).to eq :unscoped
80
- end
81
- end
82
- context 'a scoped account' do
83
- subject(:scoped) { DoubleEntry.account(:scoped, :scope => scope) }
84
-
85
- it 'has an identifier' do
86
- expect(scoped.identifier).to eq :scoped
87
- end
88
- end
89
- end
90
-
91
- describe 'transfers' do
92
- before do
93
- DoubleEntry.configure do |config|
94
- config.define_accounts do |accounts|
95
- accounts.define(:identifier => :savings)
96
- accounts.define(:identifier => :cash)
97
- accounts.define(:identifier => :trash)
98
- accounts.define(:identifier => :bitbucket, :currency => :btc)
99
- end
100
-
101
- config.define_transfers do |transfers|
102
- transfers.define(:from => :savings, :to => :cash, :code => :xfer)
103
- transfers.define(:from => :trash, :to => :bitbucket, :code => :mismatch_xfer)
104
- end
105
- end
106
- end
107
-
108
- let(:savings) { DoubleEntry.account(:savings) }
109
- let(:cash) { DoubleEntry.account(:cash) }
110
- let(:trash) { DoubleEntry.account(:trash) }
111
- let(:bitbucket) { DoubleEntry.account(:bitbucket) }
112
-
113
- it 'can transfer from an account to an account, if the transfer is allowed' do
114
- DoubleEntry.transfer(
115
- Money.new(100_00),
116
- :from => savings,
117
- :to => cash,
118
- :code => :xfer,
119
- )
120
- end
121
-
122
- it 'raises an exception when the transfer is not allowed (wrong direction)' do
123
- expect do
124
- DoubleEntry.transfer(
125
- Money.new(100_00),
126
- :from => cash,
127
- :to => savings,
128
- :code => :xfer,
129
- )
130
- end.to raise_error DoubleEntry::TransferNotAllowed
131
- end
132
-
133
- it 'raises an exception when the transfer is not allowed (wrong code)' do
134
- expect do
135
- DoubleEntry.transfer(
136
- Money.new(100_00),
137
- :from => savings,
138
- :to => cash,
139
- :code => :yfer,
140
- )
141
- end.to raise_error DoubleEntry::TransferNotAllowed
142
- end
143
-
144
- it 'raises an exception when the transfer is not allowed (does not exist, at all)' do
145
- expect do
146
- DoubleEntry.transfer(
147
- Money.new(100_00),
148
- :from => cash,
149
- :to => trash,
150
- )
151
- end.to raise_error DoubleEntry::TransferNotAllowed
152
- end
153
-
154
- it 'raises an exception when the transfer is not allowed (mismatched currencies)' do
155
- expect do
156
- DoubleEntry.transfer(
157
- Money.new(100_00),
158
- :from => trash,
159
- :to => bitbucket,
160
- :code => :mismatch_xfer,
161
- )
162
- end.to raise_error DoubleEntry::MismatchedCurrencies
163
- end
164
- end
165
-
166
- describe 'lines' do
167
- before do
168
- DoubleEntry.configure do |config|
169
- config.define_accounts do |accounts|
170
- accounts.define(:identifier => :a)
171
- accounts.define(:identifier => :b)
172
- end
173
-
174
- config.define_transfers do |transfers|
175
- transfers.define(:code => :xfer, :from => :a, :to => :b)
176
- end
177
- end
178
-
179
- DoubleEntry.transfer(Money.new(10_00), :from => account_a, :to => account_b, :code => :xfer)
180
- end
181
-
182
- let(:account_a) { DoubleEntry.account(:a) }
183
- let(:account_b) { DoubleEntry.account(:b) }
184
- let(:credit_line) { lines_for_account(account_a).first }
185
- let(:debit_line) { lines_for_account(account_b).first }
186
-
187
- it 'has an amount' do
188
- expect(credit_line.amount).to eq(Money.new(-10_00))
189
- expect(debit_line.amount).to eq(Money.new(10_00))
190
- end
191
-
192
- it 'has a code' do
193
- expect(credit_line.code).to eq(:xfer)
194
- expect(debit_line.code).to eq(:xfer)
195
- end
196
-
197
- it 'auto-sets scope when assigning account (and partner_accout, is this implementation?)' do
198
- expect(credit_line[:account]).to eq('a')
199
- expect(credit_line[:scope]).to be_nil
200
- expect(credit_line[:partner_account]).to eq('b')
201
- expect(credit_line[:partner_scope]).to be_nil
202
- end
203
-
204
- it 'has a partner_account (or is this implementation?)' do
205
- expect(credit_line.partner_account).to eq debit_line.account
206
- end
207
-
208
- it 'knows if it is an increase or decrease' do
209
- expect(credit_line).to be_decrease
210
- expect(debit_line).to be_increase
211
- expect(credit_line).to_not be_increase
212
- expect(debit_line).to_not be_decrease
213
- end
214
-
215
- it 'can reference its partner' do
216
- expect(credit_line.partner).to eq(debit_line)
217
- expect(debit_line.partner).to eq(credit_line)
218
- end
219
-
220
- it 'can ask for its pair (credit always coming first)' do
221
- expect(credit_line.pair).to eq([credit_line, debit_line])
222
- expect(debit_line.pair).to eq([credit_line, debit_line])
223
- end
224
-
225
- it 'can ask for the account (and get an instance)' do
226
- expect(credit_line.account).to eq(account_a)
227
- expect(debit_line.account).to eq(account_b)
228
- end
229
- end
230
-
231
- describe 'balances' do
232
- let(:work) { DoubleEntry.account(:work) }
233
- let(:savings) { DoubleEntry.account(:savings) }
234
- let(:cash) { DoubleEntry.account(:cash) }
235
- let(:store) { DoubleEntry.account(:store) }
236
- let(:btc_store) { DoubleEntry.account(:btc_store) }
237
- let(:btc_wallet) { DoubleEntry.account(:btc_wallet) }
238
-
239
- before do
240
- DoubleEntry.configure do |config|
241
- config.define_accounts do |accounts|
242
- accounts.define(:identifier => :work)
243
- accounts.define(:identifier => :cash)
244
- accounts.define(:identifier => :savings)
245
- accounts.define(:identifier => :store)
246
- accounts.define(:identifier => :btc_store, :currency => 'BTC')
247
- accounts.define(:identifier => :btc_wallet, :currency => 'BTC')
248
- end
249
-
250
- config.define_transfers do |transfers|
251
- transfers.define(:code => :salary, :from => :work, :to => :cash)
252
- transfers.define(:code => :xfer, :from => :cash, :to => :savings)
253
- transfers.define(:code => :xfer, :from => :savings, :to => :cash)
254
- transfers.define(:code => :purchase, :from => :cash, :to => :store)
255
- transfers.define(:code => :layby, :from => :cash, :to => :store)
256
- transfers.define(:code => :deposit, :from => :cash, :to => :store)
257
- transfers.define(:code => :btc_ex, :from => :btc_store, :to => :btc_wallet)
258
- end
259
- end
260
-
261
- Timecop.freeze 3.weeks.ago + 1.day do
262
- # got paid from work
263
- DoubleEntry.transfer(Money.new(1_000_00), :from => work, :code => :salary, :to => cash)
264
- # transfer half salary into savings
265
- DoubleEntry.transfer(Money.new(500_00), :from => cash, :code => :xfer, :to => savings)
266
- end
267
-
268
- Timecop.freeze 2.weeks.ago + 1.day do
269
- # got myself a darth vader helmet
270
- DoubleEntry.transfer(Money.new(200_00), :from => cash, :code => :purchase, :to => store)
271
- # paid off some of my darth vader suit layby (to go with the helmet)
272
- DoubleEntry.transfer(Money.new(100_00), :from => cash, :code => :layby, :to => store)
273
- # put a deposit on the darth vader voice changer module (for the helmet)
274
- DoubleEntry.transfer(Money.new(100_00), :from => cash, :code => :deposit, :to => store)
275
- end
276
-
277
- Timecop.freeze 1.week.ago + 1.day do
278
- # transfer 200 out of savings
279
- DoubleEntry.transfer(Money.new(200_00), :from => savings, :code => :xfer, :to => cash)
280
- # pay the remaining balance on the darth vader voice changer module
281
- DoubleEntry.transfer(Money.new(200_00), :from => cash, :code => :purchase, :to => store)
282
- end
283
-
284
- Timecop.freeze 1.week.from_now do
285
- # it's the future, man
286
- DoubleEntry.transfer(Money.new(200_00, 'BTC'), :from => btc_store, :code => :btc_ex, :to => btc_wallet)
287
- end
288
- end
289
-
290
- it 'has the initial balances that we expect' do
291
- expect(work.balance).to eq(Money.new(-1_000_00))
292
- expect(cash.balance).to eq(Money.new(100_00))
293
- expect(savings.balance).to eq(Money.new(300_00))
294
- expect(store.balance).to eq(Money.new(600_00))
295
- expect(btc_wallet.balance).to eq(Money.new(200_00, 'BTC'))
296
- end
297
-
298
- it 'should have correct account balance records' do
299
- [work, cash, savings, store, btc_wallet].each do |account|
300
- expect(DoubleEntry::AccountBalance.find_by_account(account).balance).to eq(account.balance)
301
- end
302
- end
303
-
304
- it 'should have correct account balance currencies' do
305
- expect(DoubleEntry::AccountBalance.find_by_account(btc_wallet).balance.currency).to eq('BTC')
306
- end
307
-
308
- it 'affects origin/destination balance after transfer' do
309
- savings_balance = savings.balance
310
- cash_balance = cash.balance
311
- amount = Money.new(10_00)
312
-
313
- DoubleEntry.transfer(amount, :from => savings, :code => :xfer, :to => cash)
314
-
315
- expect(savings.balance).to eq(savings_balance - amount)
316
- expect(cash.balance).to eq(cash_balance + amount)
317
- end
318
-
319
- it 'can be queried at a given point in time' do
320
- expect(cash.balance(:at => 1.week.ago)).to eq(Money.new(100_00))
321
- end
322
-
323
- it 'can be queries between two points in time' do
324
- expect(cash.balance(:from => 3.weeks.ago, :to => 2.weeks.ago)).to eq(Money.new(500_00))
325
- end
326
-
327
- it 'can be queried between two points in time, even in the future' do
328
- expect(btc_wallet.balance(:from => Time.now, :to => 2.weeks.from_now)).to eq(Money.new(200_00, 'BTC'))
329
- end
330
-
331
- it 'can report on balances, scoped by code' do
332
- expect(cash.balance(:code => :salary)).to eq Money.new(1_000_00)
333
- end
334
-
335
- it 'can report on balances, scoped by many codes' do
336
- expect(store.balance(:codes => [:layby, :deposit])).to eq(Money.new(200_00))
337
- end
338
-
339
- it 'has running balances for each line' do
340
- lines = lines_for_account(cash)
341
- expect(lines[0].balance).to eq(Money.new(1_000_00)) # salary
342
- expect(lines[1].balance).to eq(Money.new(500_00)) # savings
343
- expect(lines[2].balance).to eq(Money.new(300_00)) # purchase
344
- expect(lines[3].balance).to eq(Money.new(200_00)) # layby
345
- expect(lines[4].balance).to eq(Money.new(100_00)) # deposit
346
- expect(lines[5].balance).to eq(Money.new(300_00)) # savings
347
- expect(lines[6].balance).to eq(Money.new(100_00)) # purchase
348
- end
349
- end
350
-
351
- describe 'scoping of accounts' do
352
- before do
353
- DoubleEntry.configure do |config|
354
- config.define_accounts do |accounts|
355
- user_scope = accounts.active_record_scope_identifier(User)
356
- accounts.define(:identifier => :bank)
357
- accounts.define(:identifier => :cash, :scope_identifier => user_scope)
358
- accounts.define(:identifier => :savings, :scope_identifier => user_scope)
359
- end
360
-
361
- config.define_transfers do |transfers|
362
- transfers.define(:from => :bank, :to => :cash, :code => :xfer)
363
- transfers.define(:from => :cash, :to => :cash, :code => :xfer)
364
- transfers.define(:from => :cash, :to => :savings, :code => :xfer)
365
- end
366
- end
367
- end
368
-
369
- let(:bank) { DoubleEntry.account(:bank) }
370
- let(:cash) { DoubleEntry.account(:cash) }
371
- let(:savings) { DoubleEntry.account(:savings) }
372
-
373
- let(:john) { User.make! }
374
- let(:johns_cash) { DoubleEntry.account(:cash, :scope => john) }
375
- let(:johns_savings) { DoubleEntry.account(:savings, :scope => john) }
376
-
377
- let(:ryan) { User.make! }
378
- let(:ryans_cash) { DoubleEntry.account(:cash, :scope => ryan) }
379
- let(:ryans_savings) { DoubleEntry.account(:savings, :scope => ryan) }
380
-
381
- it 'treats each separately scoped account having their own separate balances' do
382
- DoubleEntry.transfer(Money.new(20_00), :from => bank, :to => johns_cash, :code => :xfer)
383
- DoubleEntry.transfer(Money.new(10_00), :from => bank, :to => ryans_cash, :code => :xfer)
384
- expect(johns_cash.balance).to eq(Money.new(20_00))
385
- expect(ryans_cash.balance).to eq(Money.new(10_00))
386
- end
387
-
388
- it 'allows transfer between two separately scoped accounts' do
389
- DoubleEntry.transfer(Money.new(10_00), :from => ryans_cash, :to => johns_cash, :code => :xfer)
390
- expect(ryans_cash.balance).to eq(Money.new(-10_00))
391
- expect(johns_cash.balance).to eq(Money.new(10_00))
392
- end
393
-
394
- it 'reports balance correctly if called from either account or finances object' do
395
- DoubleEntry.transfer(Money.new(10_00), :from => ryans_cash, :to => johns_cash, :code => :xfer)
396
- expect(ryans_cash.balance).to eq(Money.new(-10_00))
397
- expect(DoubleEntry.balance(:cash, :scope => ryan)).to eq(Money.new(-10_00))
398
- end
399
-
400
- it 'raises an exception if you try to scope with an object instance of differing class to that defined on the account' do
401
- not_a_user = double(:id => 7)
402
-
403
- expect do
404
- DoubleEntry.account(:savings, :scope => not_a_user)
405
- end.to raise_error DoubleEntry::AccountScopeMismatchError
406
-
407
- expect do
408
- DoubleEntry.balance(:savings, :scope => not_a_user)
409
- end.to raise_error DoubleEntry::AccountScopeMismatchError
410
- end
411
-
412
- it 'raises exception if you try to transfer between the same account, despite it being scoped' do
413
- expect do
414
- DoubleEntry.transfer(Money.new(10_00), :from => ryans_cash, :to => ryans_cash, :code => :xfer)
415
- end.to raise_error(DoubleEntry::TransferNotAllowed)
416
- end
417
-
418
- it 'allows transfer from one persons account to the same persons other kind of account' do
419
- DoubleEntry.transfer(Money.new(100_00), :from => ryans_cash, :to => ryans_savings, :code => :xfer)
420
- expect(ryans_cash.balance).to eq(Money.new(-100_00))
421
- expect(ryans_savings.balance).to eq(Money.new(100_00))
422
- end
423
-
424
- it 'disallows you to report on scoped accounts globally' do
425
- expect { DoubleEntry.balance(:cash) }.to raise_error DoubleEntry::UnknownAccount
426
- end
427
- end
428
- end