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.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/ARCHITECTURE.md +24 -0
  3. data/CHANGELOG.md +9 -0
  4. data/CONTRIBUTING.md +20 -0
  5. data/LICENSE +21 -0
  6. data/QUICKSTART.md +56 -0
  7. data/README.md +74 -0
  8. data/Rakefile +23 -0
  9. data/SECURITY.md +14 -0
  10. data/assets/fin_it_logo.png +0 -0
  11. data/lib/fin_it/account.rb +120 -0
  12. data/lib/fin_it/calculator/currency_conversion.rb +27 -0
  13. data/lib/fin_it/calculator/date_helpers.rb +53 -0
  14. data/lib/fin_it/calculator/variable_hashing.rb +120 -0
  15. data/lib/fin_it/calculator.rb +480 -0
  16. data/lib/fin_it/categories/category.rb +137 -0
  17. data/lib/fin_it/complex_model.rb +169 -0
  18. data/lib/fin_it/dsl/account_builder.rb +35 -0
  19. data/lib/fin_it/dsl/calculated_builder.rb +87 -0
  20. data/lib/fin_it/dsl/config_builder.rb +58 -0
  21. data/lib/fin_it/dsl/model_builder.rb +938 -0
  22. data/lib/fin_it/dsl/model_template_builder.rb +29 -0
  23. data/lib/fin_it/dsl/plan_builder.rb +52 -0
  24. data/lib/fin_it/dsl/project_inheritance_resolver.rb +46 -0
  25. data/lib/fin_it/dsl/variable_builder.rb +41 -0
  26. data/lib/fin_it/dsl.rb +13 -0
  27. data/lib/fin_it/engine.rb +15 -0
  28. data/lib/fin_it/financial_model/account_balances.rb +99 -0
  29. data/lib/fin_it/financial_model/account_hierarchy.rb +158 -0
  30. data/lib/fin_it/financial_model/category_values.rb +179 -0
  31. data/lib/fin_it/financial_model/currency_helpers.rb +14 -0
  32. data/lib/fin_it/financial_model/date_helpers.rb +58 -0
  33. data/lib/fin_it/financial_model/debugging.rb +353 -0
  34. data/lib/fin_it/financial_model/period_flows.rb +121 -0
  35. data/lib/fin_it/financial_model/validation.rb +85 -0
  36. data/lib/fin_it/financial_model/variable_matching.rb +49 -0
  37. data/lib/fin_it/financial_model.rb +395 -0
  38. data/lib/fin_it/model_template.rb +121 -0
  39. data/lib/fin_it/outputs/base_output.rb +51 -0
  40. data/lib/fin_it/outputs/console_output.rb +1528 -0
  41. data/lib/fin_it/outputs/monthly_console_output.rb +145 -0
  42. data/lib/fin_it/outputs/zaxcel_output.rb +1264 -0
  43. data/lib/fin_it/payment_schedule.rb +112 -0
  44. data/lib/fin_it/plan.rb +159 -0
  45. data/lib/fin_it/reports/balance_sheet.rb +638 -0
  46. data/lib/fin_it/reports/base_report.rb +239 -0
  47. data/lib/fin_it/reports/cash_flow_statement.rb +480 -0
  48. data/lib/fin_it/reports/custom_sheet.rb +436 -0
  49. data/lib/fin_it/reports/income_statement.rb +793 -0
  50. data/lib/fin_it/reports/period_comparison.rb +309 -0
  51. data/lib/fin_it/reports/scenario_comparison.rb +296 -0
  52. data/lib/fin_it/temporal_value.rb +349 -0
  53. data/lib/fin_it/transaction_generator/account_resolver.rb +118 -0
  54. data/lib/fin_it/transaction_generator/cache_management.rb +39 -0
  55. data/lib/fin_it/transaction_generator/date_generation.rb +57 -0
  56. data/lib/fin_it/transaction_generator.rb +357 -0
  57. data/lib/fin_it/version.rb +6 -0
  58. data/lib/fin_it.rb +27 -0
  59. data/test/fin_it/calculator_test.rb +109 -0
  60. data/test/fin_it/complex_model_test.rb +198 -0
  61. data/test/fin_it/debugging_test.rb +112 -0
  62. data/test/fin_it/driver_variables_test.rb +109 -0
  63. data/test/fin_it/dsl_test.rb +581 -0
  64. data/test/fin_it/financial_model_test.rb +196 -0
  65. data/test/fin_it/frequency_test.rb +51 -0
  66. data/test/fin_it/outputs/console_output_test.rb +249 -0
  67. data/test/fin_it/plan_test.rb +281 -0
  68. data/test/fin_it/reports/account_balance_test.rb +232 -0
  69. data/test/fin_it/reports/balance_sheet_test.rb +355 -0
  70. data/test/fin_it/reports/cash_flow_statement_test.rb +234 -0
  71. data/test/fin_it/reports/custom_sheet_test.rb +246 -0
  72. data/test/fin_it/reports/income_statement_test.rb +431 -0
  73. data/test/fin_it/reports/period_comparison_test.rb +226 -0
  74. data/test/fin_it/reports/restaurant_model_test.rb +225 -0
  75. data/test/fin_it/reports/scenario_comparison_test.rb +414 -0
  76. data/test/scripts/generate_demo_reports.rb +47 -0
  77. data/test/scripts/startup_saas_demo.rb +62 -0
  78. data/test/test_helper.rb +25 -0
  79. data/test/verify_accounting_equation.rb +91 -0
  80. metadata +264 -0
