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,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../test_helper"
4
+
5
+ class PeriodComparisonTest < 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
+ category :income, type: :income,
19
+ default_account: :checking,
20
+ defaults: { frequency: :monthly, start_date: "2024-01-01", end_date: "2024-12-31" } do
21
+ variable :sales do
22
+ value 5_000, start_date: "2024-01-01", end_date: "2024-03-31"
23
+ value 6_000, start_date: "2024-04-01", end_date: "2024-06-30"
24
+ value 7_000, start_date: "2024-07-01", end_date: "2024-09-30"
25
+ value 8_000, start_date: "2024-10-01", end_date: "2024-12-31"
26
+ end
27
+ end
28
+
29
+ category :expenses, type: :expense,
30
+ default_account: :checking,
31
+ defaults: { frequency: :monthly, start_date: "2024-01-01", end_date: "2024-12-31" } do
32
+ variable :rent do
33
+ value 1_000
34
+ end
35
+
36
+ variable :utilities do
37
+ value 200
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ def test_period_comparison_basic
44
+ comparison = FinIt::Reports::PeriodComparison.new(
45
+ @model,
46
+ periods: [
47
+ { name: "Q1 2024", start_date: "2024-01-01", end_date: "2024-03-31" },
48
+ { name: "Q2 2024", start_date: "2024-04-01", end_date: "2024-06-30" }
49
+ ],
50
+ metrics: [:net_income, :total_revenue],
51
+ output_currency: 'USD'
52
+ )
53
+
54
+ data = comparison.generate
55
+
56
+ assert_equal "PeriodComparison", data[:report_type]
57
+ assert data[:metadata]
58
+ assert data[:periods]
59
+ assert data[:variances]
60
+ assert_equal 2, data[:periods].length
61
+ end
62
+
63
+ def test_period_comparison_metadata
64
+ comparison = FinIt::Reports::PeriodComparison.new(
65
+ @model,
66
+ periods: [
67
+ { name: "Q1", start_date: "2024-01-01", end_date: "2024-03-31" },
68
+ { name: "Q2", start_date: "2024-04-01", end_date: "2024-06-30" }
69
+ ],
70
+ metrics: [:net_income],
71
+ output_currency: 'USD'
72
+ )
73
+
74
+ data = comparison.generate
75
+
76
+ assert_equal 'USD', data[:metadata][:currency]
77
+ assert_equal 2, data[:metadata][:periods].length
78
+ assert data[:metadata][:normalized_metrics]
79
+ end
80
+
81
+ def test_period_comparison_quarterly
82
+ comparison = FinIt::Reports::PeriodComparison.new(
83
+ @model,
84
+ periods: [
85
+ { name: "Q1 2024", start_date: "2024-01-01", end_date: "2024-03-31" },
86
+ { name: "Q2 2024", start_date: "2024-04-01", end_date: "2024-06-30" },
87
+ { name: "Q3 2024", start_date: "2024-07-01", end_date: "2024-09-30" },
88
+ { name: "Q4 2024", start_date: "2024-10-01", end_date: "2024-12-31" }
89
+ ],
90
+ metrics: [:net_income, :total_revenue, :total_expenses],
91
+ output_currency: 'USD'
92
+ )
93
+
94
+ data = comparison.generate
95
+
96
+ assert_equal 4, data[:periods].length
97
+
98
+ # Each period should have metrics
99
+ data[:periods].each do |period|
100
+ assert period[:name]
101
+ assert period[:start_date]
102
+ assert period[:end_date]
103
+ assert period[:metrics]
104
+ assert period[:metrics][:net_income]
105
+ assert period[:metrics][:total_revenue]
106
+ assert period[:metrics][:total_expenses]
107
+ end
108
+ end
109
+
110
+ def test_period_comparison_variances
111
+ comparison = FinIt::Reports::PeriodComparison.new(
112
+ @model,
113
+ periods: [
114
+ { name: "Q1", start_date: "2024-01-01", end_date: "2024-03-31" },
115
+ { name: "Q2", start_date: "2024-04-01", end_date: "2024-06-30" }
116
+ ],
117
+ metrics: [:total_revenue],
118
+ output_currency: 'USD'
119
+ )
120
+
121
+ data = comparison.generate
122
+
123
+ # First period has no variance (nil)
124
+ assert_nil data[:variances][0][:variances]
125
+
126
+ # Second period has variance
127
+ assert data[:variances][1][:variances]
128
+ assert data[:variances][1][:variances][:total_revenue]
129
+ assert data[:variances][1][:variances][:total_revenue][:absolute]
130
+ assert data[:variances][1][:variances][:total_revenue][:percentage]
131
+
132
+ # Q2 revenue should be higher than Q1 (6000 vs 5000)
133
+ assert data[:variances][1][:variances][:total_revenue][:absolute] > 0
134
+ end
135
+
136
+ def test_period_comparison_console_output
137
+ comparison = FinIt::Reports::PeriodComparison.new(
138
+ @model,
139
+ periods: [
140
+ { name: "Q1 2024", start_date: "2024-01-01", end_date: "2024-03-31" },
141
+ { name: "Q2 2024", start_date: "2024-04-01", end_date: "2024-06-30" }
142
+ ],
143
+ metrics: [:net_income, :total_revenue],
144
+ output_currency: 'USD'
145
+ )
146
+
147
+ output = capture_io do
148
+ comparison.output(FinIt::Outputs::ConsoleOutput)
149
+ end.first
150
+
151
+ assert_match(/PERIOD COMPARISON/, output)
152
+ assert_match(/Q1 2024/, output)
153
+ assert_match(/Q2 2024/, output)
154
+ assert_match(/Net Income/, output)
155
+ assert_match(/Total Revenue/, output)
156
+ end
157
+
158
+ def test_period_comparison_normalized_metrics
159
+ comparison = FinIt::Reports::PeriodComparison.new(
160
+ @model,
161
+ periods: [
162
+ { name: "Q1", start_date: "2024-01-01", end_date: "2024-03-31" }
163
+ ],
164
+ metrics: [:net_income, :total_revenue],
165
+ output_currency: 'USD'
166
+ )
167
+
168
+ # Test normalized_metrics accessor
169
+ normalized = comparison.normalized_metrics
170
+ assert_equal 2, normalized.length
171
+ assert_equal :net_income, normalized[0][:variable]
172
+ assert_equal "Net Income", normalized[0][:label]
173
+ end
174
+
175
+ def test_period_comparison_with_date_objects
176
+ comparison = FinIt::Reports::PeriodComparison.new(
177
+ @model,
178
+ periods: [
179
+ { name: "Q1", start_date: Date.new(2024, 1, 1), end_date: Date.new(2024, 3, 31) },
180
+ { name: "Q2", start_date: Date.new(2024, 4, 1), end_date: Date.new(2024, 6, 30) }
181
+ ],
182
+ metrics: [:net_income],
183
+ output_currency: 'USD'
184
+ )
185
+
186
+ data = comparison.generate
187
+
188
+ assert_equal Date.new(2024, 1, 1), data[:periods][0][:start_date]
189
+ assert_equal Date.new(2024, 3, 31), data[:periods][0][:end_date]
190
+ end
191
+
192
+ def test_currency_alias
193
+ comparison = FinIt::Reports::PeriodComparison.new(
194
+ @model,
195
+ periods: [{ name: "Q1", start_date: "2024-01-01", end_date: "2024-03-31" }],
196
+ metrics: [:net_income],
197
+ output_currency: 'EUR'
198
+ )
199
+
200
+ assert_equal 'EUR', comparison.currency
201
+ assert_equal 'EUR', comparison.output_currency
202
+ end
203
+
204
+ def test_period_comparison_multiple_metrics
205
+ comparison = FinIt::Reports::PeriodComparison.new(
206
+ @model,
207
+ periods: [
208
+ { name: "Q1", start_date: "2024-01-01", end_date: "2024-03-31" },
209
+ { name: "Q2", start_date: "2024-04-01", end_date: "2024-06-30" }
210
+ ],
211
+ metrics: [:net_income, :total_revenue, :total_expenses, :ending_cash, :beginning_cash],
212
+ output_currency: 'USD'
213
+ )
214
+
215
+ data = comparison.generate
216
+
217
+ # All metrics should be calculated for each period
218
+ data[:periods].each do |period|
219
+ assert period[:metrics].key?(:net_income)
220
+ assert period[:metrics].key?(:total_revenue)
221
+ assert period[:metrics].key?(:total_expenses)
222
+ assert period[:metrics].key?(:ending_cash)
223
+ assert period[:metrics].key?(:beginning_cash)
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../test_helper"
4
+
5
+ class RestaurantModelTest < 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 :operating_account do
13
+ type :asset
14
+ currency 'USD'
15
+ opening_balance 50_000
16
+ end
17
+
18
+ # Driver variables
19
+ category :drivers, type: :driver do
20
+ variable :number_of_tables do
21
+ value 20, start_date: "2024-01-01", end_date: "2024-06-30"
22
+ value 25, start_date: "2024-07-01", end_date: "2024-12-31"
23
+ end
24
+
25
+ variable :tables_turned_per_day do
26
+ value 2.5, start_date: "2024-01-01", end_date: "2024-03-31"
27
+ value 3.0, start_date: "2024-04-01", end_date: "2024-12-31"
28
+ end
29
+
30
+ variable :revenue_per_table do
31
+ value 85, start_date: "2024-01-01", end_date: "2024-05-31"
32
+ value 95, start_date: "2024-06-01", end_date: "2024-12-31"
33
+ end
34
+
35
+ variable :operating_days_per_month do
36
+ value 28
37
+ end
38
+
39
+ variable :food_cost_percentage do
40
+ value 0.30
41
+ end
42
+
43
+ variable :labor_cost_percentage do
44
+ value 0.35
45
+ end
46
+
47
+ variable :beverage_percentage_of_food do
48
+ value 0.25
49
+ end
50
+
51
+ variable :takeout_orders_per_day do
52
+ value 45, start_date: "2024-01-01", end_date: "2024-06-30"
53
+ value 60, start_date: "2024-07-01", end_date: "2024-12-31"
54
+ end
55
+
56
+ variable :takeout_revenue_per_order do
57
+ value 35
58
+ end
59
+
60
+ variable :catering_events_per_month do
61
+ value 8, start_date: "2024-01-01", end_date: "2024-05-31"
62
+ value 12, start_date: "2024-06-01", end_date: "2024-12-31"
63
+ end
64
+
65
+ variable :catering_revenue_per_event do
66
+ value 1_200
67
+ end
68
+ end
69
+
70
+ # Income categories - using type inheritance, default_account, and defaults
71
+ category :revenue, type: :income,
72
+ default_account: :operating_account,
73
+ defaults: { frequency: :monthly, start_date: "2024-01-01", end_date: "2024-12-31" } do
74
+
75
+ category :dine_in do # inherits type: :income, default_account, and defaults
76
+ calculated :dine_in_revenue,
77
+ formula: "number_of_tables * tables_turned_per_day * revenue_per_table * operating_days_per_month"
78
+ end
79
+
80
+ category :takeout do # inherits type: :income, default_account, and defaults
81
+ calculated :takeout_revenue,
82
+ formula: "takeout_orders_per_day * takeout_revenue_per_order * operating_days_per_month"
83
+ end
84
+
85
+ category :catering do # inherits type: :income, default_account, and defaults
86
+ calculated :catering_revenue,
87
+ formula: "catering_events_per_month * catering_revenue_per_event"
88
+ end
89
+
90
+ category :beverages do # inherits type: :income, default_account, and defaults
91
+ calculated :beverage_revenue,
92
+ formula: "(dine_in_revenue + takeout_revenue) * beverage_percentage_of_food"
93
+ end
94
+
95
+ calculated :total_revenue,
96
+ formula: "dine_in_revenue + takeout_revenue + catering_revenue + beverage_revenue"
97
+ end
98
+
99
+ # Expense categories - using type inheritance, default_account, and defaults
100
+ category :expenses, type: :expense,
101
+ default_account: :operating_account,
102
+ defaults: { frequency: :monthly, start_date: "2024-01-01", end_date: "2024-12-31" } do
103
+
104
+ category :cogs do # inherits type: :expense, default_account, and defaults
105
+ calculated :food_cogs,
106
+ formula: "(dine_in_revenue + takeout_revenue) * food_cost_percentage"
107
+
108
+ calculated :beverage_cogs,
109
+ formula: "beverage_revenue * 0.20"
110
+
111
+ calculated :total_cogs,
112
+ formula: "food_cogs + beverage_cogs"
113
+ end
114
+
115
+ category :labor do # inherits type: :expense, default_account, and defaults
116
+ calculated :labor_costs,
117
+ formula: "total_revenue * labor_cost_percentage"
118
+ end
119
+
120
+ category :fixed_expenses do # inherits type: :expense, default_account, and defaults
121
+ variable :rent, currency: 'USD' do
122
+ value 8_000, start_date: "2024-01-01", end_date: "2024-12-31"
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ @test_date = Date.new(2024, 2, 15)
129
+ end
130
+
131
+ def test_driver_variables_exist
132
+ assert @model.calculator.variable_names.include?(:number_of_tables)
133
+ assert @model.calculator.variable_names.include?(:food_cost_percentage)
134
+ end
135
+
136
+ def test_dine_in_revenue_calculation
137
+ # Q1: 20 tables × 2.5 turns × $85 per table × 28 days = $119,000
138
+ revenue = @model.calculator.calculate(:dine_in_revenue, date: @test_date, period_type: :monthly)
139
+ assert revenue
140
+ expected = 20 * 2.5 * 85 * 28
141
+ assert_in_delta expected, revenue.to_f, 1.0
142
+ end
143
+
144
+ def test_takeout_revenue_calculation
145
+ # Q1: 45 orders × $35 × 28 days = $44,100
146
+ revenue = @model.calculator.calculate(:takeout_revenue, date: @test_date, period_type: :monthly)
147
+ assert revenue
148
+ expected = 45 * 35 * 28
149
+ assert_in_delta expected, revenue.to_f, 1.0
150
+ end
151
+
152
+ def test_catering_revenue_calculation
153
+ # Q1: 8 events × $1,200 = $9,600
154
+ revenue = @model.calculator.calculate(:catering_revenue, date: @test_date, period_type: :monthly)
155
+ assert revenue
156
+ expected = 8 * 1_200
157
+ assert_in_delta expected, revenue.to_f, 1.0
158
+ end
159
+
160
+ def test_beverage_revenue_calculation
161
+ dine_in = @model.calculator.calculate(:dine_in_revenue, date: @test_date, period_type: :monthly)
162
+ takeout = @model.calculator.calculate(:takeout_revenue, date: @test_date, period_type: :monthly)
163
+ beverage = @model.calculator.calculate(:beverage_revenue, date: @test_date, period_type: :monthly)
164
+
165
+ assert beverage
166
+ expected = (dine_in.to_f + takeout.to_f) * 0.25
167
+ assert_in_delta expected, beverage.to_f, 1.0
168
+ end
169
+
170
+ def test_total_revenue_sums_all_streams
171
+ total = @model.calculator.calculate(:total_revenue, date: @test_date, period_type: :monthly)
172
+ dine_in = @model.calculator.calculate(:dine_in_revenue, date: @test_date, period_type: :monthly)
173
+ takeout = @model.calculator.calculate(:takeout_revenue, date: @test_date, period_type: :monthly)
174
+ catering = @model.calculator.calculate(:catering_revenue, date: @test_date, period_type: :monthly)
175
+ beverage = @model.calculator.calculate(:beverage_revenue, date: @test_date, period_type: :monthly)
176
+
177
+ assert total
178
+ expected = dine_in.to_f + takeout.to_f + catering.to_f + beverage.to_f
179
+ assert_in_delta expected, total.to_f, 1.0
180
+ end
181
+
182
+ def test_cogs_calculation
183
+ dine_in = @model.calculator.calculate(:dine_in_revenue, date: @test_date, period_type: :monthly)
184
+ takeout = @model.calculator.calculate(:takeout_revenue, date: @test_date, period_type: :monthly)
185
+ food_cogs = @model.calculator.calculate(:food_cogs, date: @test_date, period_type: :monthly)
186
+
187
+ assert food_cogs
188
+ expected = (dine_in.to_f + takeout.to_f) * 0.30
189
+ assert_in_delta expected, food_cogs.to_f, 1.0
190
+ end
191
+
192
+ def test_total_cogs_sums_food_and_beverage
193
+ food_cogs = @model.calculator.calculate(:food_cogs, date: @test_date, period_type: :monthly)
194
+ beverage_cogs = @model.calculator.calculate(:beverage_cogs, date: @test_date, period_type: :monthly)
195
+ total_cogs = @model.calculator.calculate(:total_cogs, date: @test_date, period_type: :monthly)
196
+
197
+ assert total_cogs
198
+ expected = food_cogs.to_f + beverage_cogs.to_f
199
+ assert_in_delta expected, total_cogs.to_f, 1.0
200
+ end
201
+
202
+ def test_labor_costs_calculation
203
+ total_revenue = @model.calculator.calculate(:total_revenue, date: @test_date, period_type: :monthly)
204
+ labor_costs = @model.calculator.calculate(:labor_costs, date: @test_date, period_type: :monthly)
205
+
206
+ assert labor_costs
207
+ expected = total_revenue.to_f * 0.35
208
+ assert_in_delta expected, labor_costs.to_f, 1.0
209
+ end
210
+
211
+ def test_income_statement_generation
212
+ report = FinIt::Reports::IncomeStatement.new(
213
+ @model,
214
+ start_date: Date.new(2024, 2, 1),
215
+ end_date: Date.new(2024, 2, 28),
216
+ output_currency: 'USD'
217
+ )
218
+
219
+ report_data = report.generate
220
+ assert report_data
221
+ assert report_data[:sections][:income]
222
+ assert report_data[:sections][:expenses]
223
+ assert report_data[:totals]
224
+ end
225
+ end