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,45 @@
1
+ module MudratProjector
2
+ class Projection
3
+ attr :range
4
+
5
+ SequenceEntry = Struct.new :transaction, :batch_id do
6
+ def sort_key
7
+ [transaction.date, batch_id]
8
+ end
9
+
10
+ def <=> other_entry
11
+ sort_key <=> other_entry.sort_key
12
+ end
13
+ end
14
+
15
+ def initialize range: date_range, chart: chart_of_accounts
16
+ @chart = chart
17
+ @batch_id = 0
18
+ @range = range
19
+ @transaction_sequence = []
20
+ end
21
+
22
+ def << transaction
23
+ @transaction_sequence.push SequenceEntry.new(transaction, @batch_id)
24
+ end
25
+
26
+ def add_transaction_batch batch
27
+ batch.each do |transaction|
28
+ self << transaction
29
+ @batch_id += 1
30
+ end
31
+ end
32
+
33
+ def project!
34
+ freeze
35
+ transaction_sequence.each do |transaction|
36
+ @chart.apply_transaction transaction
37
+ yield transaction if block_given?
38
+ end
39
+ end
40
+
41
+ def transaction_sequence
42
+ @transaction_sequence.tap(&:sort!).map &:transaction
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,71 @@
1
+ module MudratProjector
2
+ class Projector
3
+ extend Forwardable
4
+ extend BankerRounding
5
+
6
+ AccountDoesNotExist = Class.new StandardError
7
+ AccountExists = Class.new ArgumentError
8
+ BalanceError = Class.new StandardError
9
+ InvalidAccount = Class.new ArgumentError
10
+ InvalidTransaction = Class.new StandardError
11
+
12
+ attr :from
13
+
14
+ def initialize params = {}
15
+ @chart = params.fetch :chart, ChartOfAccounts.new
16
+ @from = params.fetch :from, ABSOLUTE_START
17
+ @transactions = []
18
+ @validator = Validator.new projector: self, chart: @chart
19
+ end
20
+
21
+ def_delegators :@chart, *%i(accounts account_balance apply_transaction balance fetch net_worth split_account)
22
+ def_delegators :@validator, *%i(must_be_balanced! validate_account! validate_transaction!)
23
+
24
+ def accounts= accounts
25
+ accounts.each do |account_id, params|
26
+ add_account account_id, params
27
+ end
28
+ end
29
+
30
+ def account_exists? account_id
31
+ @chart.exists? account_id
32
+ end
33
+
34
+ def add_account account_id, **params
35
+ validate_account! account_id, params
36
+ @chart.add_account account_id, params
37
+ end
38
+
39
+ def add_transaction params
40
+ if params.is_a? Transaction
41
+ transaction = params
42
+ else
43
+ klass = params.has_key?(:schedule) ? ScheduledTransaction : Transaction
44
+ transaction = klass.new params
45
+ validate_transaction! transaction
46
+ end
47
+ @transactions.push transaction
48
+ end
49
+
50
+ def balanced?
51
+ balance.zero?
52
+ end
53
+
54
+ def project to: end_of_projection, build_next: false, &block
55
+ must_be_balanced!
56
+ projection = Projection.new range: (from..to), chart: @chart
57
+ handler = TransactionHandler.new projection: projection
58
+ if build_next
59
+ handler.next_projector = self.class.new from: to + 1, chart: @chart
60
+ end
61
+ @transactions.each do |transaction| handler << transaction; end
62
+ projection.project! &block
63
+ handler.next_projector
64
+ end
65
+
66
+ def transactions= transactions
67
+ transactions.each do |transaction| add_transaction transaction; end
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,45 @@
1
+ module MudratProjector
2
+ class Schedule
3
+ attr :count, :scalar, :unit
4
+
5
+ def initialize params = {}
6
+ @count = params.fetch :count, nil
7
+ @scalar = params.fetch :scalar
8
+ @unit = params.fetch :unit
9
+ end
10
+
11
+ def advance_over range
12
+ split_count_over(range).reduce range.begin do |date, factor|
13
+ @count -= factor if @count
14
+ yield [date, factor]
15
+ DateDiff.advance intervals: factor, unit: unit, from: date
16
+ end
17
+ end
18
+
19
+ def finished?
20
+ (count.nil? || count > 0) ? false : true
21
+ end
22
+
23
+ def split_count_over range
24
+ diff = DateDiff.date_diff unit: unit, from: range.begin, to: range.end
25
+ full_units, final_prorate = [@count, diff].compact.min.divmod 1
26
+ ([1] * full_units).tap do |list|
27
+ list.push final_prorate unless final_prorate.zero?
28
+ end
29
+ end
30
+
31
+ def serialize
32
+ {
33
+ scalar: scalar,
34
+ unit: unit,
35
+ }.tap { |h| h[:count] = count if count }
36
+ end
37
+
38
+ def slice range, &block
39
+ bits = []
40
+ advance_over range do |date, factor| bits.push [date, factor]; end
41
+ leftover = finished? ? nil : serialize
42
+ [bits, leftover]
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,45 @@
1
+ module MudratProjector
2
+ class ScheduledTransaction < Transaction
3
+ attr :schedule
4
+
5
+ def initialize params = {}
6
+ @schedule = Schedule.new params.fetch :schedule
7
+ super params
8
+ end
9
+
10
+ def build_transactions intervals
11
+ intervals.map do |date, prorate|
12
+ clone_transaction new_date: date, multiplier: prorate
13
+ end
14
+ end
15
+
16
+ def clone_transaction new_date: date, new_schedule: nil, multiplier: 1
17
+ params = build_cloned_tranasction_params new_date, multiplier
18
+ return Transaction.new(params) if new_schedule.nil?
19
+ params[:schedule] = new_schedule
20
+ ScheduledTransaction.new params
21
+ end
22
+
23
+ def multiply entries, by: 1
24
+ entries.map do |entry| entry * by; end
25
+ end
26
+
27
+ def slice slice_date
28
+ intervals, next_schedule = schedule.slice(date..slice_date)
29
+ if next_schedule
30
+ leftover = clone_transaction new_date: slice_date + 1, new_schedule: next_schedule
31
+ end
32
+ [build_transactions(intervals), leftover]
33
+ end
34
+
35
+ private
36
+
37
+ def build_cloned_tranasction_params new_date, multiplier
38
+ {
39
+ date: new_date,
40
+ debits: multiply(debits, by: multiplier),
41
+ credits: multiply(credits, by: multiplier),
42
+ }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,154 @@
1
+ module MudratProjector
2
+ class TaxCalculation
3
+ attr :values_hash
4
+ attr :w2_gross, :se_gross, :pretax_deductions, :other_gross
5
+ attr :projector, :household, :taxes_withheld
6
+
7
+ def initialize projector, household, values_hash
8
+ @values_hash = values_hash
9
+ @projector = projector
10
+ @w2_gross = 0
11
+ @se_gross = 0
12
+ @pretax_deductions = 0
13
+ @hsa_contribs = Hash.new { |h,k| h[k] = 0 }
14
+ @household = household
15
+ @itemized_deduction = 0
16
+ @taxes_withheld = 0
17
+ @other_gross = 0
18
+ extend_year_shim
19
+ end
20
+
21
+ def method_missing method_name, *args
22
+ if args.empty? && values_hash.has_key?(method_name)
23
+ values_hash.fetch method_name
24
+ else
25
+ super
26
+ end
27
+ end
28
+
29
+ def << transaction
30
+ @w2_gross += transaction.salaries_and_wages
31
+ @se_gross += transaction.business_profit
32
+ @other_gross += transaction.dividend_income
33
+ @itemized_deduction += transaction.charitable_contributions
34
+ @itemized_deduction += transaction.interest_paid
35
+ @itemized_deduction += transaction.taxes_paid
36
+ @taxes_withheld += transaction.taxes_withheld
37
+ transaction.debits.each do |entry|
38
+ account = projector.fetch entry.account_id
39
+ if account.type == :asset && account.tag?(:hsa)
40
+ revenue = transaction.credits.select { |e| a = projector.fetch(e.account_id); a.type == :revenue && a.tag?(:salary)}
41
+ @pretax_deductions += revenue.reduce 0 do |s,e| s + e.amount; end
42
+ @hsa_contribs[account] += [entry.amount - pretax_deductions, 0].max
43
+ end
44
+ end
45
+ end
46
+
47
+ def adjustments
48
+ (self_employment_tax / 2) + hsa_contributions
49
+ end
50
+
51
+ def agi
52
+ total_income - adjustments
53
+ end
54
+
55
+ def brackets
56
+ bracket_caps = super.tap do |list|
57
+ list.push Float::INFINITY
58
+ end
59
+ values_hash.fetch(:bracket_rates).zip bracket_caps
60
+ end
61
+
62
+ def deduction
63
+ [itemized_deduction, standard_deduction].max
64
+ end
65
+
66
+ def effective_rate
67
+ (((gross - net) / gross) * 100).round 2
68
+ end
69
+
70
+ def exemption
71
+ personal_exemption * household.exemptions
72
+ end
73
+
74
+ def gross
75
+ w2_gross + se_gross + other_gross
76
+ end
77
+
78
+ def income_tax
79
+ amount_taxed = 0
80
+ brackets.reduce 0 do |sum, (rate, cap)|
81
+ if cap == Float::INFINITY
82
+ in_bracket = taxable_income - amount_taxed
83
+ else
84
+ in_bracket = [cap, taxable_income].min - amount_taxed
85
+ end
86
+ amount_taxed += in_bracket
87
+ t = (in_bracket * (rate / 100))
88
+ sum + t
89
+ end
90
+ end
91
+
92
+ def hsa_contributions
93
+ @hsa_contribs.reduce 0 do |sum, (account, amount)|
94
+ type = [:individual, :family, :senior].detect { |tag| account.tag? tag }
95
+ max = hsa_limit.fetch type
96
+ sum + [amount, max].min
97
+ end
98
+ end
99
+
100
+ def itemized_deduction
101
+ @itemized_deduction
102
+ end
103
+
104
+ def medicare_withheld
105
+ w2_gross * (hi_rate / 100)
106
+ end
107
+
108
+ def net
109
+ gross - taxes
110
+ end
111
+
112
+ def self_employment_tax
113
+ rate = (oasdi_rate + hi_rate) / 100
114
+ se_gross * (1 - rate) * (rate * 2)
115
+ end
116
+
117
+ def social_security_withheld
118
+ [w2_gross, oasdi_wage_base].min * (oasdi_rate / 100)
119
+ end
120
+
121
+ def taxable_income
122
+ agi - deduction - exemption
123
+ end
124
+
125
+ def taxes
126
+ withholding_tax + income_tax + self_employment_tax
127
+ end
128
+
129
+ def taxes_owed
130
+ taxes - taxes_withheld
131
+ end
132
+
133
+ def total_income
134
+ gross - pretax_deductions
135
+ end
136
+
137
+ def year
138
+ projector.from.year
139
+ end
140
+
141
+ def withholding_tax
142
+ medicare_withheld + social_security_withheld
143
+ end
144
+
145
+ private
146
+
147
+ def extend_year_shim
148
+ shim_module = "Year#{year}".to_sym
149
+ if TaxCalculator.const_defined? shim_module
150
+ extend TaxCalculator.const_get shim_module
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,144 @@
1
+ module MudratProjector
2
+ class TaxCalculator
3
+ module Year2012
4
+ # see http://en.wikipedia.org/wiki/Tax_Relief,_Unemployment_Insurance_Reauthorization,_and_Job_Creation_Act_of_2010
5
+ def social_security_withheld
6
+ super * (4.2.to_d / 6.2.to_d)
7
+ end
8
+ end
9
+
10
+ attr :household, :projector
11
+
12
+ HOUSEHOLD_TYPES = %i(married_filing_jointly married_filing_separately single
13
+ head_of_household)
14
+ EXPENSE_ACCOUNT_ID = :united_states_treasury
15
+
16
+ Household = Struct.new :filing_status, :exemptions do
17
+ def initialize *args
18
+ super
19
+ unless HOUSEHOLD_TYPES.include? filing_status
20
+ raise "Invalid filing status #{filing_status.inspect}"
21
+ end
22
+ end
23
+ end
24
+
25
+ def self.project projector, to: end_date, household: nil
26
+ tax_calculator = new projector: projector, household: household
27
+ (projector.from.year..to.year).each do
28
+ calculation = tax_calculator.calculate!
29
+ yield calculation if block_given?
30
+ end
31
+ tax_calculator.projector
32
+ end
33
+
34
+ class TransactionWrapper
35
+ INCOME_VALUES = %i(business_profit dividend_income salaries_and_wages)
36
+ ADJUSTMENTS_VALUES = %i(other_adjustments)
37
+ DEDUCTIONS_VALUES = %i(charitable_contributions interest_paid taxes_paid)
38
+ CREDITS_VALUES = %i()
39
+
40
+ VALUES = [INCOME_VALUES, ADJUSTMENTS_VALUES, DEDUCTIONS_VALUES, CREDITS_VALUES].flatten 1
41
+
42
+ attr :calculator, :taxes_withheld
43
+ private :calculator
44
+
45
+ def initialize calculator, transaction
46
+ @calculator = calculator
47
+ @taxes_withheld = 0
48
+ @transaction = transaction
49
+ VALUES.each do |attr_name| instance_variable_set "@#{attr_name}", 0; end
50
+ end
51
+
52
+ VALUES.each do |attr_name| attr attr_name; end
53
+
54
+ def calculate!
55
+ @transaction.entries.each do |entry|
56
+ account = calculator.projector.fetch entry.account_id
57
+ if account.type == :revenue
58
+ @salaries_and_wages += entry.delta if account.tag? :salary
59
+ @business_profit += entry.delta if account.tag? :self_employed
60
+ @dividend_income += entry.delta if account.tag? :dividend
61
+ elsif account.type == :expense
62
+ @business_profit -= entry.delta if account.tag? :self_employed
63
+ @charitable_contributions +=
64
+ entry.delta if account.tag? "501c".to_sym
65
+ @interest_paid += entry.delta if account.tag? :mortgage_interest
66
+ @taxes_paid += entry.delta if account.tag? :tax
67
+ @taxes_withheld += entry.delta if entry.account_id == EXPENSE_ACCOUNT_ID
68
+ elsif account.type == :asset
69
+ @other_adjustments += entry.delta if account.tag?(:hsa) && entry.debit?
70
+ end
71
+ end
72
+ end
73
+
74
+ def debits
75
+ @transaction.debits
76
+ end
77
+
78
+ def credits
79
+ @transaction.credits
80
+ end
81
+ end
82
+
83
+ def initialize projector: nil, household: {}
84
+ @household = Household.new(
85
+ household.fetch(:filing_status),
86
+ household.fetch(:exemptions),
87
+ )
88
+ @projector = projector
89
+ @values_hash = parse_yaml
90
+ unless projector.account_exists? EXPENSE_ACCOUNT_ID
91
+ projector.add_account EXPENSE_ACCOUNT_ID, type: :expense
92
+ end
93
+ unless projector.account_exists? :owed_taxes
94
+ projector.add_account :owed_taxes, type: :liability
95
+ end
96
+ end
97
+
98
+ def calculate!
99
+ end_of_calendar_year = Date.new year, 12, 31
100
+ calculation = TaxCalculation.new projector, household, @values_hash
101
+ next_projector = projector.project to: end_of_calendar_year, build_next: true do |transaction|
102
+ calculation << TransactionWrapper.new(self, transaction).tap(&:calculate!)
103
+ end
104
+ final_transaction = Transaction.new(
105
+ date: end_of_calendar_year,
106
+ debit: { amount: calculation.taxes_owed, account_id: EXPENSE_ACCOUNT_ID },
107
+ credit: { amount: calculation.taxes_owed, account_id: :owed_taxes },
108
+ )
109
+ projector.apply_transaction final_transaction
110
+ @projector = next_projector
111
+ @values_hash = parse_yaml
112
+ calculation
113
+ end
114
+
115
+ def year
116
+ projector.from.year
117
+ end
118
+
119
+ private
120
+
121
+ def parse_yaml
122
+ yaml = File.expand_path '../tax_values_by_year.yml', __FILE__
123
+ by_year = YAML.load File.read(yaml)
124
+ max_year = by_year.keys.max
125
+ parsed = by_year.fetch([year, max_year].min).tap do |hash|
126
+ recursively_symbolize_keys! hash
127
+ end
128
+ hash = parsed.fetch household.filing_status
129
+ parsed.each_with_object hash do |(key, value), hash|
130
+ hash[key] = value unless HOUSEHOLD_TYPES.include? key
131
+ end
132
+ hash
133
+ end
134
+
135
+ def recursively_symbolize_keys! hash
136
+ hash.keys.each do |key|
137
+ value = hash.delete key
138
+ recursively_symbolize_keys! value if value.is_a? Hash
139
+ key = key.respond_to?(:to_sym) ? key.to_sym : key
140
+ hash[key] = value
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,102 @@
1
+ 2012:
2
+ oasdi_rate: 6.2
3
+ oasdi_wage_base: 110_100
4
+ hi_rate: 1.45
5
+ personal_exemption: 3_800
6
+ hsa_limit:
7
+ individual: 3_100
8
+ family: 6_250
9
+ senior: 1000
10
+ bracket_rates:
11
+ - 10.0
12
+ - 15.0
13
+ - 25.0
14
+ - 28.0
15
+ - 33.0
16
+ - 35.0
17
+ married_filing_jointly:
18
+ standard_deduction: 11_900
19
+ brackets:
20
+ - 17_400
21
+ - 70_700
22
+ - 142_700
23
+ - 217_450
24
+ - 388_350
25
+ married_filing_separately:
26
+ standard_deduction: 5_950
27
+ brackets:
28
+ - 8_700
29
+ - 35_350
30
+ - 71_350
31
+ - 108_725
32
+ - 194_175
33
+ single:
34
+ standard_deduction: 5_950
35
+ brackets:
36
+ - 8_700
37
+ - 35_350
38
+ - 85_650
39
+ - 178_650
40
+ - 388_350
41
+ head_of_household:
42
+ standard_deduction: 8_700
43
+ brackets:
44
+ - 12_400
45
+ - 47_350
46
+ - 122_300
47
+ - 190_050
48
+ - 388_350
49
+
50
+ 2013:
51
+ oasdi_rate: 6.2
52
+ oasdi_wage_base: 113_700
53
+ hi_rate: 1.45
54
+ personal_exemption: 3_900
55
+ hsa_limit:
56
+ individual: 3_250
57
+ family: 6_450
58
+ senior: 1000
59
+ bracket_rates:
60
+ - 10.0
61
+ - 15.0
62
+ - 25.0
63
+ - 28.0
64
+ - 33.0
65
+ - 35.0
66
+ - 39.6
67
+ married_filing_jointly:
68
+ standard_deduction: 12_200
69
+ brackets:
70
+ - 17_850
71
+ - 72_500
72
+ - 146_400
73
+ - 223_050
74
+ - 398_350
75
+ - 450_000
76
+ married_filing_separately:
77
+ standard_deduction: 6_100
78
+ brackets:
79
+ - 8_925
80
+ - 36_250
81
+ - 73_200
82
+ - 111_525
83
+ - 199_175
84
+ - 225_000
85
+ single:
86
+ standard_deduction: 6_100
87
+ brackets:
88
+ - 8_925
89
+ - 36_250
90
+ - 87_850
91
+ - 183_250
92
+ - 398_350
93
+ - 400_000
94
+ head_of_household:
95
+ standard_deduction: 8_950
96
+ brackets:
97
+ - 12_750
98
+ - 48_600
99
+ - 125_450
100
+ - 203_150
101
+ - 398_350
102
+ - 425_000
@@ -0,0 +1,71 @@
1
+ module MudratProjector
2
+ class Transaction
3
+ include Enumerable
4
+
5
+ attr :credits, :date, :debits
6
+
7
+ def initialize params = {}
8
+ @date = params.fetch :date
9
+ self.credits = extract_entry_params :credit, params
10
+ self.debits = extract_entry_params :debit, params
11
+ end
12
+
13
+ def balanced?
14
+ sum_credits = build_set_for_balance credits
15
+ sum_debits = build_set_for_balance debits
16
+ (sum_credits ^ sum_debits).empty?
17
+ end
18
+
19
+ def credits= credits
20
+ @credits = build_entries :credit, credits
21
+ end
22
+
23
+ def debits= debits
24
+ @debits = build_entries :debit, debits
25
+ end
26
+
27
+ def each &block
28
+ credits.each &block
29
+ debits.each &block
30
+ end
31
+
32
+ def slice slice_date
33
+ if date > slice_date
34
+ [[], self]
35
+ else
36
+ [[self], nil]
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def extract_entry_params credit_or_debit, params
43
+ entries = Array params["#{credit_or_debit}s".to_sym]
44
+ return entries unless params.has_key? credit_or_debit
45
+ unless entries.empty?
46
+ raise ArgumentError, "You cannot supply both #{credit_or_debit} and "\
47
+ "#{credit_or_debit}s"
48
+ end
49
+ [params.fetch(credit_or_debit)]
50
+ end
51
+
52
+ def build_entries credit_or_debit, entries
53
+ entries.map do |entry_params|
54
+ if entry_params.is_a? TransactionEntry
55
+ entry_params
56
+ else
57
+ TransactionEntry.public_send "new_#{credit_or_debit}", entry_params
58
+ end
59
+ end
60
+ end
61
+
62
+ def build_set_for_balance entries
63
+ hash = Hash.new { |h,k| h[k] = 0 }
64
+ entries.each do |entry|
65
+ balance_key = entry.class == TransactionEntry ? :fixed : entry.other_account_id
66
+ hash[balance_key] += entry.scalar
67
+ end
68
+ hash.reduce Set.new do |set, (_, value)| set << value; end
69
+ end
70
+ end
71
+ end