mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 13:19:39 +02:00
Realistic demo data for performance testing (#2361)
* Realistic demo data for performance testing * Add note about performance testing * Fix bugbot issues * More realistic account values
This commit is contained in:
parent
0d62e60da1
commit
5a4c955522
16 changed files with 2166 additions and 474 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -94,6 +94,9 @@ node_modules/
|
||||||
*.roo*
|
*.roo*
|
||||||
# OS specific
|
# OS specific
|
||||||
# Task files
|
# Task files
|
||||||
|
.taskmaster/docs
|
||||||
|
.taskmaster/config.json
|
||||||
|
.taskmaster/templates
|
||||||
tasks.json
|
tasks.json
|
||||||
tasks/
|
tasks/
|
||||||
*.mcp.json
|
*.mcp.json
|
||||||
|
|
238
app/models/demo/account_generator.rb
Normal file
238
app/models/demo/account_generator.rb
Normal file
|
@ -0,0 +1,238 @@
|
||||||
|
class Demo::AccountGenerator
|
||||||
|
include Demo::DataHelper
|
||||||
|
|
||||||
|
def create_credit_card_accounts!(family, count: 1)
|
||||||
|
accounts = []
|
||||||
|
count.times do |i|
|
||||||
|
account = family.accounts.create!(
|
||||||
|
accountable: CreditCard.new,
|
||||||
|
name: account_name("Chase Credit Card", i, count),
|
||||||
|
balance: 0,
|
||||||
|
currency: "USD"
|
||||||
|
)
|
||||||
|
accounts << account
|
||||||
|
end
|
||||||
|
accounts
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_checking_accounts!(family, count: 1)
|
||||||
|
accounts = []
|
||||||
|
count.times do |i|
|
||||||
|
account = family.accounts.create!(
|
||||||
|
accountable: Depository.new,
|
||||||
|
name: account_name("Chase Checking", i, count),
|
||||||
|
balance: 0,
|
||||||
|
currency: "USD"
|
||||||
|
)
|
||||||
|
accounts << account
|
||||||
|
end
|
||||||
|
accounts
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_savings_accounts!(family, count: 1)
|
||||||
|
accounts = []
|
||||||
|
count.times do |i|
|
||||||
|
account = family.accounts.create!(
|
||||||
|
accountable: Depository.new,
|
||||||
|
name: account_name("Demo Savings", i, count),
|
||||||
|
balance: 0,
|
||||||
|
currency: "USD",
|
||||||
|
subtype: "savings"
|
||||||
|
)
|
||||||
|
accounts << account
|
||||||
|
end
|
||||||
|
accounts
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_properties_and_mortgages!(family, count: 1)
|
||||||
|
accounts = []
|
||||||
|
count.times do |i|
|
||||||
|
property = family.accounts.create!(
|
||||||
|
accountable: Property.new,
|
||||||
|
name: account_name("123 Maybe Way", i, count),
|
||||||
|
balance: 0,
|
||||||
|
currency: "USD"
|
||||||
|
)
|
||||||
|
accounts << property
|
||||||
|
|
||||||
|
mortgage = family.accounts.create!(
|
||||||
|
accountable: Loan.new,
|
||||||
|
name: account_name("Mortgage", i, count),
|
||||||
|
balance: 0,
|
||||||
|
currency: "USD"
|
||||||
|
)
|
||||||
|
accounts << mortgage
|
||||||
|
end
|
||||||
|
accounts
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_vehicles_and_loans!(family, vehicle_count: 1, loan_count: 1)
|
||||||
|
accounts = []
|
||||||
|
|
||||||
|
vehicle_count.times do |i|
|
||||||
|
vehicle = family.accounts.create!(
|
||||||
|
accountable: Vehicle.new,
|
||||||
|
name: account_name("Honda Accord", i, vehicle_count),
|
||||||
|
balance: 0,
|
||||||
|
currency: "USD"
|
||||||
|
)
|
||||||
|
accounts << vehicle
|
||||||
|
end
|
||||||
|
|
||||||
|
loan_count.times do |i|
|
||||||
|
loan = family.accounts.create!(
|
||||||
|
accountable: Loan.new,
|
||||||
|
name: account_name("Car Loan", i, loan_count),
|
||||||
|
balance: 0,
|
||||||
|
currency: "USD"
|
||||||
|
)
|
||||||
|
accounts << loan
|
||||||
|
end
|
||||||
|
|
||||||
|
accounts
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_other_accounts!(family, asset_count: 1, liability_count: 1)
|
||||||
|
accounts = []
|
||||||
|
|
||||||
|
asset_count.times do |i|
|
||||||
|
asset = family.accounts.create!(
|
||||||
|
accountable: OtherAsset.new,
|
||||||
|
name: account_name("Other Asset", i, asset_count),
|
||||||
|
balance: 0,
|
||||||
|
currency: "USD"
|
||||||
|
)
|
||||||
|
accounts << asset
|
||||||
|
end
|
||||||
|
|
||||||
|
liability_count.times do |i|
|
||||||
|
liability = family.accounts.create!(
|
||||||
|
accountable: OtherLiability.new,
|
||||||
|
name: account_name("Other Liability", i, liability_count),
|
||||||
|
balance: 0,
|
||||||
|
currency: "USD"
|
||||||
|
)
|
||||||
|
accounts << liability
|
||||||
|
end
|
||||||
|
|
||||||
|
accounts
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_investment_accounts!(family, count: 3)
|
||||||
|
accounts = []
|
||||||
|
|
||||||
|
if count <= 3
|
||||||
|
account_configs = [
|
||||||
|
{ name: "401(k)", balance: 0 },
|
||||||
|
{ name: "Roth IRA", balance: 0 },
|
||||||
|
{ name: "Taxable Brokerage", balance: 0 }
|
||||||
|
]
|
||||||
|
|
||||||
|
count.times do |i|
|
||||||
|
config = account_configs[i] || {
|
||||||
|
name: "Investment Account #{i + 1}",
|
||||||
|
balance: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
account = family.accounts.create!(
|
||||||
|
accountable: Investment.new,
|
||||||
|
name: config[:name],
|
||||||
|
balance: config[:balance],
|
||||||
|
currency: "USD"
|
||||||
|
)
|
||||||
|
accounts << account
|
||||||
|
end
|
||||||
|
else
|
||||||
|
count.times do |i|
|
||||||
|
account = family.accounts.create!(
|
||||||
|
accountable: Investment.new,
|
||||||
|
name: "Investment Account #{i + 1}",
|
||||||
|
balance: 0,
|
||||||
|
currency: "USD"
|
||||||
|
)
|
||||||
|
accounts << account
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
accounts
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def realistic_balance(type, count = 1)
|
||||||
|
return send("realistic_#{type}_balance") if count == 1
|
||||||
|
send("random_#{type}_balance")
|
||||||
|
end
|
||||||
|
def realistic_credit_card_balance
|
||||||
|
2300
|
||||||
|
end
|
||||||
|
|
||||||
|
def realistic_checking_balance
|
||||||
|
15000
|
||||||
|
end
|
||||||
|
|
||||||
|
def realistic_savings_balance
|
||||||
|
40000
|
||||||
|
end
|
||||||
|
|
||||||
|
def realistic_property_balance
|
||||||
|
560000
|
||||||
|
end
|
||||||
|
|
||||||
|
def realistic_mortgage_balance
|
||||||
|
495000
|
||||||
|
end
|
||||||
|
|
||||||
|
def realistic_vehicle_balance
|
||||||
|
18000
|
||||||
|
end
|
||||||
|
|
||||||
|
def realistic_car_loan_balance
|
||||||
|
8000
|
||||||
|
end
|
||||||
|
|
||||||
|
def realistic_other_asset_balance
|
||||||
|
10000
|
||||||
|
end
|
||||||
|
|
||||||
|
def realistic_other_liability_balance
|
||||||
|
5000
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def random_credit_card_balance
|
||||||
|
random_positive_amount(1000, 5000)
|
||||||
|
end
|
||||||
|
|
||||||
|
def random_checking_balance
|
||||||
|
random_positive_amount(10000, 50000)
|
||||||
|
end
|
||||||
|
|
||||||
|
def random_savings_balance
|
||||||
|
random_positive_amount(50000, 200000)
|
||||||
|
end
|
||||||
|
|
||||||
|
def random_property_balance
|
||||||
|
random_positive_amount(400000, 800000)
|
||||||
|
end
|
||||||
|
|
||||||
|
def random_mortgage_balance
|
||||||
|
random_positive_amount(200000, 600000)
|
||||||
|
end
|
||||||
|
|
||||||
|
def random_vehicle_balance
|
||||||
|
random_positive_amount(15000, 50000)
|
||||||
|
end
|
||||||
|
|
||||||
|
def random_car_loan_balance
|
||||||
|
random_positive_amount(5000, 25000)
|
||||||
|
end
|
||||||
|
|
||||||
|
def random_other_asset_balance
|
||||||
|
random_positive_amount(5000, 50000)
|
||||||
|
end
|
||||||
|
|
||||||
|
def random_other_liability_balance
|
||||||
|
random_positive_amount(2000, 20000)
|
||||||
|
end
|
||||||
|
end
|
30
app/models/demo/base_scenario.rb
Normal file
30
app/models/demo/base_scenario.rb
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# Base class for demo scenario handlers - subclasses must implement generate_family_data!
|
||||||
|
class Demo::BaseScenario
|
||||||
|
def initialize(generators)
|
||||||
|
@generators = generators
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate!(families, **options)
|
||||||
|
setup(**options) if respond_to?(:setup, true)
|
||||||
|
|
||||||
|
families.each do |family|
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
generate_family_data!(family, **options)
|
||||||
|
end
|
||||||
|
puts "#{scenario_name} data created for #{family.name}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def setup(**options)
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_family_data!(family, **options)
|
||||||
|
raise NotImplementedError, "Subclasses must implement generate_family_data!(family, **options)"
|
||||||
|
end
|
||||||
|
|
||||||
|
def scenario_name
|
||||||
|
self.class.name.split("::").last.downcase.gsub(/([a-z])([A-Z])/, '\1 \2')
|
||||||
|
end
|
||||||
|
end
|
31
app/models/demo/data_cleaner.rb
Normal file
31
app/models/demo/data_cleaner.rb
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# SAFETY: Only operates in development/test environments to prevent data loss
|
||||||
|
class Demo::DataCleaner
|
||||||
|
SAFE_ENVIRONMENTS = %w[development test]
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
ensure_safe_environment!
|
||||||
|
end
|
||||||
|
|
||||||
|
# Main entry point for destroying all demo data
|
||||||
|
def destroy_everything!
|
||||||
|
puts "Clearing existing data..."
|
||||||
|
|
||||||
|
# Rails associations handle cascading deletes
|
||||||
|
Family.destroy_all
|
||||||
|
Setting.destroy_all
|
||||||
|
InviteCode.destroy_all
|
||||||
|
ExchangeRate.destroy_all
|
||||||
|
Security.destroy_all
|
||||||
|
Security::Price.destroy_all
|
||||||
|
|
||||||
|
puts "Data cleared"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def ensure_safe_environment!
|
||||||
|
unless SAFE_ENVIRONMENTS.include?(Rails.env)
|
||||||
|
raise SecurityError, "Demo::DataCleaner can only be used in #{SAFE_ENVIRONMENTS.join(', ')} environments. Current: #{Rails.env}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
85
app/models/demo/data_helper.rb
Normal file
85
app/models/demo/data_helper.rb
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
module Demo::DataHelper
|
||||||
|
COLORS = %w[#e99537 #4da568 #6471eb #db5a54 #df4e92 #c44fe9 #eb5429 #61c9ea #805dee #6ad28a].freeze
|
||||||
|
|
||||||
|
PERFORMANCE_TRANSACTION_COUNTS = {
|
||||||
|
depository_sample: 75,
|
||||||
|
credit_card_sample: 75,
|
||||||
|
investment_trades: 35,
|
||||||
|
investment_transactions: 35,
|
||||||
|
other_account_sample: 20
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def random_date_within_days(max_days_ago)
|
||||||
|
Faker::Number.between(from: 0, to: max_days_ago).days.ago.to_date
|
||||||
|
end
|
||||||
|
|
||||||
|
def random_amount(min, max)
|
||||||
|
Faker::Number.between(from: min, to: max)
|
||||||
|
end
|
||||||
|
|
||||||
|
def random_positive_amount(min, max)
|
||||||
|
Faker::Number.positive(from: min, to: max)
|
||||||
|
end
|
||||||
|
|
||||||
|
def group_accounts_by_type(family)
|
||||||
|
accounts = family.accounts.includes(:accountable)
|
||||||
|
|
||||||
|
{
|
||||||
|
checking: filter_checking_accounts(accounts),
|
||||||
|
savings: filter_savings_accounts(accounts),
|
||||||
|
credit_cards: filter_credit_card_accounts(accounts),
|
||||||
|
investments: filter_investment_accounts(accounts),
|
||||||
|
loans: filter_loan_accounts(accounts),
|
||||||
|
properties: filter_property_accounts(accounts),
|
||||||
|
vehicles: filter_vehicle_accounts(accounts),
|
||||||
|
other_assets: filter_other_asset_accounts(accounts),
|
||||||
|
other_liabilities: filter_other_liability_accounts(accounts)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_checking_accounts(accounts)
|
||||||
|
accounts.select { |a| a.accountable_type == "Depository" && (a.subtype != "savings" || a.name.include?("Checking")) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_savings_accounts(accounts)
|
||||||
|
accounts.select { |a| a.accountable_type == "Depository" && (a.subtype == "savings" || a.name.include?("Savings")) }
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_credit_card_accounts(accounts)
|
||||||
|
accounts.select { |a| a.accountable_type == "CreditCard" }
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_investment_accounts(accounts)
|
||||||
|
accounts.select { |a| a.accountable_type == "Investment" }
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_loan_accounts(accounts)
|
||||||
|
accounts.select { |a| a.accountable_type == "Loan" }
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_property_accounts(accounts)
|
||||||
|
accounts.select { |a| a.accountable_type == "Property" }
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_vehicle_accounts(accounts)
|
||||||
|
accounts.select { |a| a.accountable_type == "Vehicle" }
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_other_asset_accounts(accounts)
|
||||||
|
accounts.select { |a| a.accountable_type == "OtherAsset" }
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_other_liability_accounts(accounts)
|
||||||
|
accounts.select { |a| a.accountable_type == "OtherLiability" }
|
||||||
|
end
|
||||||
|
|
||||||
|
def random_color
|
||||||
|
COLORS.sample
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_name(base_name, index, count = 1)
|
||||||
|
count == 1 ? base_name : "#{base_name} #{index + 1}"
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,155 +1,102 @@
|
||||||
class Demo::Generator
|
class Demo::Generator
|
||||||
COLORS = %w[#e99537 #4da568 #6471eb #db5a54 #df4e92 #c44fe9 #eb5429 #61c9ea #805dee #6ad28a]
|
include Demo::DataHelper
|
||||||
|
|
||||||
# Builds a semi-realistic mirror of what production data might look like
|
# Public API - these methods are called by rake tasks and must be preserved
|
||||||
def reset_and_clear_data!(family_names, require_onboarding: false)
|
def reset_and_clear_data!(family_names, require_onboarding: false)
|
||||||
puts "Clearing existing data..."
|
generate_for_scenario(:clean_slate, family_names, require_onboarding: require_onboarding)
|
||||||
|
|
||||||
destroy_everything!
|
|
||||||
|
|
||||||
puts "Data cleared"
|
|
||||||
|
|
||||||
family_names.each_with_index do |family_name, index|
|
|
||||||
create_family_and_user!(family_name, "user#{index == 0 ? "" : index + 1}@maybe.local", require_onboarding: require_onboarding)
|
|
||||||
end
|
|
||||||
|
|
||||||
puts "Users reset"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def reset_data!(family_names)
|
def reset_data!(family_names)
|
||||||
puts "Clearing existing data..."
|
generate_for_scenario(:default, family_names)
|
||||||
|
end
|
||||||
|
|
||||||
destroy_everything!
|
def generate_performance_testing_data!(family_names)
|
||||||
|
generate_for_scenario(:performance_testing, family_names)
|
||||||
puts "Data cleared"
|
|
||||||
|
|
||||||
family_names.each_with_index do |family_name, index|
|
|
||||||
create_family_and_user!(family_name, "user#{index == 0 ? "" : index + 1}@maybe.local")
|
|
||||||
end
|
|
||||||
|
|
||||||
puts "Users reset"
|
|
||||||
|
|
||||||
load_securities!
|
|
||||||
|
|
||||||
puts "Securities loaded"
|
|
||||||
|
|
||||||
family_names.each do |family_name|
|
|
||||||
family = Family.find_by(name: family_name)
|
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
|
||||||
create_tags!(family)
|
|
||||||
create_categories!(family)
|
|
||||||
create_merchants!(family)
|
|
||||||
create_rules!(family)
|
|
||||||
puts "tags, categories, merchants created for #{family_name}"
|
|
||||||
|
|
||||||
create_credit_card_account!(family)
|
|
||||||
create_checking_account!(family)
|
|
||||||
create_savings_account!(family)
|
|
||||||
|
|
||||||
create_investment_account!(family)
|
|
||||||
create_house_and_mortgage!(family)
|
|
||||||
create_car_and_loan!(family)
|
|
||||||
create_other_accounts!(family)
|
|
||||||
|
|
||||||
create_transfer_transactions!(family)
|
|
||||||
end
|
|
||||||
|
|
||||||
puts "accounts created for #{family_name}"
|
|
||||||
end
|
|
||||||
|
|
||||||
puts "Demo data loaded successfully!"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_basic_budget_data!(family_names)
|
def generate_basic_budget_data!(family_names)
|
||||||
puts "Clearing existing data..."
|
generate_for_scenario(:basic_budget, family_names)
|
||||||
|
|
||||||
destroy_everything!
|
|
||||||
|
|
||||||
puts "Data cleared"
|
|
||||||
|
|
||||||
family_names.each_with_index do |family_name, index|
|
|
||||||
create_family_and_user!(family_name, "user#{index == 0 ? "" : index + 1}@maybe.local")
|
|
||||||
end
|
|
||||||
|
|
||||||
puts "Users reset"
|
|
||||||
|
|
||||||
family_names.each do |family_name|
|
|
||||||
family = Family.find_by(name: family_name)
|
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
|
||||||
# Create parent categories
|
|
||||||
food = family.categories.create!(name: "Food & Drink", color: COLORS.sample, classification: "expense")
|
|
||||||
transport = family.categories.create!(name: "Transportation", color: COLORS.sample, classification: "expense")
|
|
||||||
|
|
||||||
# Create subcategory
|
|
||||||
restaurants = family.categories.create!(name: "Restaurants", parent: food, color: COLORS.sample, classification: "expense")
|
|
||||||
|
|
||||||
# Create checking account
|
|
||||||
checking = family.accounts.create!(
|
|
||||||
accountable: Depository.new,
|
|
||||||
name: "Demo Checking",
|
|
||||||
balance: 3000,
|
|
||||||
currency: "USD"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create one transaction for each category
|
|
||||||
create_transaction!(account: checking, amount: 100, name: "Grocery Store", category: food, date: 2.days.ago)
|
|
||||||
create_transaction!(account: checking, amount: 50, name: "Restaurant Meal", category: restaurants, date: 1.day.ago)
|
|
||||||
create_transaction!(account: checking, amount: 20, name: "Gas Station", category: transport, date: Date.current)
|
|
||||||
end
|
|
||||||
|
|
||||||
puts "Basic budget data created for #{family_name}"
|
|
||||||
end
|
|
||||||
|
|
||||||
puts "Demo data loaded successfully!"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_multi_currency_data!(family_names)
|
def generate_multi_currency_data!(family_names)
|
||||||
puts "Clearing existing data..."
|
generate_for_scenario(:multi_currency, family_names)
|
||||||
|
|
||||||
destroy_everything!
|
|
||||||
|
|
||||||
puts "Data cleared"
|
|
||||||
|
|
||||||
family_names.each_with_index do |family_name, index|
|
|
||||||
create_family_and_user!(family_name, "user#{index == 0 ? "" : index + 1}@maybe.local", currency: "EUR")
|
|
||||||
end
|
|
||||||
|
|
||||||
puts "Users reset"
|
|
||||||
|
|
||||||
family_names.each do |family_name|
|
|
||||||
puts "Generating demo data for #{family_name}"
|
|
||||||
family = Family.find_by(name: family_name)
|
|
||||||
|
|
||||||
usd_checking = family.accounts.create!(name: "USD Checking", currency: "USD", balance: 10000, accountable: Depository.new)
|
|
||||||
eur_checking = family.accounts.create!(name: "EUR Checking", currency: "EUR", balance: 4900, accountable: Depository.new)
|
|
||||||
eur_credit_card = family.accounts.create!(name: "EUR Credit Card", currency: "EUR", balance: 2300, accountable: CreditCard.new)
|
|
||||||
|
|
||||||
create_transaction!(account: eur_credit_card, amount: 1000, currency: "EUR", name: "EUR cc expense 1")
|
|
||||||
create_transaction!(account: eur_credit_card, amount: 1000, currency: "EUR", name: "EUR cc expense 2")
|
|
||||||
create_transaction!(account: eur_credit_card, amount: 300, currency: "EUR", name: "EUR cc expense 3")
|
|
||||||
|
|
||||||
create_transaction!(account: usd_checking, amount: -11000, currency: "USD", name: "USD income Transaction")
|
|
||||||
create_transaction!(account: usd_checking, amount: 1000, currency: "USD", name: "USD expense Transaction")
|
|
||||||
create_transaction!(account: usd_checking, amount: 1000, currency: "USD", name: "USD expense Transaction")
|
|
||||||
create_transaction!(account: eur_checking, amount: -5000, currency: "EUR", name: "EUR income Transaction")
|
|
||||||
create_transaction!(account: eur_checking, amount: 100, currency: "EUR", name: "EUR expense Transaction")
|
|
||||||
|
|
||||||
puts "Transactions created for #{family_name}"
|
|
||||||
end
|
|
||||||
|
|
||||||
puts "Demo data loaded successfully!"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def destroy_everything!
|
|
||||||
Family.destroy_all
|
# Registry pattern for clean scenario lookup and easy extensibility
|
||||||
Setting.destroy_all
|
def scenario_registry
|
||||||
InviteCode.destroy_all
|
@scenario_registry ||= {
|
||||||
ExchangeRate.destroy_all
|
clean_slate: Demo::Scenarios::CleanSlate,
|
||||||
Security.destroy_all
|
default: Demo::Scenarios::Default,
|
||||||
Security::Price.destroy_all
|
basic_budget: Demo::Scenarios::BasicBudget,
|
||||||
|
multi_currency: Demo::Scenarios::MultiCurrency,
|
||||||
|
performance_testing: Demo::Scenarios::PerformanceTesting
|
||||||
|
}.freeze
|
||||||
|
end
|
||||||
|
|
||||||
|
def generators
|
||||||
|
@generators ||= {
|
||||||
|
data_cleaner: Demo::DataCleaner.new,
|
||||||
|
rule_generator: Demo::RuleGenerator.new,
|
||||||
|
account_generator: Demo::AccountGenerator.new,
|
||||||
|
transaction_generator: Demo::TransactionGenerator.new,
|
||||||
|
security_generator: Demo::SecurityGenerator.new,
|
||||||
|
transfer_generator: Demo::TransferGenerator.new
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_for_scenario(scenario_key, family_names, **options)
|
||||||
|
raise ArgumentError, "Scenario key is required" if scenario_key.nil?
|
||||||
|
raise ArgumentError, "Family names must be provided" if family_names.nil? || family_names.empty?
|
||||||
|
|
||||||
|
scenario_class = scenario_registry[scenario_key]
|
||||||
|
unless scenario_class
|
||||||
|
raise ArgumentError, "Unknown scenario: #{scenario_key}. Available: #{scenario_registry.keys.join(', ')}"
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "Starting #{scenario_key} scenario generation for #{family_names.length} families..."
|
||||||
|
|
||||||
|
clear_all_data!
|
||||||
|
create_families_and_users!(family_names, **options)
|
||||||
|
families = family_names.map { |name| Family.find_by(name: name) }
|
||||||
|
|
||||||
|
scenario = scenario_class.new(generators)
|
||||||
|
scenario.generate!(families, **options)
|
||||||
|
|
||||||
|
# Sync families after generation (except for performance testing)
|
||||||
|
unless scenario_key == :performance_testing
|
||||||
|
puts "Running account sync for generated data..."
|
||||||
|
families.each do |family|
|
||||||
|
family.accounts.each do |account|
|
||||||
|
sync = Sync.create!(syncable: account)
|
||||||
|
sync.perform
|
||||||
|
end
|
||||||
|
puts " - #{family.name} accounts synced (#{family.accounts.count} accounts)"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "Demo data loaded successfully!"
|
||||||
|
end
|
||||||
|
|
||||||
|
def clear_all_data!
|
||||||
|
family_count = Family.count
|
||||||
|
|
||||||
|
if family_count > 200
|
||||||
|
raise "Too much data to clear efficiently (#{family_count} families found). " \
|
||||||
|
"Please run 'bundle exec rails db:reset' instead to quickly reset the database, " \
|
||||||
|
"then re-run your demo data task."
|
||||||
|
end
|
||||||
|
|
||||||
|
generators[:data_cleaner].destroy_everything!
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_families_and_users!(family_names, require_onboarding: false, currency: "USD")
|
||||||
|
family_names.each_with_index do |family_name, index|
|
||||||
|
create_family_and_user!(family_name, "user#{index == 0 ? "" : index + 1}@maybe.local",
|
||||||
|
currency: currency, require_onboarding: require_onboarding)
|
||||||
|
end
|
||||||
|
puts "Users reset"
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_family_and_user!(family_name, user_email, currency: "USD", require_onboarding: false)
|
def create_family_and_user!(family_name, user_email, currency: "USD", require_onboarding: false)
|
||||||
|
@ -184,341 +131,4 @@ class Demo::Generator
|
||||||
password: "password",
|
password: "password",
|
||||||
onboarded_at: require_onboarding ? nil : Time.current
|
onboarded_at: require_onboarding ? nil : Time.current
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_rules!(family)
|
|
||||||
family.rules.create!(
|
|
||||||
effective_date: 1.year.ago.to_date,
|
|
||||||
active: true,
|
|
||||||
resource_type: "transaction",
|
|
||||||
conditions: [
|
|
||||||
Rule::Condition.new(condition_type: "transaction_merchant", operator: "=", value: "Whole Foods")
|
|
||||||
],
|
|
||||||
actions: [
|
|
||||||
Rule::Action.new(action_type: "set_transaction_category", value: "Groceries")
|
|
||||||
]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_tags!(family)
|
|
||||||
[ "Trips", "Emergency Fund", "Demo Tag" ].each do |tag|
|
|
||||||
family.tags.create!(name: tag)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_categories!(family)
|
|
||||||
family.categories.bootstrap!
|
|
||||||
|
|
||||||
food = family.categories.find_by(name: "Food & Drink")
|
|
||||||
family.categories.create!(name: "Restaurants", parent: food, color: COLORS.sample, lucide_icon: "utensils", classification: "expense")
|
|
||||||
family.categories.create!(name: "Groceries", parent: food, color: COLORS.sample, lucide_icon: "shopping-cart", classification: "expense")
|
|
||||||
family.categories.create!(name: "Alcohol & Bars", parent: food, color: COLORS.sample, lucide_icon: "beer", classification: "expense")
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_merchants!(family)
|
|
||||||
merchants = [ "Amazon", "Starbucks", "McDonald's", "Target", "Costco",
|
|
||||||
"Home Depot", "Shell", "Whole Foods", "Walgreens", "Nike",
|
|
||||||
"Uber", "Netflix", "Spotify", "Delta Airlines", "Airbnb", "Sephora" ]
|
|
||||||
|
|
||||||
merchants.each do |merchant|
|
|
||||||
FamilyMerchant.create!(name: merchant, family: family, color: COLORS.sample)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_credit_card_account!(family)
|
|
||||||
cc = family.accounts.create! \
|
|
||||||
accountable: CreditCard.new,
|
|
||||||
name: "Chase Credit Card",
|
|
||||||
balance: 2300,
|
|
||||||
currency: "USD"
|
|
||||||
|
|
||||||
800.times do
|
|
||||||
merchant = random_family_record(Merchant, family)
|
|
||||||
create_transaction! \
|
|
||||||
account: cc,
|
|
||||||
name: merchant.name,
|
|
||||||
amount: Faker::Number.positive(to: 200),
|
|
||||||
tags: [ tag_for_merchant(merchant, family) ],
|
|
||||||
category: category_for_merchant(merchant, family),
|
|
||||||
merchant: merchant
|
|
||||||
end
|
|
||||||
|
|
||||||
24.times do
|
|
||||||
create_transaction! \
|
|
||||||
account: cc,
|
|
||||||
amount: Faker::Number.negative(from: -1000),
|
|
||||||
name: "CC Payment"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_checking_account!(family)
|
|
||||||
checking = family.accounts.create! \
|
|
||||||
accountable: Depository.new,
|
|
||||||
name: "Chase Checking",
|
|
||||||
balance: 15000,
|
|
||||||
currency: "USD"
|
|
||||||
|
|
||||||
# First create income transactions to ensure positive balance
|
|
||||||
50.times do
|
|
||||||
create_transaction! \
|
|
||||||
account: checking,
|
|
||||||
amount: Faker::Number.negative(from: -2000, to: -500),
|
|
||||||
name: "Income",
|
|
||||||
category: family.categories.find_by(name: "Income")
|
|
||||||
end
|
|
||||||
|
|
||||||
# Then create expenses that won't exceed the income
|
|
||||||
200.times do
|
|
||||||
create_transaction! \
|
|
||||||
account: checking,
|
|
||||||
name: "Expense",
|
|
||||||
amount: Faker::Number.positive(from: 8, to: 500)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_savings_account!(family)
|
|
||||||
savings = family.accounts.create! \
|
|
||||||
accountable: Depository.new,
|
|
||||||
name: "Demo Savings",
|
|
||||||
balance: 40000,
|
|
||||||
currency: "USD",
|
|
||||||
subtype: "savings"
|
|
||||||
|
|
||||||
# Create larger income deposits first
|
|
||||||
100.times do
|
|
||||||
create_transaction! \
|
|
||||||
account: savings,
|
|
||||||
amount: Faker::Number.negative(from: -3000, to: -1000),
|
|
||||||
tags: [ family.tags.find_by(name: "Emergency Fund") ],
|
|
||||||
category: family.categories.find_by(name: "Income"),
|
|
||||||
name: "Income"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Add some smaller withdrawals that won't exceed the deposits
|
|
||||||
50.times do
|
|
||||||
create_transaction! \
|
|
||||||
account: savings,
|
|
||||||
amount: Faker::Number.positive(from: 100, to: 1000),
|
|
||||||
name: "Savings Withdrawal"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_transfer_transactions!(family)
|
|
||||||
checking = family.accounts.find_by(name: "Chase Checking")
|
|
||||||
credit_card = family.accounts.find_by(name: "Chase Credit Card")
|
|
||||||
investment = family.accounts.find_by(name: "Robinhood")
|
|
||||||
|
|
||||||
create_transaction!(
|
|
||||||
account: checking,
|
|
||||||
date: 1.day.ago.to_date,
|
|
||||||
amount: 100,
|
|
||||||
name: "Credit Card Payment"
|
|
||||||
)
|
|
||||||
|
|
||||||
create_transaction!(
|
|
||||||
account: credit_card,
|
|
||||||
date: 1.day.ago.to_date,
|
|
||||||
amount: -100,
|
|
||||||
name: "Credit Card Payment"
|
|
||||||
)
|
|
||||||
|
|
||||||
create_transaction!(
|
|
||||||
account: checking,
|
|
||||||
date: 3.days.ago.to_date,
|
|
||||||
amount: 500,
|
|
||||||
name: "Transfer to investment"
|
|
||||||
)
|
|
||||||
|
|
||||||
create_transaction!(
|
|
||||||
account: investment,
|
|
||||||
date: 2.days.ago.to_date,
|
|
||||||
amount: -500,
|
|
||||||
name: "Transfer from checking"
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_securities!
|
|
||||||
# Create an unknown security to simulate edge cases
|
|
||||||
Security.create! ticker: "UNKNOWN", name: "Unknown Demo Stock"
|
|
||||||
|
|
||||||
securities = [
|
|
||||||
{ ticker: "AAPL", exchange_mic: "XNGS", exchange_operating_mic: "XNAS", name: "Apple Inc.", reference_price: 210 },
|
|
||||||
{ ticker: "TM", exchange_mic: "XNYS", exchange_operating_mic: "XNYS", name: "Toyota Motor Corporation", reference_price: 202 },
|
|
||||||
{ ticker: "MSFT", exchange_mic: "XNGS", exchange_operating_mic: "XNAS", name: "Microsoft Corporation", reference_price: 455 }
|
|
||||||
]
|
|
||||||
|
|
||||||
securities.each do |security_attributes|
|
|
||||||
security = Security.create! security_attributes.except(:reference_price)
|
|
||||||
|
|
||||||
# Load prices for last 2 years
|
|
||||||
(730.days.ago.to_date..Date.current).each do |date|
|
|
||||||
reference = security_attributes[:reference_price]
|
|
||||||
low_price = reference - 20
|
|
||||||
high_price = reference + 20
|
|
||||||
Security::Price.create! \
|
|
||||||
security: security,
|
|
||||||
date: date,
|
|
||||||
price: Faker::Number.positive(from: low_price, to: high_price)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_investment_account!(family)
|
|
||||||
account = family.accounts.create! \
|
|
||||||
accountable: Investment.new,
|
|
||||||
name: "Robinhood",
|
|
||||||
balance: 100000,
|
|
||||||
currency: "USD"
|
|
||||||
|
|
||||||
aapl = Security.find_by(ticker: "AAPL")
|
|
||||||
tm = Security.find_by(ticker: "TM")
|
|
||||||
msft = Security.find_by(ticker: "MSFT")
|
|
||||||
unknown = Security.find_by(ticker: "UNKNOWN")
|
|
||||||
|
|
||||||
# Buy 20 shares of the unknown stock to simulate a stock where we can't fetch security prices
|
|
||||||
account.entries.create! date: 10.days.ago.to_date, amount: 100, currency: "USD", name: "Buy unknown stock", entryable: Trade.new(qty: 20, price: 5, security: unknown, currency: "USD")
|
|
||||||
|
|
||||||
trades = [
|
|
||||||
{ security: aapl, qty: 20 }, { security: msft, qty: 10 }, { security: aapl, qty: -5 },
|
|
||||||
{ security: msft, qty: -5 }, { security: tm, qty: 10 }, { security: msft, qty: 5 },
|
|
||||||
{ security: tm, qty: 10 }, { security: aapl, qty: -5 }, { security: msft, qty: -5 },
|
|
||||||
{ security: tm, qty: 10 }, { security: msft, qty: 5 }, { security: aapl, qty: -10 }
|
|
||||||
]
|
|
||||||
|
|
||||||
trades.each do |trade|
|
|
||||||
date = Faker::Number.positive(to: 730).days.ago.to_date
|
|
||||||
security = trade[:security]
|
|
||||||
qty = trade[:qty]
|
|
||||||
price = Security::Price.find_by(security: security, date: date)&.price || 1
|
|
||||||
name_prefix = qty < 0 ? "Sell " : "Buy "
|
|
||||||
|
|
||||||
account.entries.create! \
|
|
||||||
date: date,
|
|
||||||
amount: qty * price,
|
|
||||||
currency: "USD",
|
|
||||||
name: name_prefix + "#{qty} shares of #{security.ticker}",
|
|
||||||
entryable: Trade.new(qty: qty, price: price, currency: "USD", security: security)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_house_and_mortgage!(family)
|
|
||||||
house = family.accounts.create! \
|
|
||||||
accountable: Property.new,
|
|
||||||
name: "123 Maybe Way",
|
|
||||||
balance: 560000,
|
|
||||||
currency: "USD"
|
|
||||||
|
|
||||||
create_valuation!(house, 3.years.ago.to_date, 520000)
|
|
||||||
create_valuation!(house, 2.years.ago.to_date, 540000)
|
|
||||||
create_valuation!(house, 1.years.ago.to_date, 550000)
|
|
||||||
|
|
||||||
mortgage = family.accounts.create! \
|
|
||||||
accountable: Loan.new,
|
|
||||||
name: "Mortgage",
|
|
||||||
balance: 495000,
|
|
||||||
currency: "USD"
|
|
||||||
|
|
||||||
create_valuation!(mortgage, 3.years.ago.to_date, 495000)
|
|
||||||
create_valuation!(mortgage, 2.years.ago.to_date, 490000)
|
|
||||||
create_valuation!(mortgage, 1.years.ago.to_date, 485000)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_car_and_loan!(family)
|
|
||||||
vehicle = family.accounts.create! \
|
|
||||||
accountable: Vehicle.new,
|
|
||||||
name: "Honda Accord",
|
|
||||||
balance: 18000,
|
|
||||||
currency: "USD"
|
|
||||||
|
|
||||||
create_valuation!(vehicle, 1.year.ago.to_date, 18000)
|
|
||||||
|
|
||||||
loan = family.accounts.create! \
|
|
||||||
accountable: Loan.new,
|
|
||||||
name: "Car Loan",
|
|
||||||
balance: 8000,
|
|
||||||
currency: "USD"
|
|
||||||
|
|
||||||
create_valuation!(loan, 1.year.ago.to_date, 8000)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_other_accounts!(family)
|
|
||||||
other_asset = family.accounts.create! \
|
|
||||||
accountable: OtherAsset.new,
|
|
||||||
name: "Other Asset",
|
|
||||||
balance: 10000,
|
|
||||||
currency: "USD"
|
|
||||||
|
|
||||||
other_liability = family.accounts.create! \
|
|
||||||
accountable: OtherLiability.new,
|
|
||||||
name: "Other Liability",
|
|
||||||
balance: 5000,
|
|
||||||
currency: "USD"
|
|
||||||
|
|
||||||
create_valuation!(other_asset, 1.year.ago.to_date, 10000)
|
|
||||||
create_valuation!(other_liability, 1.year.ago.to_date, 5000)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_transaction!(attributes = {})
|
|
||||||
entry_attributes = attributes.except(:category, :tags, :merchant)
|
|
||||||
transaction_attributes = attributes.slice(:category, :tags, :merchant)
|
|
||||||
|
|
||||||
entry_defaults = {
|
|
||||||
date: Faker::Number.between(from: 0, to: 730).days.ago.to_date,
|
|
||||||
currency: "USD",
|
|
||||||
entryable: Transaction.new(transaction_attributes)
|
|
||||||
}
|
|
||||||
|
|
||||||
Entry.create! entry_defaults.merge(entry_attributes)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_valuation!(account, date, amount)
|
|
||||||
Entry.create! \
|
|
||||||
account: account,
|
|
||||||
date: date,
|
|
||||||
amount: amount,
|
|
||||||
currency: "USD",
|
|
||||||
name: "Balance update",
|
|
||||||
entryable: Valuation.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def random_family_record(model, family)
|
|
||||||
family_records = model.where(family_id: family.id)
|
|
||||||
model.offset(rand(family_records.count)).first
|
|
||||||
end
|
|
||||||
|
|
||||||
def category_for_merchant(merchant, family)
|
|
||||||
mapping = {
|
|
||||||
"Amazon" => "Shopping",
|
|
||||||
"Starbucks" => "Food & Drink",
|
|
||||||
"McDonald's" => "Food & Drink",
|
|
||||||
"Target" => "Shopping",
|
|
||||||
"Costco" => "Food & Drink",
|
|
||||||
"Home Depot" => "Housing",
|
|
||||||
"Shell" => "Transportation",
|
|
||||||
"Whole Foods" => "Food & Drink",
|
|
||||||
"Walgreens" => "Healthcare",
|
|
||||||
"Nike" => "Shopping",
|
|
||||||
"Uber" => "Transportation",
|
|
||||||
"Netflix" => "Subscriptions",
|
|
||||||
"Spotify" => "Subscriptions",
|
|
||||||
"Delta Airlines" => "Transportation",
|
|
||||||
"Airbnb" => "Housing",
|
|
||||||
"Sephora" => "Shopping"
|
|
||||||
}
|
|
||||||
|
|
||||||
family.categories.find_by(name: mapping[merchant.name])
|
|
||||||
end
|
|
||||||
|
|
||||||
def tag_for_merchant(merchant, family)
|
|
||||||
mapping = {
|
|
||||||
"Delta Airlines" => "Trips",
|
|
||||||
"Airbnb" => "Trips"
|
|
||||||
}
|
|
||||||
|
|
||||||
tag_from_merchant = family.tags.find_by(name: mapping[merchant.name])
|
|
||||||
tag_from_merchant || family.tags.find_by(name: "Demo Tag")
|
|
||||||
end
|
|
||||||
|
|
||||||
def securities
|
|
||||||
@securities ||= Security.all.to_a
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
79
app/models/demo/rule_generator.rb
Normal file
79
app/models/demo/rule_generator.rb
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
class Demo::RuleGenerator
|
||||||
|
include Demo::DataHelper
|
||||||
|
|
||||||
|
def create_rules!(family)
|
||||||
|
tags = create_tags!(family)
|
||||||
|
categories = create_categories!(family)
|
||||||
|
merchants = create_merchants!(family)
|
||||||
|
|
||||||
|
rules = []
|
||||||
|
|
||||||
|
if merchants.any? && categories.any?
|
||||||
|
rule = family.rules.create!(
|
||||||
|
name: "Auto-categorize Grocery Purchases",
|
||||||
|
resource_type: "Transaction",
|
||||||
|
conditions: [
|
||||||
|
Rule::Condition.new(condition_type: "merchant_name", operator: "contains", value: "Whole Foods")
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
Rule::Action.new(action_type: "category_id", value: categories.first.id.to_s)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
rules << rule
|
||||||
|
end
|
||||||
|
|
||||||
|
rules
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_tags!(family)
|
||||||
|
tag_names = [ "Business", "Tax Deductible", "Recurring", "Emergency" ]
|
||||||
|
tags = []
|
||||||
|
|
||||||
|
tag_names.each do |name|
|
||||||
|
tag = family.tags.find_or_create_by!(name: name) do |t|
|
||||||
|
t.color = random_color
|
||||||
|
end
|
||||||
|
tags << tag
|
||||||
|
end
|
||||||
|
|
||||||
|
tags
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_categories!(family)
|
||||||
|
category_data = [
|
||||||
|
{ name: "Groceries", color: random_color },
|
||||||
|
{ name: "Transportation", color: random_color },
|
||||||
|
{ name: "Entertainment", color: random_color },
|
||||||
|
{ name: "Utilities", color: random_color },
|
||||||
|
{ name: "Healthcare", color: random_color }
|
||||||
|
]
|
||||||
|
|
||||||
|
categories = []
|
||||||
|
category_data.each do |data|
|
||||||
|
category = family.categories.find_or_create_by!(name: data[:name]) do |c|
|
||||||
|
c.color = data[:color]
|
||||||
|
end
|
||||||
|
categories << category
|
||||||
|
end
|
||||||
|
|
||||||
|
categories
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_merchants!(family)
|
||||||
|
merchant_names = [
|
||||||
|
"Whole Foods Market",
|
||||||
|
"Shell Gas Station",
|
||||||
|
"Netflix",
|
||||||
|
"Electric Company",
|
||||||
|
"Local Coffee Shop"
|
||||||
|
]
|
||||||
|
|
||||||
|
merchants = []
|
||||||
|
merchant_names.each do |name|
|
||||||
|
merchant = family.merchants.find_or_create_by!(name: name)
|
||||||
|
merchants << merchant
|
||||||
|
end
|
||||||
|
|
||||||
|
merchants
|
||||||
|
end
|
||||||
|
end
|
129
app/models/demo/scenarios/basic_budget.rb
Normal file
129
app/models/demo/scenarios/basic_budget.rb
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
# Basic budget scenario - minimal budgeting demonstration with categories
|
||||||
|
#
|
||||||
|
# This scenario creates a simple budget demonstration with parent/child categories
|
||||||
|
# and one transaction per category. Designed to showcase basic budgeting features
|
||||||
|
# without overwhelming complexity. Ideal for:
|
||||||
|
# - Basic budgeting feature demos
|
||||||
|
# - Category hierarchy demonstrations
|
||||||
|
# - Simple transaction categorization examples
|
||||||
|
# - Lightweight testing environments
|
||||||
|
#
|
||||||
|
class Demo::Scenarios::BasicBudget < Demo::BaseScenario
|
||||||
|
include Demo::DataHelper
|
||||||
|
|
||||||
|
# Scenario characteristics and configuration
|
||||||
|
SCENARIO_NAME = "Basic Budget".freeze
|
||||||
|
PURPOSE = "Simple budget demonstration with category hierarchy".freeze
|
||||||
|
TARGET_ACCOUNTS_PER_FAMILY = 1 # Single checking account
|
||||||
|
TARGET_TRANSACTIONS_PER_FAMILY = 4 # One income, three expenses
|
||||||
|
TARGET_CATEGORIES = 4 # Income + 3 expense categories (with one subcategory)
|
||||||
|
INCLUDES_SECURITIES = false
|
||||||
|
INCLUDES_TRANSFERS = false
|
||||||
|
INCLUDES_RULES = false
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Generate basic budget demonstration data
|
||||||
|
# Creates simple category hierarchy and one transaction per category
|
||||||
|
#
|
||||||
|
# @param family [Family] The family to generate data for
|
||||||
|
# @param options [Hash] Additional options (unused in this scenario)
|
||||||
|
def generate_family_data!(family, **options)
|
||||||
|
create_category_hierarchy!(family)
|
||||||
|
create_demo_checking_account!(family)
|
||||||
|
create_sample_categorized_transactions!(family)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create parent categories with one subcategory example
|
||||||
|
def create_category_hierarchy!(family)
|
||||||
|
# Create parent categories
|
||||||
|
@food_category = family.categories.create!(
|
||||||
|
name: "Food & Drink",
|
||||||
|
color: random_color,
|
||||||
|
classification: "expense"
|
||||||
|
)
|
||||||
|
|
||||||
|
@transport_category = family.categories.create!(
|
||||||
|
name: "Transportation",
|
||||||
|
color: random_color,
|
||||||
|
classification: "expense"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create subcategory to demonstrate hierarchy
|
||||||
|
@restaurants_category = family.categories.create!(
|
||||||
|
name: "Restaurants",
|
||||||
|
parent: @food_category,
|
||||||
|
color: random_color,
|
||||||
|
classification: "expense"
|
||||||
|
)
|
||||||
|
|
||||||
|
puts " - #{TARGET_CATEGORIES} categories created (with parent/child hierarchy)"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create single checking account for budget demonstration
|
||||||
|
def create_demo_checking_account!(family)
|
||||||
|
@checking_account = family.accounts.create!(
|
||||||
|
accountable: Depository.new,
|
||||||
|
name: "Demo Checking",
|
||||||
|
balance: 0, # Will be calculated from transactions
|
||||||
|
currency: "USD"
|
||||||
|
)
|
||||||
|
|
||||||
|
puts " - #{TARGET_ACCOUNTS_PER_FAMILY} demo checking account created"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create one transaction for each category to demonstrate categorization
|
||||||
|
def create_sample_categorized_transactions!(family)
|
||||||
|
# Create income category and transaction first
|
||||||
|
income_category = family.categories.create!(
|
||||||
|
name: "Income",
|
||||||
|
color: random_color,
|
||||||
|
classification: "income"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add income transaction (negative amount = inflow)
|
||||||
|
@generators[:transaction_generator].create_transaction!(
|
||||||
|
account: @checking_account,
|
||||||
|
amount: -500, # Income (negative)
|
||||||
|
name: "Salary",
|
||||||
|
category: income_category,
|
||||||
|
date: 5.days.ago
|
||||||
|
)
|
||||||
|
|
||||||
|
# Grocery transaction (parent category)
|
||||||
|
@generators[:transaction_generator].create_transaction!(
|
||||||
|
account: @checking_account,
|
||||||
|
amount: 100,
|
||||||
|
name: "Grocery Store",
|
||||||
|
category: @food_category,
|
||||||
|
date: 2.days.ago
|
||||||
|
)
|
||||||
|
|
||||||
|
# Restaurant transaction (subcategory)
|
||||||
|
@generators[:transaction_generator].create_transaction!(
|
||||||
|
account: @checking_account,
|
||||||
|
amount: 50,
|
||||||
|
name: "Restaurant Meal",
|
||||||
|
category: @restaurants_category,
|
||||||
|
date: 1.day.ago
|
||||||
|
)
|
||||||
|
|
||||||
|
# Transportation transaction
|
||||||
|
@generators[:transaction_generator].create_transaction!(
|
||||||
|
account: @checking_account,
|
||||||
|
amount: 20,
|
||||||
|
name: "Gas Station",
|
||||||
|
category: @transport_category,
|
||||||
|
date: Date.current
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update account balance to match transaction sum
|
||||||
|
@generators[:transaction_generator].update_account_balances_from_transactions!(family)
|
||||||
|
|
||||||
|
puts " - #{TARGET_TRANSACTIONS_PER_FAMILY + 1} categorized transactions created (including income)"
|
||||||
|
end
|
||||||
|
|
||||||
|
def scenario_name
|
||||||
|
SCENARIO_NAME
|
||||||
|
end
|
||||||
|
end
|
126
app/models/demo/scenarios/clean_slate.rb
Normal file
126
app/models/demo/scenarios/clean_slate.rb
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
# Clean slate scenario - minimal starter data for new user onboarding
|
||||||
|
#
|
||||||
|
# This scenario creates the absolute minimum data needed to help new users
|
||||||
|
# understand Maybe's core features without overwhelming them. Ideal for:
|
||||||
|
# - New user onboarding flows
|
||||||
|
# - Tutorial walkthroughs
|
||||||
|
# - Clean development environments
|
||||||
|
# - User acceptance testing with minimal data
|
||||||
|
#
|
||||||
|
# The scenario only generates data when explicitly requested via with_minimal_data: true,
|
||||||
|
# otherwise it creates no data at all (true "clean slate").
|
||||||
|
#
|
||||||
|
# @example Minimal data generation
|
||||||
|
# scenario = Demo::Scenarios::CleanSlate.new(generators)
|
||||||
|
# scenario.generate!(families, with_minimal_data: true)
|
||||||
|
#
|
||||||
|
# @example True clean slate (no data)
|
||||||
|
# scenario = Demo::Scenarios::CleanSlate.new(generators)
|
||||||
|
# scenario.generate!(families) # Creates nothing
|
||||||
|
#
|
||||||
|
class Demo::Scenarios::CleanSlate < Demo::BaseScenario
|
||||||
|
# Scenario characteristics and configuration
|
||||||
|
SCENARIO_NAME = "Clean Slate".freeze
|
||||||
|
PURPOSE = "Minimal starter data for new user onboarding and tutorials".freeze
|
||||||
|
TARGET_ACCOUNTS_PER_FAMILY = 1 # Single checking account only
|
||||||
|
TARGET_TRANSACTIONS_PER_FAMILY = 3 # Just enough to show transaction history
|
||||||
|
INCLUDES_SECURITIES = false
|
||||||
|
INCLUDES_TRANSFERS = false
|
||||||
|
INCLUDES_RULES = false
|
||||||
|
MINIMAL_CATEGORIES = 2 # Essential expense and income categories only
|
||||||
|
|
||||||
|
# Override the base generate! method to handle the special with_minimal_data option
|
||||||
|
# Only generates data when explicitly requested to avoid accidental data creation
|
||||||
|
#
|
||||||
|
# @param families [Array<Family>] Families to generate data for
|
||||||
|
# @param options [Hash] Options hash that may contain with_minimal_data or require_onboarding
|
||||||
|
def generate!(families, **options)
|
||||||
|
# For "empty" task, don't generate any data
|
||||||
|
# For "new_user" task, generate minimal data for onboarding users
|
||||||
|
with_minimal_data = options[:with_minimal_data] || options[:require_onboarding]
|
||||||
|
return unless with_minimal_data
|
||||||
|
|
||||||
|
super(families, **options)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Generate minimal family data for getting started
|
||||||
|
# Creates only essential accounts and transactions to demonstrate core features
|
||||||
|
#
|
||||||
|
# @param family [Family] The family to generate data for
|
||||||
|
# @param options [Hash] Additional options (with_minimal_data used for validation)
|
||||||
|
def generate_family_data!(family, **options)
|
||||||
|
create_essential_categories!(family)
|
||||||
|
create_primary_checking_account!(family)
|
||||||
|
create_sample_transaction_history!(family)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create only the most essential categories for basic expense tracking
|
||||||
|
def create_essential_categories!(family)
|
||||||
|
@food_category = family.categories.create!(
|
||||||
|
name: "Food & Drink",
|
||||||
|
color: "#4da568",
|
||||||
|
classification: "expense"
|
||||||
|
)
|
||||||
|
|
||||||
|
@income_category = family.categories.create!(
|
||||||
|
name: "Income",
|
||||||
|
color: "#6471eb",
|
||||||
|
classification: "income"
|
||||||
|
)
|
||||||
|
|
||||||
|
puts " - #{MINIMAL_CATEGORIES} essential categories created"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create a single primary checking account with a reasonable starting balance
|
||||||
|
def create_primary_checking_account!(family)
|
||||||
|
@checking_account = family.accounts.create!(
|
||||||
|
accountable: Depository.new,
|
||||||
|
name: "Main Checking",
|
||||||
|
balance: 0, # Will be calculated from transactions
|
||||||
|
currency: "USD"
|
||||||
|
)
|
||||||
|
|
||||||
|
puts " - #{TARGET_ACCOUNTS_PER_FAMILY} primary checking account created"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create minimal transaction history showing income and expense patterns
|
||||||
|
def create_sample_transaction_history!(family)
|
||||||
|
# Recent salary deposit
|
||||||
|
@generators[:transaction_generator].create_transaction!(
|
||||||
|
account: @checking_account,
|
||||||
|
amount: -3000, # Income (negative = inflow)
|
||||||
|
name: "Salary",
|
||||||
|
category: @income_category,
|
||||||
|
date: 15.days.ago
|
||||||
|
)
|
||||||
|
|
||||||
|
# Recent grocery purchase
|
||||||
|
@generators[:transaction_generator].create_transaction!(
|
||||||
|
account: @checking_account,
|
||||||
|
amount: 75, # Expense (positive = outflow)
|
||||||
|
name: "Grocery Store",
|
||||||
|
category: @food_category,
|
||||||
|
date: 5.days.ago
|
||||||
|
)
|
||||||
|
|
||||||
|
# Recent restaurant expense
|
||||||
|
@generators[:transaction_generator].create_transaction!(
|
||||||
|
account: @checking_account,
|
||||||
|
amount: 45, # Expense
|
||||||
|
name: "Restaurant",
|
||||||
|
category: @food_category,
|
||||||
|
date: 2.days.ago
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update account balance to match transaction sum
|
||||||
|
@generators[:transaction_generator].update_account_balances_from_transactions!(family)
|
||||||
|
|
||||||
|
puts " - #{TARGET_TRANSACTIONS_PER_FAMILY} sample transactions created"
|
||||||
|
end
|
||||||
|
|
||||||
|
def scenario_name
|
||||||
|
SCENARIO_NAME
|
||||||
|
end
|
||||||
|
end
|
77
app/models/demo/scenarios/default.rb
Normal file
77
app/models/demo/scenarios/default.rb
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
# Default demo scenario - comprehensive realistic data for product demonstrations
|
||||||
|
#
|
||||||
|
# This scenario creates a complete, realistic demo environment that showcases
|
||||||
|
# all of Maybe's features with believable data patterns. Ideal for:
|
||||||
|
# - Product demonstrations to potential users
|
||||||
|
# - UI/UX testing with realistic data volumes
|
||||||
|
# - Feature development with complete data sets
|
||||||
|
# - Screenshots and marketing materials
|
||||||
|
#
|
||||||
|
class Demo::Scenarios::Default < Demo::BaseScenario
|
||||||
|
# Scenario characteristics and configuration
|
||||||
|
SCENARIO_NAME = "Comprehensive Demo".freeze
|
||||||
|
PURPOSE = "Complete realistic demo environment showcasing all Maybe features".freeze
|
||||||
|
TARGET_ACCOUNTS_PER_FAMILY = 7 # 1 each: checking, savings, credit card, 3 investments, 1 property+mortgage
|
||||||
|
TARGET_TRANSACTIONS_PER_FAMILY = 50 # Realistic 3-month transaction history
|
||||||
|
INCLUDES_SECURITIES = true
|
||||||
|
INCLUDES_TRANSFERS = true
|
||||||
|
INCLUDES_RULES = true
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Load securities before generating family data
|
||||||
|
# Securities are needed for investment account trades
|
||||||
|
def setup(**options)
|
||||||
|
@generators[:security_generator].load_securities!
|
||||||
|
puts "Securities loaded for investment accounts"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate complete family financial data
|
||||||
|
# Creates all account types with realistic balances and transaction patterns
|
||||||
|
#
|
||||||
|
# @param family [Family] The family to generate data for
|
||||||
|
# @param options [Hash] Additional options (unused in this scenario)
|
||||||
|
def generate_family_data!(family, **options)
|
||||||
|
create_foundational_data!(family)
|
||||||
|
create_all_account_types!(family)
|
||||||
|
create_realistic_transaction_patterns!(family)
|
||||||
|
create_account_transfers!(family)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create rules, tags, categories, and merchants for the family
|
||||||
|
def create_foundational_data!(family)
|
||||||
|
@generators[:rule_generator].create_rules!(family)
|
||||||
|
@generators[:rule_generator].create_tags!(family)
|
||||||
|
@generators[:rule_generator].create_categories!(family)
|
||||||
|
@generators[:rule_generator].create_merchants!(family)
|
||||||
|
puts " - Rules, categories, and merchants created"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create one of each major account type to demonstrate full feature set
|
||||||
|
def create_all_account_types!(family)
|
||||||
|
@generators[:account_generator].create_credit_card_accounts!(family)
|
||||||
|
@generators[:account_generator].create_checking_accounts!(family)
|
||||||
|
@generators[:account_generator].create_savings_accounts!(family)
|
||||||
|
@generators[:account_generator].create_investment_accounts!(family)
|
||||||
|
@generators[:account_generator].create_properties_and_mortgages!(family)
|
||||||
|
@generators[:account_generator].create_vehicles_and_loans!(family)
|
||||||
|
@generators[:account_generator].create_other_accounts!(family)
|
||||||
|
puts " - All #{TARGET_ACCOUNTS_PER_FAMILY} account types created"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate realistic transaction patterns across all accounts
|
||||||
|
def create_realistic_transaction_patterns!(family)
|
||||||
|
@generators[:transaction_generator].create_realistic_transactions!(family)
|
||||||
|
puts " - Realistic transaction patterns created (~#{TARGET_TRANSACTIONS_PER_FAMILY} transactions)"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create transfer patterns between accounts (credit card payments, investments, etc.)
|
||||||
|
def create_account_transfers!(family)
|
||||||
|
@generators[:transfer_generator].create_transfer_transactions!(family)
|
||||||
|
puts " - Account transfer patterns created"
|
||||||
|
end
|
||||||
|
|
||||||
|
def scenario_name
|
||||||
|
SCENARIO_NAME
|
||||||
|
end
|
||||||
|
end
|
241
app/models/demo/scenarios/multi_currency.rb
Normal file
241
app/models/demo/scenarios/multi_currency.rb
Normal file
|
@ -0,0 +1,241 @@
|
||||||
|
# Multi-currency scenario - international financial management demonstration
|
||||||
|
#
|
||||||
|
# This scenario creates accounts and transactions in multiple currencies to showcase
|
||||||
|
# Maybe's multi-currency capabilities. Demonstrates currency conversion, international
|
||||||
|
# transactions, and mixed-currency portfolio management. Ideal for:
|
||||||
|
# - International users and use cases
|
||||||
|
# - Currency conversion feature testing
|
||||||
|
# - Multi-region financial management demos
|
||||||
|
# - Exchange rate and conversion testing
|
||||||
|
#
|
||||||
|
# Primary currency is EUR with additional USD and GBP accounts and transactions.
|
||||||
|
#
|
||||||
|
class Demo::Scenarios::MultiCurrency < Demo::BaseScenario
|
||||||
|
include Demo::DataHelper
|
||||||
|
|
||||||
|
# Scenario characteristics and configuration
|
||||||
|
SCENARIO_NAME = "Multi-Currency".freeze
|
||||||
|
PURPOSE = "International financial management with multiple currencies".freeze
|
||||||
|
PRIMARY_CURRENCY = "EUR".freeze
|
||||||
|
SUPPORTED_CURRENCIES = %w[EUR USD GBP].freeze
|
||||||
|
TARGET_ACCOUNTS_PER_FAMILY = 5 # 2 EUR (checking, credit), 1 USD, 1 GBP, 1 multi-currency investment
|
||||||
|
TARGET_TRANSACTIONS_PER_FAMILY = 10 # Distributed across currencies
|
||||||
|
INCLUDES_SECURITIES = false # Keep simple for currency focus
|
||||||
|
INCLUDES_TRANSFERS = true # Minimal transfers to avoid currency complexity
|
||||||
|
INCLUDES_RULES = false # Focus on currency, not categorization
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Generate family data with multiple currencies
|
||||||
|
# Creates accounts in EUR, USD, and GBP with appropriate transactions
|
||||||
|
#
|
||||||
|
# @param family [Family] The family to generate data for (should have EUR as primary currency)
|
||||||
|
# @param options [Hash] Additional options (unused in this scenario)
|
||||||
|
def generate_family_data!(family, **options)
|
||||||
|
create_basic_categorization!(family)
|
||||||
|
create_multi_currency_accounts!(family)
|
||||||
|
create_international_transactions!(family)
|
||||||
|
create_minimal_transfers!(family)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create basic categories for international transactions
|
||||||
|
def create_basic_categorization!(family)
|
||||||
|
@generators[:rule_generator].create_categories!(family)
|
||||||
|
@generators[:rule_generator].create_merchants!(family)
|
||||||
|
puts " - Basic categories and merchants created for international transactions"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create accounts in multiple currencies to demonstrate international capabilities
|
||||||
|
def create_multi_currency_accounts!(family)
|
||||||
|
create_eur_accounts!(family) # Primary currency accounts
|
||||||
|
create_usd_accounts!(family) # US dollar accounts
|
||||||
|
create_gbp_accounts!(family) # British pound accounts
|
||||||
|
create_investment_account!(family) # Multi-currency investment
|
||||||
|
|
||||||
|
puts " - #{TARGET_ACCOUNTS_PER_FAMILY} multi-currency accounts created (#{SUPPORTED_CURRENCIES.join(', ')})"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create EUR accounts (primary currency for this scenario)
|
||||||
|
def create_eur_accounts!(family)
|
||||||
|
# Create EUR checking account
|
||||||
|
family.accounts.create!(
|
||||||
|
accountable: Depository.new,
|
||||||
|
name: "EUR Checking Account",
|
||||||
|
balance: 0, # Will be calculated from transactions
|
||||||
|
currency: "EUR"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create EUR credit card
|
||||||
|
family.accounts.create!(
|
||||||
|
accountable: CreditCard.new,
|
||||||
|
name: "EUR Credit Card",
|
||||||
|
balance: 0, # Will be calculated from transactions
|
||||||
|
currency: "EUR"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create USD accounts for US-based transactions
|
||||||
|
def create_usd_accounts!(family)
|
||||||
|
family.accounts.create!(
|
||||||
|
accountable: Depository.new,
|
||||||
|
name: "USD Checking Account",
|
||||||
|
balance: 0, # Will be calculated from transactions
|
||||||
|
currency: "USD"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create GBP accounts for UK-based transactions
|
||||||
|
def create_gbp_accounts!(family)
|
||||||
|
family.accounts.create!(
|
||||||
|
accountable: Depository.new,
|
||||||
|
name: "GBP Savings Account",
|
||||||
|
balance: 0, # Will be calculated from transactions
|
||||||
|
currency: "GBP",
|
||||||
|
subtype: "savings"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create investment account (uses primary currency)
|
||||||
|
def create_investment_account!(family)
|
||||||
|
@generators[:account_generator].create_investment_accounts!(family, count: 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create transactions in various currencies to demonstrate international usage
|
||||||
|
def create_international_transactions!(family)
|
||||||
|
# Create initial valuations for accounts that need them
|
||||||
|
create_initial_valuations!(family)
|
||||||
|
|
||||||
|
create_eur_transaction_patterns!(family)
|
||||||
|
create_usd_transaction_patterns!(family)
|
||||||
|
create_gbp_transaction_patterns!(family)
|
||||||
|
|
||||||
|
# Update account balances to match transaction sums
|
||||||
|
@generators[:transaction_generator].update_account_balances_from_transactions!(family)
|
||||||
|
|
||||||
|
puts " - International transactions created across #{SUPPORTED_CURRENCIES.length} currencies"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create initial valuations for credit cards in this scenario
|
||||||
|
def create_initial_valuations!(family)
|
||||||
|
family.accounts.each do |account|
|
||||||
|
next unless account.accountable_type == "CreditCard"
|
||||||
|
|
||||||
|
Entry.create!(
|
||||||
|
account: account,
|
||||||
|
amount: 1000, # Initial credit card debt
|
||||||
|
name: "Initial creditcard valuation",
|
||||||
|
date: 2.years.ago.to_date,
|
||||||
|
currency: account.currency,
|
||||||
|
entryable_type: "Valuation",
|
||||||
|
entryable_attributes: {}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create EUR transactions (primary currency patterns) with both income and expenses
|
||||||
|
def create_eur_transaction_patterns!(family)
|
||||||
|
eur_accounts = family.accounts.where(currency: "EUR")
|
||||||
|
|
||||||
|
eur_accounts.each do |account|
|
||||||
|
next if account.accountable_type == "Investment"
|
||||||
|
|
||||||
|
if account.accountable_type == "CreditCard"
|
||||||
|
# Credit cards only get purchases (positive amounts)
|
||||||
|
5.times do |i|
|
||||||
|
@generators[:transaction_generator].create_transaction!(
|
||||||
|
account: account,
|
||||||
|
amount: random_positive_amount(50, 300), # Purchases (positive)
|
||||||
|
name: "EUR Purchase #{i + 1}",
|
||||||
|
date: random_date_within_days(60),
|
||||||
|
currency: "EUR"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# Checking accounts get both income and expenses
|
||||||
|
# Create income transactions (negative amounts)
|
||||||
|
2.times do |i|
|
||||||
|
@generators[:transaction_generator].create_transaction!(
|
||||||
|
account: account,
|
||||||
|
amount: -random_positive_amount(2000, 3000), # Higher income to cover transfers
|
||||||
|
name: "EUR Salary #{i + 1}",
|
||||||
|
date: random_date_within_days(60),
|
||||||
|
currency: "EUR"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create expense transactions (positive amounts)
|
||||||
|
3.times do |i|
|
||||||
|
@generators[:transaction_generator].create_transaction!(
|
||||||
|
account: account,
|
||||||
|
amount: random_positive_amount(20, 200), # Expense (positive)
|
||||||
|
name: "EUR Purchase #{i + 1}",
|
||||||
|
date: random_date_within_days(60),
|
||||||
|
currency: "EUR"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create USD transactions (US-based spending patterns) with both income and expenses
|
||||||
|
def create_usd_transaction_patterns!(family)
|
||||||
|
usd_accounts = family.accounts.where(currency: "USD")
|
||||||
|
|
||||||
|
usd_accounts.each do |account|
|
||||||
|
# Create income transaction (negative amount)
|
||||||
|
@generators[:transaction_generator].create_transaction!(
|
||||||
|
account: account,
|
||||||
|
amount: -random_positive_amount(1500, 2500), # Higher income to cover transfers
|
||||||
|
name: "USD Freelance Payment",
|
||||||
|
date: random_date_within_days(60),
|
||||||
|
currency: "USD"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create expense transactions (positive amounts)
|
||||||
|
2.times do |i|
|
||||||
|
@generators[:transaction_generator].create_transaction!(
|
||||||
|
account: account,
|
||||||
|
amount: random_positive_amount(30, 150), # Expense (positive)
|
||||||
|
name: "USD Purchase #{i + 1}",
|
||||||
|
date: random_date_within_days(60),
|
||||||
|
currency: "USD"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create GBP transactions (UK-based spending patterns) with both income and expenses
|
||||||
|
def create_gbp_transaction_patterns!(family)
|
||||||
|
gbp_accounts = family.accounts.where(currency: "GBP")
|
||||||
|
|
||||||
|
gbp_accounts.each do |account|
|
||||||
|
# Create income transaction (negative amount)
|
||||||
|
@generators[:transaction_generator].create_transaction!(
|
||||||
|
account: account,
|
||||||
|
amount: -random_positive_amount(500, 800), # Income (negative)
|
||||||
|
name: "GBP Consulting Payment",
|
||||||
|
date: random_date_within_days(60),
|
||||||
|
currency: "GBP"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create expense transaction (positive amount)
|
||||||
|
@generators[:transaction_generator].create_transaction!(
|
||||||
|
account: account,
|
||||||
|
amount: random_positive_amount(25, 100), # Expense (positive)
|
||||||
|
name: "GBP Purchase",
|
||||||
|
date: random_date_within_days(60),
|
||||||
|
currency: "GBP"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create minimal transfers to keep scenario focused on currency demonstration
|
||||||
|
def create_minimal_transfers!(family)
|
||||||
|
@generators[:transfer_generator].create_transfer_transactions!(family, count: 1)
|
||||||
|
puts " - Minimal account transfers created"
|
||||||
|
end
|
||||||
|
|
||||||
|
def scenario_name
|
||||||
|
SCENARIO_NAME
|
||||||
|
end
|
||||||
|
end
|
349
app/models/demo/scenarios/performance_testing.rb
Normal file
349
app/models/demo/scenarios/performance_testing.rb
Normal file
|
@ -0,0 +1,349 @@
|
||||||
|
# Performance testing scenario - high-volume data for load testing
|
||||||
|
#
|
||||||
|
# This scenario creates large volumes of realistic data to test application
|
||||||
|
# performance under load. Uses an efficient approach: generates one complete
|
||||||
|
# realistic family in Ruby, then uses SQL bulk operations to duplicate it
|
||||||
|
# 499 times for maximum performance. Ideal for:
|
||||||
|
# - Performance testing and benchmarking
|
||||||
|
# - Load testing database operations
|
||||||
|
# - UI performance testing with large datasets
|
||||||
|
# - Scalability validation at production scale
|
||||||
|
#
|
||||||
|
|
||||||
|
require "bcrypt"
|
||||||
|
|
||||||
|
class Demo::Scenarios::PerformanceTesting < Demo::BaseScenario
|
||||||
|
# Scenario characteristics and configuration
|
||||||
|
SCENARIO_NAME = "Performance Testing".freeze
|
||||||
|
PURPOSE = "High-volume data generation for performance testing and load validation".freeze
|
||||||
|
TARGET_FAMILIES = 500
|
||||||
|
TARGET_ACCOUNTS_PER_FAMILY = 29 # 3 credit cards, 5 checking, 2 savings, 10 investments, 2 properties+mortgages, 3 vehicles+2 loans, 4 other assets+liabilities
|
||||||
|
TARGET_TRANSACTIONS_PER_FAMILY = 200 # Reasonable volume for development performance testing
|
||||||
|
TARGET_TRANSFERS_PER_FAMILY = 10
|
||||||
|
SECURITIES_COUNT = 50 # Large number for investment account testing
|
||||||
|
INCLUDES_SECURITIES = true
|
||||||
|
INCLUDES_TRANSFERS = true
|
||||||
|
INCLUDES_RULES = true
|
||||||
|
|
||||||
|
# Override generate! to use our efficient bulk duplication approach
|
||||||
|
def generate!(families, **options)
|
||||||
|
puts "Creating performance test data for #{TARGET_FAMILIES} families using efficient bulk duplication..."
|
||||||
|
|
||||||
|
setup(**options) if respond_to?(:setup, true)
|
||||||
|
|
||||||
|
# Step 1: Create one complete realistic family
|
||||||
|
template_family = create_template_family!(families.first, **options)
|
||||||
|
|
||||||
|
# Step 2: Efficiently duplicate it 499 times using SQL
|
||||||
|
duplicate_family_data!(template_family, TARGET_FAMILIES - 1)
|
||||||
|
|
||||||
|
puts "Performance test data created successfully with #{TARGET_FAMILIES} families!"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Load large number of securities before generating family data
|
||||||
|
def setup(**options)
|
||||||
|
@generators[:security_generator].load_securities!(count: SECURITIES_COUNT)
|
||||||
|
puts "#{SECURITIES_COUNT} securities loaded for performance testing"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create one complete, realistic family that will serve as our template
|
||||||
|
def create_template_family!(family_or_name, **options)
|
||||||
|
# Handle both Family object and family name string
|
||||||
|
family = if family_or_name.is_a?(Family)
|
||||||
|
family_or_name
|
||||||
|
else
|
||||||
|
Family.find_by(name: family_or_name)
|
||||||
|
end
|
||||||
|
|
||||||
|
unless family
|
||||||
|
raise "Template family '#{family_or_name}' not found. Ensure family creation happened first."
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "Creating template family: #{family.name}..."
|
||||||
|
generate_family_data!(family, **options)
|
||||||
|
|
||||||
|
puts "Template family created with #{family.accounts.count} accounts and #{family.entries.count} entries"
|
||||||
|
family
|
||||||
|
end
|
||||||
|
|
||||||
|
# Efficiently duplicate the template family data using SQL bulk operations
|
||||||
|
def duplicate_family_data!(template_family, copies_needed)
|
||||||
|
puts "Duplicating template family #{copies_needed} times using efficient SQL operations..."
|
||||||
|
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
# Get all related data for the template family
|
||||||
|
template_data = extract_template_data(template_family)
|
||||||
|
|
||||||
|
# Create family records in batches
|
||||||
|
create_family_copies(template_family, copies_needed)
|
||||||
|
|
||||||
|
# Bulk duplicate all related data
|
||||||
|
duplicate_accounts_and_related_data(template_data, copies_needed)
|
||||||
|
end
|
||||||
|
|
||||||
|
puts "Successfully created #{copies_needed} family copies"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Extract all data related to the template family for duplication
|
||||||
|
def extract_template_data(family)
|
||||||
|
{
|
||||||
|
accounts: family.accounts.includes(:accountable),
|
||||||
|
entries: family.entries.includes(:entryable),
|
||||||
|
categories: family.categories,
|
||||||
|
merchants: family.merchants,
|
||||||
|
tags: family.tags,
|
||||||
|
rules: family.rules,
|
||||||
|
holdings: family.holdings
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create family and user records efficiently
|
||||||
|
def create_family_copies(template_family, count)
|
||||||
|
puts "Creating #{count} family records..."
|
||||||
|
|
||||||
|
families_data = []
|
||||||
|
users_data = []
|
||||||
|
password_digest = BCrypt::Password.create("password")
|
||||||
|
|
||||||
|
(2..count + 1).each do |i|
|
||||||
|
family_id = SecureRandom.uuid
|
||||||
|
family_name = "Performance Family #{i}"
|
||||||
|
|
||||||
|
families_data << {
|
||||||
|
id: family_id,
|
||||||
|
name: family_name,
|
||||||
|
currency: template_family.currency,
|
||||||
|
locale: template_family.locale,
|
||||||
|
country: template_family.country,
|
||||||
|
timezone: template_family.timezone,
|
||||||
|
date_format: template_family.date_format,
|
||||||
|
created_at: Time.current,
|
||||||
|
updated_at: Time.current
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create admin user
|
||||||
|
users_data << {
|
||||||
|
id: SecureRandom.uuid,
|
||||||
|
family_id: family_id,
|
||||||
|
email: "user#{i}@maybe.local",
|
||||||
|
first_name: "Demo",
|
||||||
|
last_name: "User",
|
||||||
|
role: "admin",
|
||||||
|
password_digest: password_digest,
|
||||||
|
onboarded_at: Time.current,
|
||||||
|
created_at: Time.current,
|
||||||
|
updated_at: Time.current
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create member user
|
||||||
|
users_data << {
|
||||||
|
id: SecureRandom.uuid,
|
||||||
|
family_id: family_id,
|
||||||
|
email: "member_user#{i}@maybe.local",
|
||||||
|
first_name: "Demo (member user)",
|
||||||
|
last_name: "User",
|
||||||
|
role: "member",
|
||||||
|
password_digest: password_digest,
|
||||||
|
onboarded_at: Time.current,
|
||||||
|
created_at: Time.current,
|
||||||
|
updated_at: Time.current
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Bulk insert families and users
|
||||||
|
Family.insert_all(families_data)
|
||||||
|
User.insert_all(users_data)
|
||||||
|
|
||||||
|
puts "Created #{count} families and #{users_data.length} users"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Efficiently duplicate accounts and all related data using SQL
|
||||||
|
def duplicate_accounts_and_related_data(template_data, count)
|
||||||
|
puts "Duplicating accounts and related data for #{count} families..."
|
||||||
|
|
||||||
|
new_families = Family.where("name LIKE 'Performance Family %'")
|
||||||
|
.where.not(id: template_data[:accounts].first&.family_id)
|
||||||
|
.limit(count)
|
||||||
|
|
||||||
|
new_families.find_each.with_index do |family, index|
|
||||||
|
duplicate_family_accounts_bulk(template_data, family)
|
||||||
|
puts "Completed family #{index + 1}/#{count}" if (index + 1) % 50 == 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Duplicate all accounts and related data for a single family using bulk operations
|
||||||
|
def duplicate_family_accounts_bulk(template_data, target_family)
|
||||||
|
return if template_data[:accounts].empty?
|
||||||
|
|
||||||
|
account_id_mapping = {}
|
||||||
|
|
||||||
|
# Create accounts one by one to handle accountables properly
|
||||||
|
template_data[:accounts].each do |template_account|
|
||||||
|
new_account = target_family.accounts.create!(
|
||||||
|
accountable: template_account.accountable.dup,
|
||||||
|
name: template_account.name,
|
||||||
|
balance: template_account.balance,
|
||||||
|
currency: template_account.currency,
|
||||||
|
subtype: template_account.subtype,
|
||||||
|
is_active: template_account.is_active
|
||||||
|
)
|
||||||
|
account_id_mapping[template_account.id] = new_account.id
|
||||||
|
end
|
||||||
|
|
||||||
|
# Bulk create other related data
|
||||||
|
create_bulk_categories(template_data[:categories], target_family)
|
||||||
|
create_bulk_entries_and_related(template_data, target_family, account_id_mapping)
|
||||||
|
rescue => e
|
||||||
|
puts "Error duplicating data for #{target_family.name}: #{e.message}"
|
||||||
|
# Continue with next family rather than failing completely
|
||||||
|
end
|
||||||
|
|
||||||
|
# Bulk create categories for a family
|
||||||
|
def create_bulk_categories(template_categories, target_family)
|
||||||
|
return if template_categories.empty?
|
||||||
|
|
||||||
|
# Create mapping from old category IDs to new category IDs
|
||||||
|
category_id_mapping = {}
|
||||||
|
|
||||||
|
# First pass: generate new IDs for all categories
|
||||||
|
template_categories.each do |template_category|
|
||||||
|
category_id_mapping[template_category.id] = SecureRandom.uuid
|
||||||
|
end
|
||||||
|
|
||||||
|
# Second pass: create category data with properly mapped parent_ids
|
||||||
|
categories_data = template_categories.map do |template_category|
|
||||||
|
# Map parent_id to the new family's category ID, or nil if no parent
|
||||||
|
new_parent_id = template_category.parent_id ? category_id_mapping[template_category.parent_id] : nil
|
||||||
|
|
||||||
|
{
|
||||||
|
id: category_id_mapping[template_category.id],
|
||||||
|
family_id: target_family.id,
|
||||||
|
name: template_category.name,
|
||||||
|
color: template_category.color,
|
||||||
|
classification: template_category.classification,
|
||||||
|
parent_id: new_parent_id,
|
||||||
|
created_at: Time.current,
|
||||||
|
updated_at: Time.current
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
Category.insert_all(categories_data)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Bulk create entries and related entryables
|
||||||
|
def create_bulk_entries_and_related(template_data, target_family, account_id_mapping)
|
||||||
|
return if template_data[:entries].empty?
|
||||||
|
|
||||||
|
entries_data = []
|
||||||
|
transactions_data = []
|
||||||
|
trades_data = []
|
||||||
|
|
||||||
|
template_data[:entries].each do |template_entry|
|
||||||
|
new_account_id = account_id_mapping[template_entry.account_id]
|
||||||
|
next unless new_account_id
|
||||||
|
|
||||||
|
new_entry_id = SecureRandom.uuid
|
||||||
|
new_entryable_id = SecureRandom.uuid
|
||||||
|
|
||||||
|
entries_data << {
|
||||||
|
id: new_entry_id,
|
||||||
|
account_id: new_account_id,
|
||||||
|
entryable_type: template_entry.entryable_type,
|
||||||
|
entryable_id: new_entryable_id,
|
||||||
|
name: template_entry.name,
|
||||||
|
date: template_entry.date,
|
||||||
|
amount: template_entry.amount,
|
||||||
|
currency: template_entry.currency,
|
||||||
|
notes: template_entry.notes,
|
||||||
|
created_at: Time.current,
|
||||||
|
updated_at: Time.current
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create entryable data based on type
|
||||||
|
case template_entry.entryable_type
|
||||||
|
when "Transaction"
|
||||||
|
transactions_data << {
|
||||||
|
id: new_entryable_id,
|
||||||
|
created_at: Time.current,
|
||||||
|
updated_at: Time.current
|
||||||
|
}
|
||||||
|
when "Trade"
|
||||||
|
trades_data << {
|
||||||
|
id: new_entryable_id,
|
||||||
|
security_id: template_entry.entryable.security_id,
|
||||||
|
qty: template_entry.entryable.qty,
|
||||||
|
price: template_entry.entryable.price,
|
||||||
|
currency: template_entry.entryable.currency,
|
||||||
|
created_at: Time.current,
|
||||||
|
updated_at: Time.current
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Bulk insert all data
|
||||||
|
Entry.insert_all(entries_data) if entries_data.any?
|
||||||
|
Transaction.insert_all(transactions_data) if transactions_data.any?
|
||||||
|
Trade.insert_all(trades_data) if trades_data.any?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generate high-volume family data for the template family
|
||||||
|
def generate_family_data!(family, **options)
|
||||||
|
create_foundational_data!(family)
|
||||||
|
create_high_volume_accounts!(family)
|
||||||
|
create_performance_transactions!(family)
|
||||||
|
create_performance_transfers!(family)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create rules, tags, categories and merchants for performance testing
|
||||||
|
def create_foundational_data!(family)
|
||||||
|
@generators[:rule_generator].create_tags!(family)
|
||||||
|
@generators[:rule_generator].create_categories!(family)
|
||||||
|
@generators[:rule_generator].create_merchants!(family)
|
||||||
|
@generators[:rule_generator].create_rules!(family)
|
||||||
|
puts " - Foundational data created (tags, categories, merchants, rules)"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create large numbers of accounts across all types for performance testing
|
||||||
|
def create_high_volume_accounts!(family)
|
||||||
|
@generators[:account_generator].create_credit_card_accounts!(family, count: 3)
|
||||||
|
puts " - 3 credit card accounts created"
|
||||||
|
|
||||||
|
@generators[:account_generator].create_checking_accounts!(family, count: 5)
|
||||||
|
puts " - 5 checking accounts created"
|
||||||
|
|
||||||
|
@generators[:account_generator].create_savings_accounts!(family, count: 2)
|
||||||
|
puts " - 2 savings accounts created"
|
||||||
|
|
||||||
|
@generators[:account_generator].create_investment_accounts!(family, count: 10)
|
||||||
|
puts " - 10 investment accounts created"
|
||||||
|
|
||||||
|
@generators[:account_generator].create_properties_and_mortgages!(family, count: 2)
|
||||||
|
puts " - 2 properties and mortgages created"
|
||||||
|
|
||||||
|
@generators[:account_generator].create_vehicles_and_loans!(family, vehicle_count: 3, loan_count: 2)
|
||||||
|
puts " - 3 vehicles and 2 loans created"
|
||||||
|
|
||||||
|
@generators[:account_generator].create_other_accounts!(family, asset_count: 4, liability_count: 4)
|
||||||
|
puts " - 4 other assets and 4 other liabilities created"
|
||||||
|
|
||||||
|
puts " - Total: #{TARGET_ACCOUNTS_PER_FAMILY} accounts created for performance testing"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create high-volume transactions for performance testing
|
||||||
|
def create_performance_transactions!(family)
|
||||||
|
@generators[:transaction_generator].create_performance_transactions!(family)
|
||||||
|
puts " - High-volume performance transactions created (~#{TARGET_TRANSACTIONS_PER_FAMILY} transactions)"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create multiple transfer cycles for performance testing
|
||||||
|
def create_performance_transfers!(family)
|
||||||
|
@generators[:transfer_generator].create_transfer_transactions!(family, count: TARGET_TRANSFERS_PER_FAMILY)
|
||||||
|
puts " - #{TARGET_TRANSFERS_PER_FAMILY} transfer transaction cycles created"
|
||||||
|
end
|
||||||
|
|
||||||
|
def scenario_name
|
||||||
|
SCENARIO_NAME
|
||||||
|
end
|
||||||
|
end
|
76
app/models/demo/security_generator.rb
Normal file
76
app/models/demo/security_generator.rb
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
class Demo::SecurityGenerator
|
||||||
|
include Demo::DataHelper
|
||||||
|
|
||||||
|
def load_securities!(count: 6)
|
||||||
|
if count <= 6
|
||||||
|
create_standard_securities!(count)
|
||||||
|
else
|
||||||
|
securities = create_standard_securities!(6)
|
||||||
|
securities.concat(create_performance_securities!(count - 6))
|
||||||
|
securities
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_standard_securities!(count)
|
||||||
|
securities_data = [
|
||||||
|
{ ticker: "AAPL", name: "Apple Inc.", exchange: "XNAS" },
|
||||||
|
{ ticker: "GOOGL", name: "Alphabet Inc.", exchange: "XNAS" },
|
||||||
|
{ ticker: "MSFT", name: "Microsoft Corporation", exchange: "XNAS" },
|
||||||
|
{ ticker: "AMZN", name: "Amazon.com Inc.", exchange: "XNAS" },
|
||||||
|
{ ticker: "TSLA", name: "Tesla Inc.", exchange: "XNAS" },
|
||||||
|
{ ticker: "NVDA", name: "NVIDIA Corporation", exchange: "XNAS" }
|
||||||
|
]
|
||||||
|
|
||||||
|
securities = []
|
||||||
|
count.times do |i|
|
||||||
|
data = securities_data[i]
|
||||||
|
security = create_security!(
|
||||||
|
ticker: data[:ticker],
|
||||||
|
name: data[:name],
|
||||||
|
exchange_operating_mic: data[:exchange]
|
||||||
|
)
|
||||||
|
securities << security
|
||||||
|
end
|
||||||
|
securities
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_performance_securities!(count)
|
||||||
|
securities = []
|
||||||
|
count.times do |i|
|
||||||
|
security = create_security!(
|
||||||
|
ticker: "SYM#{i + 7}",
|
||||||
|
name: "Company #{i + 7}",
|
||||||
|
exchange_operating_mic: "XNAS"
|
||||||
|
)
|
||||||
|
securities << security
|
||||||
|
end
|
||||||
|
securities
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_security!(ticker:, name:, exchange_operating_mic:)
|
||||||
|
security = Security.create!(ticker: ticker, name: name, exchange_operating_mic: exchange_operating_mic)
|
||||||
|
create_price_history!(security)
|
||||||
|
security
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_price_history!(security, extended: false)
|
||||||
|
days_back = extended ? 365 : 90
|
||||||
|
price_base = 100.0
|
||||||
|
prices = []
|
||||||
|
|
||||||
|
(0..days_back).each do |i|
|
||||||
|
date = i.days.ago.to_date
|
||||||
|
price_change = (rand - 0.5) * 10
|
||||||
|
price_base = [ price_base + price_change, 10.0 ].max
|
||||||
|
|
||||||
|
price = security.prices.create!(
|
||||||
|
date: date,
|
||||||
|
price: price_base.round(2),
|
||||||
|
currency: "USD"
|
||||||
|
)
|
||||||
|
prices << price
|
||||||
|
end
|
||||||
|
|
||||||
|
prices
|
||||||
|
end
|
||||||
|
end
|
448
app/models/demo/transaction_generator.rb
Normal file
448
app/models/demo/transaction_generator.rb
Normal file
|
@ -0,0 +1,448 @@
|
||||||
|
class Demo::TransactionGenerator
|
||||||
|
include Demo::DataHelper
|
||||||
|
|
||||||
|
def create_transaction!(attributes = {})
|
||||||
|
# Separate entry attributes from transaction attributes
|
||||||
|
entry_attributes = attributes.extract!(:account, :date, :amount, :currency, :name, :notes)
|
||||||
|
transaction_attributes = attributes # category, merchant, etc.
|
||||||
|
|
||||||
|
# Set defaults for entry
|
||||||
|
entry_defaults = {
|
||||||
|
date: 30.days.ago.to_date,
|
||||||
|
amount: 100,
|
||||||
|
currency: "USD",
|
||||||
|
name: "Demo Transaction"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create entry with transaction as entryable
|
||||||
|
entry = Entry.create!(
|
||||||
|
entry_defaults.merge(entry_attributes).merge(
|
||||||
|
entryable_type: "Transaction",
|
||||||
|
entryable_attributes: transaction_attributes
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
entry.entryable # Returns the Transaction
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_trade!(attributes = {})
|
||||||
|
# Separate entry attributes from trade attributes
|
||||||
|
entry_attributes = attributes.extract!(:account, :date, :amount, :currency, :name, :notes)
|
||||||
|
trade_attributes = attributes # security, qty, price, etc.
|
||||||
|
|
||||||
|
# Validate required trade attributes
|
||||||
|
security = trade_attributes[:security] || Security.first
|
||||||
|
unless security
|
||||||
|
raise ArgumentError, "Security is required for trade creation. Load securities first."
|
||||||
|
end
|
||||||
|
|
||||||
|
# Set defaults for entry
|
||||||
|
entry_defaults = {
|
||||||
|
date: 30.days.ago.to_date,
|
||||||
|
currency: "USD",
|
||||||
|
name: "Demo Trade"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set defaults for trade
|
||||||
|
trade_defaults = {
|
||||||
|
qty: 10,
|
||||||
|
price: 100,
|
||||||
|
currency: "USD"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Merge defaults with provided attributes
|
||||||
|
final_entry_attributes = entry_defaults.merge(entry_attributes)
|
||||||
|
final_trade_attributes = trade_defaults.merge(trade_attributes)
|
||||||
|
final_trade_attributes[:security] = security
|
||||||
|
|
||||||
|
# Calculate amount if not provided (qty * price)
|
||||||
|
unless final_entry_attributes[:amount]
|
||||||
|
final_entry_attributes[:amount] = final_trade_attributes[:qty] * final_trade_attributes[:price]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create entry with trade as entryable
|
||||||
|
entry = Entry.create!(
|
||||||
|
final_entry_attributes.merge(
|
||||||
|
entryable_type: "Trade",
|
||||||
|
entryable_attributes: final_trade_attributes
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
entry.entryable # Returns the Trade
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_realistic_transactions!(family)
|
||||||
|
categories = family.categories.limit(10)
|
||||||
|
accounts_by_type = group_accounts_by_type(family)
|
||||||
|
entries = []
|
||||||
|
|
||||||
|
# Create initial valuations for accounts before other transactions
|
||||||
|
entries.concat(create_initial_valuations!(family))
|
||||||
|
|
||||||
|
accounts_by_type[:checking].each do |account|
|
||||||
|
entries.concat(create_income_transactions!(account))
|
||||||
|
entries.concat(create_expense_transactions!(account, categories))
|
||||||
|
end
|
||||||
|
|
||||||
|
accounts_by_type[:credit_cards].each do |account|
|
||||||
|
entries.concat(create_credit_card_transactions!(account, categories))
|
||||||
|
end
|
||||||
|
|
||||||
|
accounts_by_type[:investments].each do |account|
|
||||||
|
entries.concat(create_investment_trades!(account))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Update account balances to match transaction sums
|
||||||
|
update_account_balances_from_transactions!(family)
|
||||||
|
|
||||||
|
entries
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_performance_transactions!(family)
|
||||||
|
categories = family.categories.limit(5)
|
||||||
|
accounts_by_type = group_accounts_by_type(family)
|
||||||
|
entries = []
|
||||||
|
|
||||||
|
# Create initial valuations for accounts before other transactions
|
||||||
|
entries.concat(create_initial_valuations!(family))
|
||||||
|
|
||||||
|
accounts_by_type[:checking].each do |account|
|
||||||
|
entries.concat(create_bulk_transactions!(account, PERFORMANCE_TRANSACTION_COUNTS[:depository_sample], income: true))
|
||||||
|
entries.concat(create_bulk_transactions!(account, PERFORMANCE_TRANSACTION_COUNTS[:depository_sample], income: false))
|
||||||
|
end
|
||||||
|
|
||||||
|
accounts_by_type[:credit_cards].each do |account|
|
||||||
|
entries.concat(create_bulk_transactions!(account, PERFORMANCE_TRANSACTION_COUNTS[:credit_card_sample], credit_card: true))
|
||||||
|
end
|
||||||
|
|
||||||
|
accounts_by_type[:investments].each do |account|
|
||||||
|
entries.concat(create_bulk_investment_trades!(account, PERFORMANCE_TRANSACTION_COUNTS[:investment_trades]))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Update account balances to match transaction sums
|
||||||
|
update_account_balances_from_transactions!(family)
|
||||||
|
|
||||||
|
entries
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create initial valuations for accounts to establish realistic starting values
|
||||||
|
# This is more appropriate than fake transactions
|
||||||
|
def create_initial_valuations!(family)
|
||||||
|
entries = []
|
||||||
|
|
||||||
|
family.accounts.each do |account|
|
||||||
|
initial_value = case account.accountable_type
|
||||||
|
when "Loan"
|
||||||
|
case account.name
|
||||||
|
when /Mortgage/i then 300000 # Initial mortgage debt
|
||||||
|
when /Auto/i, /Car/i then 15000 # Initial car loan debt
|
||||||
|
else 10000 # Other loan debt
|
||||||
|
end
|
||||||
|
when "CreditCard"
|
||||||
|
5000 # Initial credit card debt
|
||||||
|
when "Property"
|
||||||
|
500000 # Initial property value
|
||||||
|
when "Vehicle"
|
||||||
|
25000 # Initial vehicle value
|
||||||
|
when "OtherAsset"
|
||||||
|
5000 # Initial other asset value
|
||||||
|
when "OtherLiability"
|
||||||
|
2000 # Initial other liability debt
|
||||||
|
else
|
||||||
|
next # Skip accounts that don't need initial valuations
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create valuation entry
|
||||||
|
entry = Entry.create!(
|
||||||
|
account: account,
|
||||||
|
amount: initial_value,
|
||||||
|
name: "Initial #{account.accountable_type.humanize.downcase} valuation",
|
||||||
|
date: 2.years.ago.to_date,
|
||||||
|
currency: account.currency,
|
||||||
|
entryable_type: "Valuation",
|
||||||
|
entryable_attributes: {}
|
||||||
|
)
|
||||||
|
entries << entry
|
||||||
|
end
|
||||||
|
|
||||||
|
entries
|
||||||
|
end
|
||||||
|
|
||||||
|
# Update account balances to match the sum of their transactions and valuations
|
||||||
|
# This ensures realistic balances without artificial balancing transactions
|
||||||
|
def update_account_balances_from_transactions!(family)
|
||||||
|
family.accounts.each do |account|
|
||||||
|
transaction_sum = account.entries
|
||||||
|
.where(entryable_type: [ "Transaction", "Trade", "Valuation" ])
|
||||||
|
.sum(:amount)
|
||||||
|
|
||||||
|
# Calculate realistic balance based on transaction sum and account type
|
||||||
|
# For assets: balance should be positive, so we negate the transaction sum
|
||||||
|
# For liabilities: balance should reflect debt owed
|
||||||
|
new_balance = case account.classification
|
||||||
|
when "asset"
|
||||||
|
-transaction_sum # Assets: negative transaction sum = positive balance
|
||||||
|
when "liability"
|
||||||
|
transaction_sum # Liabilities: positive transaction sum = positive debt balance
|
||||||
|
else
|
||||||
|
-transaction_sum
|
||||||
|
end
|
||||||
|
|
||||||
|
# Ensure minimum realistic balance
|
||||||
|
new_balance = [ new_balance, minimum_realistic_balance(account) ].max
|
||||||
|
|
||||||
|
account.update!(balance: new_balance)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def create_income_transactions!(account)
|
||||||
|
entries = []
|
||||||
|
|
||||||
|
6.times do |i|
|
||||||
|
transaction = create_transaction!(
|
||||||
|
account: account,
|
||||||
|
name: "Salary #{i + 1}",
|
||||||
|
amount: -income_amount,
|
||||||
|
date: random_date_within_days(90),
|
||||||
|
currency: account.currency
|
||||||
|
)
|
||||||
|
entries << transaction.entry
|
||||||
|
end
|
||||||
|
|
||||||
|
entries
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_expense_transactions!(account, categories)
|
||||||
|
entries = []
|
||||||
|
expense_types = [
|
||||||
|
{ name: "Grocery Store", amount_range: [ 50, 200 ] },
|
||||||
|
{ name: "Gas Station", amount_range: [ 30, 80 ] },
|
||||||
|
{ name: "Restaurant", amount_range: [ 25, 150 ] },
|
||||||
|
{ name: "Online Purchase", amount_range: [ 20, 300 ] }
|
||||||
|
]
|
||||||
|
|
||||||
|
20.times do
|
||||||
|
expense_type = expense_types.sample
|
||||||
|
transaction = create_transaction!(
|
||||||
|
account: account,
|
||||||
|
name: expense_type[:name],
|
||||||
|
amount: expense_amount(expense_type[:amount_range][0], expense_type[:amount_range][1]),
|
||||||
|
date: random_date_within_days(90),
|
||||||
|
currency: account.currency,
|
||||||
|
category: categories.sample
|
||||||
|
)
|
||||||
|
entries << transaction.entry
|
||||||
|
end
|
||||||
|
|
||||||
|
entries
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_credit_card_transactions!(account, categories)
|
||||||
|
entries = []
|
||||||
|
|
||||||
|
credit_card_merchants = [
|
||||||
|
{ name: "Amazon Purchase", amount_range: [ 25, 500 ] },
|
||||||
|
{ name: "Target", amount_range: [ 30, 150 ] },
|
||||||
|
{ name: "Coffee Shop", amount_range: [ 5, 15 ] },
|
||||||
|
{ name: "Department Store", amount_range: [ 50, 300 ] },
|
||||||
|
{ name: "Subscription Service", amount_range: [ 10, 50 ] }
|
||||||
|
]
|
||||||
|
|
||||||
|
25.times do
|
||||||
|
merchant_data = credit_card_merchants.sample
|
||||||
|
transaction = create_transaction!(
|
||||||
|
account: account,
|
||||||
|
name: merchant_data[:name],
|
||||||
|
amount: expense_amount(merchant_data[:amount_range][0], merchant_data[:amount_range][1]),
|
||||||
|
date: random_date_within_days(90),
|
||||||
|
currency: account.currency,
|
||||||
|
category: categories.sample
|
||||||
|
)
|
||||||
|
entries << transaction.entry
|
||||||
|
end
|
||||||
|
|
||||||
|
entries
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_investment_trades!(account)
|
||||||
|
securities = Security.limit(3)
|
||||||
|
return [] unless securities.any?
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
|
||||||
|
trade_patterns = [
|
||||||
|
{ type: "buy", qty_range: [ 1, 50 ] },
|
||||||
|
{ type: "buy", qty_range: [ 10, 100 ] },
|
||||||
|
{ type: "sell", qty_range: [ 1, 25 ] }
|
||||||
|
]
|
||||||
|
|
||||||
|
15.times do
|
||||||
|
security = securities.sample
|
||||||
|
pattern = trade_patterns.sample
|
||||||
|
|
||||||
|
recent_price = security.prices.order(date: :desc).first&.price || 100.0
|
||||||
|
|
||||||
|
trade = create_trade!(
|
||||||
|
account: account,
|
||||||
|
security: security,
|
||||||
|
qty: rand(pattern[:qty_range][0]..pattern[:qty_range][1]),
|
||||||
|
price: recent_price * (0.95 + rand * 0.1),
|
||||||
|
date: random_date_within_days(90),
|
||||||
|
currency: account.currency
|
||||||
|
)
|
||||||
|
entries << trade.entry
|
||||||
|
end
|
||||||
|
|
||||||
|
entries
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_bulk_investment_trades!(account, count)
|
||||||
|
securities = Security.limit(5)
|
||||||
|
return [] unless securities.any?
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
|
||||||
|
count.times do |i|
|
||||||
|
security = securities.sample
|
||||||
|
recent_price = security.prices.order(date: :desc).first&.price || 100.0
|
||||||
|
|
||||||
|
trade = create_trade!(
|
||||||
|
account: account,
|
||||||
|
security: security,
|
||||||
|
qty: rand(1..100),
|
||||||
|
price: recent_price * (0.9 + rand * 0.2),
|
||||||
|
date: random_date_within_days(365),
|
||||||
|
currency: account.currency,
|
||||||
|
name: "Bulk Trade #{i + 1}"
|
||||||
|
)
|
||||||
|
entries << trade.entry
|
||||||
|
end
|
||||||
|
|
||||||
|
entries
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_bulk_transactions!(account, count, income: false, credit_card: false)
|
||||||
|
entries = []
|
||||||
|
|
||||||
|
# Handle credit cards specially to ensure balanced purchases and payments
|
||||||
|
if account.accountable_type == "CreditCard"
|
||||||
|
# Create a mix of purchases (positive) and payments (negative)
|
||||||
|
purchase_count = (count * 0.8).to_i # 80% purchases
|
||||||
|
payment_count = count - purchase_count # 20% payments
|
||||||
|
|
||||||
|
total_purchases = 0
|
||||||
|
|
||||||
|
# Create purchases first
|
||||||
|
purchase_count.times do |i|
|
||||||
|
amount = expense_amount(10, 200) # Credit card purchases (positive)
|
||||||
|
total_purchases += amount
|
||||||
|
|
||||||
|
transaction = create_transaction!(
|
||||||
|
account: account,
|
||||||
|
name: "Bulk CC Purchase #{i + 1}",
|
||||||
|
amount: amount,
|
||||||
|
date: random_date_within_days(365),
|
||||||
|
currency: account.currency
|
||||||
|
)
|
||||||
|
entries << transaction.entry
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create reasonable payments (negative amounts)
|
||||||
|
# Payments should be smaller than total debt available
|
||||||
|
initial_debt = 5000 # From initial valuation
|
||||||
|
available_debt = initial_debt + total_purchases
|
||||||
|
|
||||||
|
payment_count.times do |i|
|
||||||
|
# Payment should be reasonable portion of available debt
|
||||||
|
max_payment = [ available_debt * 0.1, 500 ].max # 10% of debt or min $500
|
||||||
|
amount = -expense_amount(50, max_payment.to_i) # Payment (negative)
|
||||||
|
|
||||||
|
transaction = create_transaction!(
|
||||||
|
account: account,
|
||||||
|
name: "Credit card payment #{i + 1}",
|
||||||
|
amount: amount,
|
||||||
|
date: random_date_within_days(365),
|
||||||
|
currency: account.currency
|
||||||
|
)
|
||||||
|
entries << transaction.entry
|
||||||
|
end
|
||||||
|
|
||||||
|
else
|
||||||
|
# Regular handling for non-credit card accounts
|
||||||
|
count.times do |i|
|
||||||
|
amount = if income
|
||||||
|
-income_amount # Income (negative)
|
||||||
|
elsif credit_card
|
||||||
|
expense_amount(10, 200) # This path shouldn't be reached for actual credit cards
|
||||||
|
else
|
||||||
|
expense_amount(5, 500) # Regular expenses (positive)
|
||||||
|
end
|
||||||
|
|
||||||
|
name_prefix = if income
|
||||||
|
"Bulk Income"
|
||||||
|
elsif credit_card
|
||||||
|
"Bulk CC Purchase"
|
||||||
|
else
|
||||||
|
"Bulk Expense"
|
||||||
|
end
|
||||||
|
|
||||||
|
transaction = create_transaction!(
|
||||||
|
account: account,
|
||||||
|
name: "#{name_prefix} #{i + 1}",
|
||||||
|
amount: amount,
|
||||||
|
date: random_date_within_days(365),
|
||||||
|
currency: account.currency
|
||||||
|
)
|
||||||
|
entries << transaction.entry
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
entries
|
||||||
|
end
|
||||||
|
|
||||||
|
def expense_amount(min_or_range = :small, max = nil)
|
||||||
|
if min_or_range.is_a?(Symbol)
|
||||||
|
case min_or_range
|
||||||
|
when :small then random_amount(10, 200)
|
||||||
|
when :medium then random_amount(50, 500)
|
||||||
|
when :large then random_amount(200, 1000)
|
||||||
|
when :credit_card then random_amount(20, 300)
|
||||||
|
else random_amount(10, 500)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
max_amount = max || (min_or_range + 100)
|
||||||
|
random_amount(min_or_range, max_amount)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def income_amount(type = :salary)
|
||||||
|
case type
|
||||||
|
when :salary then random_amount(3000, 7000)
|
||||||
|
when :dividend then random_amount(100, 500)
|
||||||
|
when :interest then random_amount(50, 200)
|
||||||
|
else random_amount(1000, 5000)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Determine minimum realistic balance for account type
|
||||||
|
def minimum_realistic_balance(account)
|
||||||
|
case account.accountable_type
|
||||||
|
when "Depository"
|
||||||
|
account.subtype == "savings" ? 1000 : 500
|
||||||
|
when "Investment"
|
||||||
|
5000
|
||||||
|
when "Property"
|
||||||
|
100000
|
||||||
|
when "Vehicle"
|
||||||
|
5000
|
||||||
|
when "OtherAsset"
|
||||||
|
100
|
||||||
|
when "CreditCard", "Loan", "OtherLiability"
|
||||||
|
100 # Minimum debt
|
||||||
|
else
|
||||||
|
100
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
159
app/models/demo/transfer_generator.rb
Normal file
159
app/models/demo/transfer_generator.rb
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
class Demo::TransferGenerator
|
||||||
|
include Demo::DataHelper
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_transfer_transactions!(family, count: 1)
|
||||||
|
accounts_by_type = group_accounts_by_type(family)
|
||||||
|
created_transfers = []
|
||||||
|
|
||||||
|
count.times do |i|
|
||||||
|
suffix = count > 1 ? "_#{i + 1}" : ""
|
||||||
|
|
||||||
|
created_transfers.concat(create_credit_card_payments!(accounts_by_type, suffix: suffix))
|
||||||
|
created_transfers.concat(create_investment_contributions!(accounts_by_type, suffix: suffix))
|
||||||
|
created_transfers.concat(create_savings_transfers!(accounts_by_type, suffix: suffix))
|
||||||
|
created_transfers.concat(create_loan_payments!(accounts_by_type, suffix: suffix))
|
||||||
|
end
|
||||||
|
|
||||||
|
created_transfers
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_transfer!(from_account:, to_account:, amount:, date:, description: "")
|
||||||
|
transfer = Transfer.from_accounts(
|
||||||
|
from_account: from_account,
|
||||||
|
to_account: to_account,
|
||||||
|
date: date,
|
||||||
|
amount: amount
|
||||||
|
)
|
||||||
|
|
||||||
|
transfer.inflow_transaction.entry.update!(
|
||||||
|
name: "#{description.presence || 'Transfer'} from #{from_account.name}"
|
||||||
|
)
|
||||||
|
transfer.outflow_transaction.entry.update!(
|
||||||
|
name: "#{description.presence || 'Transfer'} to #{to_account.name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
transfer.status = "confirmed"
|
||||||
|
transfer.save!
|
||||||
|
|
||||||
|
transfer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def create_credit_card_payments!(accounts_by_type, suffix: "")
|
||||||
|
checking_accounts = accounts_by_type[:checking]
|
||||||
|
credit_cards = accounts_by_type[:credit_cards]
|
||||||
|
transfers = []
|
||||||
|
|
||||||
|
return transfers unless checking_accounts.any? && credit_cards.any?
|
||||||
|
|
||||||
|
checking = checking_accounts.first
|
||||||
|
|
||||||
|
credit_cards.each_with_index do |credit_card, index|
|
||||||
|
payment_amount = [ credit_card.balance.abs * 0.3, 500 ].max
|
||||||
|
payment_date = (7 + index * 3).days.ago.to_date
|
||||||
|
|
||||||
|
transfer = create_transfer!(
|
||||||
|
from_account: checking,
|
||||||
|
to_account: credit_card,
|
||||||
|
amount: payment_amount,
|
||||||
|
date: payment_date,
|
||||||
|
description: "Credit card payment#{suffix}"
|
||||||
|
)
|
||||||
|
transfers << transfer
|
||||||
|
end
|
||||||
|
|
||||||
|
transfers
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_investment_contributions!(accounts_by_type, suffix: "")
|
||||||
|
checking_accounts = accounts_by_type[:checking]
|
||||||
|
investment_accounts = accounts_by_type[:investments]
|
||||||
|
transfers = []
|
||||||
|
|
||||||
|
return transfers unless checking_accounts.any? && investment_accounts.any?
|
||||||
|
|
||||||
|
checking = checking_accounts.first
|
||||||
|
|
||||||
|
investment_accounts.each_with_index do |investment, index|
|
||||||
|
contribution_amount = case investment.name
|
||||||
|
when /401k/i then 1500
|
||||||
|
when /Roth/i then 500
|
||||||
|
else 1000
|
||||||
|
end
|
||||||
|
|
||||||
|
contribution_date = (14 + index * 7).days.ago.to_date
|
||||||
|
|
||||||
|
transfer = create_transfer!(
|
||||||
|
from_account: checking,
|
||||||
|
to_account: investment,
|
||||||
|
amount: contribution_amount,
|
||||||
|
date: contribution_date,
|
||||||
|
description: "Investment contribution#{suffix}"
|
||||||
|
)
|
||||||
|
transfers << transfer
|
||||||
|
end
|
||||||
|
|
||||||
|
transfers
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_savings_transfers!(accounts_by_type, suffix: "")
|
||||||
|
checking_accounts = accounts_by_type[:checking]
|
||||||
|
savings_accounts = accounts_by_type[:savings]
|
||||||
|
transfers = []
|
||||||
|
|
||||||
|
return transfers unless checking_accounts.any? && savings_accounts.any?
|
||||||
|
|
||||||
|
checking = checking_accounts.first
|
||||||
|
|
||||||
|
savings_accounts.each_with_index do |savings, index|
|
||||||
|
transfer_amount = 1000
|
||||||
|
transfer_date = (21 + index * 5).days.ago.to_date
|
||||||
|
|
||||||
|
transfer = create_transfer!(
|
||||||
|
from_account: checking,
|
||||||
|
to_account: savings,
|
||||||
|
amount: transfer_amount,
|
||||||
|
date: transfer_date,
|
||||||
|
description: "Savings transfer#{suffix}"
|
||||||
|
)
|
||||||
|
transfers << transfer
|
||||||
|
end
|
||||||
|
|
||||||
|
transfers
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_loan_payments!(accounts_by_type, suffix: "")
|
||||||
|
checking_accounts = accounts_by_type[:checking]
|
||||||
|
loans = accounts_by_type[:loans]
|
||||||
|
transfers = []
|
||||||
|
|
||||||
|
return transfers unless checking_accounts.any? && loans.any?
|
||||||
|
|
||||||
|
checking = checking_accounts.first
|
||||||
|
|
||||||
|
loans.each_with_index do |loan, index|
|
||||||
|
payment_amount = case loan.name
|
||||||
|
when /Mortgage/i then 2500
|
||||||
|
when /Auto/i, /Car/i then 450
|
||||||
|
else 500
|
||||||
|
end
|
||||||
|
|
||||||
|
payment_date = (28 + index * 2).days.ago.to_date
|
||||||
|
|
||||||
|
transfer = create_transfer!(
|
||||||
|
from_account: checking,
|
||||||
|
to_account: loan,
|
||||||
|
amount: payment_amount,
|
||||||
|
date: payment_date,
|
||||||
|
description: "Loan payment#{suffix}"
|
||||||
|
)
|
||||||
|
transfers << transfer
|
||||||
|
end
|
||||||
|
|
||||||
|
transfers
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,28 +1,39 @@
|
||||||
namespace :demo_data do
|
namespace :demo_data do
|
||||||
desc "Creates or resets demo data used in development environment"
|
desc "Creates a new user with no data. Use for testing empty data states."
|
||||||
task empty: :environment do
|
task empty: :environment do
|
||||||
families = [ "Demo Family 1" ]
|
families = [ "Demo Family 1" ]
|
||||||
Demo::Generator.new.reset_and_clear_data!(families)
|
Demo::Generator.new.reset_and_clear_data!(families)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc "Creates a new user who has to go through onboarding still. Use for testing onboarding flows."
|
||||||
task new_user: :environment do
|
task new_user: :environment do
|
||||||
families = [ "Demo Family 1" ]
|
families = [ "Demo Family 1" ]
|
||||||
Demo::Generator.new.reset_and_clear_data!(families, require_onboarding: true)
|
Demo::Generator.new.reset_and_clear_data!(families, require_onboarding: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc "General data reset that loads semi-realistic data"
|
||||||
task :reset, [ :count ] => :environment do |t, args|
|
task :reset, [ :count ] => :environment do |t, args|
|
||||||
count = (args[:count] || 1).to_i
|
count = (args[:count] || 1).to_i
|
||||||
families = count.times.map { |i| "Demo Family #{i + 1}" }
|
families = count.times.map { |i| "Demo Family #{i + 1}" }
|
||||||
Demo::Generator.new.reset_data!(families)
|
Demo::Generator.new.reset_data!(families)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc "Use this when you need to test multi-currency features of the app with a minimal setup"
|
||||||
task multi_currency: :environment do
|
task multi_currency: :environment do
|
||||||
families = [ "Demo Family 1", "Demo Family 2" ]
|
families = [ "Demo Family 1", "Demo Family 2" ]
|
||||||
Demo::Generator.new.generate_multi_currency_data!(families)
|
Demo::Generator.new.generate_multi_currency_data!(families)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc "Use this when you want realistic budget data"
|
||||||
task basic_budget: :environment do
|
task basic_budget: :environment do
|
||||||
families = [ "Demo Family 1" ]
|
families = [ "Demo Family 1" ]
|
||||||
Demo::Generator.new.generate_basic_budget_data!(families)
|
Demo::Generator.new.generate_basic_budget_data!(families)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# DO NOT RUN THIS unless you're testing performance locally. It will take a long time to load/clear. Easiest to clear with a db:reset
|
||||||
|
desc "Generates realistic data for 500 families for performance testing. Creates 1 family with Ruby, then efficiently duplicates it 499 times using SQL bulk operations."
|
||||||
|
task performance_testing: :environment do
|
||||||
|
families = [ "Performance Family 1" ]
|
||||||
|
Demo::Generator.new.generate_performance_testing_data!(families)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue