mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-05 13:35:21 +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
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
|
Loading…
Add table
Add a link
Reference in a new issue