Skip to content

Commit

Permalink
Account namespace updates: part 5 (valuations) (maybe-finance#901)
Browse files Browse the repository at this point in the history
* Move Valuation to Account namespace

* Move account history to controller

* Clean up valuation controller and views

* Translations and cleanup

* Remove unused scopes and methods

* Pass brakeman
  • Loading branch information
zachgoll authored Jun 21, 2024
1 parent 0bc0d87 commit 12380dc
Show file tree
Hide file tree
Showing 45 changed files with 478 additions and 346 deletions.
61 changes: 61 additions & 0 deletions app/controllers/account/valuations_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
class Account::ValuationsController < ApplicationController
before_action :set_account
before_action :set_valuation, only: %i[ show edit update destroy ]

def new
@valuation = @account.valuations.new
end

def show
end

def create
@valuation = @account.valuations.build(valuation_params)

if @valuation.save
@valuation.sync_account_later
redirect_to account_path(@account), notice: "Valuation created"
else
# TODO: this is not an ideal way to handle errors and should eventually be improved.
# See: https://github.com/hotwired/turbo-rails/pull/367
flash[:error] = @valuation.errors.full_messages.to_sentence
redirect_to account_path(@account)
end
end

def edit
end

def update
if @valuation.update(valuation_params)
@valuation.sync_account_later
redirect_to account_path(@account), notice: t(".success")
else
# TODO: this is not an ideal way to handle errors and should eventually be improved.
# See: https://github.com/hotwired/turbo-rails/pull/367
flash[:error] = @valuation.errors.full_messages.to_sentence
redirect_to account_path(@account)
end
end

def destroy
@valuation.destroy!
@valuation.sync_account_later

redirect_to account_path(@account), notice: t(".success")
end

private

def set_account
@account = Current.family.accounts.find(params[:account_id])
end

def set_valuation
@valuation = @account.valuations.find(params[:id])
end

def valuation_params
params.require(:account_valuation).permit(:date, :value, :currency)
end
end
1 change: 0 additions & 1 deletion app/controllers/accounts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ def new

def show
@balance_series = @account.series(period: @period)
@valuation_series = @account.valuations.to_series
end

def edit
Expand Down
70 changes: 0 additions & 70 deletions app/controllers/valuations_controller.rb

This file was deleted.

2 changes: 2 additions & 0 deletions app/helpers/account/transfers_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module Account::TransfersHelper
end
23 changes: 23 additions & 0 deletions app/helpers/account/valuations_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module Account::ValuationsHelper
def valuation_icon(valuation)
if valuation.first_of_series?
"keyboard"
elsif valuation.trend.direction.up?
"arrow-up"
elsif valuation.trend.direction.down?
"arrow-down"
else
"minus"
end
end

def valuation_style(valuation)
color = valuation.first_of_series? ? "#D444F1" : valuation.trend.color

<<-STYLE.strip
background-color: color-mix(in srgb, #{color} 5%, white);
border-color: color-mix(in srgb, #{color} 10%, white);
color: #{color};
STYLE
end
end
17 changes: 17 additions & 0 deletions app/helpers/menus_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@ def contextual_menu(&block)
end
end

def contextual_menu_modal_action_item(label, url, icon: "pencil-line", turbo_frame: nil)
link_to url, class: "flex items-center rounded-lg text-gray-900 hover:bg-gray-50 py-2 px-3 gap-2", data: { turbo_frame: } do
concat(lucide_icon(icon, class: "shrink-0 w-5 h-5 text-gray-500"))
concat(tag.span(label, class: "text-sm"))
end
end

def contextual_menu_destructive_item(label, url, turbo_confirm: true, turbo_frame: nil)
button_to url,
method: :delete,
class: "flex items-center w-full rounded-lg text-red-500 hover:bg-red-500/5 py-2 px-3 gap-2",
data: { turbo_confirm: turbo_confirm, turbo_frame: } do
concat(lucide_icon("trash-2", class: "shrink-0 w-5 h-5"))
concat(tag.span(label, class: "text-sm"))
end
end

private
def contextual_menu_icon
tag.button class: "flex hover:bg-gray-100 p-2 rounded", data: { menu_target: "button" } do
Expand Down
2 changes: 0 additions & 2 deletions app/helpers/transfers_helper.rb

This file was deleted.

2 changes: 0 additions & 2 deletions app/helpers/valuations_helper.rb

This file was deleted.

4 changes: 4 additions & 0 deletions app/models/account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ def balance_on(date)
balances.where("date <= ?", date).order(date: :desc).first&.balance
end

def favorable_direction
classification == "asset" ? "up" : "down"
end

# e.g. Wise, Revolut accounts that have transactions in multiple currencies
def multi_currency?
currencies = [ valuations.pluck(:currency), transactions.pluck(:currency) ].flatten.uniq
Expand Down
52 changes: 52 additions & 0 deletions app/models/account/valuation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
class Account::Valuation < ApplicationRecord
include Monetizable

monetize :value

belongs_to :account

validates :account, :date, :value, presence: true
validates :date, uniqueness: { scope: :account_id }

scope :chronological, -> { order(:date) }
scope :reverse_chronological, -> { order(date: :desc) }

def trend
@trend ||= create_trend
end

def first_of_series?
account.valuations.chronological.limit(1).pluck(:date).first == self.date
end

def last_of_series?
account.valuations.reverse_chronological.limit(1).pluck(:date).first == self.date
end

def sync_account_later
if destroyed?
sync_start_date = previous_valuation&.date
else
sync_start_date = [ date_previously_was, date ].compact.min
end

account.sync_later(sync_start_date)
end

private

def previous_valuation
@previous_valuation ||= self.account
.valuations
.where("date < ?", date)
.order(date: :desc)
.first
end

def create_trend
TimeSeries::Trend.new \
current: self.value,
previous: previous_valuation&.value,
favorable_direction: account.favorable_direction
end
end
36 changes: 32 additions & 4 deletions app/models/time_series/trend.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
class TimeSeries::Trend
include ActiveModel::Validations

attr_reader :current, :previous

delegate :favorable_direction, to: :series
attr_reader :current, :previous, :favorable_direction

validate :values_must_be_of_same_type, :values_must_be_of_known_type

def initialize(current:, previous:, series: nil)
def initialize(current:, previous:, series: nil, favorable_direction: nil)
@current = current
@previous = previous
@series = series
@favorable_direction = get_favorable_direction(favorable_direction)

validate!
end
Expand All @@ -25,6 +24,17 @@ def direction
end.inquiry
end

def color
case direction
when "up"
favorable_direction.down? ? red_hex : green_hex
when "down"
favorable_direction.down? ? green_hex : red_hex
else
gray_hex
end
end

def value
if previous.nil?
current.is_a?(Money) ? Money.new(0) : 0
Expand Down Expand Up @@ -56,8 +66,21 @@ def as_json
end

private

attr_reader :series

def red_hex
"#F13636" # red-500
end

def green_hex
"#10A861" # green-600
end

def gray_hex
"#737373" # gray-500
end

def values_must_be_of_same_type
unless current.class == previous.class || [ previous, current ].any?(&:nil?)
errors.add :current, "must be of the same type as previous"
Expand Down Expand Up @@ -90,4 +113,9 @@ def extract_numeric(obj)
obj
end
end

def get_favorable_direction(favorable_direction)
direction = favorable_direction.presence || series&.favorable_direction
(direction.presence_in(TimeSeries::DIRECTIONS) || "up").inquiry
end
end
13 changes: 0 additions & 13 deletions app/models/valuation.rb

This file was deleted.

23 changes: 23 additions & 0 deletions app/views/account/valuations/_form.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<%# locals: (valuation:) %>
<%= form_with model: valuation,
data: { turbo_frame: "_top" },
url: valuation.new_record? ? account_valuations_path(valuation.account) : account_valuation_path(valuation.account, valuation),
builder: ActionView::Helpers::FormBuilder do |f| %>
<div class="grid grid-cols-10 p-4 items-center">
<div class="col-span-7 flex items-center gap-4">
<div class="w-8 h-8 rounded-full p-1.5 flex items-center justify-center bg-gray-500/5">
<%= lucide_icon("pencil-line", class: "w-4 h-4 text-gray-500") %>
</div>
<div class="w-full flex items-center justify-between gap-2">
<%= f.date_field :date, required: "required", max: Date.today, class: "border border-alpha-black-200 bg-white rounded-lg shadow-xs min-w-[200px] px-3 py-1.5 text-gray-900 text-sm" %>
<%= f.number_field :value, required: "required", placeholder: "0.00", step: "0.01", class: "bg-white border border-alpha-black-200 rounded-lg shadow-xs text-gray-900 text-sm px-3 py-1.5 text-right" %>
<%= f.hidden_field :currency, value: valuation.account.currency %>
</div>
</div>

<div class="col-span-3 flex gap-2 justify-end items-center">
<%= link_to t(".cancel"), account_valuations_path(valuation.account), class: "text-sm text-gray-900 hover:text-gray-800 font-medium px-3 py-1.5" %>
<%= f.submit class: "bg-gray-50 rounded-lg font-medium px-3 py-1.5 cursor-pointer hover:bg-gray-100 text-sm" %>
</div>
</div>
<% end %>
Loading

0 comments on commit 12380dc

Please sign in to comment.