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,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ice_cube'
4
+
5
+ module FinIt
6
+ # Handles payment date recurrence logic using ice_cube
7
+ class PaymentSchedule
8
+ attr_reader :schedule, :start_date, :end_date
9
+
10
+ def initialize(frequency:, payment_schedule: {}, start_date: nil, end_date: nil)
11
+ @frequency = frequency
12
+ @payment_schedule = payment_schedule
13
+ @start_date = parse_date(start_date)
14
+ @end_date = parse_date(end_date)
15
+ @schedule = build_schedule
16
+ end
17
+
18
+ # Check if a given date is a payment date
19
+ def payment_date?(date)
20
+ date = parse_date(date)
21
+ return false if date.nil?
22
+ return false if @start_date && date < @start_date
23
+ return false if @end_date && date > @end_date
24
+
25
+ @schedule.occurs_on?(date)
26
+ end
27
+
28
+ # Get the next payment date after a given date
29
+ def next_payment_date(after_date = nil)
30
+ after_date ||= Date.today
31
+ after_date = parse_date(after_date)
32
+
33
+ occurrences = @schedule.occurrences(after_date + 1)
34
+ occurrences.first
35
+ end
36
+
37
+ # Get all payment dates in a range
38
+ def payment_dates_in_range(start_date, end_date)
39
+ start_date = parse_date(start_date)
40
+ end_date = parse_date(end_date)
41
+
42
+ @schedule.occurrences_between(start_date, end_date)
43
+ end
44
+
45
+ private
46
+
47
+ def build_schedule
48
+ schedule = IceCube::Schedule.new(@start_date || Date.today)
49
+
50
+ case @frequency
51
+ when :daily
52
+ schedule.add_recurrence_rule IceCube::Rule.daily
53
+ when :weekly
54
+ day_of_week = @payment_schedule[:day_of_week] || 1 # Monday by default
55
+ schedule.add_recurrence_rule IceCube::Rule.weekly.day(day_of_week)
56
+ when :monthly
57
+ day_of_month = @payment_schedule[:day_of_month] || 1
58
+ if day_of_month == -1 || day_of_month == :last
59
+ # Last day of month
60
+ schedule.add_recurrence_rule IceCube::Rule.monthly.day_of_month(-1)
61
+ else
62
+ schedule.add_recurrence_rule IceCube::Rule.monthly.day_of_month(day_of_month)
63
+ end
64
+ when :quarterly
65
+ months = @payment_schedule[:months] || [1, 4, 7, 10]
66
+ day = @payment_schedule[:day] || 1
67
+ months.each do |month|
68
+ schedule.add_recurrence_rule IceCube::Rule.yearly.month_of_year(month).day_of_month(day)
69
+ end
70
+ when :annual, :yearly
71
+ month = @payment_schedule[:month] || 1
72
+ day = @payment_schedule[:day] || 1
73
+ schedule.add_recurrence_rule IceCube::Rule.yearly.month_of_year(month).day_of_month(day)
74
+ else
75
+ # Custom schedule rule provided
76
+ if @payment_schedule[:schedule_rule]
77
+ schedule.add_recurrence_rule @payment_schedule[:schedule_rule]
78
+ else
79
+ # Default to monthly if frequency not recognized
80
+ schedule.add_recurrence_rule IceCube::Rule.monthly.day_of_month(1)
81
+ end
82
+ end
83
+
84
+ # Apply end date if specified - add to recurrence rules
85
+ if @end_date
86
+ # Update all recurrence rules to have an end date
87
+ schedule.recurrence_rules.each do |rule|
88
+ # Check if rule already has an until time set
89
+ has_until = rule.respond_to?(:until_time) && rule.until_time
90
+ rule.until(@end_date) unless has_until
91
+ end
92
+ end
93
+
94
+ schedule
95
+ end
96
+
97
+ def parse_date(date)
98
+ return nil if date.nil?
99
+ return date if date.is_a?(Date)
100
+
101
+ case date
102
+ when String
103
+ Date.parse(date)
104
+ when Time
105
+ date.to_date
106
+ else
107
+ date
108
+ end
109
+ end
110
+ end
111
+ end
112
+
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FinIt
4
+ # Represents a scenario plan that can modify a financial model's variables
5
+ # Plans are applied to create isolated copies of models for what-if analysis
6
+ class Plan
7
+ attr_reader :name, :description, :overrides, :start_date, :end_date
8
+
9
+ def initialize(name, description: nil, start_date: nil, end_date: nil)
10
+ @name = name
11
+ @description = description
12
+ @start_date = parse_date(start_date)
13
+ @end_date = parse_date(end_date)
14
+ @overrides = []
15
+ end
16
+
17
+ # Override a variable with a new value
18
+ # @param variable_name [Symbol] The variable to override
19
+ # @param value [Numeric, Money] The new value
20
+ # @param start_date [Date, String, nil] When override starts
21
+ # @param end_date [Date, String, nil] When override ends
22
+ def set(variable_name, value, start_date: nil, end_date: nil)
23
+ @overrides << {
24
+ type: :set,
25
+ variable: variable_name.to_sym,
26
+ value: value,
27
+ start_date: parse_date(start_date),
28
+ end_date: parse_date(end_date)
29
+ }
30
+ self
31
+ end
32
+
33
+ # Multiply variable by factor
34
+ # @param variable_name [Symbol] The variable to scale
35
+ # @param factor [Numeric] The multiplication factor (e.g., 1.10 for +10%)
36
+ # @param start_date [Date, String, nil] When scaling starts
37
+ # @param end_date [Date, String, nil] When scaling ends
38
+ def scale(variable_name, factor, start_date: nil, end_date: nil)
39
+ @overrides << {
40
+ type: :scale,
41
+ variable: variable_name.to_sym,
42
+ factor: factor,
43
+ start_date: parse_date(start_date),
44
+ end_date: parse_date(end_date)
45
+ }
46
+ self
47
+ end
48
+
49
+ # Add/subtract amount from variable
50
+ # @param variable_name [Symbol] The variable to adjust
51
+ # @param amount [Numeric] The amount to add (negative to subtract)
52
+ # @param start_date [Date, String, nil] When adjustment starts
53
+ # @param end_date [Date, String, nil] When adjustment ends
54
+ def adjust(variable_name, amount, start_date: nil, end_date: nil)
55
+ @overrides << {
56
+ type: :adjust,
57
+ variable: variable_name.to_sym,
58
+ amount: amount,
59
+ start_date: parse_date(start_date),
60
+ end_date: parse_date(end_date)
61
+ }
62
+ self
63
+ end
64
+
65
+ # Replace formula for calculated variable
66
+ # @param variable_name [Symbol] The calculated variable
67
+ # @param new_formula [String] The new formula
68
+ # @param start_date [Date, String, nil] When formula starts
69
+ # @param end_date [Date, String, nil] When formula ends
70
+ def formula(variable_name, new_formula, start_date: nil, end_date: nil)
71
+ @overrides << {
72
+ type: :formula,
73
+ variable: variable_name.to_sym,
74
+ formula: new_formula,
75
+ start_date: parse_date(start_date),
76
+ end_date: parse_date(end_date)
77
+ }
78
+ self
79
+ end
80
+
81
+ # Add a new variable to the model
82
+ # @param name [Symbol] Variable name
83
+ # @param category [Symbol] Category to add variable to
84
+ # @param currency [String, nil] Currency code
85
+ # @param block [Block] Variable definition block
86
+ def add_variable(name, category:, currency: nil, &block)
87
+ @overrides << {
88
+ type: :add_variable,
89
+ name: name.to_sym,
90
+ category: category.to_sym,
91
+ currency: currency,
92
+ block: block
93
+ }
94
+ self
95
+ end
96
+
97
+ # Add a new calculated variable to the model
98
+ # @param name [Symbol] Variable name
99
+ # @param category [Symbol] Category to add to
100
+ # @param formula [String] The formula
101
+ # @param options [Hash] Additional options (start_date, end_date, etc.)
102
+ def add_calculated(name, category:, formula:, **options)
103
+ @overrides << {
104
+ type: :add_calculated,
105
+ name: name.to_sym,
106
+ category: category.to_sym,
107
+ formula: formula,
108
+ options: options
109
+ }
110
+ self
111
+ end
112
+
113
+ # Set account opening balance
114
+ # @param account_name [Symbol] The account name
115
+ # @param amount [Numeric] The new opening balance
116
+ def set_opening_balance(account_name, amount)
117
+ @overrides << {
118
+ type: :opening_balance,
119
+ account: account_name.to_sym,
120
+ amount: amount
121
+ }
122
+ self
123
+ end
124
+
125
+ # Create a copy of this plan
126
+ def dup
127
+ new_plan = Plan.new(@name, description: @description,
128
+ start_date: @start_date, end_date: @end_date)
129
+ @overrides.each do |override|
130
+ new_plan.instance_variable_get(:@overrides) << override.dup
131
+ end
132
+ new_plan
133
+ end
134
+
135
+ private
136
+
137
+ def parse_date(date)
138
+ return nil if date.nil?
139
+ return date if date.is_a?(Date)
140
+
141
+ case date
142
+ when String
143
+ if date =~ /^\d{4}$/
144
+ Date.new(date.to_i, 1, 1)
145
+ elsif date =~ /^\d{4}-\d{2}$/
146
+ Date.parse("#{date}-01")
147
+ else
148
+ Date.parse(date)
149
+ end
150
+ when Integer
151
+ Date.new(date, 1, 1)
152
+ when Time
153
+ date.to_date
154
+ else
155
+ raise ArgumentError, "Cannot parse date: #{date}"
156
+ end
157
+ end
158
+ end
159
+ end