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,355 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../test_helper"
4
+
5
+ class BalanceSheetTest < Minitest::Test
6
+ def setup
7
+ @model = FinIt.define(default_currency: 'USD') do
8
+ config do
9
+ start_date 2024
10
+ end
11
+
12
+ account :checking do
13
+ type :asset
14
+ currency 'USD'
15
+ opening_balance 10_000
16
+ end
17
+
18
+ account :savings do
19
+ type :asset
20
+ currency 'USD'
21
+ opening_balance 50_000
22
+ end
23
+
24
+ account :credit_card do
25
+ type :liability
26
+ currency 'USD'
27
+ opening_balance 5_000
28
+ end
29
+
30
+ # Assets
31
+ category :current_assets, type: :asset do
32
+ variable :cash, currency: 'USD', frequency: :annual, account: :checking do
33
+ value 10_000, start_date: "2024-01-01", end_date: "2024-12-31"
34
+ description "Cash in checking"
35
+ end
36
+
37
+ variable :savings_account, currency: 'USD', frequency: :annual, account: :savings do
38
+ value 50_000, start_date: "2024-01-01", end_date: "2024-12-31"
39
+ description "Savings account"
40
+ end
41
+ end
42
+
43
+ account :equipment_account do
44
+ type :asset
45
+ currency 'USD'
46
+ opening_balance 25_000
47
+ end
48
+
49
+ category :fixed_assets, type: :asset do
50
+ variable :equipment, currency: 'USD', frequency: :annual, account: :equipment_account do
51
+ value 25_000, start_date: "2024-01-01", end_date: "2024-12-31"
52
+ description "Equipment"
53
+ end
54
+ end
55
+
56
+ # Liabilities
57
+ category :current_liabilities, type: :liability do
58
+ variable :credit_card_debt, currency: 'USD', frequency: :annual, account: :credit_card do
59
+ value 5_000, start_date: "2024-01-01", end_date: "2024-12-31"
60
+ description "Credit card debt"
61
+ end
62
+ end
63
+
64
+ account :loan_account do
65
+ type :liability
66
+ currency 'USD'
67
+ opening_balance 20_000
68
+ end
69
+
70
+ category :long_term_liabilities, type: :liability do
71
+ # Loan account already has opening balance, variable just references it
72
+ variable :loan, currency: 'USD', frequency: :annual, account: :loan_account do
73
+ value 0, start_date: "2024-01-01", end_date: "2024-12-31"
74
+ description "Long-term loan"
75
+ end
76
+ end
77
+
78
+ # Equity
79
+ # Equity account will be auto-created with balance = Assets (85,000) - Liabilities (25,000) = 60,000
80
+ category :equity, type: :equity do
81
+ # Variable will be automatically created for the equity account
82
+ end
83
+ end
84
+ end
85
+
86
+ def test_generate_balance_sheet
87
+ report = FinIt::Reports::BalanceSheet.new(
88
+ @model,
89
+ start_date: Date.new(2024, 1, 1),
90
+ end_date: Date.new(2024, 12, 31),
91
+ output_currency: 'USD'
92
+ )
93
+
94
+ report_data = report.generate
95
+ assert report_data, "Report should generate"
96
+ assert report_data[:sections], "Report should have sections"
97
+ assert report_data[:sections][:assets], "Report should have assets section"
98
+ assert report_data[:sections][:liabilities], "Report should have liabilities section"
99
+ assert report_data[:sections][:equity], "Report should have equity section"
100
+ assert report_data[:totals], "Report should have totals"
101
+ end
102
+
103
+ def test_assets_section
104
+ report = FinIt::Reports::BalanceSheet.new(
105
+ @model,
106
+ start_date: Date.new(2024, 1, 1),
107
+ end_date: Date.new(2024, 12, 31),
108
+ output_currency: 'USD'
109
+ )
110
+
111
+ report_data = report.generate
112
+ assets_section = report_data[:sections][:assets]
113
+
114
+ assert assets_section[:current_assets], "Should have current assets"
115
+ assert assets_section[:fixed_assets], "Should have fixed assets"
116
+ assert assets_section[:total], "Should have total assets"
117
+
118
+ # Current assets: 10,000 + 50,000 = 60,000
119
+ assert_in_delta 60_000, assets_section[:current_assets], 0.01
120
+ # Fixed assets: 25,000
121
+ assert_in_delta 25_000, assets_section[:fixed_assets], 0.01
122
+ # Total assets: 85,000
123
+ assert_in_delta 85_000, assets_section[:total], 0.01
124
+ end
125
+
126
+ def test_liabilities_section
127
+ report = FinIt::Reports::BalanceSheet.new(
128
+ @model,
129
+ start_date: Date.new(2024, 1, 1),
130
+ end_date: Date.new(2024, 12, 31),
131
+ output_currency: 'USD'
132
+ )
133
+
134
+ report_data = report.generate
135
+ liabilities_section = report_data[:sections][:liabilities]
136
+
137
+ assert liabilities_section[:current_liabilities], "Should have current liabilities"
138
+ assert liabilities_section[:long_term_liabilities], "Should have long-term liabilities"
139
+ assert liabilities_section[:total], "Should have total liabilities"
140
+
141
+ # Current liabilities: 5,000
142
+ assert_in_delta 5_000, liabilities_section[:current_liabilities], 0.01
143
+ # Long-term liabilities: 20,000
144
+ assert_in_delta 20_000, liabilities_section[:long_term_liabilities], 0.01
145
+ # Total liabilities: 25,000
146
+ assert_in_delta 25_000, liabilities_section[:total], 0.01
147
+ end
148
+
149
+ def test_equity_section
150
+ report = FinIt::Reports::BalanceSheet.new(
151
+ @model,
152
+ start_date: Date.new(2024, 1, 1),
153
+ end_date: Date.new(2024, 12, 31),
154
+ output_currency: 'USD'
155
+ )
156
+
157
+ report_data = report.generate
158
+ equity_section = report_data[:sections][:equity]
159
+
160
+ assert equity_section[:total], "Should have total equity"
161
+ # Equity: 60,000
162
+ assert_in_delta 60_000, equity_section[:total], 0.01
163
+ end
164
+
165
+ def test_balance_sheet_equation
166
+ report = FinIt::Reports::BalanceSheet.new(
167
+ @model,
168
+ start_date: Date.new(2024, 1, 1),
169
+ end_date: Date.new(2024, 12, 31),
170
+ output_currency: 'USD'
171
+ )
172
+
173
+ report_data = report.generate
174
+ totals = report_data[:totals]
175
+
176
+ # Balance sheet equation: Assets = Liabilities + Equity
177
+ # Assets: 85,000 (60,000 current + 25,000 fixed)
178
+ # Liabilities: 25,000 (5,000 current + 20,000 long-term)
179
+ # Equity: 60,000
180
+ # 85,000 = 25,000 + 60,000 ✓
181
+
182
+ total_assets = totals[:assets]
183
+ total_liabilities = totals[:liabilities]
184
+ total_equity = totals[:equity]
185
+ total_liabilities_and_equity = totals[:total_liabilities_and_equity]
186
+
187
+ assert_in_delta 85_000, total_assets, 0.01, "Total assets should be 85,000"
188
+ assert_in_delta 25_000, total_liabilities, 0.01, "Total liabilities should be 25,000"
189
+ assert_in_delta 60_000, total_equity, 0.01, "Total equity should be 60,000"
190
+ assert_in_delta 85_000, total_liabilities_and_equity, 0.01, "Total liabilities and equity should equal total assets"
191
+
192
+ # Verify the balance sheet equation: Assets = Liabilities + Equity
193
+ assert_in_delta total_assets, total_liabilities_and_equity, 0.01,
194
+ "Balance sheet must balance: Assets (#{total_assets}) = Liabilities + Equity (#{total_liabilities_and_equity})"
195
+ end
196
+
197
+ def test_generate_at_specific_date
198
+ report = FinIt::Reports::BalanceSheet.new(
199
+ @model,
200
+ start_date: Date.new(2024, 1, 1),
201
+ end_date: Date.new(2024, 12, 31),
202
+ output_currency: 'USD'
203
+ )
204
+
205
+ date_report = report.generate_at(Date.new(2024, 6, 15))
206
+
207
+ assert date_report[:period], "Should have period info"
208
+ assert_equal Date.new(2024, 6, 15), date_report[:period][:start], "Should start on specified date"
209
+ assert_equal Date.new(2024, 6, 15), date_report[:period][:end], "Should end on specified date"
210
+ assert date_report[:report], "Should have report data"
211
+ end
212
+
213
+ def test_generate_monthly_balance_sheets
214
+ report = FinIt::Reports::BalanceSheet.new(
215
+ @model,
216
+ start_date: Date.new(2024, 1, 1),
217
+ end_date: Date.new(2024, 3, 31),
218
+ output_currency: 'USD'
219
+ )
220
+
221
+ monthly_reports = report.generate_monthly(Date.new(2024, 1, 1), Date.new(2024, 3, 31))
222
+
223
+ assert_equal 3, monthly_reports.length, "Should generate 3 monthly balance sheets"
224
+
225
+ # Check January report
226
+ jan_report = monthly_reports[0]
227
+ assert jan_report[:period], "Should have period info"
228
+ assert_equal Date.new(2024, 1, 1), jan_report[:period][:start], "January should start on Jan 1"
229
+ assert_equal Date.new(2024, 1, 31), jan_report[:period][:end], "January should end on Jan 31"
230
+ assert jan_report[:report], "Should have report data"
231
+ end
232
+
233
+ def test_section_value
234
+ report = FinIt::Reports::BalanceSheet.new(
235
+ @model,
236
+ start_date: Date.new(2024, 1, 1),
237
+ end_date: Date.new(2024, 12, 31),
238
+ output_currency: 'USD'
239
+ )
240
+
241
+ assets_value = report.section_value(:assets)
242
+ liabilities_value = report.section_value(:liabilities)
243
+ equity_value = report.section_value(:equity)
244
+
245
+ assert assets_value, "Should have assets value"
246
+ assert liabilities_value, "Should have liabilities value"
247
+ assert equity_value, "Should have equity value"
248
+ end
249
+
250
+ def test_section_count
251
+ report = FinIt::Reports::BalanceSheet.new(
252
+ @model,
253
+ start_date: Date.new(2024, 1, 1),
254
+ end_date: Date.new(2024, 12, 31),
255
+ output_currency: 'USD'
256
+ )
257
+
258
+ assets_count = report.section_count(:assets)
259
+ liabilities_count = report.section_count(:liabilities)
260
+ equity_count = report.section_count(:equity)
261
+
262
+ assert assets_count > 0, "Should have asset items"
263
+ assert liabilities_count > 0, "Should have liability items"
264
+ assert equity_count > 0, "Should have equity items"
265
+ end
266
+
267
+ def test_assets_show_individual_variables
268
+ report = FinIt::Reports::BalanceSheet.new(
269
+ @model,
270
+ start_date: Date.new(2024, 1, 1),
271
+ end_date: Date.new(2024, 12, 31),
272
+ output_currency: 'USD'
273
+ )
274
+
275
+ report_data = report.generate
276
+ assets_section = report_data[:sections][:assets]
277
+ current_assets_item = assets_section[:items].find { |item| item[:name] == :current_assets }
278
+
279
+ # Should have subcategories showing individual variables
280
+ assert current_assets_item[:subcategories], "Current assets should have subcategories"
281
+ assert_equal 2, current_assets_item[:subcategories].length, "Should have 2 current asset variables"
282
+
283
+ # Check that cash and savings are shown separately
284
+ variable_names = current_assets_item[:subcategories].map { |sub| sub[:name] }
285
+ assert variable_names.include?(:cash), "Should show cash separately"
286
+ assert variable_names.include?(:savings_account), "Should show savings_account separately"
287
+ end
288
+
289
+ def test_balance_sheet_validation_raises_error_when_unbalanced
290
+ # Create an unbalanced model
291
+ # Note: This test is tricky because equity account is auto-created to balance
292
+ # To create an unbalanced model, we need to manually set equity account balance incorrectly
293
+ unbalanced_model = FinIt.define(default_currency: 'USD') do
294
+ config do
295
+ start_date 2024
296
+ end
297
+
298
+ account :cash_account do
299
+ type :asset
300
+ currency 'USD'
301
+ opening_balance 100_000
302
+ end
303
+
304
+ account :debt_account do
305
+ type :liability
306
+ currency 'USD'
307
+ opening_balance 50_000
308
+ end
309
+
310
+ # Equity account will be auto-created with balance = 100,000 - 50,000 = 50,000
311
+ # But we'll manually override it to create imbalance
312
+ category :current_assets, type: :asset do
313
+ variable :cash, currency: 'USD', frequency: :annual, account: :cash_account do
314
+ value 100_000, start_date: "2024-01-01", end_date: "2024-12-31"
315
+ end
316
+ end
317
+
318
+ category :current_liabilities, type: :liability do
319
+ variable :debt, currency: 'USD', frequency: :annual, account: :debt_account do
320
+ value 50_000, start_date: "2024-01-01", end_date: "2024-12-31"
321
+ end
322
+ end
323
+
324
+ category :equity, type: :equity do
325
+ # Equity account auto-created with 50,000 balance
326
+ # But we'll manually add a variable that makes it unbalanced
327
+ variable :owner_equity, currency: 'USD', frequency: :annual do
328
+ value 30_000, start_date: "2024-01-01", end_date: "2024-12-31"
329
+ # This makes it unbalanced: Assets (100,000) != Liabilities (50,000) + Equity (50,000 + 30,000 = 80,000)
330
+ end
331
+ end
332
+ end
333
+
334
+ # Manually override equity account balance to create imbalance
335
+ # This simulates an unbalanced model
336
+ equity_account = unbalanced_model.accounts[:equity]
337
+ equity_account.instance_variable_set(:@opening_balance, 30_000) if equity_account
338
+
339
+ report = FinIt::Reports::BalanceSheet.new(
340
+ unbalanced_model,
341
+ start_date: Date.new(2024, 1, 1),
342
+ end_date: Date.new(2024, 12, 31),
343
+ output_currency: 'USD'
344
+ )
345
+
346
+ error = assert_raises(FinIt::Reports::BalanceSheetError) do
347
+ report.generate
348
+ end
349
+
350
+ assert_match(/does not balance/, error.message, "Error should mention balance issue")
351
+ assert_match(/Assets/, error.message, "Error should mention Assets")
352
+ assert_match(/Liabilities \+ Equity/, error.message, "Error should mention Liabilities + Equity")
353
+ end
354
+ end
355
+
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../test_helper"
4
+
5
+ class CashFlowStatementTest < 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 :sales_volume do
21
+ value 100
22
+ end
23
+
24
+ variable :unit_price do
25
+ value 50
26
+ end
27
+
28
+ variable :expense_rate do
29
+ value 0.30
30
+ end
31
+ end
32
+
33
+ # Income categories
34
+ category :income, type: :income,
35
+ default_account: :checking,
36
+ defaults: { frequency: :monthly, start_date: "2024-01-01", end_date: "2024-12-31" } do
37
+
38
+ calculated :product_sales,
39
+ formula: "sales_volume * unit_price"
40
+
41
+ variable :service_revenue, currency: 'USD' do
42
+ value 2_000, start_date: "2024-01-01", end_date: "2024-12-31"
43
+ end
44
+ end
45
+
46
+ # Expense categories
47
+ category :expenses, type: :expense,
48
+ default_account: :checking,
49
+ defaults: { frequency: :monthly, start_date: "2024-01-01", end_date: "2024-12-31" } do
50
+
51
+ calculated :cost_of_sales,
52
+ formula: "product_sales * expense_rate"
53
+
54
+ variable :operating_expenses, currency: 'USD' do
55
+ value 1_500, start_date: "2024-01-01", end_date: "2024-12-31"
56
+ end
57
+ end
58
+ end
59
+
60
+ @test_date = Date.new(2024, 6, 15)
61
+ end
62
+
63
+ def test_cash_flow_statement_generates
64
+ report = FinIt::Reports::CashFlowStatement.new(
65
+ @model,
66
+ start_date: Date.new(2024, 1, 1),
67
+ end_date: Date.new(2024, 6, 30),
68
+ output_currency: 'USD'
69
+ )
70
+
71
+ report_data = report.generate
72
+ assert report_data
73
+ assert report_data[:sections]
74
+ assert report_data[:totals]
75
+ end
76
+
77
+ def test_cash_flow_has_operating_section
78
+ report = FinIt::Reports::CashFlowStatement.new(
79
+ @model,
80
+ start_date: Date.new(2024, 1, 1),
81
+ end_date: Date.new(2024, 6, 30),
82
+ output_currency: 'USD'
83
+ )
84
+
85
+ report_data = report.generate
86
+ assert report_data[:sections][:operating], "Should have operating section"
87
+ assert report_data[:sections][:operating][:total].is_a?(Numeric), "Operating total should be numeric"
88
+ end
89
+
90
+ def test_cash_flow_has_investing_section
91
+ report = FinIt::Reports::CashFlowStatement.new(
92
+ @model,
93
+ start_date: Date.new(2024, 1, 1),
94
+ end_date: Date.new(2024, 6, 30),
95
+ output_currency: 'USD'
96
+ )
97
+
98
+ report_data = report.generate
99
+ assert report_data[:sections][:investing], "Should have investing section"
100
+ end
101
+
102
+ def test_cash_flow_has_financing_section
103
+ report = FinIt::Reports::CashFlowStatement.new(
104
+ @model,
105
+ start_date: Date.new(2024, 1, 1),
106
+ end_date: Date.new(2024, 6, 30),
107
+ output_currency: 'USD'
108
+ )
109
+
110
+ report_data = report.generate
111
+ assert report_data[:sections][:financing], "Should have financing section"
112
+ end
113
+
114
+ def test_cash_flow_calculates_net_change
115
+ report = FinIt::Reports::CashFlowStatement.new(
116
+ @model,
117
+ start_date: Date.new(2024, 1, 1),
118
+ end_date: Date.new(2024, 6, 30),
119
+ output_currency: 'USD'
120
+ )
121
+
122
+ report_data = report.generate
123
+ assert report_data[:sections][:net_change_in_cash], "Should have net change in cash"
124
+
125
+ net_change = report_data[:sections][:net_change_in_cash][:total]
126
+ operating = report_data[:sections][:operating][:total] || 0
127
+ investing = report_data[:sections][:investing][:total] || 0
128
+ financing = report_data[:sections][:financing][:total] || 0
129
+
130
+ assert_in_delta(operating + investing + financing, net_change, 0.01, "Net change should equal sum of sections")
131
+ end
132
+
133
+ def test_cash_flow_has_beginning_and_ending_cash
134
+ report = FinIt::Reports::CashFlowStatement.new(
135
+ @model,
136
+ start_date: Date.new(2024, 1, 1),
137
+ end_date: Date.new(2024, 6, 30),
138
+ output_currency: 'USD'
139
+ )
140
+
141
+ report_data = report.generate
142
+ assert report_data[:sections][:beginning_cash], "Should have beginning cash"
143
+ assert report_data[:sections][:ending_cash], "Should have ending cash"
144
+
145
+ beginning = report_data[:sections][:beginning_cash][:total]
146
+ net_change = report_data[:sections][:net_change_in_cash][:total]
147
+ ending = report_data[:sections][:ending_cash][:total]
148
+
149
+ assert_in_delta(beginning + net_change, ending, 0.01, "Ending cash = Beginning + Net Change")
150
+ end
151
+
152
+ def test_simple_display_mode
153
+ report = FinIt::Reports::CashFlowStatement.new(
154
+ @model,
155
+ start_date: Date.new(2024, 1, 1),
156
+ end_date: Date.new(2024, 6, 30),
157
+ output_currency: 'USD',
158
+ display_mode: :simple
159
+ )
160
+
161
+ report_data = report.generate
162
+ assert report_data[:sections][:cash_in], "Simple mode should have cash_in section"
163
+ assert report_data[:sections][:cash_out], "Simple mode should have cash_out section"
164
+ end
165
+
166
+ def test_generate_monthly
167
+ report = FinIt::Reports::CashFlowStatement.new(
168
+ @model,
169
+ start_date: Date.new(2024, 1, 1),
170
+ end_date: Date.new(2024, 6, 30),
171
+ output_currency: 'USD'
172
+ )
173
+
174
+ monthly = report.generate_monthly(Date.new(2024, 1, 1), Date.new(2024, 3, 31))
175
+
176
+ assert_equal 3, monthly.length, "Should have 3 monthly periods"
177
+ monthly.each do |period|
178
+ assert period[:period], "Each period should have period info"
179
+ assert period[:report], "Each period should have report data"
180
+ end
181
+ end
182
+
183
+ def test_period_summary
184
+ report = FinIt::Reports::CashFlowStatement.new(
185
+ @model,
186
+ start_date: Date.new(2024, 1, 1),
187
+ end_date: Date.new(2024, 6, 30),
188
+ output_currency: 'USD'
189
+ )
190
+
191
+ summary = report.period_summary(period_type: :monthly)
192
+
193
+ assert summary[:periods], "Should have periods"
194
+ assert summary[:summary], "Should have summary"
195
+ assert summary[:summary][:total_operating], "Summary should have total_operating"
196
+ assert summary[:summary][:total_net_change], "Summary should have total_net_change"
197
+ end
198
+
199
+ def test_totals_structure
200
+ report = FinIt::Reports::CashFlowStatement.new(
201
+ @model,
202
+ start_date: Date.new(2024, 1, 1),
203
+ end_date: Date.new(2024, 6, 30),
204
+ output_currency: 'USD'
205
+ )
206
+
207
+ report_data = report.generate
208
+ totals = report_data[:totals]
209
+
210
+ assert totals[:operating], "Totals should have operating"
211
+ assert totals[:investing], "Totals should have investing"
212
+ assert totals[:financing], "Totals should have financing"
213
+ assert totals[:net_change_in_cash], "Totals should have net_change_in_cash"
214
+ assert totals[:beginning_cash], "Totals should have beginning_cash"
215
+ assert totals[:ending_cash], "Totals should have ending_cash"
216
+ end
217
+
218
+ def test_indirect_method_starts_with_net_income
219
+ report = FinIt::Reports::CashFlowStatement.new(
220
+ @model,
221
+ start_date: Date.new(2024, 1, 1),
222
+ end_date: Date.new(2024, 6, 30),
223
+ output_currency: 'USD',
224
+ method: :indirect
225
+ )
226
+
227
+ report_data = report.generate
228
+ operating_items = report_data[:sections][:operating][:items]
229
+
230
+ # First item should be net income in indirect method
231
+ assert operating_items.any? { |item| item[:name] == :net_income },
232
+ "Indirect method should include net income in operating section"
233
+ end
234
+ end