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,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require_relative 'calculator'
5
+
6
+ module FinIt
7
+ # Complex models with coded formulas (e.g., loans, amortization schedules)
8
+ # These models can define input variables and calculate their own transactions
9
+ class ComplexModel
10
+ attr_reader :name, :start_date, :end_date, :frequency, :input_variables, :formula_block, :debit_account, :credit_account, :project
11
+
12
+ def initialize(name, start_date:, end_date: nil, frequency: :monthly, debit_account:, credit_account:, project: nil, &formula_block)
13
+ @name = name
14
+ @start_date = parse_date(start_date)
15
+ @end_date = parse_date(end_date)
16
+ @frequency = frequency
17
+ @debit_account = debit_account
18
+ @credit_account = credit_account
19
+ @project = project
20
+ @formula_block = formula_block
21
+ @input_variables = {}
22
+ end
23
+
24
+ # Define an input variable for this model
25
+ def input_variable(name, value)
26
+ @input_variables[name] = value
27
+ end
28
+
29
+ # Calculate transactions for this model up to end_date
30
+ def calculate_transactions(end_date, calculator, model_context = {})
31
+ end_date = parse_date(end_date)
32
+ transactions = []
33
+
34
+ # Generate dates based on frequency
35
+ dates = generate_dates(@start_date, [@end_date, end_date].compact.min, @frequency)
36
+
37
+ dates.each do |date|
38
+ # Build context with input variables and model context
39
+ context = @input_variables.merge(model_context)
40
+
41
+ # Execute formula block - can return single amount or array of transactions
42
+ result = @formula_block.call(date, context)
43
+
44
+ next unless result
45
+
46
+ # Handle array of transactions (multiple transactions per period)
47
+ if result.is_a?(Array)
48
+ result.each do |txn_hash|
49
+ next unless txn_hash.is_a?(Hash) && txn_hash[:amount]
50
+
51
+ # Convert to Money object for precision and currency safety
52
+ amount = if txn_hash[:amount].is_a?(Money)
53
+ txn_hash[:amount]
54
+ else
55
+ currency = txn_hash[:currency] || 'USD'
56
+ Money.new((txn_hash[:amount].to_f * 100).to_i, currency)
57
+ end
58
+ next if amount.zero?
59
+
60
+ transactions << {
61
+ date: date,
62
+ variable: @name,
63
+ amount: amount, # Money object
64
+ debit_account: txn_hash[:debit_account] || @debit_account,
65
+ credit_account: txn_hash[:credit_account] || @credit_account,
66
+ description: txn_hash[:description] || "Transaction for #{@name}",
67
+ currency: amount.currency.iso_code,
68
+ model_type: :complex
69
+ }
70
+ end
71
+ else
72
+ # Single transaction (backward compatibility)
73
+ # Convert to Money object for precision and currency safety
74
+ amount = if result.is_a?(Money)
75
+ result
76
+ else
77
+ Money.new((result.to_f * 100).to_i, 'USD')
78
+ end
79
+ next if amount.zero?
80
+
81
+ transactions << {
82
+ date: date,
83
+ variable: @name,
84
+ amount: amount, # Money object
85
+ debit_account: @debit_account,
86
+ credit_account: @credit_account,
87
+ description: "Transaction for #{@name}",
88
+ currency: amount.currency.iso_code,
89
+ model_type: :complex
90
+ }
91
+ end
92
+ end
93
+
94
+ transactions
95
+ end
96
+
97
+ # Calculate hash for this model (for cache invalidation)
98
+ def hash(calculator)
99
+ # Hash = hash of input variables + model definition
100
+ input_hash = @input_variables.sort_by { |k, _| k.to_s }
101
+ .map { |k, v| "#{k}:#{hash_value(v)}" }
102
+ .join('|')
103
+
104
+ model_hash_data = [
105
+ @name.to_s,
106
+ @start_date.to_s,
107
+ @end_date&.to_s,
108
+ @frequency.to_s,
109
+ @debit_account.to_s,
110
+ @credit_account.to_s,
111
+ @project&.to_s
112
+ ].compact.join('|')
113
+
114
+ require 'digest'
115
+ Digest::SHA256.hexdigest("#{input_hash}|#{model_hash_data}")
116
+ end
117
+
118
+ private
119
+
120
+ def generate_dates(start_date, end_date, frequency)
121
+ dates = []
122
+ current = start_date
123
+
124
+ while current <= end_date
125
+ dates << current
126
+
127
+ current = case frequency
128
+ when :daily
129
+ current + 1
130
+ when :weekly
131
+ current + 7
132
+ when :monthly
133
+ current >> 1
134
+ when :quarterly
135
+ current >> 3
136
+ when :annual
137
+ current >> 12
138
+ else
139
+ current >> 1 # Default monthly
140
+ end
141
+ end
142
+
143
+ dates
144
+ end
145
+
146
+ def hash_value(value)
147
+ if value.is_a?(Money)
148
+ "#{value.fractional}:#{value.currency.iso_code}"
149
+ else
150
+ value.to_s
151
+ end
152
+ end
153
+
154
+ def parse_date(date)
155
+ return date if date.is_a?(Date)
156
+ return nil if date.nil?
157
+
158
+ case date
159
+ when String
160
+ Date.parse(date)
161
+ when Time
162
+ date.to_date
163
+ else
164
+ date
165
+ end
166
+ end
167
+ end
168
+ end
169
+
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FinIt
4
+ module DSL
5
+ # Builder for account definitions
6
+ class AccountBuilder
7
+ attr_reader :account_type, :account_currency, :account_opening_balance, :account_opening_balance_credit_account
8
+
9
+ def initialize(name)
10
+ @name = name
11
+ @account_type = nil
12
+ @account_currency = nil
13
+ @account_opening_balance = nil
14
+ @account_opening_balance_credit_account = :equity
15
+ end
16
+
17
+ def type(account_type)
18
+ @account_type = account_type
19
+ end
20
+
21
+ def currency(currency_code)
22
+ @account_currency = currency_code
23
+ end
24
+
25
+ def opening_balance(amount)
26
+ @account_opening_balance = amount
27
+ end
28
+
29
+ def opening_balance_credit_account(account_name)
30
+ @account_opening_balance_credit_account = account_name
31
+ end
32
+ end
33
+ end
34
+ end
35
+
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FinIt
4
+ module DSL
5
+ # Builder for calculated variables
6
+ class CalculatedBuilder
7
+ def initialize(name, calculator)
8
+ @name = name
9
+ @calculator = calculator
10
+ @formula = nil
11
+ @round_to = nil
12
+ @start_date = nil
13
+ @end_date = nil
14
+ @frequency = nil
15
+ @payment_schedule = nil
16
+ @debit_account = nil
17
+ @credit_account = nil
18
+ @account = nil
19
+ end
20
+
21
+ def formula(expression = nil)
22
+ if expression
23
+ @formula = expression
24
+ else
25
+ @formula
26
+ end
27
+ end
28
+
29
+ def round_to(digits = nil)
30
+ if digits
31
+ @round_to = digits
32
+ else
33
+ @round_to
34
+ end
35
+ end
36
+
37
+ def start_date(date = nil)
38
+ if date
39
+ @start_date = date
40
+ else
41
+ @start_date
42
+ end
43
+ end
44
+
45
+ def end_date(date = nil)
46
+ if date
47
+ @end_date = date
48
+ else
49
+ @end_date
50
+ end
51
+ end
52
+
53
+ def frequency(freq = nil)
54
+ if freq
55
+ @frequency = freq
56
+ else
57
+ @frequency
58
+ end
59
+ end
60
+
61
+ def payment_schedule(schedule = nil)
62
+ if schedule
63
+ @payment_schedule = schedule
64
+ else
65
+ @payment_schedule
66
+ end
67
+ end
68
+
69
+ def description(text = nil)
70
+ # Store description
71
+ end
72
+
73
+ def debit_account(account)
74
+ @debit_account = account
75
+ end
76
+
77
+ def credit_account(account)
78
+ @credit_account = account
79
+ end
80
+
81
+ def account(account)
82
+ @account = account
83
+ end
84
+ end
85
+ end
86
+ end
87
+
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FinIt
4
+ module DSL
5
+ # Builder for configuration
6
+ class ConfigBuilder
7
+ def initialize(config)
8
+ @config = config
9
+ end
10
+
11
+ def with_bonus(value)
12
+ @config[:with_bonus] = value
13
+ end
14
+
15
+ def base_year(year)
16
+ @config[:base_year] = year
17
+ end
18
+
19
+ def default_currency(currency)
20
+ @config[:default_currency] = currency
21
+ end
22
+
23
+ def start_date(date_or_year)
24
+ # Accept year number (Integer) or Date
25
+ if date_or_year.is_a?(Integer)
26
+ @config[:start_date] = Date.new(date_or_year, 1, 1)
27
+ elsif date_or_year.is_a?(Date)
28
+ @config[:start_date] = date_or_year
29
+ elsif date_or_year.is_a?(String)
30
+ @config[:start_date] = Date.parse(date_or_year)
31
+ else
32
+ raise ArgumentError, "start_date must be a year (Integer), Date, or date string, got #{date_or_year.class}"
33
+ end
34
+ end
35
+
36
+ def exchange_rates(rates)
37
+ rates.each do |from, to_rates|
38
+ to_rates.each do |to, rate|
39
+ Money.add_rate(from.to_s, to.to_s, rate)
40
+ end
41
+ end
42
+ end
43
+
44
+ def method_missing(name, *args)
45
+ if args.length == 1
46
+ @config[name] = args.first
47
+ else
48
+ super
49
+ end
50
+ end
51
+
52
+ def respond_to_missing?(name, include_private = false)
53
+ true
54
+ end
55
+ end
56
+ end
57
+ end
58
+