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,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+ require_relative "../test_helper"
5
+
6
+ class FinancialModelTest < Minitest::Test
7
+ def setup
8
+ @model = FinIt.define(default_currency: 'USD') do
9
+ config do
10
+ start_date 2024
11
+ end
12
+
13
+ account :checking do
14
+ type :asset
15
+ currency 'USD'
16
+ opening_balance 10_000
17
+ end
18
+
19
+ account :savings do
20
+ type :asset
21
+ currency 'USD'
22
+ opening_balance 5_000
23
+ end
24
+
25
+ account :credit_card do
26
+ type :liability
27
+ currency 'USD'
28
+ opening_balance 2_000 # Positive value
29
+ end
30
+
31
+ category :income, type: :income do
32
+ variable :salary, currency: 'USD', frequency: :monthly, account: :checking do
33
+ value 5_000, start_date: "2024-01-01", end_date: "2024-12-31"
34
+ end
35
+ end
36
+
37
+ category :expenses, type: :expense do
38
+ variable :rent, currency: 'USD', frequency: :monthly, account: :credit_card do
39
+ value 2_000, start_date: "2024-01-01", end_date: "2024-12-31"
40
+ end
41
+
42
+ calculated :utilities,
43
+ formula: "500",
44
+ frequency: :monthly,
45
+ account: :credit_card,
46
+ start_date: "2024-01-01",
47
+ end_date: "2024-12-31" do
48
+ description "Monthly utilities"
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ def test_account_balance_calculation
55
+ # Generate transactions up to end of February
56
+ @model.generate_transactions(Date.new(2024, 2, 28))
57
+
58
+ # Checking account: opening 10,000 + 2 months salary (5,000 each) = 20,000
59
+ checking_balance = @model.account_balance(:checking, as_of_date: Date.new(2024, 2, 28))
60
+ assert_in_delta 20_000, checking_balance, 0.01, "Checking balance should be 20,000"
61
+
62
+ # Credit card: opening 2,000 + 2 months rent (2,000 each) + 2 months utilities (500 each) = 7,000
63
+ # (Liabilities increase with credits, so expenses credit the liability account)
64
+ credit_balance = @model.account_balance(:credit_card, as_of_date: Date.new(2024, 2, 28))
65
+ assert_in_delta 7_000, credit_balance, 0.01, "Credit card balance should be 7,000"
66
+ end
67
+
68
+ def test_transaction_queries
69
+ @model.generate_transactions(Date.new(2024, 3, 31))
70
+
71
+ # Query by account
72
+ checking_txns = @model.transactions(account: :checking)
73
+ assert checking_txns.any?, "Should have transactions for checking account"
74
+ assert checking_txns.all? { |t| t[:debit_account] == :checking || t[:credit_account] == :checking },
75
+ "All transactions should involve checking account"
76
+
77
+ # Query by date range
78
+ jan_txns = @model.transactions(date_range: { start: Date.new(2024, 1, 1), end: Date.new(2024, 1, 31) })
79
+ assert jan_txns.any?, "Should have transactions in January"
80
+ assert jan_txns.all? { |t| t[:date].month == 1 }, "All transactions should be in January"
81
+
82
+ # Query by variable
83
+ salary_txns = @model.transactions(variable: :salary)
84
+ assert salary_txns.any?, "Should have salary transactions"
85
+ assert salary_txns.all? { |t| t[:variable] == :salary }, "All transactions should be for salary"
86
+ end
87
+
88
+ def test_balance_sheet_validation
89
+ # Model should validate A = L + E
90
+ # Assets: checking 10,000 + savings 5,000 = 15,000
91
+ # Liabilities: credit_card 2,000 (positive value)
92
+ # Equity: auto-created to balance = 15,000 - 2,000 = 13,000
93
+
94
+ assert @model.accounts.key?(:equity), "Equity account should exist"
95
+ equity_balance = @model.accounts[:equity].opening_balance
96
+
97
+ total_assets = @model.accounts.values.select { |a| a.type == :asset }.sum { |a| a.opening_balance.to_f.abs }
98
+ total_liabilities = @model.accounts.values.select { |a| a.type == :liability }.sum { |a| a.opening_balance.to_f.abs }
99
+ equity_abs = equity_balance.to_f.abs
100
+
101
+ assert_in_delta total_assets, total_liabilities + equity_abs, 0.01,
102
+ "Balance sheet should balance: Assets = Liabilities + Equity"
103
+ end
104
+
105
+ def test_start_date_validation
106
+ # Test with year number
107
+ model1 = FinIt.define(default_currency: 'USD') do
108
+ config do
109
+ start_date 2023
110
+ end
111
+
112
+ account :checking do
113
+ type :asset
114
+ currency 'USD'
115
+ opening_balance 0
116
+ end
117
+ end
118
+
119
+ assert_equal Date.new(2023, 1, 1), model1.start_date
120
+
121
+ # Test with date string
122
+ model2 = FinIt.define(default_currency: 'USD') do
123
+ config do
124
+ start_date "2023-06-15"
125
+ end
126
+
127
+ account :checking do
128
+ type :asset
129
+ currency 'USD'
130
+ opening_balance 0
131
+ end
132
+ end
133
+
134
+ assert_equal Date.new(2023, 6, 15), model2.start_date
135
+ end
136
+
137
+ def test_date_validation_prevents_before_start_date
138
+ error = assert_raises(FinIt::StartDateValidationError) do
139
+ @model.account_balance(:checking, as_of_date: Date.new(2023, 12, 31))
140
+ end
141
+
142
+ assert_match(/before model start_date/, error.message)
143
+ end
144
+
145
+ def test_variable_value_query
146
+ # Get variable value for a specific period
147
+ jan_salary = @model.variable_value(:salary, date: Date.new(2024, 1, 15), period_type: :monthly)
148
+ assert jan_salary, "Salary should exist"
149
+ assert_in_delta 5_000, jan_salary.to_f, 0.01, "Monthly salary should be 5,000"
150
+
151
+ # Annual value
152
+ annual_salary = @model.variable_value(:salary, date: Date.new(2024, 1, 15), period_type: :annual)
153
+ assert_in_delta 60_000, annual_salary.to_f, 0.01, "Annual salary should be 60,000"
154
+ end
155
+
156
+ def test_transaction_cache_validation
157
+ # Initially cache should be invalid (no transactions generated)
158
+ assert !@model.transaction_cache_valid?, "Cache should be invalid initially"
159
+
160
+ # Generate transactions
161
+ @model.generate_transactions(Date.new(2024, 12, 31))
162
+
163
+ # Cache should now be valid
164
+ assert @model.transaction_cache_valid?, "Cache should be valid after generation"
165
+ end
166
+
167
+ def test_default_start_date_warning
168
+ # Capture warning output
169
+ warning_output = capture_warnings do
170
+ model = FinIt.define(default_currency: 'USD') do
171
+ account :checking do
172
+ type :asset
173
+ currency 'USD'
174
+ opening_balance 0
175
+ end
176
+ end
177
+
178
+ assert model.start_date, "Should have default start date"
179
+ assert_equal Date.new(Date.today.year, 1, 1), model.start_date
180
+ end
181
+
182
+ assert_match(/Warning.*start_date/, warning_output, "Should warn about default start_date")
183
+ end
184
+
185
+ private
186
+
187
+ def capture_warnings
188
+ old_stderr = $stderr
189
+ $stderr = StringIO.new
190
+ yield
191
+ $stderr.string
192
+ ensure
193
+ $stderr = old_stderr
194
+ end
195
+ end
196
+
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../test_helper"
4
+
5
+ class FrequencyTest < 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 :test_account do
13
+ type :asset
14
+ currency 'USD'
15
+ end
16
+
17
+ category :income, type: :income do
18
+ variable :daily_income, currency: 'USD', frequency: :daily, account: :test_account do
19
+ value 100 # $100/day
20
+ end
21
+
22
+ variable :monthly_salary, currency: 'USD', frequency: :monthly, account: :test_account do
23
+ value 5_000 # $5,000/month
24
+ end
25
+ end
26
+ end
27
+
28
+ @date = Date.new(2024, 6, 15)
29
+ end
30
+
31
+ def test_daily_income_annualized
32
+ daily_annual = @model.calculator.calculate(:daily_income, date: @date, period_type: :annual).to_f
33
+ assert_in_delta 36_500, daily_annual, 0.01, "Daily income should annualize to $36,500"
34
+ end
35
+
36
+ def test_monthly_salary_annualized
37
+ monthly_annual = @model.calculator.calculate(:monthly_salary, date: @date, period_type: :annual).to_f
38
+ assert_in_delta 60_000, monthly_annual, 0.01, "Monthly salary should annualize to $60,000"
39
+ end
40
+
41
+ def test_daily_income_monthly
42
+ daily_monthly = @model.calculator.calculate(:daily_income, date: @date, period_type: :monthly).to_f
43
+ assert_in_delta 3_041.67, daily_monthly, 0.01, "Daily income should scale to $3,041.67/month"
44
+ end
45
+
46
+ def test_monthly_salary_monthly
47
+ monthly_monthly = @model.calculator.calculate(:monthly_salary, date: @date, period_type: :monthly).to_f
48
+ assert_in_delta 5_000, monthly_monthly, 0.01, "Monthly salary should be $5,000/month"
49
+ end
50
+ end
51
+
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../test_helper"
4
+
5
+ class ConsoleOutputTest < Minitest::Test
6
+ def setup
7
+ @model = FinIt.define(default_currency: 'USD') do
8
+ config do
9
+ start_date 2024
10
+ end
11
+
12
+ category :current_assets, type: :asset do
13
+ account :checking do
14
+ currency 'USD'
15
+ opening_balance 10_000
16
+ end
17
+
18
+ variable :cash, currency: 'USD', frequency: :annual do
19
+ value 10_000, start_date: "2024-01-01", end_date: "2024-12-31"
20
+ end
21
+ end
22
+
23
+ category :income, type: :income do
24
+ variable :salary, currency: 'USD', frequency: :annual do
25
+ value 100_000, start_date: "2024-01-01", end_date: "2024-12-31"
26
+ description "Annual salary"
27
+ end
28
+
29
+ variable :bonus, currency: 'USD', frequency: :annual do
30
+ value 20_000, start_date: "2024-01-01", end_date: "2024-12-31"
31
+ description "Annual bonus"
32
+ end
33
+ end
34
+
35
+ category :expenses, type: :expense do
36
+ variable :rent, currency: 'USD', frequency: :monthly, account: :checking do
37
+ value 2_000, start_date: "2024-01-01", end_date: "2024-12-31"
38
+ description "Monthly rent"
39
+ end
40
+ end
41
+
42
+ category :current_liabilities, type: :liability do
43
+ variable :debt, currency: 'USD', frequency: :annual do
44
+ value 5_000, start_date: "2024-01-01", end_date: "2024-12-31"
45
+ end
46
+ end
47
+
48
+ category :equity, type: :equity do
49
+ variable :owner_equity, currency: 'USD', frequency: :annual do
50
+ value 5_000, start_date: "2024-01-01", end_date: "2024-12-31"
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ def test_monthly_income_statement_output
57
+ report = FinIt::Reports::IncomeStatement.new(
58
+ @model,
59
+ start_date: Date.new(2024, 1, 1),
60
+ end_date: Date.new(2024, 1, 31),
61
+ output_currency: 'USD'
62
+ )
63
+
64
+ # Should generate output without error
65
+ suppress_output do
66
+ result = report.output(FinIt::Outputs::ConsoleOutput)
67
+ assert_nil result, "Output should return nil"
68
+ end
69
+ end
70
+
71
+ def test_quarterly_income_statement_output
72
+ report = FinIt::Reports::IncomeStatement.new(
73
+ @model,
74
+ start_date: Date.new(2024, 1, 1),
75
+ end_date: Date.new(2024, 3, 31),
76
+ output_currency: 'USD'
77
+ )
78
+
79
+ # Should generate output without error
80
+ suppress_output do
81
+ result = report.output(FinIt::Outputs::ConsoleOutput)
82
+ assert_nil result, "Output should return nil"
83
+ end
84
+ end
85
+
86
+ def test_monthly_balance_sheet_output
87
+ report = FinIt::Reports::BalanceSheet.new(
88
+ @model,
89
+ start_date: Date.new(2024, 1, 1),
90
+ end_date: Date.new(2024, 1, 31),
91
+ output_currency: 'USD'
92
+ )
93
+
94
+ # Should generate output without error
95
+ suppress_output do
96
+ result = report.output(FinIt::Outputs::ConsoleOutput)
97
+ assert_nil result, "Output should return nil"
98
+ end
99
+ end
100
+
101
+ def test_generate_monthly_reports_output
102
+ report = FinIt::Reports::IncomeStatement.new(
103
+ @model,
104
+ start_date: Date.new(2024, 1, 1),
105
+ end_date: Date.new(2024, 3, 31),
106
+ output_currency: 'USD'
107
+ )
108
+
109
+ monthly_reports = report.generate_monthly(Date.new(2024, 1, 1), Date.new(2024, 3, 31))
110
+
111
+ assert_equal 3, monthly_reports.length
112
+
113
+ # Output each monthly report - should not raise errors
114
+ suppress_output do
115
+ monthly_reports.each do |month_data|
116
+ temp_report = FinIt::Reports::IncomeStatement.new(
117
+ @model,
118
+ start_date: month_data[:period][:start],
119
+ end_date: month_data[:period][:end],
120
+ output_currency: 'USD'
121
+ )
122
+
123
+ result = temp_report.output(FinIt::Outputs::ConsoleOutput)
124
+ assert_nil result, "Output should return nil"
125
+ end
126
+ end
127
+ end
128
+
129
+ def test_generate_yearly_report_output
130
+ report = FinIt::Reports::IncomeStatement.new(
131
+ @model,
132
+ start_date: Date.new(2024, 1, 1),
133
+ end_date: Date.new(2024, 12, 31),
134
+ output_currency: 'USD'
135
+ )
136
+
137
+ yearly_report = report.generate_yearly(2024)
138
+
139
+ assert yearly_report[:period]
140
+ assert yearly_report[:report]
141
+
142
+ # Output yearly report - should not raise errors
143
+ suppress_output do
144
+ temp_report = FinIt::Reports::IncomeStatement.new(
145
+ @model,
146
+ start_date: yearly_report[:period][:start],
147
+ end_date: yearly_report[:period][:end],
148
+ output_currency: 'USD'
149
+ )
150
+
151
+ result = temp_report.output(FinIt::Outputs::ConsoleOutput)
152
+ assert_nil result, "Output should return nil"
153
+ end
154
+ end
155
+
156
+ def test_generate_at_specific_date_output
157
+ report = FinIt::Reports::IncomeStatement.new(
158
+ @model,
159
+ start_date: Date.new(2024, 1, 1),
160
+ end_date: Date.new(2024, 12, 31),
161
+ output_currency: 'USD'
162
+ )
163
+
164
+ date_report = report.generate_at(Date.new(2024, 6, 15))
165
+
166
+ assert date_report[:period]
167
+ assert date_report[:report]
168
+
169
+ # Output date-specific report - should not raise errors
170
+ suppress_output do
171
+ temp_report = FinIt::Reports::IncomeStatement.new(
172
+ @model,
173
+ start_date: date_report[:period][:start],
174
+ end_date: date_report[:period][:end],
175
+ output_currency: 'USD'
176
+ )
177
+
178
+ result = temp_report.output(FinIt::Outputs::ConsoleOutput)
179
+ assert_nil result, "Output should return nil"
180
+ end
181
+ end
182
+
183
+ def test_balance_sheet_monthly_output
184
+ report = FinIt::Reports::BalanceSheet.new(
185
+ @model,
186
+ start_date: Date.new(2024, 1, 1),
187
+ end_date: Date.new(2024, 3, 31),
188
+ output_currency: 'USD'
189
+ )
190
+
191
+ monthly_reports = report.generate_monthly(Date.new(2024, 1, 1), Date.new(2024, 2, 28))
192
+
193
+ assert monthly_reports.length > 0
194
+
195
+ # Output each monthly balance sheet - should not raise errors
196
+ suppress_output do
197
+ monthly_reports.each do |month_data|
198
+ temp_report = FinIt::Reports::BalanceSheet.new(
199
+ @model,
200
+ start_date: month_data[:period][:start],
201
+ end_date: month_data[:period][:end],
202
+ output_currency: 'USD'
203
+ )
204
+
205
+ result = temp_report.output(FinIt::Outputs::ConsoleOutput)
206
+ assert_nil result, "Output should return nil"
207
+ end
208
+ end
209
+ end
210
+
211
+ def test_periods_as_columns_output
212
+ report = FinIt::Reports::IncomeStatement.new(
213
+ @model,
214
+ start_date: Date.new(2024, 1, 1),
215
+ end_date: Date.new(2024, 3, 31),
216
+ output_currency: 'USD'
217
+ )
218
+
219
+ monthly_reports = report.generate_monthly(Date.new(2024, 1, 1), Date.new(2024, 3, 31))
220
+
221
+ # Test output with periods as columns
222
+ suppress_output do
223
+ output = FinIt::Outputs::ConsoleOutput.new(report, periods: monthly_reports)
224
+ result = output.generate
225
+
226
+ assert_nil result, "Output should return nil"
227
+ end
228
+ end
229
+
230
+ def test_period_summary_as_columns_output
231
+ report = FinIt::Reports::IncomeStatement.new(
232
+ @model,
233
+ start_date: Date.new(2024, 1, 1),
234
+ end_date: Date.new(2024, 3, 31),
235
+ output_currency: 'USD'
236
+ )
237
+
238
+ summary = report.period_summary(period_type: :monthly)
239
+
240
+ # Test output with period summary as columns
241
+ suppress_output do
242
+ output = FinIt::Outputs::ConsoleOutput.new(report, periods: summary)
243
+ result = output.generate
244
+
245
+ assert_nil result, "Output should return nil"
246
+ end
247
+ end
248
+ end
249
+