mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 13:19:39 +02:00
Account namespace updates: part 5 (valuations) (#901)
* 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
This commit is contained in:
parent
0bc0d87768
commit
12380dc8ad
45 changed files with 478 additions and 346 deletions
61
app/controllers/account/valuations_controller.rb
Normal file
61
app/controllers/account/valuations_controller.rb
Normal file
|
@ -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
|
|
@ -35,7 +35,6 @@ class AccountsController < ApplicationController
|
|||
|
||||
def show
|
||||
@balance_series = @account.series(period: @period)
|
||||
@valuation_series = @account.valuations.to_series
|
||||
end
|
||||
|
||||
def edit
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
class ValuationsController < ApplicationController
|
||||
before_action :set_valuation, only: %i[ edit update destroy ]
|
||||
def create
|
||||
@account = Current.family.accounts.find(params[:account_id])
|
||||
|
||||
# TODO: placeholder logic until we have a better abstraction for trends
|
||||
@valuation = @account.valuations.new(valuation_params.merge(currency: @account.currency))
|
||||
if @valuation.save
|
||||
@valuation.account.sync_later(@valuation.date)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to account_path(@account), notice: "Valuation created" }
|
||||
format.turbo_stream
|
||||
end
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
flash.now[:error] = "Valuation already exists for this date"
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def show
|
||||
@valuation = Current.family.accounts.find(params[:account_id]).valuations.find(params[:id])
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
sync_start_date = [ @valuation.date, Date.parse(valuation_params[:date]) ].compact.min
|
||||
if @valuation.update(valuation_params)
|
||||
@valuation.account.sync_later(sync_start_date)
|
||||
|
||||
redirect_to account_path(@valuation.account), notice: "Valuation updated"
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
flash.now[:error] = "Valuation already exists for this date"
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def destroy
|
||||
@account = @valuation.account
|
||||
sync_start_date = @account.valuations.where("date < ?", @valuation.date).order(date: :desc).first&.date
|
||||
@valuation.destroy!
|
||||
@account.sync_later(sync_start_date)
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to account_path(@account), notice: "Valuation deleted" }
|
||||
format.turbo_stream
|
||||
end
|
||||
end
|
||||
|
||||
def new
|
||||
@account = Current.family.accounts.find(params[:account_id])
|
||||
@valuation = @account.valuations.new
|
||||
end
|
||||
|
||||
private
|
||||
# Use callbacks to share common setup or constraints between actions.
|
||||
def set_valuation
|
||||
@valuation = Valuation.find(params[:id])
|
||||
end
|
||||
|
||||
def valuation_params
|
||||
params.require(:valuation).permit(:date, :value)
|
||||
end
|
||||
end
|
2
app/helpers/account/transfers_helper.rb
Normal file
2
app/helpers/account/transfers_helper.rb
Normal file
|
@ -0,0 +1,2 @@
|
|||
module Account::TransfersHelper
|
||||
end
|
23
app/helpers/account/valuations_helper.rb
Normal file
23
app/helpers/account/valuations_helper.rb
Normal file
|
@ -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
|
|
@ -6,6 +6,23 @@ module MenusHelper
|
|||
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
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
module TransfersHelper
|
||||
end
|
|
@ -1,2 +0,0 @@
|
|||
module ValuationsHelper
|
||||
end
|
|
@ -29,6 +29,10 @@ class Account < ApplicationRecord
|
|||
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
|
||||
|
|
52
app/models/account/valuation.rb
Normal file
52
app/models/account/valuation.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
@ -25,6 +24,17 @@ class TimeSeries::Trend
|
|||
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
|
||||
|
@ -56,8 +66,21 @@ class TimeSeries::Trend
|
|||
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"
|
||||
|
@ -90,4 +113,9 @@ class TimeSeries::Trend
|
|||
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
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
class Valuation < ApplicationRecord
|
||||
include Monetizable
|
||||
|
||||
belongs_to :account
|
||||
validates :account, :date, :value, presence: true
|
||||
monetize :value
|
||||
|
||||
scope :in_period, ->(period) { period.date_range.nil? ? all : where(date: period.date_range) }
|
||||
|
||||
def self.to_series
|
||||
TimeSeries.from_collection all, :value_money
|
||||
end
|
||||
end
|
23
app/views/account/valuations/_form.html.erb
Normal file
23
app/views/account/valuations/_form.html.erb
Normal file
|
@ -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 %>
|
50
app/views/account/valuations/_valuation.html.erb
Normal file
50
app/views/account/valuations/_valuation.html.erb
Normal file
|
@ -0,0 +1,50 @@
|
|||
<%# locals: (valuation:) %>
|
||||
|
||||
<%= turbo_frame_tag dom_id(valuation) do %>
|
||||
<div class="p-4 grid grid-cols-10 items-center">
|
||||
<div class="col-span-5 flex items-center gap-4">
|
||||
<%= tag.div class: "w-8 h-8 rounded-full p-1.5 flex items-center justify-center", style: valuation_style(valuation).html_safe do %>
|
||||
<%= lucide_icon valuation_icon(valuation), class: "w-4 h-4" %>
|
||||
<% end %>
|
||||
|
||||
<div class="text-sm">
|
||||
<%= tag.p valuation.date, class: "text-gray-900 font-medium" %>
|
||||
<%= tag.p valuation.first_of_series? ? t(".start_balance") : t(".value_update"), class: "text-gray-500" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 justify-self-end">
|
||||
<%= tag.p format_money(valuation.value_money), class: "font-medium text-sm text-gray-900" %>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 justify-self-end font-medium text-sm" style="color: <%= valuation.trend.color %>">
|
||||
<% if valuation.trend.direction.flat? %>
|
||||
<%= tag.span t(".no_change"), class: "text-gray-500" %>
|
||||
<% else %>
|
||||
<%= tag.span format_money(valuation.trend.value) %>
|
||||
<%= tag.span "(#{valuation.trend.percent}%)" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="col-span-1 justify-self-end">
|
||||
<%= contextual_menu do %>
|
||||
<div class="w-48 p-1 text-sm leading-6 text-gray-900 bg-white shadow-lg shrink rounded-xl ring-1 ring-gray-900/5">
|
||||
<%= contextual_menu_modal_action_item t(".edit_entry"), edit_account_valuation_path(valuation.account, valuation) %>
|
||||
|
||||
<%= contextual_menu_destructive_item t(".delete_entry"),
|
||||
account_valuation_path(valuation.account, valuation),
|
||||
turbo_frame: "_top",
|
||||
turbo_confirm: {
|
||||
title: t(".confirm_title"),
|
||||
body: t(".confirm_body_html"),
|
||||
accept: t(".confirm_accept")
|
||||
} %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% unless valuation.last_of_series? %>
|
||||
<div class="h-px bg-alpha-black-50 ml-16 mr-4"></div>
|
||||
<% end %>
|
||||
<% end %>
|
3
app/views/account/valuations/edit.html.erb
Normal file
3
app/views/account/valuations/edit.html.erb
Normal file
|
@ -0,0 +1,3 @@
|
|||
<%= turbo_frame_tag dom_id(@valuation) do %>
|
||||
<%= render "form", valuation: @valuation %>
|
||||
<% end %>
|
15
app/views/account/valuations/index.html.erb
Normal file
15
app/views/account/valuations/index.html.erb
Normal file
|
@ -0,0 +1,15 @@
|
|||
<%= turbo_frame_tag dom_id(@account, "valuations") do %>
|
||||
<div class="grid grid-cols-10 items-center uppercase text-xs font-medium text-gray-500 px-4 py-2">
|
||||
<%= tag.p t(".date"), class: "col-span-5" %>
|
||||
<%= tag.p t(".value"), class: "col-span-2 justify-self-end" %>
|
||||
<%= tag.p t(".change"), class: "col-span-2 justify-self-end" %>
|
||||
<%= tag.div class: "col-span-1" %>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg bg-white border-alpha-black-25 shadow-xs">
|
||||
<%= turbo_frame_tag dom_id(Account::Valuation.new) %>
|
||||
<% @account.valuations.reverse_chronological.each do |valuation| %>
|
||||
<%= render valuation %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
4
app/views/account/valuations/new.html.erb
Normal file
4
app/views/account/valuations/new.html.erb
Normal file
|
@ -0,0 +1,4 @@
|
|||
<%= turbo_frame_tag dom_id(@valuation) do %>
|
||||
<%= render "form", valuation: @valuation %>
|
||||
<div class="h-px bg-alpha-black-50 ml-20 mr-4"></div>
|
||||
<% end %>
|
1
app/views/account/valuations/show.html.erb
Normal file
1
app/views/account/valuations/show.html.erb
Normal file
|
@ -0,0 +1 @@
|
|||
<%= render "valuation", valuation: @valuation %>
|
|
@ -1,29 +0,0 @@
|
|||
<%# locals: (account:, valuations:) %>
|
||||
<div class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="font-medium text-lg">History</h3>
|
||||
<%= link_to new_account_valuation_path(account), data: { turbo_frame: dom_id(Valuation.new) }, class: "flex gap-1 font-medium items-center bg-gray-50 text-gray-900 p-2 rounded-lg" do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-900") %>
|
||||
<span class="text-sm">New entry</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="rounded-xl bg-gray-25 p-1">
|
||||
<div class="flex flex-col rounded-lg space-y-1">
|
||||
<div class="text-xs font-medium text-gray-500 uppercase flex items-center px-4 py-2">
|
||||
<div class="w-16">date</div>
|
||||
<div class="flex items-center justify-between grow">
|
||||
<div></div>
|
||||
<div>value</div>
|
||||
</div>
|
||||
<div class="w-56 text-right">change</div>
|
||||
<div class="w-[72px]"></div>
|
||||
</div>
|
||||
<div class="rounded-lg bg-white border-alpha-black-25 shadow-xs">
|
||||
<%= turbo_frame_tag dom_id(Valuation.new) %>
|
||||
<%= turbo_frame_tag "valuations_list" do %>
|
||||
<%= render partial: "accounts/account_valuation_list", locals: { valuation_series: valuations } %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,57 +0,0 @@
|
|||
<%# locals: (valuation_series:) %>
|
||||
<% valuation_series.values.reverse_each.with_index do |valuation, index| %>
|
||||
<% valuation_styles = trend_styles(valuation.trend) %>
|
||||
<%= turbo_frame_tag dom_id(valuation.original) do %>
|
||||
<div class="p-4 flex items-center">
|
||||
<div class="w-16">
|
||||
<div class="w-8 h-8 rounded-full p-1.5 flex items-center justify-center <%= valuation_styles[:bg_class] %>">
|
||||
<%= lucide_icon(valuation_styles[:icon], class: "w-4 h-4 #{valuation_styles[:text_class]}") %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between grow">
|
||||
<div class="text-sm">
|
||||
<p><%= valuation.date %></p>
|
||||
<%# TODO: Add descriptive name of valuation %>
|
||||
<p class="text-gray-500">Manually entered</p>
|
||||
</div>
|
||||
<div class="flex text-sm font-medium text-right"><%= format_money valuation.value %></div>
|
||||
</div>
|
||||
<div class="flex w-56 justify-end text-right text-sm font-medium">
|
||||
<% if valuation.trend.value == 0 %>
|
||||
<span class="text-gray-500">No change</span>
|
||||
<% else %>
|
||||
<span class="<%= valuation_styles[:text_class] %>"><%= valuation_styles[:symbol] %><%= format_money valuation.trend.value.abs %></span>
|
||||
<span class="<%= valuation_styles[:text_class] %>">(<%= lucide_icon(valuation_styles[:icon], class: "w-4 h-4 align-text-bottom inline") %> <%= valuation.trend.percent %>%)</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="relative w-[72px]" data-controller="menu">
|
||||
<button
|
||||
data-menu-target="button"
|
||||
class="ml-auto flex items-center justify-center hover:bg-gray-50 w-8 h-8 rounded-lg">
|
||||
<%= lucide_icon("more-horizontal", class: "w-5 h-5 text-gray-500") %>
|
||||
</button>
|
||||
<div
|
||||
data-menu-target="content"
|
||||
class="absolute hidden min-w-[200px] z-10 top-10 right-0 bg-white p-1 rounded-sm shadow-xs border border-alpha-black-25 w-fit">
|
||||
<%= link_to edit_valuation_path(valuation.original),
|
||||
class: "flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
|
||||
<%= lucide_icon("pencil-line", class: "w-5 h-5 text-gray-500 shrink-0") %>
|
||||
<span class="text-gray-900 text-sm">Edit entry</span>
|
||||
<% end %>
|
||||
<%= link_to valuation_path(valuation.original),
|
||||
data: { turbo_method: :delete,
|
||||
turbo_confirm: { title: t(".confirm_title"),
|
||||
body: t(".confirm_body_html"),
|
||||
accept: t(".confirm_accept") } },
|
||||
class: "text-red-600 flex gap-1 items-center hover:bg-gray-50 rounded-md p-2" do %>
|
||||
<%= lucide_icon("trash-2", class: "w-5 h-5 shrink-0") %>
|
||||
<span class="text-sm">Delete entry</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% unless index == valuation_series.values.size - 1 %>
|
||||
<div class="h-px bg-alpha-black-50 ml-20 mr-4"></div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
|
@ -56,11 +56,11 @@
|
|||
<div class="p-4 flex justify-between">
|
||||
<div class="space-y-2">
|
||||
<%= render partial: "shared/value_heading", locals: {
|
||||
label: "Total Value",
|
||||
period: @period,
|
||||
value: @account.balance_money,
|
||||
trend: @balance_series.trend
|
||||
} %>
|
||||
label: "Total Value",
|
||||
period: @period,
|
||||
value: @account.balance_money,
|
||||
trend: @balance_series.trend
|
||||
} %>
|
||||
</div>
|
||||
<%= form_with url: account_path(@account), method: :get, class: "flex items-center gap-4", data: { controller: "auto-submit-form" } do %>
|
||||
<%= render partial: "shared/period_select", locals: { value: @period.name } %>
|
||||
|
@ -77,7 +77,23 @@
|
|||
</div>
|
||||
<div class="min-h-[800px]">
|
||||
<div data-tabs-target="tab" id="account-history-tab">
|
||||
<%= render partial: "accounts/account_history", locals: { account: @account, valuations: @valuation_series } %>
|
||||
<div class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
|
||||
<div class="flex items-center justify-between">
|
||||
<%= tag.h2 t(".valuations"), class: "font-medium text-lg" %>
|
||||
<%= link_to new_account_valuation_path(@account), data: { turbo_frame: dom_id(Account::Valuation.new) }, class: "flex gap-1 font-medium items-center bg-gray-50 text-gray-900 p-2 rounded-lg" do %>
|
||||
<%= lucide_icon("plus", class: "w-5 h-5 text-gray-900") %>
|
||||
<%= tag.span t(".new_entry"), class: "text-sm" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl bg-gray-25 p-1">
|
||||
<%= turbo_frame_tag dom_id(@account, "valuations"), src: account_valuations_path(@account) do %>
|
||||
<div class="p-5 flex justify-center items-center">
|
||||
<%= tag.p t(".loading_history"), class: "text-gray-500 animate-pulse text-sm" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-tabs-target="tab" id="account-transactions-tab" class="hidden">
|
||||
<%= render partial: "accounts/transactions", locals: { account: @account, transactions: @account.transactions.order(date: :desc) } %>
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
<%# locals (f:, form_icon:, submit_button_text:)%>
|
||||
<div class="h-[72px] p-4 flex items-center">
|
||||
<div class="w-16">
|
||||
<div class="w-8 h-8 rounded-full p-1.5 flex items-center justify-center bg-gray-500/5">
|
||||
<%= lucide_icon(form_icon, class: "w-4 h-4 text-gray-500") %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between grow">
|
||||
<%= 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" %>
|
||||
</div>
|
||||
<div class="w-[296px] flex gap-2 justify-end items-center">
|
||||
<%= link_to "Cancel", account_path(@valuation.account), class: "text-sm text-gray-900 hover:text-gray-800 font-medium px-3 py-1.5" %>
|
||||
<%= f.submit submit_button_text, class: "bg-gray-50 rounded-lg font-medium px-3 py-1.5 cursor-pointer hover:bg-gray-100 text-sm" %>
|
||||
</div>
|
||||
</div>
|
|
@ -1,4 +0,0 @@
|
|||
<div>
|
||||
<h1 class="font-bold text-4xl">Valuations#create</h1>
|
||||
<p>Find me in app/views/valuations/create.html.erb</p>
|
||||
</div>
|
|
@ -1,4 +0,0 @@
|
|||
<%= turbo_stream.replace Valuation.new, body: turbo_frame_tag(dom_id(Valuation.new)) %>
|
||||
<%= turbo_stream.append "notification-tray", partial: "shared/notification", locals: { type: "success", content: { body: "Valuation created" } } %>
|
||||
<%= turbo_stream.replace "valuations_list", partial: "accounts/account_valuation_list", locals: { valuation_series: @account.valuations.to_series } %>
|
||||
<%= turbo_stream.replace "sync_message", partial: "accounts/sync_message", locals: { is_syncing: true } %>
|
|
@ -1,4 +0,0 @@
|
|||
<div>
|
||||
<h1 class="font-bold text-4xl">Valuations#destroy</h1>
|
||||
<p>Find me in app/views/valuations/destroy.html.erb</p>
|
||||
</div>
|
|
@ -1,4 +0,0 @@
|
|||
<%= turbo_stream.remove @valuation %>
|
||||
<%= turbo_stream.append "notification-tray", partial: "shared/notification", locals: { type: "success", content: { body: "Valuation deleted" } } %>
|
||||
<%= turbo_stream.replace "valuations_list", partial: "accounts/account_valuation_list", locals: { valuation_series: @account.valuations.to_series } %>
|
||||
<%= turbo_stream.replace "sync_message", partial: "accounts/sync_message", locals: { is_syncing: true } %>
|
|
@ -1,8 +0,0 @@
|
|||
<div>
|
||||
<h1 class="font-bold text-4xl">Edit Valuation</h1>
|
||||
<%= turbo_frame_tag dom_id(@valuation) do %>
|
||||
<%= form_with model: @valuation, url: valuation_path(@valuation), html: { class: "" } do |f| %>
|
||||
<%= render "form_row", f: f, form_icon: "pencil-line", submit_button_text: "Update" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
|
@ -1,9 +0,0 @@
|
|||
<div>
|
||||
<h1 class="font-bold text-4xl">Add Valuation: <%= @account.name %></h1>
|
||||
<%= turbo_frame_tag dom_id(Valuation.new) do %>
|
||||
<%= form_with model: [@account, @valuation], url: account_valuations_path(@account), html: { class: "" } do |f| %>
|
||||
<%= render "form_row", f: f, form_icon: "plus", submit_button_text: "Add" %>
|
||||
<% end %>
|
||||
<div class="h-px bg-alpha-black-50 ml-20 mr-4"></div>
|
||||
<% end %>
|
||||
</div>
|
|
@ -1,4 +0,0 @@
|
|||
<div>
|
||||
<h1 class="font-bold text-4xl">Valuation: <%= @valuation.type %></h1>
|
||||
<p>Find me in app/views/valuations/show.html.erb</p>
|
||||
</div>
|
|
@ -1,4 +0,0 @@
|
|||
<div>
|
||||
<h1 class="font-bold text-4xl">Valuations#update</h1>
|
||||
<p>Find me in app/views/valuations/update.html.erb</p>
|
||||
</div>
|
26
config/locales/views/account/valuations/en.yml
Normal file
26
config/locales/views/account/valuations/en.yml
Normal file
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
en:
|
||||
account:
|
||||
valuations:
|
||||
destroy:
|
||||
success: Valuation deleted
|
||||
form:
|
||||
cancel: Cancel
|
||||
index:
|
||||
change: change
|
||||
date: date
|
||||
value: value
|
||||
update:
|
||||
success: Valuation updated
|
||||
valuation:
|
||||
confirm_accept: Delete entry
|
||||
confirm_body_html: "<p>Deleting this entry will remove it from the account’s
|
||||
history which will impact different parts of your account. This includes
|
||||
the net worth and account graphs.</p></br><p>The only way you’ll be able
|
||||
to add this entry back is by re-entering it manually via a new entry</p>"
|
||||
confirm_title: Delete Entry?
|
||||
delete_entry: Delete entry
|
||||
edit_entry: Edit entry
|
||||
no_change: No change
|
||||
start_balance: Starting balance
|
||||
value_update: Value update
|
|
@ -1,13 +1,6 @@
|
|||
---
|
||||
en:
|
||||
accounts:
|
||||
account_valuation_list:
|
||||
confirm_accept: Delete entry
|
||||
confirm_body_html: "<p>Deleting this entry will remove it from the account’s
|
||||
history which will impact different parts of your account. This includes the
|
||||
net worth and account graphs.</p></br><p>The only way you’ll be able to add
|
||||
this entry back is by re-entering it manually via a new entry</p>"
|
||||
confirm_title: Delete Entry?
|
||||
create:
|
||||
success: New account created successfully
|
||||
destroy:
|
||||
|
@ -65,9 +58,12 @@ en:
|
|||
confirm_title: Delete account?
|
||||
edit: Edit
|
||||
import: Import transactions
|
||||
loading_history: Loading account history...
|
||||
new_entry: New entry
|
||||
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.
|
||||
valuations: Value history
|
||||
summary:
|
||||
new: New account
|
||||
sync:
|
||||
|
|
|
@ -80,8 +80,10 @@ Rails.application.routes.draw do
|
|||
post :sync
|
||||
end
|
||||
|
||||
resource :logo, only: :show, module: :account
|
||||
resources :valuations, shallow: true
|
||||
scope module: :account do
|
||||
resource :logo, only: :show
|
||||
resources :valuations
|
||||
end
|
||||
end
|
||||
|
||||
resources :institutions, except: %i[ index show ]
|
||||
|
|
5
db/migrate/20240620221801_rename_valuation_table.rb
Normal file
5
db/migrate/20240620221801_rename_valuation_table.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
class RenameValuationTable < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
rename_table :valuations, :account_valuations
|
||||
end
|
||||
end
|
26
db/schema.rb
generated
26
db/schema.rb
generated
|
@ -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_06_20_125026) do
|
||||
ActiveRecord::Schema[7.2].define(version: 2024_06_20_221801) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pgcrypto"
|
||||
enable_extension "plpgsql"
|
||||
|
@ -37,6 +37,17 @@ ActiveRecord::Schema[7.2].define(version: 2024_06_20_125026) do
|
|||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "account_valuations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.uuid "account_id", null: false
|
||||
t.date "date", null: false
|
||||
t.decimal "value", precision: 19, scale: 4, null: false
|
||||
t.string "currency", default: "USD", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["account_id", "date"], name: "index_account_valuations_on_account_id_and_date", unique: true
|
||||
t.index ["account_id"], name: "index_account_valuations_on_account_id"
|
||||
end
|
||||
|
||||
create_table "accounts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.string "subtype"
|
||||
t.uuid "family_id", null: false
|
||||
|
@ -335,23 +346,13 @@ ActiveRecord::Schema[7.2].define(version: 2024_06_20_125026) do
|
|||
t.index ["family_id"], name: "index_users_on_family_id"
|
||||
end
|
||||
|
||||
create_table "valuations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.uuid "account_id", null: false
|
||||
t.date "date", null: false
|
||||
t.decimal "value", precision: 19, scale: 4, null: false
|
||||
t.string "currency", default: "USD", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["account_id", "date"], name: "index_valuations_on_account_id_and_date", unique: true
|
||||
t.index ["account_id"], name: "index_valuations_on_account_id"
|
||||
end
|
||||
|
||||
create_table "vehicles", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
add_foreign_key "account_balances", "accounts", on_delete: :cascade
|
||||
add_foreign_key "account_valuations", "accounts", on_delete: :cascade
|
||||
add_foreign_key "accounts", "families"
|
||||
add_foreign_key "accounts", "institutions"
|
||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||
|
@ -367,5 +368,4 @@ ActiveRecord::Schema[7.2].define(version: 2024_06_20_125026) do
|
|||
add_foreign_key "transactions", "categories", on_delete: :nullify
|
||||
add_foreign_key "transactions", "merchants"
|
||||
add_foreign_key "users", "families"
|
||||
add_foreign_key "valuations", "accounts", on_delete: :cascade
|
||||
end
|
||||
|
|
71
test/controllers/account/valuations_controller_test.rb
Normal file
71
test/controllers/account/valuations_controller_test.rb
Normal file
|
@ -0,0 +1,71 @@
|
|||
require "test_helper"
|
||||
|
||||
class Account::ValuationsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in @user = users(:family_admin)
|
||||
@valuation = account_valuations(:savings_one)
|
||||
@account = @valuation.account
|
||||
end
|
||||
|
||||
test "get valuations for an account" do
|
||||
get account_valuations_url(@account)
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "new" do
|
||||
get new_account_valuation_url(@account)
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should create valuation" do
|
||||
assert_difference("Account::Valuation.count") do
|
||||
post account_valuations_url(@account), params: {
|
||||
account_valuation: {
|
||||
value: 19800,
|
||||
date: Date.current
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
assert_equal "Valuation created", flash[:notice]
|
||||
assert_enqueued_with job: AccountSyncJob
|
||||
assert_redirected_to account_path(@account)
|
||||
end
|
||||
|
||||
test "error when valuation already exists for date" do
|
||||
assert_difference("Account::Valuation.count", 0) do
|
||||
post account_valuations_url(@account), params: {
|
||||
account_valuation: {
|
||||
value: 19800,
|
||||
date: @valuation.date
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
assert_equal "Date has already been taken", flash[:error]
|
||||
assert_redirected_to account_path(@account)
|
||||
end
|
||||
|
||||
test "should update valuation" do
|
||||
patch account_valuation_url(@account, @valuation), params: {
|
||||
account_valuation: {
|
||||
value: 19550,
|
||||
date: Date.current
|
||||
}
|
||||
}
|
||||
|
||||
assert_equal "Valuation updated", flash[:notice]
|
||||
assert_enqueued_with job: AccountSyncJob
|
||||
assert_redirected_to account_path(@account)
|
||||
end
|
||||
|
||||
test "should destroy valuation" do
|
||||
assert_difference("Account::Valuation.count", -1) do
|
||||
delete account_valuation_url(@account, @valuation)
|
||||
end
|
||||
|
||||
assert_equal "Valuation deleted", flash[:notice]
|
||||
assert_enqueued_with job: AccountSyncJob
|
||||
assert_redirected_to account_path(@account)
|
||||
end
|
||||
end
|
|
@ -44,7 +44,7 @@ class AccountsControllerTest < ActionDispatch::IntegrationTest
|
|||
end
|
||||
|
||||
test "should create an account" do
|
||||
assert_difference [ "Account.count", "Valuation.count" ], 1 do
|
||||
assert_difference [ "Account.count", "Account::Valuation.count" ], 1 do
|
||||
post accounts_path, params: {
|
||||
account: {
|
||||
accountable_type: "Depository",
|
||||
|
@ -60,7 +60,7 @@ class AccountsControllerTest < ActionDispatch::IntegrationTest
|
|||
end
|
||||
|
||||
test "can add optional start date and balance to an account on create" do
|
||||
assert_difference -> { Account.count } => 1, -> { Valuation.count } => 2 do
|
||||
assert_difference -> { Account.count } => 1, -> { Account::Valuation.count } => 2 do
|
||||
post accounts_path, params: {
|
||||
account: {
|
||||
accountable_type: "Depository",
|
||||
|
|
|
@ -75,7 +75,7 @@ class TransactionsControllerTest < ActionDispatch::IntegrationTest
|
|||
end
|
||||
|
||||
assert_equal transaction_params[:amount].to_d, Transaction.order(created_at: :desc).first.amount
|
||||
assert_equal flash[:notice], "New transaction created successfully"
|
||||
assert_equal "New transaction created successfully", flash[:notice]
|
||||
assert_enqueued_with(job: AccountSyncJob)
|
||||
assert_redirected_to transactions_url
|
||||
end
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class ValuationsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
sign_in @user = users(:family_admin)
|
||||
@valuation = valuations(:savings_one)
|
||||
@account = @valuation.account
|
||||
end
|
||||
|
||||
test "new" do
|
||||
get new_account_valuation_url(@account)
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "should create valuation" do
|
||||
assert_difference("Valuation.count") do
|
||||
post account_valuations_url(@account), params: { valuation: { value: 1, date: Date.current, type: "Appraisal" } }
|
||||
end
|
||||
|
||||
assert_redirected_to account_path(@valuation.account)
|
||||
end
|
||||
|
||||
test "should create valuation with account's currency" do
|
||||
foreign_account = accounts(:eur_checking)
|
||||
post account_valuations_url(foreign_account), params: { valuation: { value: 1, date: Date.current, type: "Appraisal" } }
|
||||
assert_equal foreign_account.currency, Valuation.order(created_at: :desc).first.currency
|
||||
end
|
||||
|
||||
test "create should sync account with correct start date" do
|
||||
date = Date.current - 1.day
|
||||
|
||||
assert_enqueued_with(job: AccountSyncJob, args: [ @account, date ]) do
|
||||
post account_valuations_url(@account), params: { valuation: { value: 2, date:, type: "Appraisal" } }
|
||||
end
|
||||
end
|
||||
|
||||
test "should update valuation" do
|
||||
date = @valuation.date
|
||||
patch valuation_url(@valuation), params: { valuation: { account_id: @valuation.account_id, value: 1, date:, type: "Appraisal" } }
|
||||
assert_redirected_to account_path(@valuation.account)
|
||||
end
|
||||
|
||||
test "update should sync account with correct start date" do
|
||||
new_date = @valuation.date - 1.day
|
||||
assert_enqueued_with(job: AccountSyncJob, args: [ @account, new_date ]) do
|
||||
patch valuation_url(@valuation), params: { valuation: { account_id: @valuation.account_id, value: @valuation.value, date: new_date, type: "Appraisal" } }
|
||||
end
|
||||
|
||||
new_date = @valuation.reload.date + 1.day
|
||||
assert_enqueued_with(job: AccountSyncJob, args: [ @account, @valuation.date ]) do
|
||||
patch valuation_url(@valuation), params: { valuation: { account_id: @valuation.account_id, value: @valuation.value, date: new_date, type: "Appraisal" } }
|
||||
end
|
||||
end
|
||||
|
||||
test "should destroy valuation" do
|
||||
assert_difference("Valuation.count", -1) do
|
||||
delete valuation_url(@valuation)
|
||||
end
|
||||
|
||||
assert_redirected_to account_path(@account)
|
||||
end
|
||||
|
||||
test "destroy should sync account with correct start date" do
|
||||
first, second = @account.valuations.order(:date).all
|
||||
|
||||
assert_enqueued_with(job: AccountSyncJob, args: [ @account, first.date ]) do
|
||||
delete valuation_url(second)
|
||||
end
|
||||
|
||||
assert_enqueued_with(job: AccountSyncJob, args: [ @account, nil ]) do
|
||||
delete valuation_url(first)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
require "test_helper"
|
||||
|
||||
class TransferTest < ActiveSupport::TestCase
|
||||
class Account::TransferTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
# Transfers can be posted on different dates
|
||||
@outflow = accounts(:checking).transactions.create! date: 1.day.ago.to_date, name: "Transfer to Savings", amount: 100, marked_as_transfer: true
|
39
test/models/account/valuation_test.rb
Normal file
39
test/models/account/valuation_test.rb
Normal file
|
@ -0,0 +1,39 @@
|
|||
require "test_helper"
|
||||
|
||||
class Account::ValuationTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@valuation = account_valuations :savings_one
|
||||
@family = families :dylan_family
|
||||
end
|
||||
|
||||
test "one valuation per day" do
|
||||
assert_equal 12.days.ago.to_date, account_valuations(:savings_one).date
|
||||
invalid_valuation = Account::Valuation.new date: 12.days.ago.to_date, value: 20000
|
||||
assert invalid_valuation.invalid?
|
||||
end
|
||||
|
||||
test "triggers sync with correct start date when valuation is set to prior date" do
|
||||
prior_date = @valuation.date - 1
|
||||
@valuation.update! date: prior_date
|
||||
|
||||
@valuation.account.expects(:sync_later).with(prior_date)
|
||||
@valuation.sync_account_later
|
||||
end
|
||||
|
||||
test "triggers sync with correct start date when valuation is set to future date" do
|
||||
prior_date = @valuation.date
|
||||
@valuation.update! date: @valuation.date + 1
|
||||
|
||||
@valuation.account.expects(:sync_later).with(prior_date)
|
||||
@valuation.sync_account_later
|
||||
end
|
||||
|
||||
test "triggers sync with correct start date when valuation deleted" do
|
||||
prior_valuation = account_valuations :savings_two # 25 days ago
|
||||
current_valuation = account_valuations :savings_one # 12 days ago
|
||||
current_valuation.destroy!
|
||||
|
||||
current_valuation.account.expects(:sync_later).with(prior_valuation.date)
|
||||
current_valuation.sync_account_later
|
||||
end
|
||||
end
|
|
@ -111,7 +111,7 @@ class AccountTest < ActiveSupport::TestCase
|
|||
end
|
||||
|
||||
test "should destroy dependent valuations" do
|
||||
assert_difference("Valuation.count", -@account.valuations.count) do
|
||||
assert_difference("Account::Valuation.count", -@account.valuations.count) do
|
||||
@account.destroy
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,11 +11,13 @@ class TimeSeries::TrendTest < ActiveSupport::TestCase
|
|||
test "up" do
|
||||
trend = TimeSeries::Trend.new(current: 100, previous: 50)
|
||||
assert_equal "up", trend.direction
|
||||
assert_equal "#10A861", trend.color
|
||||
end
|
||||
|
||||
test "down" do
|
||||
trend = TimeSeries::Trend.new(current: 50, previous: 100)
|
||||
assert_equal "down", trend.direction
|
||||
assert_equal "#F13636", trend.color
|
||||
end
|
||||
|
||||
test "flat" do
|
||||
|
@ -25,6 +27,7 @@ class TimeSeries::TrendTest < ActiveSupport::TestCase
|
|||
assert_equal "flat", trend1.direction
|
||||
assert_equal "flat", trend2.direction
|
||||
assert_equal "flat", trend3.direction
|
||||
assert_equal "#737373", trend1.color
|
||||
end
|
||||
|
||||
test "infinitely up" do
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class ValuationTest < ActiveSupport::TestCase
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue