mudrat_projector 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +3 -0
  5. data/Gemfile +4 -0
  6. data/Gemfile.lock +49 -0
  7. data/README.md +46 -0
  8. data/Rakefile +26 -0
  9. data/bin/testrb +3 -0
  10. data/finance_fu.txt +41 -0
  11. data/lib/mudrat_projector/account.rb +75 -0
  12. data/lib/mudrat_projector/amortizer.rb +103 -0
  13. data/lib/mudrat_projector/banker_rounding.rb +11 -0
  14. data/lib/mudrat_projector/chart_of_accounts.rb +119 -0
  15. data/lib/mudrat_projector/date_diff.rb +169 -0
  16. data/lib/mudrat_projector/projection.rb +45 -0
  17. data/lib/mudrat_projector/projector.rb +71 -0
  18. data/lib/mudrat_projector/schedule.rb +45 -0
  19. data/lib/mudrat_projector/scheduled_transaction.rb +45 -0
  20. data/lib/mudrat_projector/tax_calculation.rb +154 -0
  21. data/lib/mudrat_projector/tax_calculator.rb +144 -0
  22. data/lib/mudrat_projector/tax_values_by_year.yml +102 -0
  23. data/lib/mudrat_projector/transaction.rb +71 -0
  24. data/lib/mudrat_projector/transaction_entry.rb +126 -0
  25. data/lib/mudrat_projector/transaction_handler.rb +19 -0
  26. data/lib/mudrat_projector/validator.rb +49 -0
  27. data/lib/mudrat_projector/version.rb +3 -0
  28. data/lib/mudrat_projector.rb +27 -0
  29. data/mudrat_projector.gemspec +28 -0
  30. data/test/integrations/long_term_projection_test.rb +42 -0
  31. data/test/integrations/mortgage_test.rb +85 -0
  32. data/test/integrations/self_employed_tax_calculation_test.rb +44 -0
  33. data/test/models/accounts_test.rb +39 -0
  34. data/test/models/chart_of_accounts_test.rb +170 -0
  35. data/test/models/date_diff_test.rb +117 -0
  36. data/test/models/projection_test.rb +62 -0
  37. data/test/models/projector_test.rb +168 -0
  38. data/test/models/schedule_test.rb +68 -0
  39. data/test/models/scheduled_transaction_test.rb +47 -0
  40. data/test/models/tax_calculator_test.rb +145 -0
  41. data/test/models/transaction_entry_test.rb +37 -0
  42. data/test/models/transaction_handler_test.rb +67 -0
  43. data/test/models/transaction_test.rb +66 -0
  44. data/test/models/validator_test.rb +45 -0
  45. data/test/test_helper.rb +69 -0
  46. metadata +204 -0