@@ -0,0 +1,414 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../test_helper"
4
+
5
+ class ScenarioComparisonTest < 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
+ end
61
+
62
+ @start_date = Date.new(2024, 1, 1)
63
+ @end_date = Date.new(2024, 12, 31)
64
+ end
65
+
66
+ def test_scenario_comparison_basic
67
+ # Generate transactions for base model
68
+ @model.generate_transactions(@end_date)
69
+
70
+ # Create scenarios
71
+ growth_model = @model.with_plan(:growth_scenario)
72
+ growth_model.generate_transactions(@end_date)
73
+
74
+ cost_model = @model.with_plan(:cost_reduction)
75
+ cost_model.generate_transactions(@end_date)
76
+
77
+ # Create comparison report
78
+ comparison = FinIt::Reports::ScenarioComparison.new(
79
+ @model,
80
+ scenarios: { growth: growth_model, cost_reduction: cost_model },
81
+ start_date: @start_date,
82
+ end_date: @end_date,
83
+ output_currency: 'USD'
84
+ )
85
+
86
+ data = comparison.generate
87
+
88
+ # Verify structure
89
+ assert_equal "ScenarioComparison", data[:report_type]
90
+ assert data[:metadata]
91
+ assert data[:base]
92
+ assert data[:scenarios]
93
+ assert data[:variances]
94
+
95
+ # Verify metadata
96
+ assert_equal @start_date, data[:metadata][:start_date]
97
+ assert_equal @end_date, data[:metadata][:end_date]
98
+ assert_equal 'USD', data[:metadata][:currency]
99
+ assert_includes data[:metadata][:scenario_names], :growth
100
+ assert_includes data[:metadata][:scenario_names], :cost_reduction
101
+ end
102
+
103
+ def test_scenario_comparison_metrics
104
+ @model.generate_transactions(@end_date)
105
+
106
+ growth_model = @model.with_plan(:growth_scenario)
107
+ growth_model.generate_transactions(@end_date)
108
+
109
+ comparison = FinIt::Reports::ScenarioComparison.new(
110
+ @model,
111
+ scenarios: { growth: growth_model },
112
+ start_date: @start_date,
113
+ end_date: @end_date,
114
+ metrics: [:net_income, :total_revenue, :total_expenses]
115
+ )
116
+
117
+ data = comparison.generate
118
+
119
+ # Verify metrics are calculated
120
+ assert data[:base][:net_income]
121
+ assert data[:base][:total_revenue]
122
+ assert data[:base][:total_expenses]
123
+
124
+ # Verify scenario metrics
125
+ assert data[:scenarios][:growth][:net_income]
126
+ assert data[:scenarios][:growth][:total_revenue]
127
+ assert data[:scenarios][:growth][:total_expenses]
128
+ end
129
+
130
+ def test_scenario_comparison_variances
131
+ @model.generate_transactions(@end_date)
132
+
133
+ growth_model = @model.with_plan(:growth_scenario)
134
+ growth_model.generate_transactions(@end_date)
135
+
136
+ comparison = FinIt::Reports::ScenarioComparison.new(
137
+ @model,
138
+ scenarios: { growth: growth_model },
139
+ start_date: @start_date,
140
+ end_date: @end_date,
141
+ metrics: [:net_income]
142
+ )
143
+
144
+ data = comparison.generate
145
+
146
+ # Verify variance is calculated
147
+ assert data[:variances][:growth]
148
+ assert data[:variances][:growth][:net_income]
149
+ assert data[:variances][:growth][:net_income][:absolute]
150
+ assert data[:variances][:growth][:net_income][:percentage]
151
+
152
+ # Growth scenario should have higher net income (positive variance)
153
+ variance = data[:variances][:growth][:net_income]
154
+ assert variance[:absolute] > 0, "Growth scenario should have higher net income"
155
+ assert variance[:percentage] > 0, "Growth percentage should be positive"
156
+ end
157
+
158
+ def test_scenario_comparison_console_output
159
+ @model.generate_transactions(@end_date)
160
+
161
+ growth_model = @model.with_plan(:growth_scenario)
162
+ growth_model.generate_transactions(@end_date)
163
+
164
+ comparison = FinIt::Reports::ScenarioComparison.new(
165
+ @model,
166
+ scenarios: { growth: growth_model },
167
+ start_date: @start_date,
168
+ end_date: @end_date
169
+ )
170
+
171
+ # Capture stdout
172
+ output = capture_io do
173
+ comparison.output(FinIt::Outputs::ConsoleOutput)
174
+ end.first
175
+
176
+ # Verify output contains expected elements
177
+ assert_match(/SCENARIO COMPARISON/, output)
178
+ assert_match(/Net Income/, output)
179
+ assert_match(/Base/, output)
180
+ assert_match(/Growth/, output)
181
+ end
182
+
183
+ def test_scenario_comparison_custom_metrics
184
+ @model.generate_transactions(@end_date)
185
+
186
+ growth_model = @model.with_plan(:growth_scenario)
187
+ growth_model.generate_transactions(@end_date)
188
+
189
+ # Test with custom metrics including ending_cash
190
+ comparison = FinIt::Reports::ScenarioComparison.new(
191
+ @model,
192
+ scenarios: { growth: growth_model },
193
+ start_date: @start_date,
194
+ end_date: @end_date,
195
+ metrics: [:net_income, :ending_cash]
196
+ )
197
+
198
+ data = comparison.generate
199
+
200
+ # Verify all requested metrics are present
201
+ assert data[:base][:net_income]
202
+ assert data[:base][:ending_cash]
203
+ assert data[:scenarios][:growth][:net_income]
204
+ assert data[:scenarios][:growth][:ending_cash]
205
+ end
206
+
207
+ def test_scenario_comparison_multiple_scenarios
208
+ @model.generate_transactions(@end_date)
209
+
210
+ growth_model = @model.with_plan(:growth_scenario)
211
+ growth_model.generate_transactions(@end_date)
212
+
213
+ cost_model = @model.with_plan(:cost_reduction)
214
+ cost_model.generate_transactions(@end_date)
215
+
216
+ combined_model = @model.with_plans(:growth_scenario, :cost_reduction)
217
+ combined_model.generate_transactions(@end_date)
218
+
219
+ comparison = FinIt::Reports::ScenarioComparison.new(
220
+ @model,
221
+ scenarios: {
222
+ growth: growth_model,
223
+ cost_cut: cost_model,
224
+ combined: combined_model
225
+ },
226
+ start_date: @start_date,
227
+ end_date: @end_date
228
+ )
229
+
230
+ data = comparison.generate
231
+
232
+ # Verify all scenarios are present
233
+ assert_equal 3, data[:scenarios].keys.length
234
+ assert data[:scenarios][:growth]
235
+ assert data[:scenarios][:cost_cut]
236
+ assert data[:scenarios][:combined]
237
+
238
+ # Verify variances for all scenarios
239
+ assert_equal 3, data[:variances].keys.length
240
+ end
241
+
242
+ def test_scenario_comparison_hash_metrics
243
+ @model.generate_transactions(@end_date)
244
+
245
+ growth_model = @model.with_plan(:growth_scenario)
246
+ growth_model.generate_transactions(@end_date)
247
+
248
+ # Test hash format metrics
249
+ comparison = FinIt::Reports::ScenarioComparison.new(
250
+ @model,
251
+ scenarios: { growth: growth_model },
252
+ start_date: @start_date,
253
+ end_date: @end_date,
254
+ metrics: [
255
+ :net_income, # Symbol format
256
+ { variable: :total_revenue, label: "Annual Revenue" }, # Hash format
257
+ { variable: :total_expenses, period_type: :monthly, label: "Monthly Expenses" }
258
+ ]
259
+ )
260
+
261
+ data = comparison.generate
262
+
263
+ # Verify normalized metrics have correct labels
264
+ normalized = data[:metadata][:normalized_metrics]
265
+ assert_equal 3, normalized.length
266
+ assert_equal "Net Income", normalized[0][:label]
267
+ assert_equal "Annual Revenue", normalized[1][:label]
268
+ assert_equal "Monthly Expenses", normalized[2][:label]
269
+
270
+ # Verify data is present
271
+ assert data[:base][:net_income]
272
+ assert data[:base][:total_revenue]
273
+ assert data[:base][:total_expenses]
274
+ end
275
+
276
+ def test_scenario_comparison_hash_metrics_with_date_override
277
+ @model.generate_transactions(@end_date)
278
+
279
+ growth_model = @model.with_plan(:growth_scenario)
280
+ growth_model.generate_transactions(@end_date)
281
+
282
+ # Test hash metrics with date range override
283
+ comparison = FinIt::Reports::ScenarioComparison.new(
284
+ @model,
285
+ scenarios: { growth: growth_model },
286
+ start_date: @start_date,
287
+ end_date: @end_date,
288
+ metrics: [
289
+ :net_income,
290
+ {
291
+ variable: :total_revenue,
292
+ start_date: "2024-01-01",
293
+ end_date: "2024-06-30",
294
+ label: "H1 Revenue",
295
+ key: :h1_revenue
296
+ },
297
+ {
298
+ variable: :total_revenue,
299
+ start_date: "2024-07-01",
300
+ end_date: "2024-12-31",
301
+ label: "H2 Revenue",
302
+ key: :h2_revenue
303
+ }
304
+ ]
305
+ )
306
+
307
+ data = comparison.generate
308
+
309
+ # Verify different keys for same variable with different date ranges
310
+ assert data[:base][:net_income]
311
+ assert data[:base][:h1_revenue]
312
+ assert data[:base][:h2_revenue]
313
+ end
314
+
315
+ def test_scenario_comparison_hash_metrics_with_period_name
316
+ @model.generate_transactions(@end_date)
317
+
318
+ growth_model = @model.with_plan(:growth_scenario)
319
+ growth_model.generate_transactions(@end_date)
320
+
321
+ # Test period_name for automatic key/label generation
322
+ comparison = FinIt::Reports::ScenarioComparison.new(
323
+ @model,
324
+ scenarios: { growth: growth_model },
325
+ start_date: @start_date,
326
+ end_date: @end_date,
327
+ metrics: [
328
+ {
329
+ variable: :total_revenue,
330
+ period_name: "Q1",
331
+ start_date: "2024-01-01",
332
+ end_date: "2024-03-31"
333
+ },
334
+ {
335
+ variable: :total_revenue,
336
+ period_name: "Q2",
337
+ start_date: "2024-04-01",
338
+ end_date: "2024-06-30"
339
+ }
340
+ ]
341
+ )
342
+
343
+ data = comparison.generate
344
+
345
+ # period_name generates key like :total_revenue_q1
346
+ normalized = data[:metadata][:normalized_metrics]
347
+ assert_equal :total_revenue_q1, normalized[0][:key]
348
+ assert_equal :total_revenue_q2, normalized[1][:key]
349
+ assert_equal "Total Revenue (Q1)", normalized[0][:label]
350
+ assert_equal "Total Revenue (Q2)", normalized[1][:label]
351
+ end
352
+
353
+ def test_scenario_comparison_normalized_metrics_accessor
354
+ @model.generate_transactions(@end_date)
355
+
356
+ growth_model = @model.with_plan(:growth_scenario)
357
+ growth_model.generate_transactions(@end_date)
358
+
359
+ comparison = FinIt::Reports::ScenarioComparison.new(
360
+ @model,
361
+ scenarios: { growth: growth_model },
362
+ start_date: @start_date,
363
+ end_date: @end_date,
364
+ metrics: [:net_income, { variable: :total_revenue, label: "Revenue" }]
365
+ )
366
+
367
+ # Test normalized_metrics accessor before generate
368
+ normalized = comparison.normalized_metrics
369
+ assert_equal 2, normalized.length
370
+ assert_equal :net_income, normalized[0][:variable]
371
+ assert_equal :total_revenue, normalized[1][:variable]
372
+ assert_equal "Revenue", normalized[1][:label]
373
+ end
374
+
375
+ def test_scenario_comparison_invalid_metric_format
376
+ @model.generate_transactions(@end_date)
377
+
378
+ growth_model = @model.with_plan(:growth_scenario)
379
+ growth_model.generate_transactions(@end_date)
380
+
381
+ # Hash without :variable key should raise error
382
+ assert_raises(ArgumentError) do
383
+ FinIt::Reports::ScenarioComparison.new(
384
+ @model,
385
+ scenarios: { growth: growth_model },
386
+ start_date: @start_date,
387
+ end_date: @end_date,
388
+ metrics: [{ label: "Invalid" }] # Missing :variable key
389
+ )
390
+ end
391
+ end
392
+
393
+ def test_scenario_comparison_hash_metrics_format_option
394
+ @model.generate_transactions(@end_date)
395
+
396
+ growth_model = @model.with_plan(:growth_scenario)
397
+ growth_model.generate_transactions(@end_date)
398
+
399
+ comparison = FinIt::Reports::ScenarioComparison.new(
400
+ @model,
401
+ scenarios: { growth: growth_model },
402
+ start_date: @start_date,
403
+ end_date: @end_date,
404
+ metrics: [
405
+ { variable: :net_income, format: :currency },
406
+ { variable: :total_revenue, format: :number }
407
+ ]
408
+ )
409
+
410
+ normalized = comparison.normalized_metrics
411
+ assert_equal :currency, normalized[0][:format]
412
+ assert_equal :number, normalized[1][:format]
413
+ end
414
+ end
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
5
+
6
+ require "fin_it"
7
+ require "date"
8
+
9
+ model = FinIt.define(default_currency: "USD") do
10
+ config do
11
+ default_currency "USD"
12
+ start_date 2026
13
+ end
14
+
15
+ account :operating_cash do
16
+ type :asset
17
+ currency "USD"
18
+ opening_balance 100_000
19
+ end
20
+
21
+ category :income, type: :income do
22
+ variable :product_sales, currency: "USD", frequency: :monthly, account: :operating_cash do
23
+ value 45_000, start_date: "2026-01-01", end_date: "2026-12-31"
24
+ end
25
+ end
26
+
27
+ category :expenses, type: :expense do
28
+ variable :team_payroll, currency: "USD", frequency: :monthly, account: :operating_cash do
29
+ value 20_000, start_date: "2026-01-01", end_date: "2026-12-31"
30
+ end
31
+
32
+ variable :cloud_infrastructure, currency: "USD", frequency: :monthly, account: :operating_cash do
33
+ value 6_500, start_date: "2026-01-01", end_date: "2026-12-31"
34
+ end
35
+ end
36
+ end
37
+
38
+ report = FinIt::Reports::IncomeStatement.new(
39
+ model,
40
+ start_date: Date.new(2026, 1, 1),
41
+ end_date: Date.new(2026, 12, 31),
42
+ output_currency: "USD"
43
+ )
44
+
45
+ puts "Demo Income Statement (Fictional)"
46
+ puts "=" * 80
47
+ report.output(FinIt::Outputs::ConsoleOutput)
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
5
+
6
+ require "fin_it"
7
+ require "date"
8
+
9
+ model = FinIt.define(default_currency: "USD") do
10
+ config do
11
+ default_currency "USD"
12
+ start_date 2026
13
+ end
14
+
15
+ account :cash do
16
+ type :asset
17
+ currency "USD"
18
+ opening_balance 250_000
19
+ end
20
+
21
+ category :income, type: :income do
22
+ variable :new_mrr, currency: "USD", frequency: :monthly, account: :cash do
23
+ value 35_000, start_date: "2026-01-01", end_date: "2026-12-31"
24
+ end
25
+
26
+ variable :expansion_mrr, currency: "USD", frequency: :monthly, account: :cash do
27
+ value 7_000, start_date: "2026-01-01", end_date: "2026-12-31"
28
+ end
29
+ end
30
+
31
+ category :expenses, type: :expense do
32
+ variable :engineering_payroll, currency: "USD", frequency: :monthly, account: :cash do
33
+ value 22_000, start_date: "2026-01-01", end_date: "2026-12-31"
34
+ end
35
+
36
+ variable :sales_marketing, currency: "USD", frequency: :monthly, account: :cash do
37
+ value 9_000, start_date: "2026-01-01", end_date: "2026-12-31"
38
+ end
39
+ end
40
+ end
41
+
42
+ model.generate_transactions(Date.new(2026, 12, 31))
43
+
44
+ income_statement = FinIt::Reports::IncomeStatement.new(
45
+ model,
46
+ start_date: Date.new(2026, 1, 1),
47
+ end_date: Date.new(2026, 12, 31),
48
+ output_currency: "USD"
49
+ )
50
+
51
+ balance_sheet = FinIt::Reports::BalanceSheet.new(
52
+ model,
53
+ start_date: Date.new(2026, 1, 1),
54
+ end_date: Date.new(2026, 12, 31),
55
+ output_currency: "USD"
56
+ )
57
+
58
+ puts "Fictional SaaS Demo"
59
+ puts "=" * 80
60
+ income_statement.output(FinIt::Outputs::ConsoleOutput)
61
+ puts
62
+ balance_sheet.output(FinIt::Outputs::ConsoleOutput)
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
+
5
+ require "fin_it"
6
+ require "minitest/autorun"
7
+ require "date"
8
+ require "stringio"
9
+
10
+ # Helper to suppress output during tests
11
+ module OutputSuppression
12
+ def suppress_output
13
+ original_stdout = $stdout
14
+ $stdout = StringIO.new
15
+ yield
16
+ ensure
17
+ $stdout = original_stdout
18
+ end
19
+ end
20
+
21
+ # Include in all test classes
22
+ class Minitest::Test
23
+ include OutputSuppression
24
+ end
25
+
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/fin_it'
5
+ require 'date'
6
+
7
+ model = FinIt.define(default_currency: 'USD') do
8
+ config { start_date 2024 }
9
+
10
+ account :checking do
11
+ type :asset
12
+ currency 'USD'
13
+ opening_balance 30_000
14
+ end
15
+
16
+ account :mortgage do
17
+ type :liability
18
+ currency 'USD'
19
+ opening_balance(-200_000)
20
+ end
21
+
22
+ category :current_assets, type: :asset do
23
+ calculated :checking_balance, formula: '0', account: :checking
24
+ end
25
+
26
+ category :long_term_liabilities, type: :liability do
27
+ calculated :mortgage_balance, formula: '0', account: :mortgage
28
+ end
29
+
30
+ category :equity, type: :equity do
31
+ calculated :equity_balance, formula: '0', account: :equity
32
+ end
33
+
34
+ category :interest_expense, type: :expense do
35
+ calculated :mortgage_interest, formula: '0', account: :equity
36
+ end
37
+
38
+ define_model :mortgage_payment do
39
+ variable :principal
40
+ variable :rate
41
+
42
+ calculation do |date, context|
43
+ principal = context[:principal] || 200_000
44
+ rate = (context[:rate] || 0.035) / 12.0
45
+ interest = principal * rate
46
+ principal_payment = 1000 - interest
47
+
48
+ [
49
+ { amount: principal_payment, debit_account: :mortgage, credit_account: :checking },
50
+ { amount: interest, debit_account: :equity, credit_account: :checking }
51
+ ]
52
+ end
53
+ end
54
+
55
+ mortgage_payment do
56
+ start_date '2024-06-01'
57
+ principal 200_000
58
+ rate 0.035
59
+ end
60
+ end
61
+
62
+ model.generate_transactions(Date.new(2024, 12, 31))
63
+
64
+ puts '=' * 80
65
+ puts 'ACCOUNTING EQUATION VERIFICATION (A = L + E)'
66
+ puts '=' * 80
67
+ puts
68
+
69
+ (1..12).each do |month|
70
+ month_end = Date.new(2024, month, -1)
71
+ assets = model.account_balance(:checking, as_of_date: month_end)
72
+ liabilities_raw = model.account_balance(:mortgage, as_of_date: month_end)
73
+ equity_raw = model.account_balance(:equity, as_of_date: month_end)
74
+
75
+ # Liabilities are stored as negative, so convert to positive for equation
76
+ liabilities = liabilities_raw.abs
77
+ equity = equity_raw
78
+
79
+ total_liab_equity = liabilities + equity
80
+ difference = (assets - total_liab_equity).abs
81
+
82
+ status = difference < 0.01 ? 'BALANCED' : 'NOT BALANCED'
83
+ puts "#{month_end.strftime('%b %Y')}:"
84
+ puts " Assets: $#{assets.round(2)}"
85
+ puts " Liabilities: $#{liabilities.round(2)} (stored as: $#{liabilities_raw.round(2)})"
86
+ puts " Equity: $#{equity.round(2)}"
87
+ puts " L + E: $#{total_liab_equity.round(2)}"
88
+ puts " Difference: $#{difference.round(2)} [#{status}]"
89
+ puts
90
+ end
91
+