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.
- checksums.yaml +7 -0
- data/ARCHITECTURE.md +24 -0
- data/CHANGELOG.md +9 -0
- data/CONTRIBUTING.md +20 -0
- data/LICENSE +21 -0
- data/QUICKSTART.md +56 -0
- data/README.md +74 -0
- data/Rakefile +23 -0
- data/SECURITY.md +14 -0
- data/assets/fin_it_logo.png +0 -0
- data/lib/fin_it/account.rb +120 -0
- data/lib/fin_it/calculator/currency_conversion.rb +27 -0
- data/lib/fin_it/calculator/date_helpers.rb +53 -0
- data/lib/fin_it/calculator/variable_hashing.rb +120 -0
- data/lib/fin_it/calculator.rb +480 -0
- data/lib/fin_it/categories/category.rb +137 -0
- data/lib/fin_it/complex_model.rb +169 -0
- data/lib/fin_it/dsl/account_builder.rb +35 -0
- data/lib/fin_it/dsl/calculated_builder.rb +87 -0
- data/lib/fin_it/dsl/config_builder.rb +58 -0
- data/lib/fin_it/dsl/model_builder.rb +938 -0
- data/lib/fin_it/dsl/model_template_builder.rb +29 -0
- data/lib/fin_it/dsl/plan_builder.rb +52 -0
- data/lib/fin_it/dsl/project_inheritance_resolver.rb +46 -0
- data/lib/fin_it/dsl/variable_builder.rb +41 -0
- data/lib/fin_it/dsl.rb +13 -0
- data/lib/fin_it/engine.rb +15 -0
- data/lib/fin_it/financial_model/account_balances.rb +99 -0
- data/lib/fin_it/financial_model/account_hierarchy.rb +158 -0
- data/lib/fin_it/financial_model/category_values.rb +179 -0
- data/lib/fin_it/financial_model/currency_helpers.rb +14 -0
- data/lib/fin_it/financial_model/date_helpers.rb +58 -0
- data/lib/fin_it/financial_model/debugging.rb +353 -0
- data/lib/fin_it/financial_model/period_flows.rb +121 -0
- data/lib/fin_it/financial_model/validation.rb +85 -0
- data/lib/fin_it/financial_model/variable_matching.rb +49 -0
- data/lib/fin_it/financial_model.rb +395 -0
- data/lib/fin_it/model_template.rb +121 -0
- data/lib/fin_it/outputs/base_output.rb +51 -0
- data/lib/fin_it/outputs/console_output.rb +1528 -0
- data/lib/fin_it/outputs/monthly_console_output.rb +145 -0
- data/lib/fin_it/outputs/zaxcel_output.rb +1264 -0
- data/lib/fin_it/payment_schedule.rb +112 -0
- data/lib/fin_it/plan.rb +159 -0
- data/lib/fin_it/reports/balance_sheet.rb +638 -0
- data/lib/fin_it/reports/base_report.rb +239 -0
- data/lib/fin_it/reports/cash_flow_statement.rb +480 -0
- data/lib/fin_it/reports/custom_sheet.rb +436 -0
- data/lib/fin_it/reports/income_statement.rb +793 -0
- data/lib/fin_it/reports/period_comparison.rb +309 -0
- data/lib/fin_it/reports/scenario_comparison.rb +296 -0
- data/lib/fin_it/temporal_value.rb +349 -0
- data/lib/fin_it/transaction_generator/account_resolver.rb +118 -0
- data/lib/fin_it/transaction_generator/cache_management.rb +39 -0
- data/lib/fin_it/transaction_generator/date_generation.rb +57 -0
- data/lib/fin_it/transaction_generator.rb +357 -0
- data/lib/fin_it/version.rb +6 -0
- data/lib/fin_it.rb +27 -0
- data/test/fin_it/calculator_test.rb +109 -0
- data/test/fin_it/complex_model_test.rb +198 -0
- data/test/fin_it/debugging_test.rb +112 -0
- data/test/fin_it/driver_variables_test.rb +109 -0
- data/test/fin_it/dsl_test.rb +581 -0
- data/test/fin_it/financial_model_test.rb +196 -0
- data/test/fin_it/frequency_test.rb +51 -0
- data/test/fin_it/outputs/console_output_test.rb +249 -0
- data/test/fin_it/plan_test.rb +281 -0
- data/test/fin_it/reports/account_balance_test.rb +232 -0
- data/test/fin_it/reports/balance_sheet_test.rb +355 -0
- data/test/fin_it/reports/cash_flow_statement_test.rb +234 -0
- data/test/fin_it/reports/custom_sheet_test.rb +246 -0
- data/test/fin_it/reports/income_statement_test.rb +431 -0
- data/test/fin_it/reports/period_comparison_test.rb +226 -0
- data/test/fin_it/reports/restaurant_model_test.rb +225 -0
- data/test/fin_it/reports/scenario_comparison_test.rb +414 -0
- data/test/scripts/generate_demo_reports.rb +47 -0
- data/test/scripts/startup_saas_demo.rb +62 -0
- data/test/test_helper.rb +25 -0
- data/test/verify_accounting_equation.rb +91 -0
- metadata +264 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 59e004f074fbfaec9a9898cd16213992433339e63994579ba5ea8daa8cde0470
|
|
4
|
+
data.tar.gz: 37448fbcd255d8cc44bf36fedb6f4b522a6365c87cc5531c6c1e6528d42dda33
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 24d9587217a55d4ee8bf370b24b2235ece9520ed9332ad3b8dbb69b7946691e181095dee2c0dea73c31e73647ab358976ce2df8e6cb99a48d7a436ba7d960073
|
|
7
|
+
data.tar.gz: c16eac875c5058a854fad9be3d40e0359dfc112fe87b8a3077772abdb72cb40ee2c0cdd47adb93d9b52cdc11b39396894db8a93e7417ac66fa554e21aa252e6b
|
data/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## Core Layers
|
|
4
|
+
|
|
5
|
+
- `lib/fin_it.rb`: entrypoint and component loading
|
|
6
|
+
- `lib/fin_it/dsl/`: model definition DSL
|
|
7
|
+
- `lib/fin_it/calculator.rb`: formula evaluation and variable calculation
|
|
8
|
+
- `lib/fin_it/financial_model.rb`: runtime model orchestration
|
|
9
|
+
- `lib/fin_it/reports/`: report builders
|
|
10
|
+
- `lib/fin_it/outputs/`: output adapters (console and spreadsheet)
|
|
11
|
+
|
|
12
|
+
## Data Flow
|
|
13
|
+
|
|
14
|
+
1. DSL definitions build model components (accounts, categories, variables, formulas).
|
|
15
|
+
2. Calculator resolves variable values and formulas by date/period.
|
|
16
|
+
3. Transaction generator materializes accounting movements.
|
|
17
|
+
4. Reports aggregate values into financial statements.
|
|
18
|
+
5. Output adapters render result sets.
|
|
19
|
+
|
|
20
|
+
## Extension Points
|
|
21
|
+
|
|
22
|
+
- Add custom report builders under `lib/fin_it/reports`.
|
|
23
|
+
- Add custom output formatters under `lib/fin_it/outputs`.
|
|
24
|
+
- Use model templates and plans to compose reusable scenarios.
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-03-07
|
|
4
|
+
|
|
5
|
+
- Public rebrand to `fin_it`.
|
|
6
|
+
- API namespace updated to `FinIt`.
|
|
7
|
+
- Private/sensitive scripts removed and replaced with fictional demos.
|
|
8
|
+
- Added GitHub Actions for PR validation and guarded publish-on-master.
|
|
9
|
+
- Added public-facing documentation set and project branding assets.
|
data/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bundle install
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Run test suite
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bundle exec rake test
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Pull request checklist
|
|
16
|
+
|
|
17
|
+
- Keep changes focused and documented.
|
|
18
|
+
- Add or update tests for behavior changes.
|
|
19
|
+
- Ensure CI is green.
|
|
20
|
+
- Avoid committing private or customer data.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Growth Constant
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/QUICKSTART.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Quickstart
|
|
2
|
+
|
|
3
|
+
## 1. Install
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bundle add fin_it
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## 2. Define a model
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
require "fin_it"
|
|
13
|
+
|
|
14
|
+
model = FinIt.define(default_currency: "USD") do
|
|
15
|
+
config { start_date "2026-01-01" }
|
|
16
|
+
|
|
17
|
+
account :operating_cash do
|
|
18
|
+
type :asset
|
|
19
|
+
currency "USD"
|
|
20
|
+
opening_balance 40_000
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
category :income, type: :income do
|
|
24
|
+
variable :consulting_revenue, currency: "USD", frequency: :monthly, account: :operating_cash do
|
|
25
|
+
value 22_000, start_date: "2026-01-01", end_date: "2026-12-31"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
category :expenses, type: :expense do
|
|
30
|
+
variable :payroll, currency: "USD", frequency: :monthly, account: :operating_cash do
|
|
31
|
+
value 12_000, start_date: "2026-01-01", end_date: "2026-12-31"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 3. Generate reports
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
require "date"
|
|
41
|
+
|
|
42
|
+
income_statement = FinIt::Reports::IncomeStatement.new(
|
|
43
|
+
model,
|
|
44
|
+
start_date: Date.new(2026, 1, 1),
|
|
45
|
+
end_date: Date.new(2026, 12, 31),
|
|
46
|
+
output_currency: "USD"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
income_statement.output(FinIt::Outputs::ConsoleOutput)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 4. Run tests locally
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
bundle exec rake test
|
|
56
|
+
```
|
data/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# fin_it
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
`fin_it` is a Ruby gem for scenario-based financial modeling, formula-driven calculations, and multi-format reporting.
|
|
6
|
+
|
|
7
|
+
## Why fin_it
|
|
8
|
+
|
|
9
|
+
- Define models with a Ruby DSL
|
|
10
|
+
- Calculate values over time with period-aware logic
|
|
11
|
+
- Build Income Statement, Balance Sheet, and Cash Flow outputs
|
|
12
|
+
- Export to console and spreadsheet formats
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
gem "fin_it"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
require "fin_it"
|
|
24
|
+
require "date"
|
|
25
|
+
|
|
26
|
+
model = FinIt.define(default_currency: "USD") do
|
|
27
|
+
config do
|
|
28
|
+
start_date "2026-01-01"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
account :cash do
|
|
32
|
+
type :asset
|
|
33
|
+
currency "USD"
|
|
34
|
+
opening_balance 25_000
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
category :income, type: :income do
|
|
38
|
+
variable :subscription_revenue, currency: "USD", frequency: :monthly, account: :cash do
|
|
39
|
+
value 18_000, start_date: "2026-01-01", end_date: "2026-12-31"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
category :expenses, type: :expense do
|
|
44
|
+
variable :hosting_cost, currency: "USD", frequency: :monthly, account: :cash do
|
|
45
|
+
value 2_500, start_date: "2026-01-01", end_date: "2026-12-31"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
report = FinIt::Reports::IncomeStatement.new(
|
|
51
|
+
model,
|
|
52
|
+
start_date: Date.new(2026, 1, 1),
|
|
53
|
+
end_date: Date.new(2026, 12, 31),
|
|
54
|
+
output_currency: "USD"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
report.output(FinIt::Outputs::ConsoleOutput)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Documentation
|
|
61
|
+
|
|
62
|
+
- [QUICKSTART.md](QUICKSTART.md)
|
|
63
|
+
- [ARCHITECTURE.md](ARCHITECTURE.md)
|
|
64
|
+
- [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
65
|
+
- [SECURITY.md](SECURITY.md)
|
|
66
|
+
- [CHANGELOG.md](CHANGELOG.md)
|
|
67
|
+
|
|
68
|
+
## Migration Note
|
|
69
|
+
|
|
70
|
+
This project was rebranded from the previous internal package naming. Public API now uses `FinIt` and `require "fin_it"`.
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT. See [LICENSE](LICENSE).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rake/testtask"
|
|
5
|
+
|
|
6
|
+
desc "Run all Minitest tests (use TEST=path/to/test.rb to run specific file)"
|
|
7
|
+
Rake::TestTask.new(:test) do |t|
|
|
8
|
+
t.libs << "test"
|
|
9
|
+
t.libs << "lib"
|
|
10
|
+
t.test_files = if ENV["TEST"]
|
|
11
|
+
FileList[ENV["TEST"]]
|
|
12
|
+
else
|
|
13
|
+
FileList["test/**/*_test.rb"]
|
|
14
|
+
end
|
|
15
|
+
t.verbose = true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
task default: :test
|
|
19
|
+
|
|
20
|
+
desc "Run income statement generator"
|
|
21
|
+
task :income_statement do
|
|
22
|
+
ruby "test/scripts/generate_demo_reports.rb"
|
|
23
|
+
end
|
data/SECURITY.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Please report security issues privately to `oss@growth-constant.com`.
|
|
6
|
+
|
|
7
|
+
Include:
|
|
8
|
+
|
|
9
|
+
- Impact summary
|
|
10
|
+
- Reproduction steps
|
|
11
|
+
- Affected versions
|
|
12
|
+
- Suggested remediation (if available)
|
|
13
|
+
|
|
14
|
+
Do not open a public issue for unpatched vulnerabilities.
|
|
Binary file
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'money'
|
|
4
|
+
|
|
5
|
+
module FinIt
|
|
6
|
+
# Represents a financial account (asset, liability, equity, income, or expense)
|
|
7
|
+
# All account balances are stored as positive values.
|
|
8
|
+
# The accounting equation is: Assets = Liabilities + Equity
|
|
9
|
+
# Accounts can be hierarchical (parent-child relationships)
|
|
10
|
+
class Account
|
|
11
|
+
attr_reader :name, :type, :currency, :opening_balance, :opening_balance_credit_account, :parent
|
|
12
|
+
attr_accessor :children
|
|
13
|
+
|
|
14
|
+
def initialize(name, type:, currency: 'USD', opening_balance: 0, opening_balance_credit_account: :equity, parent: nil)
|
|
15
|
+
@name = name
|
|
16
|
+
@type = type # :asset, :liability, :equity, :income, or :expense
|
|
17
|
+
@currency = currency
|
|
18
|
+
@opening_balance = opening_balance
|
|
19
|
+
@opening_balance_credit_account = opening_balance_credit_account
|
|
20
|
+
@parent = parent
|
|
21
|
+
@children = []
|
|
22
|
+
|
|
23
|
+
validate!
|
|
24
|
+
|
|
25
|
+
# Add self to parent's children if parent exists
|
|
26
|
+
if @parent
|
|
27
|
+
@parent.children << self unless @parent.children.include?(self)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Get the full hierarchical path from root to this account
|
|
32
|
+
def path
|
|
33
|
+
if @parent
|
|
34
|
+
@parent.path + [@name]
|
|
35
|
+
else
|
|
36
|
+
[@name]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Get the full hierarchical name (e.g., "cost_of_goods_sold.shrimp_cost_regular")
|
|
41
|
+
def full_name
|
|
42
|
+
path.join('.')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get all descendants recursively
|
|
46
|
+
def descendants
|
|
47
|
+
@children + @children.flat_map(&:descendants)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Check if this account is a root account (no parent)
|
|
51
|
+
def root?
|
|
52
|
+
@parent.nil?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Check if this account is a leaf account (no children)
|
|
56
|
+
def leaf?
|
|
57
|
+
@children.empty?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_h
|
|
61
|
+
{
|
|
62
|
+
name: @name,
|
|
63
|
+
type: @type,
|
|
64
|
+
currency: @currency,
|
|
65
|
+
opening_balance: @opening_balance,
|
|
66
|
+
opening_balance_credit_account: @opening_balance_credit_account,
|
|
67
|
+
parent: @parent&.name,
|
|
68
|
+
children: @children.map(&:name)
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Deep clone this account (without parent/children relationships)
|
|
73
|
+
def deep_clone
|
|
74
|
+
Account.new(
|
|
75
|
+
@name,
|
|
76
|
+
type: @type,
|
|
77
|
+
currency: @currency,
|
|
78
|
+
opening_balance: @opening_balance,
|
|
79
|
+
opening_balance_credit_account: @opening_balance_credit_account,
|
|
80
|
+
parent: nil # Parent will be set up separately during model cloning
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def validate!
|
|
87
|
+
valid_types = [:asset, :liability, :equity, :income, :expense]
|
|
88
|
+
unless valid_types.include?(@type)
|
|
89
|
+
raise ArgumentError, "Account type must be one of #{valid_types.join(', ')}, got #{@type}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
unless @name.is_a?(Symbol) || @name.is_a?(String)
|
|
93
|
+
raise ArgumentError, "Account name must be a Symbol or String"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Validate parent-child type compatibility
|
|
97
|
+
if @parent
|
|
98
|
+
# Income/expense can be children of income/expense accounts
|
|
99
|
+
# Asset/liability/equity can be children of same type
|
|
100
|
+
if [:income, :expense].include?(@type)
|
|
101
|
+
unless [:income, :expense].include?(@parent.type)
|
|
102
|
+
raise ArgumentError, "Income/expense accounts can only be children of income/expense accounts"
|
|
103
|
+
end
|
|
104
|
+
else
|
|
105
|
+
unless @parent.type == @type
|
|
106
|
+
raise ArgumentError, "Account type #{@type} must match parent type #{@parent.type}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Custom error for account not found
|
|
114
|
+
class AccountNotFoundError < StandardError
|
|
115
|
+
def initialize(account_name)
|
|
116
|
+
super("Account '#{account_name}' not found. Please define it before referencing it in variables.")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FinIt
|
|
4
|
+
class Calculator
|
|
5
|
+
# Currency conversion and exchange rate management
|
|
6
|
+
module CurrencyConversion
|
|
7
|
+
def configure_exchange_rates
|
|
8
|
+
# In production, fetch from API
|
|
9
|
+
# For now, use example rates
|
|
10
|
+
Money.add_rate('USD', 'MXN', 17.5)
|
|
11
|
+
Money.add_rate('MXN', 'USD', 0.057)
|
|
12
|
+
Money.add_rate('USD', 'EUR', 0.92)
|
|
13
|
+
Money.add_rate('EUR', 'USD', 1.09)
|
|
14
|
+
Money.add_rate('EUR', 'MXN', 18.9)
|
|
15
|
+
Money.add_rate('MXN', 'EUR', 0.053)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def convert_currency(money, target_currency)
|
|
19
|
+
return money unless money.is_a?(Money)
|
|
20
|
+
return money if money.currency.iso_code == target_currency
|
|
21
|
+
|
|
22
|
+
money.exchange_to(target_currency)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FinIt
|
|
4
|
+
class Calculator
|
|
5
|
+
# Date parsing and period generation helpers
|
|
6
|
+
module DateHelpers
|
|
7
|
+
def parse_date(date)
|
|
8
|
+
return date if date.is_a?(Date)
|
|
9
|
+
return nil if date.nil?
|
|
10
|
+
|
|
11
|
+
case date
|
|
12
|
+
when String
|
|
13
|
+
if date =~ /^\d{4}-\d{2}$/ # YYYY-MM format
|
|
14
|
+
Date.parse("#{date}-01")
|
|
15
|
+
else
|
|
16
|
+
Date.parse(date)
|
|
17
|
+
end
|
|
18
|
+
when Time
|
|
19
|
+
date.to_date
|
|
20
|
+
else
|
|
21
|
+
date
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def generate_dates(start_date, end_date, frequency)
|
|
26
|
+
dates = []
|
|
27
|
+
current = start_date
|
|
28
|
+
|
|
29
|
+
while current <= end_date
|
|
30
|
+
dates << current
|
|
31
|
+
|
|
32
|
+
current = case frequency
|
|
33
|
+
when :daily
|
|
34
|
+
current + 1
|
|
35
|
+
when :weekly
|
|
36
|
+
current + 7
|
|
37
|
+
when :monthly
|
|
38
|
+
current >> 1
|
|
39
|
+
when :quarterly
|
|
40
|
+
current >> 3
|
|
41
|
+
when :annual
|
|
42
|
+
current >> 12
|
|
43
|
+
else
|
|
44
|
+
current >> 1 # Default monthly
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
dates
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
5
|
+
module FinIt
|
|
6
|
+
class Calculator
|
|
7
|
+
# Handles variable hashing for cache invalidation
|
|
8
|
+
module VariableHashing
|
|
9
|
+
# Calculate hash for a variable (for cache invalidation)
|
|
10
|
+
def variable_hash(variable_name)
|
|
11
|
+
var_key = variable_name.is_a?(Symbol) ? variable_name : variable_name.to_sym
|
|
12
|
+
|
|
13
|
+
# Check if it's a calculated variable
|
|
14
|
+
var_def = @variables[var_key]
|
|
15
|
+
if var_def.is_a?(Hash) && var_def[:type] == :calculated
|
|
16
|
+
return hash_calculated_variable(var_key, var_def)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Check if it's a temporal value (driver or financial variable)
|
|
20
|
+
temporal_value = @temporal_values[var_key]
|
|
21
|
+
if temporal_value
|
|
22
|
+
return hash_temporal_variable(temporal_value)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Fallback: hash the variable value itself
|
|
26
|
+
value = @variables[var_key]
|
|
27
|
+
hash_value(value)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Get all variable hashes
|
|
31
|
+
def all_variable_hashes
|
|
32
|
+
hashes = {}
|
|
33
|
+
|
|
34
|
+
# Hash all variables
|
|
35
|
+
(@variables.keys + @temporal_values.keys).uniq.each do |var_name|
|
|
36
|
+
hashes[var_name] = variable_hash(var_name)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
hashes
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Calculate hash for a complex model
|
|
43
|
+
def complex_model_hash(complex_model)
|
|
44
|
+
complex_model.hash(self)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def hash_calculated_variable(var_name, var_def)
|
|
50
|
+
# Hash = hash of dependencies + formula + dates + frequency + payment_schedule
|
|
51
|
+
deps = var_def[:dependencies] || extract_variable_dependencies(var_def[:formula])
|
|
52
|
+
dep_hashes = deps.map { |dep| variable_hash(dep) }.sort.join('|')
|
|
53
|
+
|
|
54
|
+
hash_data = [
|
|
55
|
+
dep_hashes,
|
|
56
|
+
var_def[:formula],
|
|
57
|
+
var_def[:start_date]&.to_s,
|
|
58
|
+
var_def[:end_date]&.to_s,
|
|
59
|
+
var_def[:frequency]&.to_s,
|
|
60
|
+
var_def[:round_to]&.to_s,
|
|
61
|
+
hash_payment_schedule(var_def[:payment_schedule])
|
|
62
|
+
].compact.join('|')
|
|
63
|
+
|
|
64
|
+
Digest::SHA256.hexdigest(hash_data)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def hash_temporal_variable(temporal_value)
|
|
68
|
+
# Hash = hash of all periods (values + dates + metadata)
|
|
69
|
+
periods = temporal_value.instance_variable_get(:@periods)
|
|
70
|
+
|
|
71
|
+
period_data = periods.map do |period|
|
|
72
|
+
value_str = if period[:value].is_a?(Money)
|
|
73
|
+
"#{period[:value].fractional}:#{period[:value].currency.iso_code}"
|
|
74
|
+
else
|
|
75
|
+
period[:value].to_s
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
[
|
|
79
|
+
value_str,
|
|
80
|
+
period[:start_date]&.to_s,
|
|
81
|
+
period[:end_date]&.to_s,
|
|
82
|
+
hash_metadata(period[:metadata])
|
|
83
|
+
].compact.join('|')
|
|
84
|
+
end.join('||')
|
|
85
|
+
|
|
86
|
+
Digest::SHA256.hexdigest(period_data)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def hash_payment_schedule(schedule)
|
|
90
|
+
return '' unless schedule
|
|
91
|
+
|
|
92
|
+
# Hash payment schedule if it's a PaymentSchedule object
|
|
93
|
+
if schedule.respond_to?(:frequency) && schedule.respond_to?(:payment_schedule)
|
|
94
|
+
[
|
|
95
|
+
schedule.frequency.to_s,
|
|
96
|
+
schedule.payment_schedule.to_s
|
|
97
|
+
].join('|')
|
|
98
|
+
else
|
|
99
|
+
schedule.to_s
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def hash_metadata(metadata)
|
|
104
|
+
return '' unless metadata
|
|
105
|
+
|
|
106
|
+
# Sort metadata keys for consistent hashing
|
|
107
|
+
metadata.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}:#{v}" }.join(',')
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def hash_value(value)
|
|
111
|
+
if value.is_a?(Money)
|
|
112
|
+
Digest::SHA256.hexdigest("#{value.fractional}:#{value.currency.iso_code}")
|
|
113
|
+
else
|
|
114
|
+
Digest::SHA256.hexdigest(value.to_s)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|