fin_it 0.1.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.
- checksums.yaml +7 -0
- data/ARCHITECTURE.md +24 -0
- data/CHANGELOG.md +9 -0
- data/CONTRIBUTING.md +20 -0
- data/LICENSE +21 -0
- data/QUICKSTART.md +56 -0
- data/README.md +74 -0
- data/Rakefile +23 -0
- data/SECURITY.md +14 -0
- data/assets/fin_it_logo.png +0 -0
- data/lib/fin_it/account.rb +120 -0
- data/lib/fin_it/calculator/currency_conversion.rb +27 -0
- data/lib/fin_it/calculator/date_helpers.rb +53 -0
- data/lib/fin_it/calculator/variable_hashing.rb +120 -0
- data/lib/fin_it/calculator.rb +480 -0
- data/lib/fin_it/categories/category.rb +137 -0
- data/lib/fin_it/complex_model.rb +169 -0
- data/lib/fin_it/dsl/account_builder.rb +35 -0
- data/lib/fin_it/dsl/calculated_builder.rb +87 -0
- data/lib/fin_it/dsl/config_builder.rb +58 -0
- data/lib/fin_it/dsl/model_builder.rb +938 -0
- data/lib/fin_it/dsl/model_template_builder.rb +29 -0
- data/lib/fin_it/dsl/plan_builder.rb +52 -0
- data/lib/fin_it/dsl/project_inheritance_resolver.rb +46 -0
- data/lib/fin_it/dsl/variable_builder.rb +41 -0
- data/lib/fin_it/dsl.rb +13 -0
- data/lib/fin_it/engine.rb +15 -0
- data/lib/fin_it/financial_model/account_balances.rb +99 -0
- data/lib/fin_it/financial_model/account_hierarchy.rb +158 -0
- data/lib/fin_it/financial_model/category_values.rb +179 -0
- data/lib/fin_it/financial_model/currency_helpers.rb +14 -0
- data/lib/fin_it/financial_model/date_helpers.rb +58 -0
- data/lib/fin_it/financial_model/debugging.rb +353 -0
- data/lib/fin_it/financial_model/period_flows.rb +121 -0
- data/lib/fin_it/financial_model/validation.rb +85 -0
- data/lib/fin_it/financial_model/variable_matching.rb +49 -0
- data/lib/fin_it/financial_model.rb +395 -0
- data/lib/fin_it/model_template.rb +121 -0
- data/lib/fin_it/outputs/base_output.rb +51 -0
- data/lib/fin_it/outputs/console_output.rb +1528 -0
- data/lib/fin_it/outputs/monthly_console_output.rb +145 -0
- data/lib/fin_it/outputs/zaxcel_output.rb +1264 -0
- data/lib/fin_it/payment_schedule.rb +112 -0
- data/lib/fin_it/plan.rb +159 -0
- data/lib/fin_it/reports/balance_sheet.rb +638 -0
- data/lib/fin_it/reports/base_report.rb +239 -0
- data/lib/fin_it/reports/cash_flow_statement.rb +480 -0
- data/lib/fin_it/reports/custom_sheet.rb +436 -0
- data/lib/fin_it/reports/income_statement.rb +793 -0
- data/lib/fin_it/reports/period_comparison.rb +309 -0
- data/lib/fin_it/reports/scenario_comparison.rb +296 -0
- data/lib/fin_it/temporal_value.rb +349 -0
- data/lib/fin_it/transaction_generator/account_resolver.rb +118 -0
- data/lib/fin_it/transaction_generator/cache_management.rb +39 -0
- data/lib/fin_it/transaction_generator/date_generation.rb +57 -0
- data/lib/fin_it/transaction_generator.rb +357 -0
- data/lib/fin_it/version.rb +6 -0
- data/lib/fin_it.rb +27 -0
- data/test/fin_it/calculator_test.rb +109 -0
- data/test/fin_it/complex_model_test.rb +198 -0
- data/test/fin_it/debugging_test.rb +112 -0
- data/test/fin_it/driver_variables_test.rb +109 -0
- data/test/fin_it/dsl_test.rb +581 -0
- data/test/fin_it/financial_model_test.rb +196 -0
- data/test/fin_it/frequency_test.rb +51 -0
- data/test/fin_it/outputs/console_output_test.rb +249 -0
- data/test/fin_it/plan_test.rb +281 -0
- data/test/fin_it/reports/account_balance_test.rb +232 -0
- data/test/fin_it/reports/balance_sheet_test.rb +355 -0
- data/test/fin_it/reports/cash_flow_statement_test.rb +234 -0
- data/test/fin_it/reports/custom_sheet_test.rb +246 -0
- data/test/fin_it/reports/income_statement_test.rb +431 -0
- data/test/fin_it/reports/period_comparison_test.rb +226 -0
- data/test/fin_it/reports/restaurant_model_test.rb +225 -0
- data/test/fin_it/reports/scenario_comparison_test.rb +414 -0
- data/test/scripts/generate_demo_reports.rb +47 -0
- data/test/scripts/startup_saas_demo.rb +62 -0
- data/test/test_helper.rb +25 -0
- data/test/verify_accounting_equation.rb +91 -0
- metadata +264 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../test_helper"
|
|
4
|
+
|
|
5
|
+
class PlanTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
@model = FinIt.define(default_currency: 'USD') do
|
|
8
|
+
config do
|
|
9
|
+
start_date 2024
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
account :checking do
|
|
13
|
+
type :asset
|
|
14
|
+
currency 'USD'
|
|
15
|
+
opening_balance 10_000
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Driver variables
|
|
19
|
+
category :drivers, type: :driver do
|
|
20
|
+
variable :units_sold do
|
|
21
|
+
value 100
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
variable :price_per_unit do
|
|
25
|
+
value 50
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Income categories
|
|
30
|
+
category :income, type: :income,
|
|
31
|
+
default_account: :checking,
|
|
32
|
+
defaults: { frequency: :monthly, start_date: "2024-01-01", end_date: "2024-12-31" } do
|
|
33
|
+
|
|
34
|
+
calculated :product_sales,
|
|
35
|
+
formula: "units_sold * price_per_unit"
|
|
36
|
+
|
|
37
|
+
variable :service_revenue, currency: 'USD' do
|
|
38
|
+
value 2_000, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Expense categories
|
|
43
|
+
category :expenses, type: :expense,
|
|
44
|
+
default_account: :checking,
|
|
45
|
+
defaults: { frequency: :monthly, start_date: "2024-01-01", end_date: "2024-12-31" } do
|
|
46
|
+
|
|
47
|
+
variable :operating_expenses, currency: 'USD' do
|
|
48
|
+
value 1_500, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Define plans
|
|
53
|
+
plan :growth_scenario, description: "10% revenue growth" do
|
|
54
|
+
scale :service_revenue, 1.10
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
plan :cost_reduction, description: "Reduce costs by 20%" do
|
|
58
|
+
scale :operating_expenses, 0.80
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
plan :opening_balance_change do
|
|
62
|
+
set_opening_balance :checking, 50_000
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
@test_date = Date.new(2024, 6, 15)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_plan_class_creation
|
|
70
|
+
plan = FinIt::Plan.new(:test_plan, description: "Test")
|
|
71
|
+
assert_equal :test_plan, plan.name
|
|
72
|
+
assert_equal "Test", plan.description
|
|
73
|
+
assert_empty plan.overrides
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_plan_set_override
|
|
77
|
+
plan = FinIt::Plan.new(:test)
|
|
78
|
+
plan.set(:revenue, 5000)
|
|
79
|
+
|
|
80
|
+
assert_equal 1, plan.overrides.length
|
|
81
|
+
assert_equal :set, plan.overrides.first[:type]
|
|
82
|
+
assert_equal :revenue, plan.overrides.first[:variable]
|
|
83
|
+
assert_equal 5000, plan.overrides.first[:value]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_plan_scale_override
|
|
87
|
+
plan = FinIt::Plan.new(:test)
|
|
88
|
+
plan.scale(:revenue, 1.15)
|
|
89
|
+
|
|
90
|
+
assert_equal 1, plan.overrides.length
|
|
91
|
+
assert_equal :scale, plan.overrides.first[:type]
|
|
92
|
+
assert_equal :revenue, plan.overrides.first[:variable]
|
|
93
|
+
assert_equal 1.15, plan.overrides.first[:factor]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_plan_adjust_override
|
|
97
|
+
plan = FinIt::Plan.new(:test)
|
|
98
|
+
plan.adjust(:revenue, 10000)
|
|
99
|
+
|
|
100
|
+
assert_equal 1, plan.overrides.length
|
|
101
|
+
assert_equal :adjust, plan.overrides.first[:type]
|
|
102
|
+
assert_equal :revenue, plan.overrides.first[:variable]
|
|
103
|
+
assert_equal 10000, plan.overrides.first[:amount]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_plan_formula_override
|
|
107
|
+
plan = FinIt::Plan.new(:test)
|
|
108
|
+
plan.formula(:calculated_var, "new_formula * 2")
|
|
109
|
+
|
|
110
|
+
assert_equal 1, plan.overrides.length
|
|
111
|
+
assert_equal :formula, plan.overrides.first[:type]
|
|
112
|
+
assert_equal :calculated_var, plan.overrides.first[:variable]
|
|
113
|
+
assert_equal "new_formula * 2", plan.overrides.first[:formula]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def test_plan_set_opening_balance
|
|
117
|
+
plan = FinIt::Plan.new(:test)
|
|
118
|
+
plan.set_opening_balance(:checking, 50_000)
|
|
119
|
+
|
|
120
|
+
assert_equal 1, plan.overrides.length
|
|
121
|
+
assert_equal :opening_balance, plan.overrides.first[:type]
|
|
122
|
+
assert_equal :checking, plan.overrides.first[:account]
|
|
123
|
+
assert_equal 50_000, plan.overrides.first[:amount]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def test_plan_chaining
|
|
127
|
+
plan = FinIt::Plan.new(:test)
|
|
128
|
+
result = plan
|
|
129
|
+
.scale(:revenue, 1.10)
|
|
130
|
+
.adjust(:expenses, -1000)
|
|
131
|
+
.set(:bonus, 5000)
|
|
132
|
+
|
|
133
|
+
assert_equal plan, result
|
|
134
|
+
assert_equal 3, plan.overrides.length
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def test_model_has_plans
|
|
138
|
+
assert @model.plans.key?(:growth_scenario)
|
|
139
|
+
assert @model.plans.key?(:cost_reduction)
|
|
140
|
+
assert @model.plans.key?(:opening_balance_change)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def test_model_clone_creates_independent_copy
|
|
144
|
+
@model.generate_transactions(Date.new(2024, 12, 31))
|
|
145
|
+
|
|
146
|
+
cloned = @model.clone_model
|
|
147
|
+
|
|
148
|
+
# Cloned model should be a different object
|
|
149
|
+
refute_same @model, cloned
|
|
150
|
+
refute_same @model.calculator, cloned.calculator
|
|
151
|
+
refute_same @model.accounts, cloned.accounts
|
|
152
|
+
refute_same @model.categories, cloned.categories
|
|
153
|
+
|
|
154
|
+
# But values should be the same
|
|
155
|
+
assert_equal @model.accounts[:checking].opening_balance, cloned.accounts[:checking].opening_balance
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def test_with_plan_returns_new_model
|
|
159
|
+
original_net_income = @model.period_net_income(
|
|
160
|
+
Date.new(2024, 1, 1), Date.new(2024, 12, 31)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
growth_model = @model.with_plan(:growth_scenario)
|
|
164
|
+
|
|
165
|
+
# Original model should be unchanged
|
|
166
|
+
assert_equal original_net_income, @model.period_net_income(
|
|
167
|
+
Date.new(2024, 1, 1), Date.new(2024, 12, 31)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Growth model should be different
|
|
171
|
+
refute_same @model, growth_model
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def test_with_plan_applies_scale_override
|
|
175
|
+
@model.generate_transactions(Date.new(2024, 12, 31))
|
|
176
|
+
|
|
177
|
+
# Service revenue is 2000 monthly = 24000 annual
|
|
178
|
+
base_revenue = @model.calculator.calculate(
|
|
179
|
+
:service_revenue,
|
|
180
|
+
date: Date.new(2024, 6, 15),
|
|
181
|
+
output_currency: 'USD'
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
growth_model = @model.with_plan(:growth_scenario)
|
|
185
|
+
growth_model.generate_transactions(Date.new(2024, 12, 31))
|
|
186
|
+
|
|
187
|
+
scaled_revenue = growth_model.calculator.calculate(
|
|
188
|
+
:service_revenue,
|
|
189
|
+
date: Date.new(2024, 6, 15),
|
|
190
|
+
output_currency: 'USD'
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Should be 10% higher
|
|
194
|
+
expected = base_revenue.to_f * 1.10
|
|
195
|
+
assert_in_delta expected, scaled_revenue.to_f, 1.0
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def test_with_plan_applies_opening_balance_override
|
|
199
|
+
assert_equal 10_000, @model.accounts[:checking].opening_balance
|
|
200
|
+
|
|
201
|
+
modified_model = @model.with_plan(:opening_balance_change)
|
|
202
|
+
|
|
203
|
+
# Original unchanged
|
|
204
|
+
assert_equal 10_000, @model.accounts[:checking].opening_balance
|
|
205
|
+
|
|
206
|
+
# Modified model has new opening balance
|
|
207
|
+
assert_equal 50_000, modified_model.accounts[:checking].opening_balance
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def test_with_plans_applies_multiple
|
|
211
|
+
# Apply both growth and cost reduction
|
|
212
|
+
combined_model = @model.with_plans(:growth_scenario, :cost_reduction)
|
|
213
|
+
|
|
214
|
+
refute_same @model, combined_model
|
|
215
|
+
|
|
216
|
+
# Verify both plans were applied
|
|
217
|
+
combined_model.generate_transactions(Date.new(2024, 12, 31))
|
|
218
|
+
|
|
219
|
+
# Check that service_revenue was scaled up
|
|
220
|
+
scaled_revenue = combined_model.calculator.calculate(
|
|
221
|
+
:service_revenue,
|
|
222
|
+
date: Date.new(2024, 6, 15),
|
|
223
|
+
output_currency: 'USD'
|
|
224
|
+
)
|
|
225
|
+
base_revenue = @model.calculator.calculate(
|
|
226
|
+
:service_revenue,
|
|
227
|
+
date: Date.new(2024, 6, 15),
|
|
228
|
+
output_currency: 'USD'
|
|
229
|
+
)
|
|
230
|
+
expected_revenue = base_revenue.to_f * 1.10
|
|
231
|
+
assert_in_delta expected_revenue, scaled_revenue.to_f, 1.0
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def test_with_external_plan
|
|
235
|
+
# Create plan outside of model definition
|
|
236
|
+
external_plan = FinIt::Plan.new(:external, description: "External plan")
|
|
237
|
+
external_plan.set_opening_balance(:checking, 100_000)
|
|
238
|
+
|
|
239
|
+
modified_model = @model.with_plan(external_plan)
|
|
240
|
+
|
|
241
|
+
assert_equal 10_000, @model.accounts[:checking].opening_balance
|
|
242
|
+
assert_equal 100_000, modified_model.accounts[:checking].opening_balance
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def test_plan_with_temporal_scope
|
|
246
|
+
plan = FinIt::Plan.new(:q1_only, start_date: "2024-01-01", end_date: "2024-03-31")
|
|
247
|
+
plan.scale(:service_revenue, 2.0)
|
|
248
|
+
|
|
249
|
+
assert_equal Date.new(2024, 1, 1), plan.start_date
|
|
250
|
+
assert_equal Date.new(2024, 3, 31), plan.end_date
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def test_model_isolation_after_multiple_operations
|
|
254
|
+
@model.generate_transactions(Date.new(2024, 12, 31))
|
|
255
|
+
base_income = @model.period_net_income(Date.new(2024, 1, 1), Date.new(2024, 12, 31))
|
|
256
|
+
|
|
257
|
+
# Create scenario A
|
|
258
|
+
scenario_a = @model.with_plan(:growth_scenario)
|
|
259
|
+
scenario_a.generate_transactions(Date.new(2024, 12, 31))
|
|
260
|
+
|
|
261
|
+
# Create scenario B
|
|
262
|
+
scenario_b = @model.with_plan(:cost_reduction)
|
|
263
|
+
scenario_b.generate_transactions(Date.new(2024, 12, 31))
|
|
264
|
+
|
|
265
|
+
# Create scenario C (both plans)
|
|
266
|
+
scenario_c = @model.with_plans(:growth_scenario, :cost_reduction)
|
|
267
|
+
scenario_c.generate_transactions(Date.new(2024, 12, 31))
|
|
268
|
+
|
|
269
|
+
# Base model should still have original value
|
|
270
|
+
@model.generate_transactions(Date.new(2024, 12, 31))
|
|
271
|
+
final_base_income = @model.period_net_income(Date.new(2024, 1, 1), Date.new(2024, 12, 31))
|
|
272
|
+
|
|
273
|
+
assert_equal base_income, final_base_income, "Base model should be unchanged after creating scenarios"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def test_plan_not_found_raises_error
|
|
277
|
+
assert_raises(ArgumentError) do
|
|
278
|
+
@model.with_plan(:nonexistent_plan)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../test_helper"
|
|
4
|
+
|
|
5
|
+
class AccountBalanceTest < Minitest::Test
|
|
6
|
+
def setup
|
|
7
|
+
@model = FinIt.define(default_currency: 'USD') do
|
|
8
|
+
config do
|
|
9
|
+
start_date 2024
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
account :checking do
|
|
13
|
+
type :asset
|
|
14
|
+
currency 'USD'
|
|
15
|
+
opening_balance 10_000
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
account :credit_card do
|
|
19
|
+
type :liability
|
|
20
|
+
currency 'USD'
|
|
21
|
+
opening_balance 0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Income that goes to checking account
|
|
25
|
+
category :income, type: :income do
|
|
26
|
+
variable :salary, currency: 'USD', frequency: :annual, account: :checking do
|
|
27
|
+
value 100_000, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
28
|
+
description "Annual salary"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
variable :bonus, currency: 'USD', frequency: :annual, account: :checking do
|
|
32
|
+
value 20_000, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
33
|
+
description "Annual bonus"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Expenses that come from checking or credit card
|
|
38
|
+
category :expenses, type: :expense do
|
|
39
|
+
variable :rent, currency: 'USD', frequency: :monthly, account: :checking do
|
|
40
|
+
value 2_000, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
41
|
+
description "Monthly rent"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
variable :utilities, currency: 'USD', frequency: :monthly, account: :checking do
|
|
45
|
+
value 300, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
46
|
+
description "Monthly utilities"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
variable :groceries, currency: 'USD', frequency: :monthly, account: :credit_card do
|
|
50
|
+
value 500, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
51
|
+
description "Monthly groceries"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Assets (checking account balance)
|
|
56
|
+
category :current_assets, type: :asset do
|
|
57
|
+
variable :checking_balance, currency: 'USD', frequency: :annual, account: :checking do
|
|
58
|
+
value 10_000, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
59
|
+
description "Checking account balance"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Liabilities (credit card balance)
|
|
64
|
+
category :current_liabilities, type: :liability do
|
|
65
|
+
variable :credit_card_balance, currency: 'USD', frequency: :annual, account: :credit_card do
|
|
66
|
+
value 0, start_date: "2024-01-01", end_date: "2024-12-31"
|
|
67
|
+
description "Credit card balance"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Equity (to balance the sheet: Assets = Liabilities + Equity)
|
|
72
|
+
# Use calculated variable with account to get actual balance
|
|
73
|
+
category :equity, type: :equity do
|
|
74
|
+
calculated :owner_equity, formula: '0', account: :equity do
|
|
75
|
+
description "Owner's equity"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_income_statement_shows_revenue
|
|
82
|
+
report = FinIt::Reports::IncomeStatement.new(
|
|
83
|
+
@model,
|
|
84
|
+
start_date: Date.new(2024, 1, 1),
|
|
85
|
+
end_date: Date.new(2024, 12, 31),
|
|
86
|
+
output_currency: 'USD'
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
report_data = report.generate
|
|
90
|
+
income_total = report_data[:totals][:income]
|
|
91
|
+
|
|
92
|
+
# Total income: 100,000 + 20,000 = 120,000
|
|
93
|
+
assert_in_delta 120_000, income_total, 0.01, "Total income should be $120,000"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def test_income_statement_shows_expenses
|
|
97
|
+
report = FinIt::Reports::IncomeStatement.new(
|
|
98
|
+
@model,
|
|
99
|
+
start_date: Date.new(2024, 1, 1),
|
|
100
|
+
end_date: Date.new(2024, 12, 31),
|
|
101
|
+
output_currency: 'USD'
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
report_data = report.generate
|
|
105
|
+
expenses_total = report_data[:totals][:expenses]
|
|
106
|
+
|
|
107
|
+
# Total expenses: (2,000 + 300) * 12 + (500 * 12) = 27,600 + 6,000 = 33,600
|
|
108
|
+
# Actually, these are monthly values that get annualized, so:
|
|
109
|
+
# rent: 2,000 * 12 = 24,000
|
|
110
|
+
# utilities: 300 * 12 = 3,600
|
|
111
|
+
# groceries: 500 * 12 = 6,000
|
|
112
|
+
# Total: 33,600
|
|
113
|
+
expected_expenses = (2_000 + 300 + 500) * 12
|
|
114
|
+
assert_in_delta expected_expenses, expenses_total, 1.0, "Total expenses should be correct"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def test_balance_sheet_shows_assets
|
|
118
|
+
# Generate transactions to get actual account balances
|
|
119
|
+
@model.generate_transactions(Date.new(2024, 12, 31))
|
|
120
|
+
|
|
121
|
+
report = FinIt::Reports::BalanceSheet.new(
|
|
122
|
+
@model,
|
|
123
|
+
start_date: Date.new(2024, 1, 1),
|
|
124
|
+
end_date: Date.new(2024, 12, 31),
|
|
125
|
+
output_currency: 'USD'
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
report_data = report.generate
|
|
129
|
+
assets_total = report_data[:totals][:assets]
|
|
130
|
+
|
|
131
|
+
# Assets: checking account balance at end of year
|
|
132
|
+
# Opening: 10,000 + Income: 120,000 - Expenses from checking: (2,000 + 300) * 12 = 27,600
|
|
133
|
+
# = 10,000 + 120,000 - 27,600 = 102,400
|
|
134
|
+
expected_assets = @model.account_balance(:checking, as_of_date: Date.new(2024, 12, 31))
|
|
135
|
+
assert_in_delta expected_assets, assets_total, 0.01, "Total assets should show checking balance"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def test_balance_sheet_shows_liabilities
|
|
139
|
+
# Generate transactions to get actual account balances
|
|
140
|
+
@model.generate_transactions(Date.new(2024, 12, 31))
|
|
141
|
+
|
|
142
|
+
report = FinIt::Reports::BalanceSheet.new(
|
|
143
|
+
@model,
|
|
144
|
+
start_date: Date.new(2024, 1, 1),
|
|
145
|
+
end_date: Date.new(2024, 12, 31),
|
|
146
|
+
output_currency: 'USD'
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
report_data = report.generate
|
|
150
|
+
liabilities_total = report_data[:totals][:liabilities]
|
|
151
|
+
|
|
152
|
+
# Liabilities: credit card balance at end of year
|
|
153
|
+
# Opening: 0 + Expenses on credit card: 500 * 12 = 6,000
|
|
154
|
+
expected_liabilities = @model.account_balance(:credit_card, as_of_date: Date.new(2024, 12, 31))
|
|
155
|
+
assert_in_delta expected_liabilities, liabilities_total, 0.01, "Total liabilities should show credit card balance"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def test_account_opening_balances
|
|
159
|
+
checking = @model.accounts[:checking]
|
|
160
|
+
credit_card = @model.accounts[:credit_card]
|
|
161
|
+
|
|
162
|
+
assert_equal 10_000, checking.opening_balance, "Checking should have $10,000 opening balance"
|
|
163
|
+
assert_equal 0, credit_card.opening_balance, "Credit card should have $0 opening balance"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def test_income_variables_have_accounts
|
|
167
|
+
income_category = @model.categories.find { |c| c.name == :income }
|
|
168
|
+
assert income_category, "Should have income category"
|
|
169
|
+
|
|
170
|
+
salary_var = income_category.variables.find { |v| v[:name] == :salary }
|
|
171
|
+
bonus_var = income_category.variables.find { |v| v[:name] == :bonus }
|
|
172
|
+
|
|
173
|
+
assert_equal :checking, salary_var[:account], "Salary should be associated with checking account"
|
|
174
|
+
assert_equal :checking, bonus_var[:account], "Bonus should be associated with checking account"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def test_expense_variables_have_accounts
|
|
178
|
+
expenses_category = @model.categories.find { |c| c.name == :expenses }
|
|
179
|
+
assert expenses_category, "Should have expenses category"
|
|
180
|
+
|
|
181
|
+
rent_var = expenses_category.variables.find { |v| v[:name] == :rent }
|
|
182
|
+
groceries_var = expenses_category.variables.find { |v| v[:name] == :groceries }
|
|
183
|
+
|
|
184
|
+
assert_equal :checking, rent_var[:account], "Rent should be associated with checking account"
|
|
185
|
+
assert_equal :credit_card, groceries_var[:account], "Groceries should be associated with credit card account"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def test_net_income_calculation
|
|
189
|
+
income_report = FinIt::Reports::IncomeStatement.new(
|
|
190
|
+
@model,
|
|
191
|
+
start_date: Date.new(2024, 1, 1),
|
|
192
|
+
end_date: Date.new(2024, 12, 31),
|
|
193
|
+
output_currency: 'USD'
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
income_data = income_report.generate
|
|
197
|
+
net_income = income_data[:totals][:net_income]
|
|
198
|
+
|
|
199
|
+
# Net income = income - expenses
|
|
200
|
+
# 120,000 - 33,600 = 86,400
|
|
201
|
+
assert net_income > 0, "Net income should be positive"
|
|
202
|
+
assert_in_delta 120_000 - 33_600, net_income, 1.0, "Net income should be income minus expenses"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def test_both_reports_use_same_model
|
|
206
|
+
# Verify both reports can be generated from the same model
|
|
207
|
+
income_report = FinIt::Reports::IncomeStatement.new(
|
|
208
|
+
@model,
|
|
209
|
+
start_date: Date.new(2024, 1, 1),
|
|
210
|
+
end_date: Date.new(2024, 12, 31),
|
|
211
|
+
output_currency: 'USD'
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
balance_sheet = FinIt::Reports::BalanceSheet.new(
|
|
215
|
+
@model,
|
|
216
|
+
start_date: Date.new(2024, 1, 1),
|
|
217
|
+
end_date: Date.new(2024, 12, 31),
|
|
218
|
+
output_currency: 'USD'
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
income_data = income_report.generate
|
|
222
|
+
balance_data = balance_sheet.generate
|
|
223
|
+
|
|
224
|
+
assert income_data, "Income statement should generate"
|
|
225
|
+
assert balance_data, "Balance sheet should generate"
|
|
226
|
+
|
|
227
|
+
# Both should have the same currency
|
|
228
|
+
assert_equal 'USD', income_data[:currency], "Income statement should be in USD"
|
|
229
|
+
assert_equal 'USD', balance_data[:currency], "Balance sheet should be in USD"
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|