@@ -0,0 +1,126 @@
1
+ module MudratProjector
2
+ class TransactionEntry
3
+ attr :account_id, :scalar
4
+
5
+ module TransactionEntry::Factory
6
+ def new params = {}
7
+ catch :instance do
8
+ maybe_build_new_fixed_entry params
9
+ maybe_build_new_percentage_entry params
10
+ super
11
+ end
12
+ end
13
+
14
+ def new_credit params = {}
15
+ params = { credit_or_debit: :credit }.merge params
16
+ new params
17
+ end
18
+
19
+ def new_debit params = {}
20
+ params = { credit_or_debit: :debit }.merge params
21
+ new params
22
+ end
23
+
24
+ private
25
+
26
+ def maybe_build_new_fixed_entry params
27
+ return unless params.has_key? :amount
28
+ params = params.dup
29
+ params[:scalar] = params.delete :amount
30
+ throw :instance, new(params).tap(&:calculate)
31
+ end
32
+
33
+ def maybe_build_new_percentage_entry params
34
+ return unless params.has_key?(:percent) && self == TransactionEntry
35
+ params = params.dup
36
+ params[:scalar] = params.delete :percent
37
+ params[:other_account_id] = params.delete :of
38
+ throw :instance, PercentageTransactionEntry.new(params)
39
+ end
40
+ end
41
+
42
+ extend TransactionEntry::Factory
43
+
44
+ attr :amount, :delta
45
+
46
+ def initialize params = {}
47
+ @account_id = params.fetch :account_id
48
+ @scalar = params.fetch :scalar
49
+ @credit_or_debit = params.fetch :credit_or_debit
50
+ validate!
51
+ end
52
+
53
+ def * multiplier
54
+ return self if multiplier == 1
55
+ self.class.new serialize.merge(scalar: scalar * multiplier)
56
+ end
57
+
58
+ def calculate chart_of_accounts = nil
59
+ @amount = calculate_amount chart_of_accounts
60
+ return if chart_of_accounts.nil?
61
+ account_type = chart_of_accounts.fetch(account_id).type
62
+ check = %i(asset expense).include?(account_type) ? :debit? : :credit?
63
+ @delta = send(check) ? amount : -amount
64
+ end
65
+
66
+ def calculate_amount chart_of_accounts
67
+ @amount = scalar
68
+ end
69
+
70
+ def credit?
71
+ @credit_or_debit == :credit
72
+ end
73
+
74
+ def debit?
75
+ @credit_or_debit == :debit
76
+ end
77
+
78
+ def inspect
79
+ "#<#{self.class}: amount=#{fmt(scalar)}, account_id=#{account_id.inspect} type=#{@credit_or_debit.inspect}>"
80
+ end
81
+
82
+ def serialize
83
+ {
84
+ account_id: account_id,
85
+ scalar: scalar,
86
+ credit_or_debit: @credit_or_debit,
87
+ }
88
+ end
89
+
90
+ def validate!
91
+ if scalar == 0
92
+ raise ArgumentError, "You cannot supply a scalar of 0"
93
+ end
94
+ unless %i(credit debit).include? @credit_or_debit
95
+ raise ArgumentError, "Must supply :credit or :debit, not #{@credit_or_debit.inspect}"
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def fmt number
102
+ number.respond_to?(:round) ? number.round(2).to_f : number
103
+ end
104
+ end
105
+
106
+ class PercentageTransactionEntry < TransactionEntry
107
+ attr :other_account_id
108
+
109
+ def initialize params = {}
110
+ @other_account_id = params.fetch :other_account_id
111
+ super params
112
+ end
113
+
114
+ def calculate_amount chart_of_accounts
115
+ @amount = scalar * chart_of_accounts.fetch(other_account_id).balance
116
+ end
117
+
118
+ def inspect
119
+ "#<#{self.class}: percent=#{fmt(scalar * 100)}%, account_id=#{account_id.inspect} type=#{@credit_or_debit.inspect}, other_account_id=#{other_account_id.inspect}>"
120
+ end
121
+
122
+ def serialize
123
+ super.tap do |hash| hash[:other_account_id] = other_account_id; end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,19 @@
1
+ module MudratProjector
2
+ class TransactionHandler
3
+ attr_accessor :next_projector
4
+
5
+ def initialize projection: projection
6
+ @projection = projection
7
+ end
8
+
9
+ def << transaction
10
+ in_projection, leftover = transaction.slice @projection.range.end
11
+ @projection.add_transaction_batch in_projection
12
+ defer leftover if leftover
13
+ end
14
+
15
+ def defer transaction
16
+ next_projector.add_transaction transaction if next_projector
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,49 @@
1
+ module MudratProjector
2
+ class Validator
3
+ attr :chart, :projector
4
+
5
+ def initialize projector: projector, chart: chart
6
+ @projector = projector
7
+ @chart = chart
8
+ end
9
+
10
+ def must_be_balanced!
11
+ unless projector.balanced?
12
+ raise Projector::BalanceError, "Cannot project unless the accounts "\
13
+ "are in balance"
14
+ end
15
+ end
16
+
17
+ def validate_account! account_id, params
18
+ if chart.exists? account_id
19
+ raise Projector::AccountExists, "Account #{account_id.inspect} exists"
20
+ end
21
+ unless Account::TYPES.include? params[:type]
22
+ raise Projector::InvalidAccount, "Account #{account_id.inspect} has "\
23
+ "invalid type #{params[:type].inspect}"
24
+ end
25
+ if params.has_key?(:open_date) && params[:open_date] > projector.from
26
+ if params.has_key? :opening_balance
27
+ raise Projector::InvalidAccount, "Account #{account_id.inspect} opens "\
28
+ "after projector, but has an opening balance"
29
+ end
30
+ end
31
+ end
32
+
33
+ def validate_transaction! transaction
34
+ if transaction.date < projector.from
35
+ raise Projector::InvalidTransaction, "Transactions cannot occur before "\
36
+ "projection start date. (#{projector.from} vs. #{transaction.date})"
37
+ end
38
+ unless transaction.balanced?
39
+ raise Projector::BalanceError, "Credits and debit entries both "\
40
+ "must be supplied; they cannot amount to zero"
41
+ end
42
+ if transaction.credits.empty? || transaction.debits.empty?
43
+ raise Projector::InvalidTransaction, "You must supply at least a debit "\
44
+ "and a credit on each transaction"
45
+ end
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,3 @@
1
+ module MudratProjector
2
+ VERSION = "0.9.0"
3
+ end
@@ -0,0 +1,27 @@
1
+ require "bigdecimal"
2
+ require "bigdecimal/util"
3
+ require "date"
4
+ require "forwardable"
5
+ require "yaml"
6
+
7
+ require "mudrat_projector/version"
8
+
9
+ module MudratProjector
10
+ ABSOLUTE_START = Date.new 1970
11
+ ABSOLUTE_END = Date.new 9999
12
+
13
+ def self.classify sym
14
+ "_#{sym}".gsub %r{_[a-z]} do |bit|
15
+ bit.slice! 0, 1
16
+ bit.upcase!
17
+ bit
18
+ end
19
+ end
20
+
21
+ Dir.glob File.expand_path("../mudrat_projector/**/*.rb", __FILE__) do |path|
22
+ base_without_ext = File.basename path, ".*"
23
+ klass_name = classify base_without_ext
24
+ relative_path = File.join "mudrat_projector", base_without_ext
25
+ autoload klass_name, relative_path
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mudrat_projector/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mudrat_projector"
8
+ spec.version = MudratProjector::VERSION
9
+ spec.authors = ["ntl"]
10
+ spec.email = ["nathanladd+github@gmail.com"]
11
+ spec.description = %q{Mudrat Projector is a simple financial projection engine.}
12
+ spec.summary = %q{Mudrat Projector is a simple financial projection engine designed for personal finance computations.}
13
+ spec.homepage = "https://github.com/ntl/mudrat-projector"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^test/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "minitest"
23
+ spec.add_development_dependency "minitest-reporters"
24
+ spec.add_development_dependency "pry"
25
+ spec.add_development_dependency "rake"
26
+ spec.add_development_dependency "simplecov"
27
+ spec.add_development_dependency "timecop"
28
+ end
@@ -0,0 +1,42 @@
1
+ require 'test_helper'
2
+
3
+ class LongTermProjectiontest < Minitest::Test
4
+ def setup
5
+ @projector = Projector.new from: jan_1_2012
6
+ @projector.add_account :checking, type: :asset
7
+ @projector.add_account :job, type: :revenue, tags: %i(salary)
8
+ @projector.add_account :investment, type: :asset
9
+ @projector.add_account :dividends, type: :revenue, tags: %i(dividend)
10
+
11
+ @projector.add_transaction(
12
+ date: jan_1_2012,
13
+ credit: { amount: 6000, account_id: :job },
14
+ debit: { amount: 6000, account_id: :checking },
15
+ schedule: every_month,
16
+ )
17
+ @projector.add_transaction(
18
+ date: jan_1_2012,
19
+ credit: { percent: (6 / 1200.to_d), of: :investment, account_id: :dividends },
20
+ debit: { percent: (6 / 1200.to_d), of: :investment, account_id: :investment },
21
+ schedule: every_month,
22
+ )
23
+ @projector.add_transaction(
24
+ date: jan_1_2012,
25
+ credit: { amount: 500, account_id: :checking },
26
+ debit: { amount: 500, account_id: :investment },
27
+ schedule: every_month,
28
+ )
29
+
30
+ @household = { filing_status: :single, exemptions: 1 }
31
+ end
32
+
33
+ def test_two_years
34
+ projector = TaxCalculator.project @projector, to: dec_31_2013, household: @household
35
+
36
+ # used http://investor.gov/tools/calculators/compound-interest-calculator
37
+ assert_in_epsilon 715.98, projector.account_balance(:dividends)
38
+ expected_gross = 72000 + 72000 + 715.98
39
+ expected_taxes = 15702.45 + 17073.80
40
+ assert_in_epsilon expected_gross - expected_taxes, projector.net_worth
41
+ end
42
+ end
@@ -0,0 +1,85 @@
1
+ require 'test_helper'
2
+
3
+ class MortgageTest < Minitest::Test
4
+ def setup
5
+ @loan_amount = 200_000
6
+ @home_value = 250_000
7
+ @down_payment = @home_value - @loan_amount
8
+
9
+ @interest_rate = 5.0.to_d
10
+ @property_tax_rate = 1.25.to_d
11
+ @loan_term = 30
12
+ @start_date = jan_1_2012
13
+
14
+ @projector = Projector.new from: jan_1_2012
15
+
16
+ @projector.add_account :checking, type: :asset, opening_balance: 50000
17
+ @projector.add_account :job, type: :revenue, opening_balance: 50000
18
+ @projector.add_account :home, type: :asset, open_date: feb_1_2012
19
+ @projector.add_account :mortgage, type: :liability, open_date: feb_1_2012
20
+ @projector.add_account :interest, type: :expense, open_date: feb_1_2012
21
+
22
+ @projector.add_transaction(
23
+ date: jan_1_2012,
24
+ credit: { amount: 6000, account_id: :job },
25
+ debit: { amount: 6000, account_id: :checking },
26
+ schedule: { unit: :month, scalar: 1 },
27
+ )
28
+
29
+ @projector.add_transaction(
30
+ date: feb_1_2012,
31
+ credits: [{ amount: @down_payment, account_id: :checking },
32
+ { amount: @loan_amount, account_id: :mortgage }],
33
+ debit: { amount: @home_value, account_id: :home },
34
+ )
35
+ end
36
+
37
+ def test_balance_after_first_year
38
+ pay_off_loan years: 30
39
+ @projector.project to: dec_31_2012
40
+ assert_in_epsilon 197_300.83, @projector.account_balance(:mortgage).to_f
41
+ end
42
+
43
+ def test_net_worth_after_first_year
44
+ expected_checking_balance = 72000 - (9110.90 + 2699.17)
45
+ expected_net_worth = expected_checking_balance + @home_value - 197_300.83
46
+
47
+ pay_off_loan years: 30
48
+ @projector.project to: dec_31_2012
49
+
50
+ assert_in_epsilon expected_net_worth, @projector.net_worth
51
+ end
52
+
53
+ def test_mortgage_balances_to_zero_at_end_of_term
54
+ pay_off_loan years: 30
55
+ @projector.project to: jan_31_2042
56
+ assert_in_epsilon 0, @projector.account_balance(:mortgage)
57
+ end
58
+
59
+ def test_pay_off_mortgage_early
60
+ pay_off_loan years: 15
61
+ @projector.project to: jan_31_2027
62
+ assert_in_epsilon 0, @projector.account_balance(:mortgage)
63
+ end
64
+
65
+ private
66
+
67
+ def calculate_monthly_payment r, years = @loan_term
68
+ x = (1 + r) ** (years * 12)
69
+ mp = (@loan_amount * r * x) / (x - 1)
70
+ end
71
+
72
+ def pay_off_loan years: @loan_term
73
+ r = @interest_rate / 1200
74
+ mp = calculate_monthly_payment r, years
75
+
76
+ @projector.add_transaction(
77
+ date: feb_1_2012,
78
+ credits: [{ percent: r, of: :mortgage, account_id: :mortgage},
79
+ { amount: mp, account_id: :checking }],
80
+ debits: [{ amount: mp, account_id: :mortgage },
81
+ { percent: r, of: :mortgage, account_id: :interest}],
82
+ schedule: { unit: :month, scalar: 1, count: (years * 12) },
83
+ )
84
+ end
85
+ end
@@ -0,0 +1,44 @@
1
+ require 'test_helper'
2
+
3
+ # http://taxes.about.com/od/paymentoptions/a/estimated_tax_3.htm
4
+ class SelfEmployedTaxCalculationTest < Minitest::Test
5
+ def setup
6
+ @projector = Projector.new from: jan_1_2013
7
+
8
+ @projector.accounts = {
9
+ checking: { type: :asset },
10
+ se_expenses: { type: :expense, tags: %i(self_employed) },
11
+ se_job: { type: :revenue, tags: %i(self_employed) },
12
+ }
13
+
14
+ @projector.transactions = [{
15
+ date: jan_1_2013,
16
+ credit: { amount: 25000 / 3.0.to_d, account_id: :se_job },
17
+ debit: { amount: 25000 / 3.0.to_d, account_id: :checking },
18
+ schedule: every_month,
19
+ },{
20
+ date: jan_1_2013,
21
+ credit: { amount: 7500 / 3.0.to_d, account_id: :checking },
22
+ debit: { amount: 7500 / 3.0.to_d, account_id: :se_expenses },
23
+ schedule: every_month,
24
+ }]
25
+ end
26
+
27
+ def test_shelley
28
+ @tax_calculator = TaxCalculator.new projector: @projector, household: single
29
+ calculation = @tax_calculator.calculate!
30
+ assert_in_epsilon 70000, calculation.total_income
31
+ assert_in_epsilon 9890.685, calculation.self_employment_tax
32
+ assert_in_epsilon 4945.34, calculation.adjustments
33
+ assert_in_epsilon 65054.66, calculation.agi
34
+ assert_in_epsilon 55054.66, calculation.taxable_income
35
+ assert_in_epsilon 9692.50, calculation.income_tax
36
+ assert_in_epsilon 19583.19, calculation.taxes
37
+ end
38
+
39
+ private
40
+
41
+ def single
42
+ { filing_status: :single, exemptions: 1 }
43
+ end
44
+ end
@@ -0,0 +1,39 @@
1
+ require 'test_helper'
2
+
3
+ class AccountTest < Minitest::Test
4
+ def setup
5
+ @chart = ChartOfAccounts.new
6
+ @account = @chart.add_account :foo, type: :asset
7
+ end
8
+
9
+ # Closed means "are the books closed?", e.g. are there any transactions that
10
+ # haven't been processed
11
+ def test_closed_returns_true_iff_no_transaction_entries
12
+ assert @account.closed?
13
+ add_transaction_entry
14
+ refute @account.closed?
15
+ end
16
+
17
+ def test_close_freezes_and_returns_account_if_closed
18
+ new_account = @account.close!
19
+
20
+ assert @account.frozen?
21
+ assert_equal new_account.object_id, @account.object_id
22
+ end
23
+
24
+ def test_close_freezes_and_returns_new_closed_account_if_unclosed
25
+ add_transaction_entry
26
+ new_account = @account.close!
27
+
28
+ assert @account.frozen?
29
+ refute_equal new_account.object_id, @account.object_id
30
+ end
31
+
32
+ private
33
+
34
+ def add_transaction_entry
35
+ entry = TransactionEntry.new_debit account_id: :foo, amount: 1
36
+ entry.calculate @chart
37
+ @account.add_entry entry
38
+ end
39
+ end
@@ -0,0 +1,170 @@
1
+ require 'test_helper'
2
+
3
+ class ChartOfAccountsTest < Minitest::Test
4
+ def setup
5
+ @chart = ChartOfAccounts.new
6
+ end
7
+
8
+ def test_can_add_an_account
9
+ @chart.add_account :checking, type: :asset, open_date: jan_1_2000
10
+ assert_equal 1, @chart.size
11
+ end
12
+
13
+ def test_adding_account_can_default_open_date
14
+ @chart.add_account :checking, type: :asset
15
+ assert_equal 1, @chart.size
16
+ assert_equal ABSOLUTE_START, @chart.fetch(:checking).open_date
17
+ end
18
+
19
+ def test_can_set_opening_balance
20
+ @chart.add_account :checking, type: :asset, opening_balance: 500
21
+ assert_equal [500], @chart.map(&:balance)
22
+ end
23
+
24
+ def test_balanced_iff_opening_balances_zero_out
25
+ assert_equal 0, @chart.balance
26
+ @chart.add_account :checking, type: :asset, opening_balance: 500
27
+ assert_equal 500, @chart.balance
28
+ @chart.add_account :slush, type: :equity, opening_balance: 500
29
+ assert_equal 0, @chart.balance
30
+ end
31
+
32
+ def test_can_tag
33
+ @chart.add_account :checking, type: :asset, tags: %i(foo)
34
+ assert @chart.fetch(:checking).tag? :foo
35
+ end
36
+
37
+ def test_can_add_a_child_account
38
+ @chart.add_account :my_bank, type: :asset
39
+ @chart.split_account :my_bank, into: %i(checking)
40
+ end
41
+
42
+ def test_can_add_a_child_account_with_opening_balances
43
+ @chart.add_account :my_bank, type: :asset, opening_balance: 200
44
+ @chart.split_account :my_bank, into: { checking: { amount: 50 }, savings: { amount: 150 }}
45
+ assert_equal 200, @chart.fetch(:my_bank).balance
46
+ assert_equal 50, @chart.fetch(:checking).balance
47
+ assert_equal 150, @chart.fetch(:savings).balance
48
+ assert_equal 200, @chart.balance
49
+ end
50
+
51
+ def test_can_add_a_child_account_with_tags
52
+ @chart.add_account :my_bank, type: :asset, tags: %i(foo)
53
+ @chart.split_account :my_bank, into: [:checking, [:savings, { tags: %i(bar) }]]
54
+ assert @chart.fetch(:checking).tag? :foo
55
+ refute @chart.fetch(:checking).tag? :bar
56
+ assert @chart.fetch(:savings).tag? :foo
57
+ assert @chart.fetch(:savings).tag? :bar
58
+ end
59
+
60
+ def test_net_worth
61
+ add_smattering_of_accounts
62
+ # Net worth is assets minus liabilities
63
+ assert_equal (25 + 400 + 15000) - (13000), @chart.net_worth
64
+ end
65
+
66
+ def test_applying_fixed_transaction
67
+ @chart.add_account :checking, type: :asset
68
+ @chart.add_account :job, type: :revenue
69
+
70
+ fixed_transaction = Transaction.new(
71
+ date: jan_1_2000,
72
+ debits: [{ amount: 1234, account_id: :checking, credit_or_debit: :debit },
73
+ { amount: 12, account_id: :job, credit_or_debit: :debit }],
74
+ credits: [{ amount: 1234, account_id: :job, credit_or_debit: :credit},
75
+ { amount: 12, account_id: :checking, credit_or_debit: :credit}],
76
+ )
77
+
78
+ @chart.apply_transaction fixed_transaction
79
+ assert_equal 1222, @chart.net_worth
80
+ end
81
+
82
+ def test_applying_transaction_with_percentages
83
+ @chart.add_account :checking, type: :asset
84
+ @chart.add_account :investment, type: :asset, opening_balance: 200000
85
+ @chart.add_account :investment_revenue, type: :revenue
86
+
87
+ percentage_transaction = Transaction.new(
88
+ date: dec_31_2000,
89
+ debit: { percent: 0.06, of: :investment, account_id: :checking, credit_or_debit: :debit },
90
+ credit: { percent: 0.06, of: :investment, account_id: :investment_revenue, credit_or_debit: :credit },
91
+ )
92
+
93
+ @chart.apply_transaction percentage_transaction
94
+ assert_equal 212000, @chart.net_worth
95
+ assert_equal 12000, @chart.fetch(:checking).balance
96
+ end
97
+
98
+ def test_applying_transaction_for_non_existent_or_future_accounts_raises_error
99
+ @chart.add_account :checking, type: :asset, open_date: jan_1_2000
100
+ @chart.add_account :job, type: :revenue
101
+
102
+ transaction = Transaction.new(
103
+ date: jan_2_2000,
104
+ debit: { amount: 1234, account_id: :no_existe, credit_or_debit: :debit },
105
+ credit: { amount: 1234, account_id: :job, credit_or_debit: :credit },
106
+ )
107
+
108
+ assert_raises Projector::AccountDoesNotExist do
109
+ @chart.apply_transaction transaction
110
+ end
111
+ end
112
+
113
+ def test_serialize
114
+ add_smattering_of_accounts
115
+ expected_hash = {
116
+ checking: { type: :asset, opening_balance: 25 },
117
+ savings: { type: :asset, opening_balance: 400 },
118
+ auto: { type: :asset, opening_balance: 15000 },
119
+ bills: { type: :expense, opening_balance: 26 },
120
+ car_loan: { type: :liability, opening_balance: 13000 },
121
+ new_job: { type: :revenue, opening_balance: 500 },
122
+ }
123
+ assert_equal expected_hash.keys, @chart.serialize.keys
124
+ expected_hash.each do |key, expected_value|
125
+ assert_equal expected_value, @chart.serialize.fetch(key)
126
+ end
127
+ assert_equal expected_hash, @chart.serialize
128
+ end
129
+
130
+ def test_account_balance_with_splits
131
+ @chart.add_account :my_bank, type: :asset
132
+ @chart.add_account :job, type: :revenue
133
+ @chart.split_account :my_bank, into: %i(checking savings)
134
+ @chart.split_account :checking, into: %i(checking_1 checking_2)
135
+
136
+ @chart.apply_transaction Transaction.new(
137
+ date: jan_1_2000,
138
+ debit: { amount: 1, account_id: :my_bank, credit_or_debit: :debit },
139
+ credit: { amount: 1, account_id: :job, credit_or_debit: :credit },
140
+ )
141
+ @chart.apply_transaction Transaction.new(
142
+ date: jan_1_2000,
143
+ debit: { amount: 5, account_id: :checking, credit_or_debit: :debit },
144
+ credit: { amount: 5, account_id: :job, credit_or_debit: :credit },
145
+ )
146
+ @chart.apply_transaction Transaction.new(
147
+ date: jan_1_2000,
148
+ debit: { amount: 12, account_id: :checking_1, credit_or_debit: :debit },
149
+ credit: { amount: 12, account_id: :job, credit_or_debit: :credit },
150
+ )
151
+
152
+ assert_equal 12, @chart.account_balance(:checking_1)
153
+ assert_equal 0, @chart.account_balance(:checking_2)
154
+ assert_equal 18, @chart.account_balance(:job)
155
+ assert_equal 17, @chart.account_balance(:checking)
156
+ assert_equal 0, @chart.account_balance(:savings)
157
+ assert_equal 18, @chart.account_balance(:my_bank)
158
+ end
159
+
160
+ private
161
+
162
+ def add_smattering_of_accounts
163
+ @chart.add_account :checking, type: :asset, opening_balance: 25
164
+ @chart.add_account :savings, type: :asset, opening_balance: 400
165
+ @chart.add_account :auto, type: :asset, opening_balance: 15000
166
+ @chart.add_account :bills, type: :expense, opening_balance: 26
167
+ @chart.add_account :car_loan, type: :liability, opening_balance: 13000
168
+ @chart.add_account :new_job, type: :revenue, opening_balance: 500
169
+ end
170
+ end