1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-19 05:09:38 +02:00
Maybe/app/models/demo/generator.rb
Zach Gollwitzer e657c40d19
Account:: namespace simplifications and cleanup (#2110)
* Flatten Holding model

* Flatten balance model

* Entries domain renames

* Fix valuations reference

* Fix trades stream

* Fix brakeman warnings

* Fix tests

* Replace existing entryable type references in DB
2025-04-14 11:40:34 -04:00

510 lines
17 KiB
Ruby

class Demo::Generator
COLORS = %w[#e99537 #4da568 #6471eb #db5a54 #df4e92 #c44fe9 #eb5429 #61c9ea #805dee #6ad28a]
# Builds a semi-realistic mirror of what production data might look like
def reset_and_clear_data!(family_names)
puts "Clearing existing data..."
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"
end
def reset_data!(family_names)
puts "Clearing existing data..."
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"
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)
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
def generate_basic_budget_data!(family_names)
puts "Clearing existing data..."
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
def generate_multi_currency_data!(family_names)
puts "Clearing existing data..."
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
private
def destroy_everything!
Family.destroy_all
Setting.destroy_all
InviteCode.destroy_all
ExchangeRate.destroy_all
Security.destroy_all
Security::Price.destroy_all
end
def create_family_and_user!(family_name, user_email, data_enrichment_enabled: false, currency: "USD")
base_uuid = "d99e3c6e-d513-4452-8f24-dc263f8528c0"
id = Digest::UUID.uuid_v5(base_uuid, family_name)
family = Family.create!(
id: id,
name: family_name,
currency: currency,
stripe_subscription_status: "active",
data_enrichment_enabled: data_enrichment_enabled,
locale: "en",
country: "US",
timezone: "America/New_York",
date_format: "%m-%d-%Y"
)
family.users.create! \
email: user_email,
first_name: "Demo",
last_name: "User",
role: "admin",
password: "password",
onboarded_at: Time.current
family.users.create! \
email: "member_#{user_email}",
first_name: "Demo (member user)",
last_name: "User",
role: "member",
password: "password",
onboarded_at: Time.current
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_defaults
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|
family.merchants.create!(name: merchant, 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