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,581 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../test_helper"
|
|
4
|
+
|
|
5
|
+
class DSLTest < Minitest::Test
|
|
6
|
+
def test_model_builder_creates_model
|
|
7
|
+
model = FinIt.define(default_currency: 'USD') do
|
|
8
|
+
config do
|
|
9
|
+
base_year 2024
|
|
10
|
+
start_date 2024
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
assert model, "Model should be created"
|
|
15
|
+
assert_equal 2024, model.config[:base_year], "Config should be set"
|
|
16
|
+
assert_equal Date.new(2024, 1, 1), model.start_date, "Start date should be set"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_start_date_as_year_number
|
|
20
|
+
model = FinIt.define(default_currency: 'USD') do
|
|
21
|
+
config do
|
|
22
|
+
start_date 2023
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
assert_equal Date.new(2023, 1, 1), model.start_date, "Start date should parse year number"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def test_start_date_as_date_string
|
|
30
|
+
model = FinIt.define(default_currency: 'USD') do
|
|
31
|
+
config do
|
|
32
|
+
start_date "2023-06-15"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
assert_equal Date.new(2023, 6, 15), model.start_date, "Start date should parse date string"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def test_equity_account_auto_created
|
|
40
|
+
model = FinIt.define(default_currency: 'USD') do
|
|
41
|
+
config do
|
|
42
|
+
start_date 2024
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
account :checking do
|
|
46
|
+
type :asset
|
|
47
|
+
currency 'USD'
|
|
48
|
+
opening_balance 10_000
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
assert model.accounts.key?(:equity), "Equity account should be auto-created"
|
|
53
|
+
assert_equal :equity, model.accounts[:equity].type, "Equity account type should be correct"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_account_definition
|
|
57
|
+
model = FinIt.define(default_currency: 'USD') do
|
|
58
|
+
config do
|
|
59
|
+
start_date 2024
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
account :checking do
|
|
63
|
+
type :asset
|
|
64
|
+
currency 'USD'
|
|
65
|
+
opening_balance 10_000
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
assert model.accounts.key?(:checking), "Account should be defined"
|
|
70
|
+
assert_equal :asset, model.accounts[:checking].type, "Account type should be set"
|
|
71
|
+
assert_equal 10_000, model.accounts[:checking].opening_balance, "Opening balance should be set"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def test_accounts_exist
|
|
75
|
+
model = FinIt.define(default_currency: 'USD') do
|
|
76
|
+
config do
|
|
77
|
+
start_date 2024
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
account :checking do
|
|
81
|
+
type :asset
|
|
82
|
+
currency 'USD'
|
|
83
|
+
opening_balance 10_000
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
account :savings do
|
|
87
|
+
type :asset
|
|
88
|
+
currency 'USD'
|
|
89
|
+
opening_balance 50_000
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
assert model.accounts.key?(:checking), "Checking account should exist"
|
|
94
|
+
assert model.accounts.key?(:savings), "Savings account should exist"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def test_account_opening_balances
|
|
98
|
+
model = FinIt.define(default_currency: 'USD') do
|
|
99
|
+
config do
|
|
100
|
+
start_date 2024
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
account :checking do
|
|
104
|
+
type :asset
|
|
105
|
+
currency 'USD'
|
|
106
|
+
opening_balance 10_000
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
account :savings do
|
|
110
|
+
type :asset
|
|
111
|
+
currency 'USD'
|
|
112
|
+
opening_balance 50_000
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
assert_equal 10_000, model.accounts[:checking].opening_balance, "Checking opening balance"
|
|
117
|
+
assert_equal 50_000, model.accounts[:savings].opening_balance, "Savings opening balance"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def test_account_auto_created_for_variables_in_category
|
|
121
|
+
# Accounts are auto-created when inside income/expense categories
|
|
122
|
+
model = FinIt.define(default_currency: 'USD') do
|
|
123
|
+
config do
|
|
124
|
+
start_date 2024
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
account :valid_account do
|
|
128
|
+
type :asset
|
|
129
|
+
currency 'USD'
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
category :test, type: :income do
|
|
133
|
+
# Account is auto-created for variable
|
|
134
|
+
variable :bad_var, currency: 'USD' do
|
|
135
|
+
value 100
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Verify account was auto-created
|
|
141
|
+
assert model.accounts.key?(:bad_var), "Account should be auto-created for variable in income category"
|
|
142
|
+
assert_equal :income, model.accounts[:bad_var].type, "Account type should match category type"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def test_category_definition
|
|
146
|
+
model = FinIt.define(default_currency: 'USD') do
|
|
147
|
+
config do
|
|
148
|
+
start_date 2024
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
account :checking do
|
|
152
|
+
type :asset
|
|
153
|
+
currency 'USD'
|
|
154
|
+
opening_balance 0
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
category :income, type: :income, description: "Test income" do
|
|
158
|
+
variable :salary, currency: 'USD', frequency: :annual, account: :checking do
|
|
159
|
+
value 100_000
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
income_category = model.categories.find { |c| c.name == :income }
|
|
165
|
+
assert income_category, "Category should be defined"
|
|
166
|
+
assert_equal :income, income_category.type, "Category type should be set"
|
|
167
|
+
assert_equal 1, income_category.variables.length, "Category should have one variable"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def test_calculated_variable_auto_creates_accounts_in_income_category
|
|
171
|
+
# Accounts are now auto-created for calculated variables
|
|
172
|
+
model = FinIt.define(default_currency: 'USD') do
|
|
173
|
+
config do
|
|
174
|
+
start_date 2024
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
category :income, type: :income do
|
|
178
|
+
calculated :revenue, formula: "1000" do
|
|
179
|
+
# Account is auto-created
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Verify account was auto-created
|
|
185
|
+
assert model.accounts.key?(:revenue), "Account should be auto-created for calculated variable"
|
|
186
|
+
assert_equal :income, model.accounts[:revenue].type, "Account type should match category type"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def test_account_within_category_inherits_type
|
|
190
|
+
model = FinIt.define(default_currency: 'USD') do
|
|
191
|
+
config do
|
|
192
|
+
start_date 2024
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
category :assets, type: :asset, description: 'Assets' do
|
|
196
|
+
account :checking do
|
|
197
|
+
currency 'USD'
|
|
198
|
+
opening_balance 10_000
|
|
199
|
+
# type :asset is inherited from category
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
assert model.accounts.key?(:checking), "Account should be defined"
|
|
205
|
+
assert_equal :asset, model.accounts[:checking].type, "Account type should be inherited from category"
|
|
206
|
+
|
|
207
|
+
# Check that variable was automatically created
|
|
208
|
+
asset_category = model.categories.find { |c| c.name == :assets }
|
|
209
|
+
assert asset_category, "Asset category should exist"
|
|
210
|
+
assert_equal 1, asset_category.variables.length, "Category should have one variable"
|
|
211
|
+
|
|
212
|
+
var = asset_category.variables.first
|
|
213
|
+
assert_equal :checking, var[:name], "Variable name should match account name"
|
|
214
|
+
assert_equal :checking, var[:account], "Variable should reference the account"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def test_account_within_nested_category
|
|
218
|
+
model = FinIt.define(default_currency: 'USD') do
|
|
219
|
+
config do
|
|
220
|
+
start_date 2024
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
category :assets, type: :asset, description: 'Assets' do
|
|
224
|
+
category :real_estate, description: 'Real Estate' do # inherits type: :asset
|
|
225
|
+
account :homes do
|
|
226
|
+
currency 'USD'
|
|
227
|
+
opening_balance 500_000
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
assert model.accounts.key?(:homes), "Account should be defined"
|
|
234
|
+
assert_equal :asset, model.accounts[:homes].type, "Account type should be inherited"
|
|
235
|
+
|
|
236
|
+
# Check that variable was created in nested category
|
|
237
|
+
asset_category = model.categories.find { |c| c.name == :assets }
|
|
238
|
+
real_estate_category = asset_category.children.find { |c| c.name == :real_estate }
|
|
239
|
+
assert_equal 1, real_estate_category.variables.length, "Nested category should have one variable"
|
|
240
|
+
assert_equal :homes, real_estate_category.variables.first[:name], "Variable should reference account"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def test_equity_category_custom_name
|
|
244
|
+
model = FinIt.define(default_currency: 'USD') do
|
|
245
|
+
config do
|
|
246
|
+
start_date 2024
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
account :cash do
|
|
250
|
+
type :asset
|
|
251
|
+
currency 'USD'
|
|
252
|
+
opening_balance 100_000
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
category :networth, type: :equity, description: 'Net Worth' do
|
|
256
|
+
# Equity account will be auto-created
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Equity account should be auto-created
|
|
261
|
+
assert model.accounts.key?(:equity), "Equity account should be auto-created"
|
|
262
|
+
|
|
263
|
+
# Net worth category should exist
|
|
264
|
+
networth_category = model.categories.find { |c| c.name == :networth }
|
|
265
|
+
assert networth_category, "Net worth category should exist"
|
|
266
|
+
assert_equal :equity, networth_category.type, "Category type should be equity"
|
|
267
|
+
|
|
268
|
+
# Variable should be added to networth category
|
|
269
|
+
assert networth_category.variables.any?, "Net worth category should have variables"
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def test_account_outside_category_still_requires_type
|
|
273
|
+
error = assert_raises(ArgumentError) do
|
|
274
|
+
FinIt.define(default_currency: 'USD') do
|
|
275
|
+
config do
|
|
276
|
+
start_date 2024
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
account :checking do
|
|
280
|
+
currency 'USD'
|
|
281
|
+
opening_balance 10_000
|
|
282
|
+
# Missing type - should raise error
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
assert_match(/must specify type/, error.message, "Error should mention missing type")
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def test_top_level_calculated_value_no_accounts_required
|
|
291
|
+
model = FinIt.define(default_currency: 'USD') do
|
|
292
|
+
config do
|
|
293
|
+
start_date 2024
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
calculated_value :profit_margin, formula: "net_income / revenue"
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Should not raise error - top-level calculated values don't need accounts
|
|
300
|
+
assert model, "Model should be created"
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# === Type Inheritance Tests ===
|
|
304
|
+
|
|
305
|
+
def test_nested_category_inherits_type_from_parent
|
|
306
|
+
model = FinIt.define(default_currency: 'USD') do
|
|
307
|
+
config do
|
|
308
|
+
start_date 2024
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
account :checking do
|
|
312
|
+
type :asset
|
|
313
|
+
currency 'USD'
|
|
314
|
+
opening_balance 10_000
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
category :expenses, type: :expense do
|
|
318
|
+
category :cogs do # No type specified - should inherit :expense
|
|
319
|
+
category :food do # No type - should inherit :expense
|
|
320
|
+
variable :food_cost, account: :checking do
|
|
321
|
+
value 1000
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
expenses_cat = model.categories.find { |c| c.name == :expenses }
|
|
329
|
+
cogs_cat = expenses_cat.children.find { |c| c.name == :cogs }
|
|
330
|
+
food_cat = cogs_cat.children.find { |c| c.name == :food }
|
|
331
|
+
|
|
332
|
+
assert_equal :expense, cogs_cat.type, "COGS should inherit :expense from parent"
|
|
333
|
+
assert_equal :expense, food_cat.type, "Food should inherit :expense from grandparent"
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def test_nested_category_can_override_type
|
|
337
|
+
model = FinIt.define(default_currency: 'USD') do
|
|
338
|
+
config do
|
|
339
|
+
start_date 2024
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
category :operations, type: :expense do
|
|
343
|
+
category :metrics, type: :metric do # Override type
|
|
344
|
+
variable :conversion_rate do
|
|
345
|
+
value 0.05
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
operations_cat = model.categories.find { |c| c.name == :operations }
|
|
352
|
+
metrics_cat = operations_cat.children.find { |c| c.name == :metrics }
|
|
353
|
+
|
|
354
|
+
assert_equal :expense, operations_cat.type
|
|
355
|
+
assert_equal :metric, metrics_cat.type, "Should use explicit type, not inherit"
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def test_top_level_category_requires_type
|
|
359
|
+
error = assert_raises(ArgumentError) do
|
|
360
|
+
FinIt.define(default_currency: 'USD') do
|
|
361
|
+
config do
|
|
362
|
+
start_date 2024
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
category :income do # No type - should raise
|
|
366
|
+
variable :revenue do
|
|
367
|
+
value 1000
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
assert_match(/must specify type/, error.message)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# === Default Account Inheritance Tests ===
|
|
377
|
+
|
|
378
|
+
def test_default_account_inherited_by_children
|
|
379
|
+
model = FinIt.define(default_currency: 'USD') do
|
|
380
|
+
config do
|
|
381
|
+
start_date 2024
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
account :operating_account do
|
|
385
|
+
type :asset
|
|
386
|
+
currency 'USD'
|
|
387
|
+
opening_balance 50_000
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
category :revenue, type: :income, default_account: :operating_account do
|
|
391
|
+
category :sales do
|
|
392
|
+
calculated :product_sales, formula: "100 * 10"
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
category :services do
|
|
396
|
+
calculated :consulting, formula: "500 * 5"
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
revenue_cat = model.categories.find { |c| c.name == :revenue }
|
|
402
|
+
sales_cat = revenue_cat.children.find { |c| c.name == :sales }
|
|
403
|
+
services_cat = revenue_cat.children.find { |c| c.name == :services }
|
|
404
|
+
|
|
405
|
+
assert_equal :operating_account, revenue_cat.default_account
|
|
406
|
+
assert_equal :operating_account, sales_cat.default_account, "Sales should inherit default_account"
|
|
407
|
+
assert_equal :operating_account, services_cat.default_account, "Services should inherit default_account"
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def test_child_category_can_override_default_account
|
|
411
|
+
model = FinIt.define(default_currency: 'USD') do
|
|
412
|
+
config do
|
|
413
|
+
start_date 2024
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
account :main_account do
|
|
417
|
+
type :asset
|
|
418
|
+
currency 'USD'
|
|
419
|
+
opening_balance 50_000
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
account :special_account do
|
|
423
|
+
type :asset
|
|
424
|
+
currency 'USD'
|
|
425
|
+
opening_balance 10_000
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
category :income, type: :income, default_account: :main_account do
|
|
429
|
+
category :special, default_account: :special_account do
|
|
430
|
+
calculated :special_income, formula: "1000"
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
income_cat = model.categories.find { |c| c.name == :income }
|
|
436
|
+
special_cat = income_cat.children.find { |c| c.name == :special }
|
|
437
|
+
|
|
438
|
+
assert_equal :main_account, income_cat.default_account
|
|
439
|
+
assert_equal :special_account, special_cat.default_account, "Should use overridden default_account"
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# === Defaults Hash Tests (frequency, start_date, end_date) ===
|
|
443
|
+
|
|
444
|
+
def test_defaults_hash_inherited_by_children
|
|
445
|
+
model = FinIt.define(default_currency: 'USD') do
|
|
446
|
+
config do
|
|
447
|
+
start_date 2024
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
account :checking do
|
|
451
|
+
type :asset
|
|
452
|
+
currency 'USD'
|
|
453
|
+
opening_balance 10_000
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
category :expenses, type: :expense,
|
|
457
|
+
default_account: :checking,
|
|
458
|
+
defaults: { frequency: :monthly, start_date: "2024-01-01", end_date: "2024-12-31" } do
|
|
459
|
+
category :cogs do
|
|
460
|
+
calculated :food_costs, formula: "1000"
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
expenses_cat = model.categories.find { |c| c.name == :expenses }
|
|
466
|
+
cogs_cat = expenses_cat.children.find { |c| c.name == :cogs }
|
|
467
|
+
|
|
468
|
+
expected_defaults = { frequency: :monthly, start_date: "2024-01-01", end_date: "2024-12-31" }
|
|
469
|
+
|
|
470
|
+
assert_equal expected_defaults, expenses_cat.defaults
|
|
471
|
+
assert_equal expected_defaults, cogs_cat.defaults, "COGS should inherit defaults from parent"
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def test_child_can_merge_defaults
|
|
475
|
+
model = FinIt.define(default_currency: 'USD') do
|
|
476
|
+
config do
|
|
477
|
+
start_date 2024
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
account :checking do
|
|
481
|
+
type :asset
|
|
482
|
+
currency 'USD'
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
category :expenses, type: :expense,
|
|
486
|
+
default_account: :checking,
|
|
487
|
+
defaults: { frequency: :monthly, start_date: "2024-01-01", end_date: "2024-12-31" } do
|
|
488
|
+
category :special, defaults: { frequency: :weekly } do # Override frequency only
|
|
489
|
+
calculated :special_expense, formula: "500"
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
expenses_cat = model.categories.find { |c| c.name == :expenses }
|
|
495
|
+
special_cat = expenses_cat.children.find { |c| c.name == :special }
|
|
496
|
+
|
|
497
|
+
assert_equal :monthly, expenses_cat.defaults[:frequency]
|
|
498
|
+
assert_equal :weekly, special_cat.defaults[:frequency], "Should override frequency"
|
|
499
|
+
assert_equal "2024-01-01", special_cat.defaults[:start_date], "Should inherit start_date"
|
|
500
|
+
assert_equal "2024-12-31", special_cat.defaults[:end_date], "Should inherit end_date"
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# === Formula Variable Validation Tests ===
|
|
504
|
+
|
|
505
|
+
def test_undefined_variable_in_formula_raises_error
|
|
506
|
+
error = assert_raises(FinIt::UndefinedVariableError) do
|
|
507
|
+
FinIt.define(default_currency: 'USD') do
|
|
508
|
+
config do
|
|
509
|
+
start_date 2024
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
account :checking do
|
|
513
|
+
type :asset
|
|
514
|
+
currency 'USD'
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
category :income, type: :income do
|
|
518
|
+
calculated :revenue, formula: "undefined_var * 2", account: :checking
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
assert_match(/revenue/, error.message, "Should mention the variable with bad formula")
|
|
524
|
+
assert_match(/undefined_var/, error.message, "Should mention the undefined variable")
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def test_multiple_undefined_variables_listed_in_error
|
|
528
|
+
error = assert_raises(FinIt::UndefinedVariableError) do
|
|
529
|
+
FinIt.define(default_currency: 'USD') do
|
|
530
|
+
config do
|
|
531
|
+
start_date 2024
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
account :checking do
|
|
535
|
+
type :asset
|
|
536
|
+
currency 'USD'
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
category :income, type: :income do
|
|
540
|
+
calculated :revenue, formula: "var_a + var_b + var_c", account: :checking
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
assert_match(/var_a/, error.message)
|
|
546
|
+
assert_match(/var_b/, error.message)
|
|
547
|
+
assert_match(/var_c/, error.message)
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def test_defined_variables_pass_validation
|
|
551
|
+
# Should not raise any error
|
|
552
|
+
model = FinIt.define(default_currency: 'USD') do
|
|
553
|
+
config do
|
|
554
|
+
start_date 2024
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
account :checking do
|
|
558
|
+
type :asset
|
|
559
|
+
currency 'USD'
|
|
560
|
+
opening_balance 10_000
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
category :drivers, type: :driver do
|
|
564
|
+
variable :units_sold do
|
|
565
|
+
value 100
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
variable :price_per_unit do
|
|
569
|
+
value 50
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
category :income, type: :income do
|
|
574
|
+
calculated :revenue, formula: "units_sold * price_per_unit", account: :checking
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
assert model, "Model should be created when all variables are defined"
|
|
579
|
+
end
|
|
580
|
+
end
|
|
581
|
+
|