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,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)
|
data/test/test_helper.rb
ADDED
|
@@ -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
|
+
|