Skip to content

Commit

Permalink
Add loan and credit card views (maybe-finance#1268)
Browse files Browse the repository at this point in the history
* Add loan and credit card views

* Lint fix

* Clean up overview card markup

* Lint fix

* Test fix
  • Loading branch information
zachgoll authored Oct 8, 2024
1 parent 9263dd3 commit fd941d7
Show file tree
Hide file tree
Showing 34 changed files with 564 additions and 102 deletions.
7 changes: 2 additions & 5 deletions app/controllers/accounts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,11 @@ def show
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

Expand Down
41 changes: 41 additions & 0 deletions app/controllers/credit_cards_controller.rb
Original file line number Diff line number Diff line change
@@ -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
39 changes: 39 additions & 0 deletions app/controllers/loans_controller.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 1 addition & 2 deletions app/controllers/properties_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ def create
end

def update
@account.update!(account_params)
@account.sync_later
@account.update_with_sync!(account_params)
redirect_to @account, notice: t(".success")
end

Expand Down
3 changes: 1 addition & 2 deletions app/controllers/vehicles_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ def create
end

def update
@account.update!(account_params)
@account.sync_later
@account.update_with_sync!(account_params)
redirect_to @account, notice: t(".success")
end

Expand Down
14 changes: 14 additions & 0 deletions app/helpers/accounts_helper.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -31,6 +36,10 @@ def new_account_form_url(account)
properties_path
when "Vehicle"
vehicles_path
when "Loan"
loans_path
when "CreditCard"
credit_cards_path
else
accounts_path
end
Expand All @@ -42,6 +51,10 @@ def edit_account_form_url(account)
property_path(account)
when "Vehicle"
vehicle_path(account)
when "Loan"
loan_path(account)
when "CreditCard"
credit_card_path(account)
else
account_path(account)
end
Expand All @@ -58,6 +71,7 @@ def account_tabs(account)
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
Expand Down
4 changes: 4 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,16 @@ def period_label(period)
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] })
Expand Down
13 changes: 13 additions & 0 deletions app/models/account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ def create_with_optional_start_balance!(attributes:, start_date: nil, start_bala
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
Expand All @@ -93,6 +97,15 @@ def favorable_direction
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)

Expand Down
9 changes: 0 additions & 9 deletions app/models/address.rb
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -15,10 +12,4 @@ def to_s
postal_code: postal_code
)
end

private

def postal_code_required?
country.in?(%w[US CA GB])
end
end
2 changes: 1 addition & 1 deletion app/models/concerns/accountable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def series(period: Period.all, currency: account.currency)
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([])
Expand Down
12 changes: 12 additions & 0 deletions app/models/loan.rb
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions app/models/time_series.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion app/views/accounts/_overview.html.erb
Original file line number Diff line number Diff line change
@@ -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 } %>
8 changes: 8 additions & 0 deletions app/views/accounts/_summary_card.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<%# locals: (title:, content:) %>

<div class="rounded-xl bg-white shadow-xs border border-alpha-black-25 p-4">
<h4 class="text-gray-500 text-sm"><%= title %></h4>
<p class="text-xl font-medium text-gray-900">
<%= content %>
</p>
</div>
21 changes: 21 additions & 0 deletions app/views/accounts/accountables/_credit_card.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<div>
<hr class="my-4">

<div class="space-y-2">
<%= f.fields_for :accountable do |credit_card_form| %>
<div class="flex items-center gap-2">
<%= credit_card_form.text_field :available_credit, label: t(".available_credit"), placeholder: t(".available_credit_placeholder") %>
</div>

<div class="flex items-center gap-2">
<%= 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") %>
</div>

<div class="flex items-center gap-2">
<%= 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") %>
</div>
<% end %>
</div>
</div>
16 changes: 16 additions & 0 deletions app/views/accounts/accountables/_loan.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<div>
<hr class="my-4">

<div class="space-y-2">
<%= f.fields_for :accountable do |loan_form| %>
<div class="flex items-center gap-2">
<%= 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") } %>
</div>

<div class="flex items-center gap-2">
<%= loan_form.number_field :term_months, label: t(".term_months"), placeholder: t(".term_months_placeholder") %>
</div>
<% end %>
</div>
</div>
10 changes: 6 additions & 4 deletions app/views/accounts/accountables/_property.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
<div>
<hr class="my-4">

<h3 class="my-4 font-medium"><%= t(".additional_info") %> (<%= t(".optional") %>)</h3>

<div class="space-y-2">
<%= f.fields_for :accountable do |af| %>
<div class="flex gap-2">
Expand All @@ -15,18 +17,18 @@

<%= af.fields_for :address do |address_form| %>
<div class="flex gap-2">
<%= 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" %>
</div>

<div class="flex gap-2">
<%= 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" %>
</div>

<div class="flex gap-2">
<%= 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" %>
</div>
<% end %>
<% end %>
Expand Down
27 changes: 27 additions & 0 deletions app/views/accounts/accountables/credit_card/_overview.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<%# locals: (account:) %>

<div class="grid grid-cols-3 gap-2">
<%= 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 %>
</div>
Loading

0 comments on commit fd941d7

Please sign in to comment.