mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 21:29:38 +02:00
Improve speed of transactions page (#1752)
* Make demo data more realistic * Fix N+1 transactions query * Lint fixes * Totals query * Consolidate stats calcs * Fix preload * Fix filter clearing * Fix N+1 queries for family sync detection * Reduce queries for rendering transfers * Fix tests * Remove flaky test
This commit is contained in:
parent
53f4b32c33
commit
2c2b600163
22 changed files with 209 additions and 195 deletions
2
Gemfile
2
Gemfile
|
@ -28,6 +28,7 @@ gem "good_job"
|
||||||
|
|
||||||
# Error logging
|
# Error logging
|
||||||
gem "stackprof"
|
gem "stackprof"
|
||||||
|
gem "rack-mini-profiler"
|
||||||
gem "sentry-ruby"
|
gem "sentry-ruby"
|
||||||
gem "sentry-rails"
|
gem "sentry-rails"
|
||||||
|
|
||||||
|
@ -67,6 +68,7 @@ group :development do
|
||||||
gem "ruby-lsp-rails"
|
gem "ruby-lsp-rails"
|
||||||
gem "web-console"
|
gem "web-console"
|
||||||
gem "faker"
|
gem "faker"
|
||||||
|
gem "benchmark-ips"
|
||||||
end
|
end
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
|
|
|
@ -101,6 +101,7 @@ GEM
|
||||||
base64 (0.2.0)
|
base64 (0.2.0)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.20)
|
||||||
benchmark (0.4.0)
|
benchmark (0.4.0)
|
||||||
|
benchmark-ips (2.14.0)
|
||||||
better_html (2.1.1)
|
better_html (2.1.1)
|
||||||
actionview (>= 6.0)
|
actionview (>= 6.0)
|
||||||
activesupport (>= 6.0)
|
activesupport (>= 6.0)
|
||||||
|
@ -317,6 +318,8 @@ GEM
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.1.8)
|
rack (3.1.8)
|
||||||
|
rack-mini-profiler (3.3.1)
|
||||||
|
rack (>= 1.2.0)
|
||||||
rack-session (2.1.0)
|
rack-session (2.1.0)
|
||||||
base64 (>= 0.1.0)
|
base64 (>= 0.1.0)
|
||||||
rack (>= 3.0.0)
|
rack (>= 3.0.0)
|
||||||
|
@ -501,6 +504,7 @@ PLATFORMS
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
aws-sdk-s3 (~> 1.177.0)
|
aws-sdk-s3 (~> 1.177.0)
|
||||||
bcrypt (~> 3.1)
|
bcrypt (~> 3.1)
|
||||||
|
benchmark-ips
|
||||||
bootsnap
|
bootsnap
|
||||||
brakeman
|
brakeman
|
||||||
capybara
|
capybara
|
||||||
|
@ -531,6 +535,7 @@ DEPENDENCIES
|
||||||
plaid
|
plaid
|
||||||
propshaft
|
propshaft
|
||||||
puma (>= 5.0)
|
puma (>= 5.0)
|
||||||
|
rack-mini-profiler
|
||||||
rails (~> 7.2.2)
|
rails (~> 7.2.2)
|
||||||
rails-settings-cached
|
rails-settings-cached
|
||||||
redcarpet
|
redcarpet
|
||||||
|
|
|
@ -21,7 +21,7 @@ class PagesController < ApplicationController
|
||||||
|
|
||||||
@accounts = Current.family.accounts.active
|
@accounts = Current.family.accounts.active
|
||||||
@account_groups = @accounts.by_group(period: @period, currency: Current.family.currency)
|
@account_groups = @accounts.by_group(period: @period, currency: Current.family.currency)
|
||||||
@transaction_entries = Current.family.entries.account_transactions.limit(6).reverse_chronological
|
@transaction_entries = Current.family.entries.incomes_and_expenses.limit(6).reverse_chronological
|
||||||
|
|
||||||
# TODO: Placeholders for trendlines
|
# TODO: Placeholders for trendlines
|
||||||
placeholder_series_data = 10.times.map do |i|
|
placeholder_series_data = 10.times.map do |i|
|
||||||
|
|
|
@ -7,30 +7,36 @@ class TransactionsController < ApplicationController
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@q = search_params
|
@q = search_params
|
||||||
search_query = Current.family.transactions.search(@q).reverse_chronological
|
search_query = Current.family.transactions.search(@q)
|
||||||
|
|
||||||
set_focused_record(search_query, params[:focused_record_id], default_per_page: 50)
|
set_focused_record(search_query, params[:focused_record_id], default_per_page: 50)
|
||||||
|
|
||||||
@pagy, @transaction_entries = pagy(
|
@pagy, @transaction_entries = pagy(
|
||||||
search_query,
|
search_query.reverse_chronological.preload(
|
||||||
|
:account,
|
||||||
|
entryable: [
|
||||||
|
:category, :merchant, :tags,
|
||||||
|
:transfer_as_inflow,
|
||||||
|
transfer_as_outflow: {
|
||||||
|
inflow_transaction: { entry: :account },
|
||||||
|
outflow_transaction: { entry: :account }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
),
|
||||||
limit: params[:per_page].presence || default_params[:per_page],
|
limit: params[:per_page].presence || default_params[:per_page],
|
||||||
params: ->(params) { params.except(:focused_record_id) }
|
params: ->(params) { params.except(:focused_record_id) }
|
||||||
)
|
)
|
||||||
|
|
||||||
totals_query = search_query.incomes_and_expenses
|
@transfers = @transaction_entries.map { |entry| entry.entryable.transfer_as_outflow }.compact
|
||||||
family_currency = Current.family.currency
|
@totals = search_query.stats(Current.family.currency)
|
||||||
count_with_transfers = search_query.count
|
|
||||||
count_without_transfers = totals_query.count
|
|
||||||
|
|
||||||
@totals = {
|
|
||||||
count: ((count_with_transfers - count_without_transfers) / 2) + count_without_transfers,
|
|
||||||
income: totals_query.income_total(family_currency).abs,
|
|
||||||
expense: totals_query.expense_total(family_currency)
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_filter
|
def clear_filter
|
||||||
updated_params = stored_params.deep_dup
|
updated_params = {
|
||||||
|
"q" => search_params,
|
||||||
|
"page" => params[:page],
|
||||||
|
"per_page" => params[:per_page]
|
||||||
|
}
|
||||||
|
|
||||||
q_params = updated_params["q"] || {}
|
q_params = updated_params["q"] || {}
|
||||||
|
|
||||||
|
|
|
@ -8,10 +8,10 @@ module Account::EntriesHelper
|
||||||
transfers.map(&:transfer).uniq
|
transfers.map(&:transfer).uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
def entries_by_date(entries, selectable: true, totals: false)
|
def entries_by_date(entries, transfers: [], selectable: true, totals: false)
|
||||||
entries.reverse_chronological.group_by(&:date).map do |date, grouped_entries|
|
entries.group_by(&:date).map do |date, grouped_entries|
|
||||||
content = capture do
|
content = capture do
|
||||||
yield grouped_entries
|
yield [ grouped_entries, transfers.select { |t| t.outflow_transaction.entry.date == date } ]
|
||||||
end
|
end
|
||||||
|
|
||||||
next if content.blank?
|
next if content.blank?
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
class Account::Entry < ApplicationRecord
|
class Account::Entry < ApplicationRecord
|
||||||
include Monetizable
|
include Monetizable
|
||||||
|
|
||||||
|
Stats = Struct.new(:currency, :count, :income_total, :expense_total, keyword_init: true)
|
||||||
|
|
||||||
monetize :amount
|
monetize :amount
|
||||||
|
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
@ -33,11 +35,11 @@ class Account::Entry < ApplicationRecord
|
||||||
# All non-transfer entries, rejected transfers, and the outflow of a loan payment transfer are incomes/expenses
|
# All non-transfer entries, rejected transfers, and the outflow of a loan payment transfer are incomes/expenses
|
||||||
scope :incomes_and_expenses, -> {
|
scope :incomes_and_expenses, -> {
|
||||||
joins("INNER JOIN account_transactions ON account_transactions.id = account_entries.entryable_id AND account_entries.entryable_type = 'Account::Transaction'")
|
joins("INNER JOIN account_transactions ON account_transactions.id = account_entries.entryable_id AND account_entries.entryable_type = 'Account::Transaction'")
|
||||||
.joins("LEFT JOIN transfers ON transfers.inflow_transaction_id = account_transactions.id OR transfers.outflow_transaction_id = account_transactions.id")
|
.joins("LEFT JOIN transfers ON transfers.inflow_transaction_id = account_transactions.id OR transfers.outflow_transaction_id = account_transactions.id")
|
||||||
.joins("LEFT JOIN account_transactions inflow_txns ON inflow_txns.id = transfers.inflow_transaction_id")
|
.joins("LEFT JOIN account_transactions inflow_txns ON inflow_txns.id = transfers.inflow_transaction_id")
|
||||||
.joins("LEFT JOIN account_entries inflow_entries ON inflow_entries.entryable_id = inflow_txns.id AND inflow_entries.entryable_type = 'Account::Transaction'")
|
.joins("LEFT JOIN account_entries inflow_entries ON inflow_entries.entryable_id = inflow_txns.id AND inflow_entries.entryable_type = 'Account::Transaction'")
|
||||||
.joins("LEFT JOIN accounts inflow_accounts ON inflow_accounts.id = inflow_entries.account_id")
|
.joins("LEFT JOIN accounts inflow_accounts ON inflow_accounts.id = inflow_entries.account_id")
|
||||||
.where("transfers.id IS NULL OR transfers.status = 'rejected' OR (account_entries.amount > 0 AND inflow_accounts.accountable_type = 'Loan')")
|
.where("transfers.id IS NULL OR transfers.status = 'rejected' OR (account_entries.amount > 0 AND inflow_accounts.accountable_type = 'Loan')")
|
||||||
}
|
}
|
||||||
|
|
||||||
scope :incomes, -> {
|
scope :incomes, -> {
|
||||||
|
@ -154,20 +156,24 @@ class Account::Entry < ApplicationRecord
|
||||||
all.size
|
all.size
|
||||||
end
|
end
|
||||||
|
|
||||||
def income_total(currency = "USD", start_date: nil, end_date: nil)
|
def stats(currency = "USD")
|
||||||
total = incomes.where(date: start_date..end_date)
|
result = all
|
||||||
.map { |e| e.amount_money.exchange_to(currency, date: e.date, fallback_rate: 0) }
|
.incomes_and_expenses
|
||||||
.sum
|
.joins(sanitize_sql_array([ "LEFT JOIN exchange_rates er ON account_entries.date = er.date AND account_entries.currency = er.from_currency AND er.to_currency = ?", currency ]))
|
||||||
|
.select(
|
||||||
|
"COUNT(*) AS count",
|
||||||
|
"SUM(CASE WHEN account_entries.amount < 0 THEN (account_entries.amount * COALESCE(er.rate, 1)) ELSE 0 END) AS income_total",
|
||||||
|
"SUM(CASE WHEN account_entries.amount > 0 THEN (account_entries.amount * COALESCE(er.rate, 1)) ELSE 0 END) AS expense_total"
|
||||||
|
)
|
||||||
|
.to_a
|
||||||
|
.first
|
||||||
|
|
||||||
Money.new(total, currency)
|
Stats.new(
|
||||||
end
|
currency: currency,
|
||||||
|
count: result.count,
|
||||||
def expense_total(currency = "USD", start_date: nil, end_date: nil)
|
income_total: result.income_total ? result.income_total * -1 : 0,
|
||||||
total = expenses.where(date: start_date..end_date)
|
expense_total: result.expense_total || 0
|
||||||
.map { |e| e.amount_money.exchange_to(currency, date: e.date, fallback_rate: 0) }
|
)
|
||||||
.sum
|
|
||||||
|
|
||||||
Money.new(total, currency)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,92 +1,115 @@
|
||||||
class Demo::Generator
|
class Demo::Generator
|
||||||
COLORS = %w[#e99537 #4da568 #6471eb #db5a54 #df4e92 #c44fe9 #eb5429 #61c9ea #805dee #6ad28a]
|
COLORS = %w[#e99537 #4da568 #6471eb #db5a54 #df4e92 #c44fe9 #eb5429 #61c9ea #805dee #6ad28a]
|
||||||
|
|
||||||
def initialize
|
# Builds a semi-realistic mirror of what production data might look like
|
||||||
@family = reset_family!
|
def reset_and_clear_data!(family_names)
|
||||||
end
|
puts "Clearing existing data..."
|
||||||
|
|
||||||
def reset_and_clear_data!
|
destroy_everything!
|
||||||
reset_settings!
|
|
||||||
clear_data!
|
|
||||||
create_user!
|
|
||||||
|
|
||||||
puts "user reset"
|
puts "Data cleared"
|
||||||
end
|
|
||||||
|
|
||||||
def reset_data!
|
family_names.each_with_index do |family_name, index|
|
||||||
Family.transaction do
|
create_family_and_user!(family_name, "user#{index == 0 ? "" : index + 1}@maybe.local")
|
||||||
reset_settings!
|
|
||||||
clear_data!
|
|
||||||
create_user!
|
|
||||||
|
|
||||||
puts "user reset"
|
|
||||||
|
|
||||||
create_tags!
|
|
||||||
create_categories!
|
|
||||||
create_merchants!
|
|
||||||
|
|
||||||
puts "tags, categories, merchants created"
|
|
||||||
|
|
||||||
create_credit_card_account!
|
|
||||||
create_checking_account!
|
|
||||||
create_savings_account!
|
|
||||||
|
|
||||||
create_investment_account!
|
|
||||||
create_house_and_mortgage!
|
|
||||||
create_car_and_loan!
|
|
||||||
create_other_accounts!
|
|
||||||
|
|
||||||
create_transfer_transactions!
|
|
||||||
|
|
||||||
puts "accounts created"
|
|
||||||
puts "Demo data loaded successfully!"
|
|
||||||
end
|
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", data_enrichment_enabled: index == 0)
|
||||||
|
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
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
def destroy_everything!
|
||||||
attr_reader :family
|
Family.destroy_all
|
||||||
|
Setting.destroy_all
|
||||||
def reset_family!
|
|
||||||
family_id = "d99e3c6e-d513-4452-8f24-dc263f8528c0" # deterministic demo id
|
|
||||||
|
|
||||||
family = Family.find_by(id: family_id)
|
|
||||||
Transfer.destroy_all
|
|
||||||
family.destroy! if family
|
|
||||||
|
|
||||||
Family.create!(id: family_id, name: "Demo Family", stripe_subscription_status: "active").tap(&:reload)
|
|
||||||
end
|
|
||||||
|
|
||||||
def clear_data!
|
|
||||||
Transfer.destroy_all
|
|
||||||
InviteCode.destroy_all
|
InviteCode.destroy_all
|
||||||
User.find_by_email("user@maybe.local")&.destroy
|
|
||||||
ExchangeRate.destroy_all
|
ExchangeRate.destroy_all
|
||||||
Security.destroy_all
|
Security.destroy_all
|
||||||
Security::Price.destroy_all
|
Security::Price.destroy_all
|
||||||
end
|
end
|
||||||
|
|
||||||
def reset_settings!
|
def create_family_and_user!(family_name, user_email, data_enrichment_enabled: false)
|
||||||
Setting.destroy_all
|
base_uuid = "d99e3c6e-d513-4452-8f24-dc263f8528c0"
|
||||||
end
|
id = Digest::UUID.uuid_v5(base_uuid, family_name)
|
||||||
|
|
||||||
|
family = Family.create!(
|
||||||
|
id: id,
|
||||||
|
name: family_name,
|
||||||
|
stripe_subscription_status: "active",
|
||||||
|
data_enrichment_enabled: data_enrichment_enabled,
|
||||||
|
locale: "en",
|
||||||
|
country: "US",
|
||||||
|
timezone: "America/New_York",
|
||||||
|
date_format: "%m-%d-%Y"
|
||||||
|
)
|
||||||
|
|
||||||
def create_user!
|
|
||||||
family.users.create! \
|
family.users.create! \
|
||||||
email: "user@maybe.local",
|
email: user_email,
|
||||||
first_name: "Demo",
|
first_name: "Demo",
|
||||||
last_name: "User",
|
last_name: "User",
|
||||||
role: "admin",
|
role: "admin",
|
||||||
password: "password",
|
password: "password",
|
||||||
onboarded_at: Time.current
|
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
|
end
|
||||||
|
|
||||||
def create_tags!
|
def create_tags!(family)
|
||||||
[ "Trips", "Emergency Fund", "Demo Tag" ].each do |tag|
|
[ "Trips", "Emergency Fund", "Demo Tag" ].each do |tag|
|
||||||
family.tags.create!(name: tag)
|
family.tags.create!(name: tag)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_categories!
|
def create_categories!(family)
|
||||||
family.categories.bootstrap_defaults
|
family.categories.bootstrap_defaults
|
||||||
|
|
||||||
food = family.categories.find_by(name: "Food & Drink")
|
food = family.categories.find_by(name: "Food & Drink")
|
||||||
|
@ -95,7 +118,7 @@ class Demo::Generator
|
||||||
family.categories.create!(name: "Alcohol & Bars", parent: food, color: COLORS.sample, classification: "expense")
|
family.categories.create!(name: "Alcohol & Bars", parent: food, color: COLORS.sample, classification: "expense")
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_merchants!
|
def create_merchants!(family)
|
||||||
merchants = [ "Amazon", "Starbucks", "McDonald's", "Target", "Costco",
|
merchants = [ "Amazon", "Starbucks", "McDonald's", "Target", "Costco",
|
||||||
"Home Depot", "Shell", "Whole Foods", "Walgreens", "Nike",
|
"Home Depot", "Shell", "Whole Foods", "Walgreens", "Nike",
|
||||||
"Uber", "Netflix", "Spotify", "Delta Airlines", "Airbnb", "Sephora" ]
|
"Uber", "Netflix", "Spotify", "Delta Airlines", "Airbnb", "Sephora" ]
|
||||||
|
@ -105,25 +128,25 @@ class Demo::Generator
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_credit_card_account!
|
def create_credit_card_account!(family)
|
||||||
cc = family.accounts.create! \
|
cc = family.accounts.create! \
|
||||||
accountable: CreditCard.new,
|
accountable: CreditCard.new,
|
||||||
name: "Chase Credit Card",
|
name: "Chase Credit Card",
|
||||||
balance: 2300,
|
balance: 2300,
|
||||||
currency: "USD"
|
currency: "USD"
|
||||||
|
|
||||||
50.times do
|
800.times do
|
||||||
merchant = random_family_record(Merchant)
|
merchant = random_family_record(Merchant, family)
|
||||||
create_transaction! \
|
create_transaction! \
|
||||||
account: cc,
|
account: cc,
|
||||||
name: merchant.name,
|
name: merchant.name,
|
||||||
amount: Faker::Number.positive(to: 200),
|
amount: Faker::Number.positive(to: 200),
|
||||||
tags: [ tag_for_merchant(merchant) ],
|
tags: [ tag_for_merchant(merchant, family) ],
|
||||||
category: category_for_merchant(merchant),
|
category: category_for_merchant(merchant, family),
|
||||||
merchant: merchant
|
merchant: merchant
|
||||||
end
|
end
|
||||||
|
|
||||||
5.times do
|
24.times do
|
||||||
create_transaction! \
|
create_transaction! \
|
||||||
account: cc,
|
account: cc,
|
||||||
amount: Faker::Number.negative(from: -1000),
|
amount: Faker::Number.negative(from: -1000),
|
||||||
|
@ -131,30 +154,30 @@ class Demo::Generator
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_checking_account!
|
def create_checking_account!(family)
|
||||||
checking = family.accounts.create! \
|
checking = family.accounts.create! \
|
||||||
accountable: Depository.new,
|
accountable: Depository.new,
|
||||||
name: "Chase Checking",
|
name: "Chase Checking",
|
||||||
balance: 15000,
|
balance: 15000,
|
||||||
currency: "USD"
|
currency: "USD"
|
||||||
|
|
||||||
10.times do
|
200.times do
|
||||||
create_transaction! \
|
create_transaction! \
|
||||||
account: checking,
|
account: checking,
|
||||||
name: "Expense",
|
name: "Expense",
|
||||||
amount: Faker::Number.positive(from: 100, to: 1000)
|
amount: Faker::Number.positive(from: 100, to: 1000)
|
||||||
end
|
end
|
||||||
|
|
||||||
10.times do
|
50.times do
|
||||||
create_transaction! \
|
create_transaction! \
|
||||||
account: checking,
|
account: checking,
|
||||||
amount: Faker::Number.negative(from: -2000),
|
amount: Faker::Number.negative(from: -2000),
|
||||||
name: "Income",
|
name: "Income",
|
||||||
category: income_category
|
category: family.categories.find_by(name: "Income")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_savings_account!
|
def create_savings_account!(family)
|
||||||
savings = family.accounts.create! \
|
savings = family.accounts.create! \
|
||||||
accountable: Depository.new,
|
accountable: Depository.new,
|
||||||
name: "Demo Savings",
|
name: "Demo Savings",
|
||||||
|
@ -162,20 +185,17 @@ class Demo::Generator
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
subtype: "savings"
|
subtype: "savings"
|
||||||
|
|
||||||
income_category = categories.find { |c| c.name == "Income" }
|
100.times do
|
||||||
income_tag = tags.find { |t| t.name == "Emergency Fund" }
|
|
||||||
|
|
||||||
20.times do
|
|
||||||
create_transaction! \
|
create_transaction! \
|
||||||
account: savings,
|
account: savings,
|
||||||
amount: Faker::Number.negative(from: -2000),
|
amount: Faker::Number.negative(from: -2000),
|
||||||
tags: [ income_tag ],
|
tags: [ family.tags.find_by(name: "Emergency Fund") ],
|
||||||
category: income_category,
|
category: family.categories.find_by(name: "Income"),
|
||||||
name: "Income"
|
name: "Income"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_transfer_transactions!
|
def create_transfer_transactions!(family)
|
||||||
checking = family.accounts.find_by(name: "Chase Checking")
|
checking = family.accounts.find_by(name: "Chase Checking")
|
||||||
credit_card = family.accounts.find_by(name: "Chase Credit Card")
|
credit_card = family.accounts.find_by(name: "Chase Credit Card")
|
||||||
investment = family.accounts.find_by(name: "Robinhood")
|
investment = family.accounts.find_by(name: "Robinhood")
|
||||||
|
@ -235,9 +255,7 @@ class Demo::Generator
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_investment_account!
|
def create_investment_account!(family)
|
||||||
load_securities!
|
|
||||||
|
|
||||||
account = family.accounts.create! \
|
account = family.accounts.create! \
|
||||||
accountable: Investment.new,
|
accountable: Investment.new,
|
||||||
name: "Robinhood",
|
name: "Robinhood",
|
||||||
|
@ -275,7 +293,7 @@ class Demo::Generator
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_house_and_mortgage!
|
def create_house_and_mortgage!(family)
|
||||||
house = family.accounts.create! \
|
house = family.accounts.create! \
|
||||||
accountable: Property.new,
|
accountable: Property.new,
|
||||||
name: "123 Maybe Way",
|
name: "123 Maybe Way",
|
||||||
|
@ -293,7 +311,7 @@ class Demo::Generator
|
||||||
currency: "USD"
|
currency: "USD"
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_car_and_loan!
|
def create_car_and_loan!(family)
|
||||||
family.accounts.create! \
|
family.accounts.create! \
|
||||||
accountable: Vehicle.new,
|
accountable: Vehicle.new,
|
||||||
name: "Honda Accord",
|
name: "Honda Accord",
|
||||||
|
@ -307,7 +325,7 @@ class Demo::Generator
|
||||||
currency: "USD"
|
currency: "USD"
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_other_accounts!
|
def create_other_accounts!(family)
|
||||||
family.accounts.create! \
|
family.accounts.create! \
|
||||||
accountable: OtherAsset.new,
|
accountable: OtherAsset.new,
|
||||||
name: "Other Asset",
|
name: "Other Asset",
|
||||||
|
@ -326,7 +344,7 @@ class Demo::Generator
|
||||||
transaction_attributes = attributes.slice(:category, :tags, :merchant)
|
transaction_attributes = attributes.slice(:category, :tags, :merchant)
|
||||||
|
|
||||||
entry_defaults = {
|
entry_defaults = {
|
||||||
date: Faker::Number.between(from: 0, to: 90).days.ago.to_date,
|
date: Faker::Number.between(from: 0, to: 730).days.ago.to_date,
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
entryable: Account::Transaction.new(transaction_attributes)
|
entryable: Account::Transaction.new(transaction_attributes)
|
||||||
}
|
}
|
||||||
|
@ -344,12 +362,12 @@ class Demo::Generator
|
||||||
entryable: Account::Valuation.new
|
entryable: Account::Valuation.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def random_family_record(model)
|
def random_family_record(model, family)
|
||||||
family_records = model.where(family_id: family.id)
|
family_records = model.where(family_id: family.id)
|
||||||
model.offset(rand(family_records.count)).first
|
model.offset(rand(family_records.count)).first
|
||||||
end
|
end
|
||||||
|
|
||||||
def category_for_merchant(merchant)
|
def category_for_merchant(merchant, family)
|
||||||
mapping = {
|
mapping = {
|
||||||
"Amazon" => "Shopping",
|
"Amazon" => "Shopping",
|
||||||
"Starbucks" => "Food & Drink",
|
"Starbucks" => "Food & Drink",
|
||||||
|
@ -369,41 +387,20 @@ class Demo::Generator
|
||||||
"Sephora" => "Shopping"
|
"Sephora" => "Shopping"
|
||||||
}
|
}
|
||||||
|
|
||||||
categories.find { |c| c.name == mapping[merchant.name] }
|
family.categories.find_by(name: mapping[merchant.name])
|
||||||
end
|
end
|
||||||
|
|
||||||
def tag_for_merchant(merchant)
|
def tag_for_merchant(merchant, family)
|
||||||
mapping = {
|
mapping = {
|
||||||
"Delta Airlines" => "Trips",
|
"Delta Airlines" => "Trips",
|
||||||
"Airbnb" => "Trips"
|
"Airbnb" => "Trips"
|
||||||
}
|
}
|
||||||
|
|
||||||
tag_from_merchant = tags.find { |t| t.name == mapping[merchant.name] }
|
tag_from_merchant = family.tags.find_by(name: mapping[merchant.name])
|
||||||
|
tag_from_merchant || family.tags.find_by(name: "Demo Tag")
|
||||||
tag_from_merchant || tags.find { |t| t.name == "Demo Tag" }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def securities
|
def securities
|
||||||
@securities ||= Security.all.to_a
|
@securities ||= Security.all.to_a
|
||||||
end
|
end
|
||||||
|
|
||||||
def merchants
|
|
||||||
@merchants ||= family.merchants
|
|
||||||
end
|
|
||||||
|
|
||||||
def categories
|
|
||||||
@categories ||= family.categories
|
|
||||||
end
|
|
||||||
|
|
||||||
def tags
|
|
||||||
@tags ||= family.tags
|
|
||||||
end
|
|
||||||
|
|
||||||
def income_tag
|
|
||||||
@income_tag ||= tags.find { |t| t.name == "Emergency Fund" }
|
|
||||||
end
|
|
||||||
|
|
||||||
def income_category
|
|
||||||
@income_category ||= categories.find { |c| c.name == "Income" }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -44,7 +44,12 @@ class Family < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def syncing?
|
def syncing?
|
||||||
super || accounts.manual.any?(&:syncing?) || plaid_items.any?(&:syncing?)
|
Sync.where(
|
||||||
|
"(syncable_type = 'Family' AND syncable_id = ?) OR
|
||||||
|
(syncable_type = 'Account' AND syncable_id IN (SELECT id FROM accounts WHERE family_id = ? AND plaid_account_id IS NULL)) OR
|
||||||
|
(syncable_type = 'PlaidItem' AND syncable_id IN (SELECT id FROM plaid_items WHERE family_id = ?))",
|
||||||
|
id, id, id
|
||||||
|
).where(status: [ "pending", "syncing" ]).exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
def eu?
|
def eu?
|
||||||
|
|
|
@ -17,7 +17,8 @@ class User < ApplicationRecord
|
||||||
enum :role, { member: "member", admin: "admin", super_admin: "super_admin" }, validate: true
|
enum :role, { member: "member", admin: "admin", super_admin: "super_admin" }, validate: true
|
||||||
|
|
||||||
has_one_attached :profile_image do |attachable|
|
has_one_attached :profile_image do |attachable|
|
||||||
attachable.variant :thumbnail, resize_to_fill: [ 300, 300 ]
|
attachable.variant :thumbnail, resize_to_fill: [ 300, 300 ], convert: :webp, saver: { quality: 80 }
|
||||||
|
attachable.variant :small, resize_to_fill: [ 36, 36 ], convert: :webp, saver: { quality: 80 }
|
||||||
end
|
end
|
||||||
|
|
||||||
validate :profile_image_size
|
validate :profile_image_size
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
<p class="text-gray-500 py-4"><%= t(".no_trades") %></p>
|
<p class="text-gray-500 py-4"><%= t(".no_trades") %></p>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<%= entries_by_date(@entries) do |entries| %>
|
<%= entries_by_date(@entries) do |entries, _transfers| %>
|
||||||
<%= render partial: "account/trades/trade", collection: entries, as: :entry %>
|
<%= render partial: "account/trades/trade", collection: entries, as: :entry %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
<%# locals: (entry:, selectable: true, balance_trend: nil) %>
|
<%# locals: (entry:, selectable: true, balance_trend: nil) %>
|
||||||
<% transaction, account = entry.account_transaction, entry.account %>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-12 items-center text-gray-900 text-sm font-medium p-4 <%= @focused_record == entry ? "border border-gray-900 rounded-lg" : "" %>">
|
<div class="grid grid-cols-12 items-center text-gray-900 text-sm font-medium p-4 <%= @focused_record == entry ? "border border-gray-900 rounded-lg" : "" %>">
|
||||||
<div class="pr-10 flex items-center gap-4 <%= balance_trend ? "col-span-6" : "col-span-8" %>">
|
<div class="pr-10 flex items-center gap-4 <%= balance_trend ? "col-span-6" : "col-span-8" %>">
|
||||||
<% if selectable %>
|
<% if selectable %>
|
||||||
<%= check_box_tag dom_id(entry, "selection"),
|
<%= check_box_tag dom_id(entry, "selection"),
|
||||||
disabled: entry.account_transaction.transfer?,
|
disabled: entry.entryable.transfer?,
|
||||||
class: "maybe-checkbox maybe-checkbox--light",
|
class: "maybe-checkbox maybe-checkbox--light",
|
||||||
data: { id: entry.id, "bulk-select-target": "row", action: "bulk-select#toggleRowSelection" } %>
|
data: { id: entry.id, "bulk-select-target": "row", action: "bulk-select#toggleRowSelection" } %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div class="max-w-full">
|
<div class="max-w-full">
|
||||||
<%= content_tag :div, class: ["flex items-center gap-2"] do %>
|
<%= content_tag :div, class: ["flex items-center gap-2"] do %>
|
||||||
<% if transaction.merchant&.icon_url %>
|
<% if entry.entryable.merchant&.icon_url %>
|
||||||
<%= image_tag transaction.merchant.icon_url, class: "w-6 h-6 rounded-full", loading: "lazy" %>
|
<%= image_tag entry.entryable.merchant.icon_url, class: "w-6 h-6 rounded-full", loading: "lazy" %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= render "shared/circle_logo", name: entry.display_name, size: "sm" %>
|
<%= render "shared/circle_logo", name: entry.display_name, size: "sm" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
@ -24,8 +23,8 @@
|
||||||
<% if entry.new_record? %>
|
<% if entry.new_record? %>
|
||||||
<%= content_tag :p, entry.display_name %>
|
<%= content_tag :p, entry.display_name %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= link_to entry.account_transaction.transfer? ? entry.account_transaction.transfer.name : entry.display_name,
|
<%= link_to entry.entryable.transfer? ? entry.entryable.transfer.name : entry.display_name,
|
||||||
entry.account_transaction.transfer? ? transfer_path(entry.account_transaction.transfer) : account_entry_path(entry),
|
entry.entryable.transfer? ? transfer_path(entry.entryable.transfer) : account_entry_path(entry),
|
||||||
data: { turbo_frame: "drawer", turbo_prefetch: false },
|
data: { turbo_frame: "drawer", turbo_prefetch: false },
|
||||||
class: "hover:underline hover:text-gray-800" %>
|
class: "hover:underline hover:text-gray-800" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
@ -36,14 +35,14 @@
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% if entry.account_transaction.transfer? %>
|
<% if entry.entryable.transfer? %>
|
||||||
<%= render "account/transactions/transfer_match", entry: entry %>
|
<%= render "account/transactions/transfer_match", entry: entry %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-gray-500 text-xs font-normal">
|
<div class="text-gray-500 text-xs font-normal">
|
||||||
<% if entry.account_transaction.transfer? %>
|
<% if entry.entryable.transfer? %>
|
||||||
<%= render "transfers/account_links", transfer: entry.account_transaction.transfer, is_inflow: entry.account_transaction.transfer_as_inflow.present? %>
|
<%= render "transfers/account_links", transfer: entry.entryable.transfer, is_inflow: entry.entryable.transfer_as_inflow.present? %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= link_to entry.account.name, account_path(entry.account, tab: "transactions", focused_record_id: entry.id), data: { turbo_frame: "_top" }, class: "hover:underline" %>
|
<%= link_to entry.account.name, account_path(entry.account, tab: "transactions", focused_record_id: entry.id), data: { turbo_frame: "_top" }, class: "hover:underline" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
<p class="text-gray-500 py-4"><%= t(".no_transactions") %></p>
|
<p class="text-gray-500 py-4"><%= t(".no_transactions") %></p>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<%= entries_by_date(@entries) do |entries| %>
|
<%= entries_by_date(@entries) do |entries, _transfers| %>
|
||||||
<%= render entries %>
|
<%= render entries %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -77,7 +77,7 @@
|
||||||
<div class="rounded-tl-lg rounded-tr-lg bg-white border-alpha-black-25 shadow-xs">
|
<div class="rounded-tl-lg rounded-tr-lg bg-white border-alpha-black-25 shadow-xs">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<% calculator = Account::BalanceTrendCalculator.for(@entries) %>
|
<% calculator = Account::BalanceTrendCalculator.for(@entries) %>
|
||||||
<%= entries_by_date(@entries) do |entries| %>
|
<%= entries_by_date(@entries) do |entries, _transfers| %>
|
||||||
<% entries.each do |entry| %>
|
<% entries.each do |entry| %>
|
||||||
<%= render entry, balance_trend: calculator&.trend_for(entry) %>
|
<%= render entry, balance_trend: calculator&.trend_for(entry) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -5,13 +5,13 @@
|
||||||
<div id="user-menu" data-controller="menu">
|
<div id="user-menu" data-controller="menu">
|
||||||
<button data-menu-target="button">
|
<button data-menu-target="button">
|
||||||
<div class="w-9 h-9">
|
<div class="w-9 h-9">
|
||||||
<%= render "settings/user_avatar", user: Current.user %>
|
<%= render "settings/user_avatar", user: Current.user, variant: :small %>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div data-menu-target="content" class="hidden absolute w-[240px] z-10 left-[255px] top-[72px] bg-white rounded-sm shadow-xs border border-alpha-black-25">
|
<div data-menu-target="content" class="hidden absolute w-[240px] z-10 left-[255px] top-[72px] bg-white rounded-sm shadow-xs border border-alpha-black-25">
|
||||||
<div class="p-3 flex items-center gap-3">
|
<div class="p-3 flex items-center gap-3">
|
||||||
<div class="w-9 h-9 shrink-0">
|
<div class="w-9 h-9 shrink-0">
|
||||||
<%= render "settings/user_avatar", user: Current.user %>
|
<%= render "settings/user_avatar", user: Current.user, variant: :small, lazy: true %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-hidden text-ellipsis">
|
<div class="overflow-hidden text-ellipsis">
|
||||||
|
|
|
@ -190,7 +190,7 @@
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="text-gray-500 p-1 space-y-1 bg-gray-25 rounded-xl">
|
<div class="text-gray-500 p-1 space-y-1 bg-gray-25 rounded-xl">
|
||||||
<%= entries_by_date(@transaction_entries, selectable: false) do |entries| %>
|
<%= entries_by_date(@transaction_entries, selectable: false) do |entries, _transfers| %>
|
||||||
<%= render entries, selectable: false %>
|
<%= render entries, selectable: false %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<%# locals: (user:) %>
|
<%# locals: (user:, variant: :thumbnail, lazy: false) %>
|
||||||
|
|
||||||
<% if user.profile_image.attached? %>
|
<% if user.profile_image.attached? %>
|
||||||
<%= image_tag user.profile_image.variant(:thumbnail), class: "rounded-full w-full h-full object-cover" %>
|
<%= image_tag user.profile_image.variant(variant), class: "rounded-full w-full h-full object-cover", loading: lazy ? "lazy" : "eager" %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="text-white w-full h-full bg-gray-400 rounded-full flex items-center justify-center text-lg uppercase"><%= user.initial %></div>
|
<div class="text-white w-full h-full bg-gray-400 rounded-full flex items-center justify-center text-lg uppercase"><%= user.initial %></div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -2,18 +2,18 @@
|
||||||
<div class="grid grid-cols-3 bg-white rounded-xl border border-alpha-black-25 shadow-xs divide-x divide-alpha-black-100">
|
<div class="grid grid-cols-3 bg-white rounded-xl border border-alpha-black-25 shadow-xs divide-x divide-alpha-black-100">
|
||||||
<div class="p-4 space-y-2">
|
<div class="p-4 space-y-2">
|
||||||
<p class="text-sm text-gray-500">Total transactions</p>
|
<p class="text-sm text-gray-500">Total transactions</p>
|
||||||
<p class="text-gray-900 font-medium text-xl" id="total-transactions"><%= totals[:count] %></p>
|
<p class="text-gray-900 font-medium text-xl" id="total-transactions"><%= totals.count %></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-4 space-y-2">
|
<div class="p-4 space-y-2">
|
||||||
<p class="text-sm text-gray-500">Income</p>
|
<p class="text-sm text-gray-500">Income</p>
|
||||||
<p class="text-gray-900 font-medium text-xl" id="total-income">
|
<p class="text-gray-900 font-medium text-xl" id="total-income">
|
||||||
<%= format_money totals[:income] %>
|
<%= format_money Money.new(totals.income_total, totals.currency) %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-4 space-y-2">
|
<div class="p-4 space-y-2">
|
||||||
<p class="text-sm text-gray-500">Expenses</p>
|
<p class="text-sm text-gray-500">Expenses</p>
|
||||||
<p class="text-gray-900 font-medium text-xl" id="total-expense">
|
<p class="text-gray-900 font-medium text-xl" id="total-expense">
|
||||||
<%= format_money totals[:expense] %>
|
<%= format_money Money.new(totals.expense_total, totals.currency) %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<%= render "account/transactions/selection_bar" %>
|
<%= render "account/transactions/selection_bar" %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if @transaction_entries.present? %>
|
<% if @pagy.count > 0 %>
|
||||||
<div class="grow overflow-y-auto">
|
<div class="grow overflow-y-auto">
|
||||||
<div class="grid grid-cols-12 bg-gray-25 rounded-xl px-5 py-3 text-xs uppercase font-medium text-gray-500 items-center mb-4">
|
<div class="grid grid-cols-12 bg-gray-25 rounded-xl px-5 py-3 text-xs uppercase font-medium text-gray-500 items-center mb-4">
|
||||||
<div class="pl-0.5 col-span-8 flex items-center gap-4">
|
<div class="pl-0.5 col-span-8 flex items-center gap-4">
|
||||||
|
@ -28,13 +28,11 @@
|
||||||
<p class="col-span-2 justify-self-end">amount</p>
|
<p class="col-span-2 justify-self-end">amount</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<%= entries_by_date(@transaction_entries, totals: true) do |entries| %>
|
<%= entries_by_date(@transaction_entries, transfers: @transfers, totals: true) do |entries, transfers| %>
|
||||||
<%# Render transfers by selecting one side of the transfer (to prevent double-rendering the same transfer across date groups) %>
|
<%= render partial: "transfers/transfer", collection: transfers %>
|
||||||
<%= render partial: "transfers/transfer",
|
|
||||||
collection: entries.select { |e| e.account_transaction.transfer? && e.account_transaction.transfer_as_outflow.present? }.map { |e| e.account_transaction.transfer_as_outflow } %>
|
|
||||||
|
|
||||||
<%# Render regular entries %>
|
<%# Render regular entries %>
|
||||||
<%= render partial: "account/entries/entry", collection: entries.reject { |e| e.account_transaction.transfer? } %>
|
<%= render partial: "account/entries/entry", collection: entries.reject { |e| e.entryable.transfer? } %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -41,7 +41,10 @@
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= button_to clear_filter_transactions_path(param_key: param_key, param_value: param_value), method: :delete, data: { turbo: false }, class: "flex items-center" do %>
|
<%= button_to clear_filter_transactions_path(param_key: param_key, param_value: param_value, **request.query_parameters),
|
||||||
|
method: :delete,
|
||||||
|
data: { turbo: false },
|
||||||
|
class: "flex items-center" do %>
|
||||||
<%= lucide_icon "x", class: "w-4 h-4 text-gray-500" %>
|
<%= lucide_icon "x", class: "w-4 h-4 text-gray-500" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
namespace :demo_data do
|
namespace :demo_data do
|
||||||
desc "Creates or resets demo data used in development environment"
|
desc "Creates or resets demo data used in development environment"
|
||||||
task empty: :environment do
|
task empty: :environment do
|
||||||
Demo::Generator.new.reset_and_clear_data!
|
families = [ "Demo Family 1" ]
|
||||||
|
Demo::Generator.new.reset_and_clear_data!(families)
|
||||||
end
|
end
|
||||||
|
|
||||||
task reset: :environment do
|
task reset: :environment do
|
||||||
Demo::Generator.new.reset_data!
|
families = [ "Demo Family 1", "Demo Family 2", "Demo Family 3", "Demo Family 4", "Demo Family 5" ]
|
||||||
|
Demo::Generator.new.reset_data!(families)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -67,23 +67,18 @@ class Account::EntryTest < ActiveSupport::TestCase
|
||||||
assert_equal 0, family.entries.search(params).size
|
assert_equal 0, family.entries.search(params).size
|
||||||
end
|
end
|
||||||
|
|
||||||
test "can calculate total spending for a group of transactions" do
|
test "can calculate totals for a group of transactions" do
|
||||||
family = families(:empty)
|
family = families(:empty)
|
||||||
account = family.accounts.create! name: "Test", balance: 0, currency: "USD", accountable: Depository.new
|
account = family.accounts.create! name: "Test", balance: 0, currency: "USD", accountable: Depository.new
|
||||||
create_transaction(account: account, amount: 100)
|
create_transaction(account: account, amount: 100)
|
||||||
create_transaction(account: account, amount: 100)
|
create_transaction(account: account, amount: 100)
|
||||||
create_transaction(account: account, amount: -500) # income, will be ignored
|
create_transaction(account: account, amount: -500)
|
||||||
|
|
||||||
assert_equal Money.new(200), family.entries.expense_total("USD")
|
totals = family.entries.stats("USD")
|
||||||
end
|
|
||||||
|
|
||||||
test "can calculate total income for a group of transactions" do
|
assert_equal 3, totals.count
|
||||||
family = families(:empty)
|
assert_equal 500, totals.income_total
|
||||||
account = family.accounts.create! name: "Test", balance: 0, currency: "USD", accountable: Depository.new
|
assert_equal 200, totals.expense_total
|
||||||
create_transaction(account: account, amount: -100)
|
assert_equal "USD", totals.currency
|
||||||
create_transaction(account: account, amount: -100)
|
|
||||||
create_transaction(account: account, amount: 500) # income, will be ignored
|
|
||||||
|
|
||||||
assert_equal Money.new(-200), family.entries.income_total("USD")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -69,11 +69,6 @@ class AccountsTest < ApplicationSystemTestCase
|
||||||
assert_account_created("OtherLiability")
|
assert_account_created("OtherLiability")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "can sync all accounts on accounts page" do
|
|
||||||
visit accounts_url
|
|
||||||
assert_button "Sync all"
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def open_new_account_modal
|
def open_new_account_modal
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue