From fd941d714d730a337d2a673fbfbd7734bbbafe7d Mon Sep 17 00:00:00 2001 From: Zach Gollwitzer Date: Tue, 8 Oct 2024 17:16:37 -0400 Subject: [PATCH] Add loan and credit card views (#1268) * Add loan and credit card views * Lint fix * Clean up overview card markup * Lint fix * Test fix --- app/controllers/accounts_controller.rb | 7 +- app/controllers/credit_cards_controller.rb | 41 +++++++++ app/controllers/loans_controller.rb | 39 +++++++++ app/controllers/properties_controller.rb | 3 +- app/controllers/vehicles_controller.rb | 3 +- app/helpers/accounts_helper.rb | 14 ++++ app/helpers/application_helper.rb | 4 + app/models/account.rb | 13 +++ app/models/address.rb | 9 -- app/models/concerns/accountable.rb | 2 +- app/models/loan.rb | 12 +++ app/models/time_series.rb | 4 +- app/views/accounts/_overview.html.erb | 2 +- app/views/accounts/_summary_card.html.erb | 8 ++ .../accountables/_credit_card.html.erb | 21 +++++ .../accounts/accountables/_loan.html.erb | 16 ++++ .../accounts/accountables/_property.html.erb | 10 ++- .../credit_card/_overview.html.erb | 27 ++++++ .../accountables/loan/_overview.html.erb | 41 +++++++++ .../accountables/property/_overview.html.erb | 39 ++++----- .../accountables/vehicle/_overview.html.erb | 50 ++++------- app/views/accounts/show.html.erb | 6 +- config/locales/views/accounts/en.yml | 54 +++++++++++- config/routes.rb | 2 + .../20241008122449_add_debt_account_views.rb | 17 ++++ db/schema.rb | 10 ++- .../credit_cards_controller_test.rb | 83 +++++++++++++++++++ test/controllers/loans_controller_test.rb | 75 +++++++++++++++++ .../controllers/properties_controller_test.rb | 2 +- test/controllers/vehicles_controller_test.rb | 2 +- test/fixtures/credit_cards.yml | 8 +- test/fixtures/loans.yml | 5 +- test/models/loan_test.rb | 17 +++- test/system/accounts_test.rb | 20 +++-- 34 files changed, 564 insertions(+), 102 deletions(-) create mode 100644 app/controllers/credit_cards_controller.rb create mode 100644 app/controllers/loans_controller.rb create mode 100644 app/views/accounts/_summary_card.html.erb create mode 100644 app/views/accounts/accountables/credit_card/_overview.html.erb create mode 100644 app/views/accounts/accountables/loan/_overview.html.erb create mode 100644 db/migrate/20241008122449_add_debt_account_views.rb create mode 100644 test/controllers/credit_cards_controller_test.rb create mode 100644 test/controllers/loans_controller_test.rb diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 4ecc9b27..4dcbbb27 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -41,14 +41,11 @@ class AccountsController < ApplicationController end def edit + @account.accountable.build_address if @account.accountable.is_a?(Property) && @account.accountable.address.blank? end def update - Account.transaction do - @account.update! account_params.except(:accountable_type, :balance) - @account.update_balance!(account_params[:balance]) if account_params[:balance] - end - @account.sync_later + @account.update_with_sync!(account_params) redirect_back_or_to account_path(@account), notice: t(".success") end diff --git a/app/controllers/credit_cards_controller.rb b/app/controllers/credit_cards_controller.rb new file mode 100644 index 00000000..41316db9 --- /dev/null +++ b/app/controllers/credit_cards_controller.rb @@ -0,0 +1,41 @@ +class CreditCardsController < ApplicationController + before_action :set_account, only: :update + + def create + account = Current.family + .accounts + .create_with_optional_start_balance! \ + attributes: account_params.except(:start_date, :start_balance), + start_date: account_params[:start_date], + start_balance: account_params[:start_balance] + + account.sync_later + redirect_to account, notice: t(".success") + end + + def update + @account.update_with_sync!(account_params) + redirect_to @account, notice: t(".success") + end + + private + + def set_account + @account = Current.family.accounts.find(params[:id]) + end + + def account_params + params.require(:account) + .permit( + :name, :balance, :start_date, :start_balance, :currency, :accountable_type, + accountable_attributes: [ + :id, + :available_credit, + :minimum_payment, + :apr, + :annual_fee, + :expiration_date + ] + ) + end +end diff --git a/app/controllers/loans_controller.rb b/app/controllers/loans_controller.rb new file mode 100644 index 00000000..65d3fdc3 --- /dev/null +++ b/app/controllers/loans_controller.rb @@ -0,0 +1,39 @@ +class LoansController < ApplicationController + before_action :set_account, only: :update + + def create + account = Current.family + .accounts + .create_with_optional_start_balance! \ + attributes: account_params.except(:start_date, :start_balance), + start_date: account_params[:start_date], + start_balance: account_params[:start_balance] + + account.sync_later + redirect_to account, notice: t(".success") + end + + def update + @account.update_with_sync!(account_params) + redirect_to @account, notice: t(".success") + end + + private + + def set_account + @account = Current.family.accounts.find(params[:id]) + end + + def account_params + params.require(:account) + .permit( + :name, :balance, :start_date, :start_balance, :currency, :accountable_type, + accountable_attributes: [ + :id, + :rate_type, + :interest_rate, + :term_months + ] + ) + end +end diff --git a/app/controllers/properties_controller.rb b/app/controllers/properties_controller.rb index b663b6ae..b15c5f86 100644 --- a/app/controllers/properties_controller.rb +++ b/app/controllers/properties_controller.rb @@ -14,8 +14,7 @@ class PropertiesController < ApplicationController end def update - @account.update!(account_params) - @account.sync_later + @account.update_with_sync!(account_params) redirect_to @account, notice: t(".success") end diff --git a/app/controllers/vehicles_controller.rb b/app/controllers/vehicles_controller.rb index b2d81ed6..edfc6def 100644 --- a/app/controllers/vehicles_controller.rb +++ b/app/controllers/vehicles_controller.rb @@ -14,8 +14,7 @@ class VehiclesController < ApplicationController end def update - @account.update!(account_params) - @account.sync_later + @account.update_with_sync!(account_params) redirect_to @account, notice: t(".success") end diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb index 78316762..4594ac99 100644 --- a/app/helpers/accounts_helper.rb +++ b/app/helpers/accounts_helper.rb @@ -1,4 +1,9 @@ module AccountsHelper + def summary_card(title:, &block) + content = capture(&block) + render "accounts/summary_card", title: title, content: content + end + def to_accountable_title(accountable) accountable.model_name.human end @@ -31,6 +36,10 @@ module AccountsHelper properties_path when "Vehicle" vehicles_path + when "Loan" + loans_path + when "CreditCard" + credit_cards_path else accounts_path end @@ -42,6 +51,10 @@ module AccountsHelper property_path(account) when "Vehicle" vehicle_path(account) + when "Loan" + loan_path(account) + when "CreditCard" + credit_card_path(account) else account_path(account) end @@ -58,6 +71,7 @@ module AccountsHelper return [ value_tab ] if account.other_asset? || account.other_liability? return [ overview_tab, value_tab ] if account.property? || account.vehicle? return [ holdings_tab, cash_tab, trades_tab ] if account.investment? + return [ overview_tab, value_tab, transactions_tab ] if account.loan? || account.credit_card? [ value_tab, transactions_tab ] end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 5c8e38c3..8cbbcf21 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -137,12 +137,16 @@ module ApplicationHelper end def format_money(number_or_money, options = {}) + return nil unless number_or_money + money = Money.new(number_or_money) options.reverse_merge!(money.format_options(I18n.locale)) number_to_currency(money.amount, options) end def format_money_without_symbol(number_or_money, options = {}) + return nil unless number_or_money + money = Money.new(number_or_money) options.reverse_merge!(money.format_options(I18n.locale)) ActiveSupport::NumberHelper.number_to_delimited(money.amount.round(options[:precision] || 0), { delimiter: options[:delimiter], separator: options[:separator] }) diff --git a/app/models/account.rb b/app/models/account.rb index fcba6c26..b6920e9e 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -82,6 +82,10 @@ class Account < ApplicationRecord end end + def original_balance + balances.chronological.first&.balance || balance + end + def owns_ticker?(ticker) security_id = Security.find_by(ticker: ticker)&.id entries.account_trades @@ -93,6 +97,15 @@ class Account < ApplicationRecord classification == "asset" ? "up" : "down" end + def update_with_sync!(attributes) + transaction do + update!(attributes) + update_balance!(attributes[:balance]) if attributes[:balance] + end + + sync_later + end + def update_balance!(balance) valuation = entries.account_valuations.find_by(date: Date.current) diff --git a/app/models/address.rb b/app/models/address.rb index 6ad09276..42b8a2fc 100644 --- a/app/models/address.rb +++ b/app/models/address.rb @@ -1,9 +1,6 @@ class Address < ApplicationRecord belongs_to :addressable, polymorphic: true - validates :line1, :locality, presence: true - validates :postal_code, presence: true, if: :postal_code_required? - def to_s I18n.t("address.format", line1: line1, @@ -15,10 +12,4 @@ class Address < ApplicationRecord postal_code: postal_code ) end - - private - - def postal_code_required? - country.in?(%w[US CA GB]) - end end diff --git a/app/models/concerns/accountable.rb b/app/models/concerns/accountable.rb index 7e146312..a7a10284 100644 --- a/app/models/concerns/accountable.rb +++ b/app/models/concerns/accountable.rb @@ -28,7 +28,7 @@ module Accountable if balance_series.empty? && period.date_range.end == Date.current TimeSeries.new([ { date: Date.current, value: account.balance_money.exchange_to(currency) } ]) else - TimeSeries.from_collection(balance_series, :balance_money) + TimeSeries.from_collection(balance_series, :balance_money, favorable_direction: account.asset? ? "up" : "down") end rescue Money::ConversionError TimeSeries.new([]) diff --git a/app/models/loan.rb b/app/models/loan.rb index 80454a15..cc6dbcb5 100644 --- a/app/models/loan.rb +++ b/app/models/loan.rb @@ -1,3 +1,15 @@ class Loan < ApplicationRecord include Accountable + + def monthly_payment + return nil if term_months.nil? || interest_rate.nil? || rate_type.nil? || rate_type != "fixed" + return Money.new(0, account.currency) if account.original_balance.zero? || term_months.zero? + + annual_rate = interest_rate / 100.0 + monthly_rate = annual_rate / 12.0 + + payment = (account.original_balance * monthly_rate * (1 + monthly_rate)**term_months) / ((1 + monthly_rate)**term_months - 1) + + Money.new(payment.round, account.currency) + end end diff --git a/app/models/time_series.rb b/app/models/time_series.rb index 70d5aa4e..09091e8f 100644 --- a/app/models/time_series.rb +++ b/app/models/time_series.rb @@ -3,14 +3,14 @@ class TimeSeries attr_reader :values, :favorable_direction - def self.from_collection(collection, value_method) + def self.from_collection(collection, value_method, favorable_direction: "up") collection.map do |obj| { date: obj.date, value: obj.public_send(value_method), original: obj } - end.then { |data| new(data) } + end.then { |data| new(data, favorable_direction: favorable_direction) } end def initialize(data, favorable_direction: "up") diff --git a/app/views/accounts/_overview.html.erb b/app/views/accounts/_overview.html.erb index 51569c02..d586e780 100644 --- a/app/views/accounts/_overview.html.erb +++ b/app/views/accounts/_overview.html.erb @@ -1,3 +1,3 @@ <%# locals: (account:) %> -<%= render partial: "accounts/accountables/#{account.accountable_type.downcase}/overview", locals: { account: account } %> +<%= render partial: "accounts/accountables/#{account.accountable_type.underscore}/overview", locals: { account: account } %> diff --git a/app/views/accounts/_summary_card.html.erb b/app/views/accounts/_summary_card.html.erb new file mode 100644 index 00000000..424a2a2c --- /dev/null +++ b/app/views/accounts/_summary_card.html.erb @@ -0,0 +1,8 @@ +<%# locals: (title:, content:) %> + +
+

<%= title %>

+

+ <%= content %> +

+
diff --git a/app/views/accounts/accountables/_credit_card.html.erb b/app/views/accounts/accountables/_credit_card.html.erb index e69de29b..848a885e 100644 --- a/app/views/accounts/accountables/_credit_card.html.erb +++ b/app/views/accounts/accountables/_credit_card.html.erb @@ -0,0 +1,21 @@ +
+
+ +
+ <%= f.fields_for :accountable do |credit_card_form| %> +
+ <%= credit_card_form.text_field :available_credit, label: t(".available_credit"), placeholder: t(".available_credit_placeholder") %> +
+ +
+ <%= credit_card_form.text_field :minimum_payment, label: t(".minimum_payment"), placeholder: t(".minimum_payment_placeholder") %> + <%= credit_card_form.text_field :apr, label: t(".apr"), placeholder: t(".apr_placeholder") %> +
+ +
+ <%= credit_card_form.date_field :expiration_date, label: t(".expiration_date") %> + <%= credit_card_form.text_field :annual_fee, label: t(".annual_fee"), placeholder: t(".annual_fee_placeholder") %> +
+ <% end %> +
+
diff --git a/app/views/accounts/accountables/_loan.html.erb b/app/views/accounts/accountables/_loan.html.erb index e69de29b..6a6e823d 100644 --- a/app/views/accounts/accountables/_loan.html.erb +++ b/app/views/accounts/accountables/_loan.html.erb @@ -0,0 +1,16 @@ +
+
+ +
+ <%= f.fields_for :accountable do |loan_form| %> +
+ <%= loan_form.text_field :interest_rate, label: t(".interest_rate"), placeholder: t(".interest_rate_placeholder") %> + <%= loan_form.select :rate_type, options_for_select([["Fixed", "fixed"], ["Variable", "variable"], ["Adjustable", "adjustable"]]), { label: t(".rate_type") } %> +
+ +
+ <%= loan_form.number_field :term_months, label: t(".term_months"), placeholder: t(".term_months_placeholder") %> +
+ <% end %> +
+
diff --git a/app/views/accounts/accountables/_property.html.erb b/app/views/accounts/accountables/_property.html.erb index 47f84a0f..898074c8 100644 --- a/app/views/accounts/accountables/_property.html.erb +++ b/app/views/accounts/accountables/_property.html.erb @@ -3,6 +3,8 @@

+

<%= t(".additional_info") %> (<%= t(".optional") %>)

+
<%= f.fields_for :accountable do |af| %>
@@ -15,18 +17,18 @@ <%= af.fields_for :address do |address_form| %>
- <%= address_form.text_field :line1, label: t(".line1"), placeholder: "123 Main St", required: true %> + <%= address_form.text_field :line1, label: t(".line1"), placeholder: "123 Main St" %> <%= address_form.text_field :line2, label: t(".line2"), placeholder: "Apt 1" %>
- <%= address_form.text_field :locality, label: t(".city"), placeholder: "Sacramento", required: true %> - <%= address_form.text_field :region, label: t(".state"), placeholder: "CA", required: true %> + <%= address_form.text_field :locality, label: t(".city"), placeholder: "Sacramento" %> + <%= address_form.text_field :region, label: t(".state"), placeholder: "CA" %>
<%= address_form.text_field :postal_code, label: t(".postal_code"), placeholder: "95814" %> - <%= address_form.text_field :country, label: t(".country"), placeholder: "USA", required: true %> + <%= address_form.text_field :country, label: t(".country"), placeholder: "USA" %>
<% end %> <% end %> diff --git a/app/views/accounts/accountables/credit_card/_overview.html.erb b/app/views/accounts/accountables/credit_card/_overview.html.erb new file mode 100644 index 00000000..fcedf192 --- /dev/null +++ b/app/views/accounts/accountables/credit_card/_overview.html.erb @@ -0,0 +1,27 @@ +<%# locals: (account:) %> + +
+ <%= summary_card title: t(".amount_owed") do %> + <%= format_money(account.balance) %> + <% end %> + + <%= summary_card title: t(".available_credit") do %> + <%= format_money(account.credit_card.available_credit) || t(".unknown") %> + <% end %> + + <%= summary_card title: t(".minimum_payment") do %> + <%= format_money(account.credit_card.minimum_payment) || t(".unknown") %> + <% end %> + + <%= summary_card title: t(".apr") do %> + <%= account.credit_card.apr ? number_to_percentage(account.credit_card.apr, precision: 2) : t(".unknown") %> + <% end %> + + <%= summary_card title: t(".expiration_date") do %> + <%= account.credit_card.expiration_date ? l(account.credit_card.expiration_date, format: :long) : t(".unknown") %> + <% end %> + + <%= summary_card title: t(".annual_fee") do %> + <%= format_money(account.credit_card.annual_fee) || t(".unknown") %> + <% end %> +
diff --git a/app/views/accounts/accountables/loan/_overview.html.erb b/app/views/accounts/accountables/loan/_overview.html.erb new file mode 100644 index 00000000..e71caf58 --- /dev/null +++ b/app/views/accounts/accountables/loan/_overview.html.erb @@ -0,0 +1,41 @@ +<%# locals: (account:) %> + +
+ <%= summary_card title: t(".original_principal") do %> + <%= format_money account.original_balance %> + <% end %> + + <%= summary_card title: t(".remaining_principal") do %> + <%= format_money account.balance %> + <% end %> + + <%= summary_card title: t(".interest_rate") do %> + <% if account.loan.interest_rate.present? %> + <%= number_to_percentage(account.loan.interest_rate, precision: 2) %> + <% else %> + <%= t(".unknown") %> + <% end %> + <% end %> + + <%= summary_card title: t(".monthly_payment") do %> + <% if account.loan.rate_type.present? && account.loan.rate_type != 'fixed' %> + <%= t(".not_applicable") %> + <% elsif account.loan.rate_type == 'fixed' && account.loan.monthly_payment.present? %> + <%= format_money(account.loan.monthly_payment) %> + <% else %> + <%= t(".unknown") %> + <% end %> + <% end %> + + <%= summary_card title: t(".term") do %> + <% if account.loan.term_months.present? %> + <%= pluralize(account.loan.term_months / 12, "year") %> + <% else %> + <%= t(".unknown") %> + <% end %> + <% end %> + + <%= summary_card title: t(".type") do %> + <%= account.loan.rate_type&.titleize || t(".unknown") %> + <% end %> +
diff --git a/app/views/accounts/accountables/property/_overview.html.erb b/app/views/accounts/accountables/property/_overview.html.erb index 5e11257c..f7ede76a 100644 --- a/app/views/accounts/accountables/property/_overview.html.erb +++ b/app/views/accounts/accountables/property/_overview.html.erb @@ -1,20 +1,15 @@ <%# locals: (account:) %>
-
-

<%= t(".market_value") %>

-

<%= format_money(account.balance_money) %>

-
+ <%= summary_card title: t(".market_value") do %> + <%= format_money(account.balance_money) %> + <% end %> -
-

<%= t(".purchase_price") %>

-

- <%= account.property.purchase_price ? format_money(account.property.purchase_price) : t(".unknown") %> -

-
+ <%= summary_card title: t(".purchase_price") do %> + <%= account.property.purchase_price ? format_money(account.property.purchase_price) : t(".unknown") %> + <% end %> -
-

<%= t(".trend") %>

+ <%= summary_card title: t(".trend") do %>

<%= account.property.trend.value %> @@ -22,19 +17,13 @@

(<%= account.property.trend.percent %>%)

-
+ <% end %> -
-

<%= t(".year_built") %>

-

- <%= account.property.year_built || t(".unknown") %> -

-
+ <%= summary_card title: t(".year_built") do %> + <%= account.property.year_built || t(".unknown") %> + <% end %> -
-

<%= t(".living_area") %>

-

- <%= account.property.area || t(".unknown") %> -

-
+ <%= summary_card title: t(".living_area") do %> + <%= account.property.area || t(".unknown") %> + <% end %>
diff --git a/app/views/accounts/accountables/vehicle/_overview.html.erb b/app/views/accounts/accountables/vehicle/_overview.html.erb index c371a7bb..2455c67b 100644 --- a/app/views/accounts/accountables/vehicle/_overview.html.erb +++ b/app/views/accounts/accountables/vehicle/_overview.html.erb @@ -1,43 +1,27 @@ <%# locals: (account:) %>
-
-

<%= t(".make_model") %>

-

- <%= [account.vehicle.make, account.vehicle.model].compact.join(" ").presence || t(".unknown") %> -

-
+ <%= summary_card title: t(".make_model") do %> + <%= [account.vehicle.make, account.vehicle.model].compact.join(" ").presence || t(".unknown") %> + <% end %> -
-

<%= t(".year") %>

-

- <%= account.vehicle.year || t(".unknown") %> -

-
+ <%= summary_card title: t(".year") do %> + <%= account.vehicle.year || t(".unknown") %> + <% end %> -
-

<%= t(".mileage") %>

-

- <%= account.vehicle.mileage || t(".unknown") %> -

-
+ <%= summary_card title: t(".mileage") do %> + <%= account.vehicle.mileage || t(".unknown") %> + <% end %> -
-

<%= t(".purchase_price") %>

-

- <%= format_money account.vehicle.purchase_price %> -

-
+ <%= summary_card title: t(".purchase_price") do %> + <%= format_money account.vehicle.purchase_price %> + <% end %> -
-

<%= t(".current_price") %>

-

- <%= format_money account.balance_money %> -

-
+ <%= summary_card title: t(".current_price") do %> + <%= format_money account.balance_money %> + <% end %> -
-

<%= t(".trend") %>

+ <%= summary_card title: t(".trend") do %>

<%= account.vehicle.trend.value %> @@ -45,5 +29,5 @@

(<%= account.vehicle.trend.percent %>%)

-
+ <% end %>
diff --git a/app/views/accounts/show.html.erb b/app/views/accounts/show.html.erb index fef8517d..c3f5fd6f 100644 --- a/app/views/accounts/show.html.erb +++ b/app/views/accounts/show.html.erb @@ -60,7 +60,11 @@
- <%= tag.p t(".total_value"), class: "text-sm font-medium text-gray-500" %> + <% if @account.asset? %> + <%= tag.p t(".total_value"), class: "text-sm font-medium text-gray-500" %> + <% else %> + <%= tag.p t(".total_owed"), class: "text-sm font-medium text-gray-500" %> + <% end %>
<%= render "tooltip", account: @account if @account.investment? %>
diff --git a/config/locales/views/accounts/en.yml b/config/locales/views/accounts/en.yml index b1c816af..c1e86e92 100644 --- a/config/locales/views/accounts/en.yml +++ b/config/locales/views/accounts/en.yml @@ -5,13 +5,48 @@ en: has_issues: Issue detected. troubleshoot: Troubleshoot accountables: + credit_card: + annual_fee: Annual fee + annual_fee_placeholder: '99' + apr: APR + apr_placeholder: '15.99' + available_credit: Available credit + available_credit_placeholder: '10000' + expiration_date: Expiration date + minimum_payment: Minimum payment + minimum_payment_placeholder: '100' + overview: + amount_owed: Amount Owed + annual_fee: Annual Fee + apr: APR + available_credit: Available Credit + expiration_date: Expiration Date + minimum_payment: Minimum Payment + unknown: Unknown + loan: + interest_rate: Interest rate + interest_rate_placeholder: '5.25' + overview: + interest_rate: Interest Rate + monthly_payment: Monthly Payment + not_applicable: N/A + original_principal: Original Principal + remaining_principal: Remaining Principal + term: Term + type: Type + unknown: Unknown + rate_type: Rate type + term_months: Term (months) + term_months_placeholder: '360' property: + additional_info: Additional info area_unit: Area unit - area_value: Area value (optional) + area_value: Area value city: City country: Country line1: Address line 1 - line2: Address line 2 (optional) + line2: Address line 2 + optional: optional overview: living_area: Living Area market_value: Market Value @@ -19,9 +54,9 @@ en: trend: Trend unknown: Unknown year_built: Year Built - postal_code: Postal code (optional) + postal_code: Postal code state: State - year_built: Year built (optional) + year_built: Year built vehicle: make: Make make_placeholder: Toyota @@ -102,6 +137,7 @@ en: sync_message_missing_rates: Since exchange rates haven't been synced, balance graphs may not reflect accurate values. sync_message_unknown_error: An error has occurred during the sync. + total_owed: Total Owed total_value: Total Value trades: Transactions transactions: Transactions @@ -124,3 +160,13 @@ en: value, minus margin loans. update: success: Account updated + credit_cards: + create: + success: Credit card created successfully + update: + success: Credit card updated successfully + loans: + create: + success: Loan created successfully + update: + success: Loan updated successfully diff --git a/config/routes.rb b/config/routes.rb index 42b91592..df6b857c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -75,6 +75,8 @@ Rails.application.routes.draw do resources :properties, only: %i[create update] resources :vehicles, only: %i[create update] + resources :credit_cards, only: %i[create update] + resources :loans, only: %i[create update] resources :transactions, only: %i[index new create] do collection do diff --git a/db/migrate/20241008122449_add_debt_account_views.rb b/db/migrate/20241008122449_add_debt_account_views.rb new file mode 100644 index 00000000..3b9d320e --- /dev/null +++ b/db/migrate/20241008122449_add_debt_account_views.rb @@ -0,0 +1,17 @@ +class AddDebtAccountViews < ActiveRecord::Migration[7.2] + def change + change_table :loans do |t| + t.string :rate_type + t.decimal :interest_rate, precision: 10, scale: 2 + t.integer :term_months + end + + change_table :credit_cards do |t| + t.decimal :available_credit, precision: 10, scale: 2 + t.decimal :minimum_payment, precision: 10, scale: 2 + t.decimal :apr, precision: 10, scale: 2 + t.date :expiration_date + t.decimal :annual_fee, precision: 10, scale: 2 + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 502ddb48..7e5e7c12 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2024_10_07_211438) do +ActiveRecord::Schema[7.2].define(version: 2024_10_08_122449) do # These are extensions that must be enabled in order to support this database enable_extension "pgcrypto" enable_extension "plpgsql" @@ -184,6 +184,11 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_07_211438) do create_table "credit_cards", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.decimal "available_credit", precision: 10, scale: 2 + t.decimal "minimum_payment", precision: 10, scale: 2 + t.decimal "apr", precision: 10, scale: 2 + t.date "expiration_date" + t.decimal "annual_fee", precision: 10, scale: 2 end create_table "cryptos", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| @@ -408,6 +413,9 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_07_211438) do create_table "loans", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.string "rate_type" + t.decimal "interest_rate", precision: 10, scale: 2 + t.integer "term_months" end create_table "merchants", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| diff --git a/test/controllers/credit_cards_controller_test.rb b/test/controllers/credit_cards_controller_test.rb new file mode 100644 index 00000000..d8f6772c --- /dev/null +++ b/test/controllers/credit_cards_controller_test.rb @@ -0,0 +1,83 @@ +require "test_helper" + +class CreditCardsControllerTest < ActionDispatch::IntegrationTest + setup do + sign_in @user = users(:family_admin) + @account = accounts(:credit_card) + end + + test "creates credit card" do + assert_difference -> { Account.count } => 1, + -> { CreditCard.count } => 1, + -> { Account::Valuation.count } => 2, + -> { Account::Entry.count } => 2 do + post credit_cards_path, params: { + account: { + name: "New Credit Card", + balance: 1000, + currency: "USD", + accountable_type: "CreditCard", + start_date: 1.month.ago.to_date, + start_balance: 0, + accountable_attributes: { + available_credit: 5000, + minimum_payment: 25, + apr: 15.99, + expiration_date: 2.years.from_now.to_date, + annual_fee: 99 + } + } + } + end + + created_account = Account.order(:created_at).last + + assert_equal "New Credit Card", created_account.name + assert_equal 1000, created_account.balance + assert_equal "USD", created_account.currency + assert_equal 5000, created_account.credit_card.available_credit + assert_equal 25, created_account.credit_card.minimum_payment + assert_equal 15.99, created_account.credit_card.apr + assert_equal 2.years.from_now.to_date, created_account.credit_card.expiration_date + assert_equal 99, created_account.credit_card.annual_fee + + assert_redirected_to account_path(created_account) + assert_equal "Credit card created successfully", flash[:notice] + assert_enqueued_with(job: AccountSyncJob) + end + + test "updates credit card" do + assert_no_difference [ "Account.count", "CreditCard.count" ] do + patch credit_card_path(@account), params: { + account: { + name: "Updated Credit Card", + balance: 2000, + currency: "USD", + accountable_type: "CreditCard", + accountable_attributes: { + id: @account.accountable_id, + available_credit: 6000, + minimum_payment: 50, + apr: 14.99, + expiration_date: 3.years.from_now.to_date, + annual_fee: 0 + } + } + } + end + + @account.reload + + assert_equal "Updated Credit Card", @account.name + assert_equal 2000, @account.balance + assert_equal 6000, @account.credit_card.available_credit + assert_equal 50, @account.credit_card.minimum_payment + assert_equal 14.99, @account.credit_card.apr + assert_equal 3.years.from_now.to_date, @account.credit_card.expiration_date + assert_equal 0, @account.credit_card.annual_fee + + assert_redirected_to account_path(@account) + assert_equal "Credit card updated successfully", flash[:notice] + assert_enqueued_with(job: AccountSyncJob) + end +end diff --git a/test/controllers/loans_controller_test.rb b/test/controllers/loans_controller_test.rb new file mode 100644 index 00000000..3ec9efc0 --- /dev/null +++ b/test/controllers/loans_controller_test.rb @@ -0,0 +1,75 @@ +require "test_helper" + +class LoansControllerTest < ActionDispatch::IntegrationTest + setup do + sign_in @user = users(:family_admin) + @account = accounts(:loan) + end + + test "creates loan" do + assert_difference -> { Account.count } => 1, + -> { Loan.count } => 1, + -> { Account::Valuation.count } => 2, + -> { Account::Entry.count } => 2 do + post loans_path, params: { + account: { + name: "New Loan", + balance: 50000, + currency: "USD", + accountable_type: "Loan", + start_date: 1.month.ago.to_date, + start_balance: 50000, + accountable_attributes: { + interest_rate: 5.5, + term_months: 60, + rate_type: "fixed" + } + } + } + end + + created_account = Account.order(:created_at).last + + assert_equal "New Loan", created_account.name + assert_equal 50000, created_account.balance + assert_equal "USD", created_account.currency + assert_equal 5.5, created_account.loan.interest_rate + assert_equal 60, created_account.loan.term_months + assert_equal "fixed", created_account.loan.rate_type + + assert_redirected_to account_path(created_account) + assert_equal "Loan created successfully", flash[:notice] + assert_enqueued_with(job: AccountSyncJob) + end + + test "updates loan" do + assert_no_difference [ "Account.count", "Loan.count" ] do + patch loan_path(@account), params: { + account: { + name: "Updated Loan", + balance: 45000, + currency: "USD", + accountable_type: "Loan", + accountable_attributes: { + id: @account.accountable_id, + interest_rate: 4.5, + term_months: 48, + rate_type: "fixed" + } + } + } + end + + @account.reload + + assert_equal "Updated Loan", @account.name + assert_equal 45000, @account.balance + assert_equal 4.5, @account.loan.interest_rate + assert_equal 48, @account.loan.term_months + assert_equal "fixed", @account.loan.rate_type + + assert_redirected_to account_path(@account) + assert_equal "Loan updated successfully", flash[:notice] + assert_enqueued_with(job: AccountSyncJob) + end +end diff --git a/test/controllers/properties_controller_test.rb b/test/controllers/properties_controller_test.rb index c2debe3e..1e017eed 100644 --- a/test/controllers/properties_controller_test.rb +++ b/test/controllers/properties_controller_test.rb @@ -47,7 +47,7 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest end test "updates property" do - assert_no_difference [ "Account.count", "Property.count", "Account::Valuation.count", "Account::Entry.count" ] do + assert_no_difference [ "Account.count", "Property.count" ] do patch property_path(@account), params: { account: { name: "Updated Property", diff --git a/test/controllers/vehicles_controller_test.rb b/test/controllers/vehicles_controller_test.rb index 2aecb42f..6908f969 100644 --- a/test/controllers/vehicles_controller_test.rb +++ b/test/controllers/vehicles_controller_test.rb @@ -44,7 +44,7 @@ class VehiclesControllerTest < ActionDispatch::IntegrationTest end test "updates vehicle" do - assert_no_difference [ "Account.count", "Vehicle.count", "Account::Valuation.count", "Account::Entry.count" ] do + assert_no_difference [ "Account.count", "Vehicle.count" ] do patch vehicle_path(@account), params: { account: { name: "Updated Vehicle", diff --git a/test/fixtures/credit_cards.yml b/test/fixtures/credit_cards.yml index e0553ab0..041ba521 100644 --- a/test/fixtures/credit_cards.yml +++ b/test/fixtures/credit_cards.yml @@ -1 +1,7 @@ -one: { } \ No newline at end of file +one: + available_credit: 5000.00 + minimum_payment: 100.00 + apr: 18.99 + expiration_date: <%= 4.years.from_now.to_date %> + annual_fee: 95.00 + \ No newline at end of file diff --git a/test/fixtures/loans.yml b/test/fixtures/loans.yml index e0553ab0..2307dcf7 100644 --- a/test/fixtures/loans.yml +++ b/test/fixtures/loans.yml @@ -1 +1,4 @@ -one: { } \ No newline at end of file +one: + interest_rate: 3.5 + term_months: 360 + rate_type: fixed diff --git a/test/models/loan_test.rb b/test/models/loan_test.rb index 1d60eb6c..c39679bf 100644 --- a/test/models/loan_test.rb +++ b/test/models/loan_test.rb @@ -1,7 +1,18 @@ require "test_helper" class LoanTest < ActiveSupport::TestCase - # test "the truth" do - # assert true - # end + test "calculates correct monthly payment for fixed rate loan" do + loan_account = Account.create! \ + family: families(:dylan_family), + name: "Mortgage Loan", + balance: 500000, + currency: "USD", + accountable: Loan.create!( + interest_rate: 3.5, + term_months: 360, + rate_type: "fixed" + ) + + assert_equal 2245, loan_account.loan.monthly_payment.amount + end end diff --git a/test/system/accounts_test.rb b/test/system/accounts_test.rb index 666fa843..79d4e9a1 100644 --- a/test/system/accounts_test.rb +++ b/test/system/accounts_test.rb @@ -22,13 +22,13 @@ class AccountsTest < ApplicationSystemTestCase test "can create property account" do assert_account_created "Property" do - fill_in "Year built (optional)", with: 2005 - fill_in "Area value (optional)", with: 2250 + fill_in "Year built", with: 2005 + fill_in "Area value", with: 2250 fill_in "Address line 1", with: "123 Main St" fill_in "Address line 2", with: "Apt 4B" fill_in "City", with: "San Francisco" fill_in "State", with: "CA" - fill_in "Postal code (optional)", with: "94101" + fill_in "Postal code", with: "94101" fill_in "Country", with: "US" end end @@ -47,11 +47,21 @@ class AccountsTest < ApplicationSystemTestCase end test "can create credit card account" do - assert_account_created("CreditCard") + assert_account_created "CreditCard" do + fill_in "Available credit", with: 1000 + fill_in "Minimum payment", with: 25 + fill_in "APR", with: 15.25 + fill_in "Expiration date", with: 1.year.from_now.to_date + fill_in "Annual fee", with: 100 + end end test "can create loan account" do - assert_account_created("Loan") + assert_account_created "Loan" do + fill_in "Interest rate", with: 5.25 + select "Fixed", from: "Rate type" + fill_in "Term (months)", with: 360 + end end test "can create other liability account" do