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:) %> + +
+ <%= content %> +
+<%= format_money(account.balance_money) %>
-- <%= account.property.purchase_price ? format_money(account.property.purchase_price) : t(".unknown") %> -
-<%= account.property.trend.value %> @@ -22,19 +17,13 @@
(<%= account.property.trend.percent %>%)
- <%= account.property.year_built || t(".unknown") %> -
-- <%= account.property.area || t(".unknown") %> -
-- <%= [account.vehicle.make, account.vehicle.model].compact.join(" ").presence || t(".unknown") %> -
-- <%= account.vehicle.year || t(".unknown") %> -
-- <%= account.vehicle.mileage || t(".unknown") %> -
-- <%= format_money account.vehicle.purchase_price %> -
-- <%= format_money account.balance_money %> -
-<%= account.vehicle.trend.value %> @@ -45,5 +29,5 @@
(<%= account.vehicle.trend.percent %>%)