From 14641d16de77454b09245d8f71e89058d5f2f343 Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Thu, 29 Feb 2024 16:35:54 -0500 Subject: [PATCH] Add Local Development Demo Data (#502) * Clean up seeds, add development demo data * Handle liability account display and sync * Fix tests --- README.md | 7 +- app/helpers/application_helper.rb | 7 +- app/models/account.rb | 15 + app/models/account/balance_calculator.rb | 2 + app/views/accounts/_account_history.html.erb | 2 +- .../accounts/_account_valuation_list.html.erb | 4 +- app/views/accounts/show.html.erb | 2 +- db/seeds.rb | 54 +--- lib/tasks/demo_data.rake | 291 ++++++++++++++++++ test/fixtures/accounts.yml | 6 + test/fixtures/transactions.yml | 25 ++ .../models/account/balance_calculator_test.rb | 15 + 12 files changed, 368 insertions(+), 62 deletions(-) create mode 100644 lib/tasks/demo_data.rake diff --git a/README.md b/README.md index 09381403..5122be5b 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,15 @@ cd maybe cp .env.example .env bin/setup bin/dev + +# Optionally, load demo data +rake demo_data:reset ``` And visit http://localhost:3000 to see the app. You can use the following credentials to log in (generated by DB seed): -Email: `user@maybe.local` -Password: `password` +- Email: `user@maybe.local` +- Password: `password` For further instructions, see guides below. diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 16757070..59d8ec7c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -49,12 +49,13 @@ module ApplicationHelper end # Styles to use when displaying a change in value - def trend_styles(trend) + def trend_styles(trend, mode: :asset) + puts mode == :liability ? "it is a liability" : "it is an asset" bg_class, text_class, symbol, icon = case trend.direction when "up" - [ "bg-green-500/5", "text-green-500", "+", "arrow-up" ] + mode == :liability ? [ "bg-red-500/5", "text-red-500", "+", "arrow-up" ] : [ "bg-green-500/5", "text-green-500", "+", "arrow-up" ] when "down" - [ "bg-red-500/5", "text-red-500", "-", "arrow-down" ] + mode == :liability ? [ "bg-green-500/5", "text-green-500", "-", "arrow-down" ] : [ "bg-red-500/5", "text-red-500", "-", "arrow-down" ] when "flat" [ "bg-gray-500/5", "text-gray-500", "", "minus" ] else diff --git a/app/models/account.rb b/app/models/account.rb index 5164b355..70851434 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -12,6 +12,21 @@ class Account < ApplicationRecord delegate :type_name, to: :accountable before_create :check_currency + def classification + classifications = { + "Account::Depository" => :asset, + "Account::Investment" => :asset, + "Account::Property" => :asset, + "Account::Vehicle" => :asset, + "Account::OtherAsset" => :asset, + "Account::Loan" => :liability, + "Account::Credit" => :liability, + "Account::OtherLiability" => :liability + } + + classifications[accountable_type] + end + def balance_series(period) filtered_balances = balances.in_period(period).order(:date) return nil if filtered_balances.empty? diff --git a/app/models/account/balance_calculator.rb b/app/models/account/balance_calculator.rb index 2e905417..435f4407 100644 --- a/app/models/account/balance_calculator.rb +++ b/app/models/account/balance_calculator.rb @@ -11,6 +11,7 @@ class Account::BalanceCalculator oldest_entry = [ valuations.first, transactions.first ].compact.min_by(&:date) net_transaction_flows = transactions.sum(&:amount) + net_transaction_flows *= -1 if @account.classification == :liability implied_start_balance = oldest_entry.is_a?(Valuation) ? oldest_entry.value : @account.balance + net_transaction_flows prior_balance = implied_start_balance @@ -21,6 +22,7 @@ class Account::BalanceCalculator current_balance = valuation.value else current_day_net_transaction_flows = transactions.select { |t| t.date == date }.sum(&:amount) + current_day_net_transaction_flows *= -1 if @account.classification == :liability current_balance = prior_balance - current_day_net_transaction_flows end diff --git a/app/views/accounts/_account_history.html.erb b/app/views/accounts/_account_history.html.erb index 25f839c9..2fbad8de 100644 --- a/app/views/accounts/_account_history.html.erb +++ b/app/views/accounts/_account_history.html.erb @@ -21,7 +21,7 @@
<%= turbo_frame_tag dom_id(Valuation.new) %> <%= turbo_frame_tag "valuations_list" do %> - <%= render partial: "accounts/account_valuation_list", locals: { valuation_series: valuation_series } %> + <%= render partial: "accounts/account_valuation_list", locals: { valuation_series: valuation_series, classification: account.classification } %> <% end %>
diff --git a/app/views/accounts/_account_valuation_list.html.erb b/app/views/accounts/_account_valuation_list.html.erb index dfe52615..9a2c7336 100644 --- a/app/views/accounts/_account_valuation_list.html.erb +++ b/app/views/accounts/_account_valuation_list.html.erb @@ -1,7 +1,7 @@ -<%# locals: (valuation_series:) %> +<%# locals: (valuation_series:, classification:) %> <% valuation_series.with_index do |valuation_item, index| %> <% valuation, trend = valuation_item.values_at(:value, :trend) %> - <% valuation_styles = trend_styles(valuation_item[:trend]) %> + <% valuation_styles = trend_styles(valuation_item[:trend], mode: classification) %> <%= turbo_frame_tag dom_id(valuation) do %>
diff --git a/app/views/accounts/show.html.erb b/app/views/accounts/show.html.erb index 001fce51..a759f15e 100644 --- a/app/views/accounts/show.html.erb +++ b/app/views/accounts/show.html.erb @@ -1,5 +1,5 @@ <%= turbo_stream_from @account %> -<% balance_trend_styles = @balance_series.nil? ? {} : trend_styles(@balance_series[:trend]) %> +<% balance_trend_styles = @balance_series.nil? ? {} : trend_styles(@balance_series[:trend], mode: @account.classification) %>
diff --git a/db/seeds.rb b/db/seeds.rb index 9b28173d..df4fff69 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,60 +1,8 @@ # This file should ensure the existence of records required to run the application in every environment (production, # development, test). The code here should be idempotent so that it can be executed at any point in every environment. # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). -# -# Example: -# -# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| -# MovieGenre.find_or_create_by!(name: genre_name) -# end - -# https://github.com/rails/rails/issues/29112#issuecomment-320653056 -ApplicationRecord.reset_column_information - -# Create the default user -family = Family.create_or_find_by(name: "The Maybe Family") -puts "Family created: #{family.name}" -user = User.create_or_find_by(email: "user@maybe.local") do |u| - u.first_name = "Josh" - u.last_name = "Maybe" - u.password = "password" - u.password_confirmation = "password" - u.family_id = family.id -end -puts "User created: #{user.email} for family: #{family.name}" # Create default currency Currency.find_or_create_by(iso_code: "USD", name: "United States Dollar") -checking_account = Account::Depository.new -account = Account.create_or_find_by( - name: "Seed Checking Account", - accountable: checking_account, - family: family, - balance: 5000 - ) -puts "Account created: #{account.name}" - -valuations = [ - { date: 1.year.ago.to_date, value: 4200 }, - { date: 250.days.ago.to_date, value: 4500 }, - { date: 200.days.ago.to_date, value: 4444.96 } -] - -account.valuations.upsert_all(valuations, unique_by: :index_valuations_on_account_id_and_date) - -puts "Valuations created: #{valuations.count}" - -transactions = [ - { date: Date.today - 27, amount: 7.56, name: "Starbucks" }, - { date: Date.today - 18, amount: -500, name: "Paycheck" }, - { date: Date.today - 18, amount: 18.20, name: "Walgreens" }, - { date: Date.today - 13, amount: 34.20, name: "Chipotle" }, - { date: Date.today - 9, amount: -200, name: "Birthday check" }, - { date: Date.today - 5, amount: 85.00, name: "Amazon stuff" } -] -transactions.each do |t| - account.transactions.find_or_create_by(t) -end - -puts "Transactions created: #{transactions.count}" +puts 'Run the following command to create demo data: `rake demo_data:reset`' if Rails.env.development? diff --git a/lib/tasks/demo_data.rake b/lib/tasks/demo_data.rake new file mode 100644 index 00000000..54ff9294 --- /dev/null +++ b/lib/tasks/demo_data.rake @@ -0,0 +1,291 @@ +namespace :demo_data do + desc "Creates or resets demo data used in development environment" + task reset: :environment do + family = Family.find_or_create_by(name: "Demo Family") + user = User.find_or_create_by(email: "user@maybe.local") do |u| + u.password = "password" + u.family = family + end + + puts "Reset user: #{user.email} with family: #{family.name}" + + + checking = Account.find_or_create_by(name: "Demo Checking") do |a| + a.family = family + a.accountable = Account::Depository.new + a.balance = 5000 + end + + checking_transactions = [ + { date: Date.today - 84, amount: -3000, name: "Direct Deposit" }, + { date: Date.today - 70, amount: 1500, name: "Credit Card Payment" }, + { date: Date.today - 70, amount: 200, name: "Utility Bill" }, + { date: Date.today - 56, amount: -3000, name: "Direct Deposit" }, + { date: Date.today - 42, amount: 1500, name: "Credit Card Payment" }, + { date: Date.today - 42, amount: 100, name: "Internet Bill" }, + { date: Date.today - 28, amount: -3000, name: "Direct Deposit" }, + { date: Date.today - 28, amount: 1500, name: "Credit Card Payment" }, + { date: Date.today - 28, amount: 50, name: "Mobile Bill" }, + { date: Date.today - 14, amount: -3000, name: "Direct Deposit" }, + { date: Date.today - 14, amount: 1500, name: "Credit Card Payment" }, + { date: Date.today - 14, amount: 200, name: "Car Loan Payment" }, + { date: Date.today - 7, amount: 150, name: "Insurance" }, + { date: Date.today - 2, amount: 100, name: "Gym Membership" } + ] + + checking_transactions.each do |t| + checking.transactions.find_or_create_by(date: t[:date], amount: t[:amount], name: t[:name]) + end + + checking.sync + + savings = Account.find_or_create_by(name: "Demo Savings") do |a| + a.family = family + a.accountable = Account::Depository.new + a.balance = 20000 + end + + savings_transactions = [ + { date: Date.today - 360, amount: -1000, name: "Initial Deposit" }, + { date: Date.today - 330, amount: -200, name: "Monthly Savings" }, + { date: Date.today - 300, amount: -200, name: "Monthly Savings" }, + { date: Date.today - 270, amount: -200, name: "Monthly Savings" }, + { date: Date.today - 240, amount: -200, name: "Monthly Savings" }, + { date: Date.today - 210, amount: -200, name: "Monthly Savings" }, + { date: Date.today - 180, amount: -200, name: "Monthly Savings" }, + { date: Date.today - 150, amount: -200, name: "Monthly Savings" }, + { date: Date.today - 120, amount: -200, name: "Monthly Savings" }, + { date: Date.today - 90, amount: 1000, name: "Withdrawal" }, + { date: Date.today - 60, amount: -200, name: "Monthly Savings" }, + { date: Date.today - 30, amount: -200, name: "Monthly Savings" } + ] + + savings_transactions.each do |t| + savings.transactions.find_or_create_by(date: t[:date], amount: t[:amount], name: t[:name]) + end + + savings.sync + + credit_card = Account.find_or_create_by(name: "Demo Credit Card") do |a| + a.family = family + a.accountable = Account::Credit.new + a.balance = 1500 + end + + credit_card_transactions = [ + { date: Date.today - 90, amount: 75, name: "Grocery Store" }, + { date: Date.today - 89, amount: 30, name: "Gas Station" }, + { date: Date.today - 88, amount: 12, name: "Coffee Shop" }, + { date: Date.today - 85, amount: 50, name: "Restaurant" }, + { date: Date.today - 84, amount: 25, name: "Online Subscription" }, + { date: Date.today - 82, amount: 100, name: "Clothing Store" }, + { date: Date.today - 80, amount: 60, name: "Pharmacy" }, + { date: Date.today - 78, amount: 40, name: "Utility Bill" }, + { date: Date.today - 75, amount: 90, name: "Home Improvement Store" }, + { date: Date.today - 74, amount: 20, name: "Book Store" }, + { date: Date.today - 72, amount: 15, name: "Movie Theater" }, + { date: Date.today - 70, amount: 200, name: "Electronics Store" }, + { date: Date.today - 68, amount: 35, name: "Pet Store" }, + { date: Date.today - 65, amount: 80, name: "Sporting Goods Store" }, + { date: Date.today - 63, amount: 55, name: "Department Store" }, + { date: Date.today - 60, amount: 110, name: "Auto Repair Shop" }, + { date: Date.today - 58, amount: 45, name: "Beauty Salon" }, + { date: Date.today - 55, amount: 95, name: "Furniture Store" }, + { date: Date.today - 53, amount: 22, name: "Fast Food" }, + { date: Date.today - 50, amount: 120, name: "Airline Ticket" }, + { date: Date.today - 48, amount: 65, name: "Hotel" }, + { date: Date.today - 45, amount: 30, name: "Car Rental" }, + { date: Date.today - 43, amount: 18, name: "Music Store" }, + { date: Date.today - 40, amount: 70, name: "Grocery Store" }, + { date: Date.today - 38, amount: 32, name: "Gas Station" }, + { date: Date.today - 36, amount: 14, name: "Coffee Shop" }, + { date: Date.today - 33, amount: 52, name: "Restaurant" }, + { date: Date.today - 31, amount: 28, name: "Online Subscription" }, + { date: Date.today - 29, amount: 105, name: "Clothing Store" }, + { date: Date.today - 27, amount: 62, name: "Pharmacy" }, + { date: Date.today - 25, amount: 42, name: "Utility Bill" }, + { date: Date.today - 22, amount: 92, name: "Home Improvement Store" }, + { date: Date.today - 20, amount: 23, name: "Book Store" }, + { date: Date.today - 18, amount: 17, name: "Movie Theater" }, + { date: Date.today - 15, amount: 205, name: "Electronics Store" }, + { date: Date.today - 13, amount: 37, name: "Pet Store" }, + { date: Date.today - 10, amount: 83, name: "Sporting Goods Store" }, + { date: Date.today - 8, amount: 57, name: "Department Store" }, + { date: Date.today - 5, amount: 115, name: "Auto Repair Shop" }, + { date: Date.today - 3, amount: 47, name: "Beauty Salon" }, + { date: Date.today - 1, amount: 98, name: "Furniture Store" }, + { date: Date.today - 60, amount: -800, name: "Credit Card Payment" }, + { date: Date.today - 30, amount: -900, name: "Credit Card Payment" }, + { date: Date.today, amount: -1000, name: "Credit Card Payment" } + ] + + credit_card_transactions.each do |t| + credit_card.transactions.find_or_create_by(date: t[:date], amount: t[:amount], name: t[:name]) + end + + credit_card.sync + + retirement = Account.find_or_create_by(name: "Demo 401k") do |a| + a.family = family + a.accountable = Account::Investment.new + a.balance = 100000 + end + + retirement_valuations = [ + { date: 1.year.ago.to_date, value: 90000 }, + { date: 200.days.ago.to_date, value: 95000 }, + { date: 100.days.ago.to_date, value: 94444.96 }, + { date: 20.days.ago.to_date, value: 100000 } + ] + + retirement.valuations.upsert_all(retirement_valuations, unique_by: :index_valuations_on_account_id_and_date) + + retirement.sync + + brokerage = Account.find_or_create_by(name: "Demo Brokerage Account") do |a| + a.family = family + a.accountable = Account::Investment.new + a.balance = 10000 + end + + brokerage_valuations = [ + { date: 1.year.ago.to_date, value: 9000 }, + { date: 200.days.ago.to_date, value: 9500 }, + { date: 100.days.ago.to_date, value: 9444.96 }, + { date: 20.days.ago.to_date, value: 10000 } + ] + + brokerage.valuations.upsert_all(brokerage_valuations, unique_by: :index_valuations_on_account_id_and_date) + + brokerage.sync + + mortgage = Account.find_or_create_by(name: "Demo Mortgage") do |a| + a.family = family + a.accountable = Account::Loan.new + a.balance = 450000 + end + + mortgage_transactions = [ + { date: Date.today - 90, amount: -1500, name: "Mortgage Payment" }, + { date: Date.today - 60, amount: -1500, name: "Mortgage Payment" }, + { date: Date.today - 30, amount: -1500, name: "Mortgage Payment" } + ] + + mortgage_transactions.each do |t| + mortgage.transactions.find_or_create_by(date: t[:date], amount: t[:amount], name: t[:name]) + end + + mortgage_valuations = [ + { date: 2.years.ago.to_date, value: 500000 }, + { date: 6.months.ago.to_date, value: 455000 } + ] + + mortgage.valuations.upsert_all(mortgage_valuations, unique_by: :index_valuations_on_account_id_and_date) + + mortgage.sync + + car_loan = Account.find_or_create_by(name: "Demo Car Loan") do |a| + a.family = family + a.accountable = Account::Loan.new + a.balance = 10000 + end + + car_loan_transactions = [ + { date: 12.months.ago.to_date, amount: -1250, name: "Car Loan Payment" }, + { date: 11.months.ago.to_date, amount: -1250, name: "Car Loan Payment" }, + { date: 10.months.ago.to_date, amount: -1250, name: "Car Loan Payment" }, + { date: 9.months.ago.to_date, amount: -1250, name: "Car Loan Payment" }, + { date: 8.months.ago.to_date, amount: -1250, name: "Car Loan Payment" }, + { date: 7.months.ago.to_date, amount: -1250, name: "Car Loan Payment" }, + { date: 6.months.ago.to_date, amount: -1250, name: "Car Loan Payment" }, + { date: 5.months.ago.to_date, amount: -1250, name: "Car Loan Payment" }, + { date: 4.months.ago.to_date, amount: -1250, name: "Car Loan Payment" }, + { date: 3.months.ago.to_date, amount: -1250, name: "Car Loan Payment" }, + { date: 2.months.ago.to_date, amount: -1250, name: "Car Loan Payment" }, + { date: 1.month.ago.to_date, amount: -1250, name: "Car Loan Payment" } + ] + + car_loan_transactions.each do |t| + car_loan.transactions.find_or_create_by(date: t[:date], amount: t[:amount], name: t[:name]) + end + + car_loan.sync + + house = Account.find_or_create_by(name: "Demo Primary Residence") do |a| + a.family = family + a.accountable = Account::Property.new + a.balance = 500000 + end + + house_valuations = [ + { date: 5.years.ago.to_date, value: 450000 }, + { date: 4.years.ago.to_date, value: 470000 }, + { date: 3.years.ago.to_date, value: 460000 }, + { date: 2.years.ago.to_date, value: 480000 }, + { date: 1.year.ago.to_date, value: 500000 } + ] + + house.valuations.upsert_all(house_valuations, unique_by: :index_valuations_on_account_id_and_date) + + house.sync + + main_car = Account.find_or_create_by(name: "Demo Main Car") do |a| + a.family = family + a.accountable = Account::Vehicle.new + a.balance = 25000 + end + + main_car_valuations = [ + { date: 1.year.ago.to_date, value: 25000 } + ] + + main_car.valuations.upsert_all(main_car_valuations, unique_by: :index_valuations_on_account_id_and_date) + + main_car.sync + + second_car = Account.find_or_create_by(name: "Demo Secondary Car") do |a| + a.family = family + a.accountable = Account::Vehicle.new + a.balance = 12000 + end + + second_car_valuations = [ + { date: 2.years.ago.to_date, value: 11000 }, + { date: 1.year.ago.to_date, value: 12000 } + ] + + second_car.valuations.upsert_all(second_car_valuations, unique_by: :index_valuations_on_account_id_and_date) + + second_car.sync + + cash = Account.find_or_create_by(name: "Demo Physical Cash") do |a| + a.family = family + a.accountable = Account::OtherAsset.new + a.balance = 500 + end + + cash_valuations = [ + { date: 1.month.ago.to_date, value: 500 } + ] + + cash.valuations.upsert_all(cash_valuations, unique_by: :index_valuations_on_account_id_and_date) + + cash.sync + + personal_iou = Account.find_or_create_by(name: "Demo Personal IOU") do |a| + a.family = family + a.accountable = Account::OtherLiability.new + a.balance = 1000 + end + + personal_iou_valuations = [ + { date: 1.month.ago.to_date, value: 1000 } + ] + + personal_iou.valuations.upsert_all(personal_iou_valuations, unique_by: :index_valuations_on_account_id_and_date) + + personal_iou.sync + + puts "Demo data reset complete" + end +end diff --git a/test/fixtures/accounts.yml b/test/fixtures/accounts.yml index 1d8e5989..dffb3151 100644 --- a/test/fixtures/accounts.yml +++ b/test/fixtures/accounts.yml @@ -21,3 +21,9 @@ savings_with_valuation_overrides: family: dylan_family name: Savings account with valuation overrides balance: 20000 + +# Liability account +credit_card: + family: dylan_family + name: Credit Card + balance: 1000 diff --git a/test/fixtures/transactions.yml b/test/fixtures/transactions.yml index 8280841d..9cccabb6 100644 --- a/test/fixtures/transactions.yml +++ b/test/fixtures/transactions.yml @@ -53,3 +53,28 @@ savings_four: date: <%= 29.days.ago.to_date %> amount: -500 account: savings_with_valuation_overrides + +# Credit card account transactions +credit_card_one: + name: Starbucks + date: <%= 5.days.ago.to_date %> + amount: 10 + account: credit_card + +credit_card_two: + name: Chipotle + date: <%= 12.days.ago.to_date %> + amount: 30 + account: credit_card + +credit_card_three: + name: Amazon + date: <%= 15.days.ago.to_date %> + amount: 20 + account: credit_card + +credit_card_four: + name: CC Payment + date: <%= 29.days.ago.to_date %> + amount: -100 + account: credit_card diff --git a/test/models/account/balance_calculator_test.rb b/test/models/account/balance_calculator_test.rb index ebfeaa1c..81ea3d87 100644 --- a/test/models/account/balance_calculator_test.rb +++ b/test/models/account/balance_calculator_test.rb @@ -47,4 +47,19 @@ class Account::BalanceCalculatorTest < ActiveSupport::TestCase assert_equal expected_balances, daily_balances.map { |b| b[:balance] } end + + test "syncs liability account" do + account = accounts(:credit_card) + account.accountable = account_credits(:one) + daily_balances = Account::BalanceCalculator.new(account).daily_balances + + expected_balances = [ + 1040, 940, 940, 940, 940, 940, 940, 940, 940, 940, + 940, 940, 940, 940, 940, 960, 960, 960, 990, 990, + 990, 990, 990, 990, 990, 1000, 1000, 1000, 1000, 1000, + 1000 + ].map(&:to_d) + + assert_equal expected_balances, daily_balances.map { |b| b[:balance] } + end end