mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-07-19 13:19:39 +02:00
Scaffold out Account Syncing (#474)
* Add trends, time series, seed data * Remove test data * Replace old view values with helpers * Fix tooltip bugs in D3 chart * Fix tests * Fix smoke test * Add CRUD actions for valuations * Scaffold out inline editing with Turbo * Refactor series logic * Scaffold out basic sync process for accounts * Fix tests
This commit is contained in:
parent
b5b2d335fd
commit
7e324f1b53
25 changed files with 328 additions and 185 deletions
|
@ -10,6 +10,21 @@ class AccountsController < ApplicationController
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@account = Current.family.accounts.find(params[:id])
|
@account = Current.family.accounts.find(params[:id])
|
||||||
|
|
||||||
|
@period = Period.find_by_name(params[:period])
|
||||||
|
if @period.nil?
|
||||||
|
start_date = params[:start_date].presence&.to_date
|
||||||
|
end_date = params[:end_date].presence&.to_date
|
||||||
|
if start_date.is_a?(Date) && end_date.is_a?(Date) && start_date <= end_date
|
||||||
|
@period = Period.new(name: "custom", date_range: start_date..end_date)
|
||||||
|
else
|
||||||
|
params[:period] = "last_30_days"
|
||||||
|
@period = Period.find_by_name(params[:period])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@balance_series = @account.balance_series(@period)
|
||||||
|
@valuation_series = @account.valuation_series
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
|
|
@ -4,8 +4,8 @@ class ValuationsController < ApplicationController
|
||||||
def create
|
def create
|
||||||
@account = Current.family.accounts.find(params[:account_id])
|
@account = Current.family.accounts.find(params[:account_id])
|
||||||
|
|
||||||
# TODO: handle STI once we allow for different types of valuations
|
# TODO: placeholder logic until we have a better abstraction for trends
|
||||||
@valuation = @account.valuations.new(valuation_params.merge(type: "Appraisal", currency: Current.family.currency))
|
@valuation = @account.valuations.new(valuation_params.merge(currency: Current.family.currency))
|
||||||
if @valuation.save
|
if @valuation.save
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to account_path(@account), notice: "Valuation created" }
|
format.html { redirect_to account_path(@account), notice: "Valuation created" }
|
||||||
|
@ -41,11 +41,11 @@ class ValuationsController < ApplicationController
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@valuation = Valuation.find(params[:id])
|
@valuation = Valuation.find(params[:id])
|
||||||
account = @valuation.account
|
@account = @valuation.account
|
||||||
@valuation.destroy
|
@valuation.destroy
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to account_path(account), notice: "Valuation deleted" }
|
format.html { redirect_to account_path(@account), notice: "Valuation deleted" }
|
||||||
format.turbo_stream
|
format.turbo_stream
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -49,8 +49,8 @@ module ApplicationHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
# Styles to use when displaying a change in value
|
# Styles to use when displaying a change in value
|
||||||
def trend_styles(trend_direction)
|
def trend_styles(trend)
|
||||||
bg_class, text_class, symbol, icon = case trend_direction
|
bg_class, text_class, symbol, icon = case trend.direction
|
||||||
when "up"
|
when "up"
|
||||||
[ "bg-green-500/5", "text-green-500", "+", "arrow-up" ]
|
[ "bg-green-500/5", "text-green-500", "+", "arrow-up" ]
|
||||||
when "down"
|
when "down"
|
||||||
|
@ -58,14 +58,19 @@ module ApplicationHelper
|
||||||
when "flat"
|
when "flat"
|
||||||
[ "bg-gray-500/5", "text-gray-500", "", "minus" ]
|
[ "bg-gray-500/5", "text-gray-500", "", "minus" ]
|
||||||
else
|
else
|
||||||
raise ArgumentError, "Invalid trend direction: #{trend_direction}"
|
raise ArgumentError, "Invalid trend direction: #{trend.direction}"
|
||||||
end
|
end
|
||||||
|
|
||||||
{ bg_class: bg_class, text_class: text_class, symbol: symbol, icon: icon }
|
{ bg_class: bg_class, text_class: text_class, symbol: symbol, icon: icon }
|
||||||
end
|
end
|
||||||
|
|
||||||
def trend_label(date_range)
|
def trend_label(period)
|
||||||
start_date, end_date = date_range.values_at(:start, :end)
|
return "since account creation" if period.date_range.nil?
|
||||||
|
start_date, end_date = period.date_range.first, period.date_range.last
|
||||||
|
|
||||||
|
return "Starting from #{start_date.strftime('%b %d, %Y')}" if end_date.nil?
|
||||||
|
return "Ending at #{end_date.strftime('%b %d, %Y')}" if start_date.nil?
|
||||||
|
|
||||||
days_apart = (end_date - start_date).to_i
|
days_apart = (end_date - start_date).to_i
|
||||||
|
|
||||||
case days_apart
|
case days_apart
|
||||||
|
|
|
@ -36,30 +36,20 @@ export default class extends Controller {
|
||||||
}[trendDirection];
|
}[trendDirection];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Array} balances - An array of objects where each object represents a balance entry. Each object should have the following properties:
|
|
||||||
* - date: {Date} The date of the balance entry.
|
|
||||||
* - value: {number} The numerical value of the balance.
|
|
||||||
* - formatted: {string} The formatted string representation of the balance value.
|
|
||||||
* - trend: {Object} An object containing information about the trend compared to the previous balance entry. It should have:
|
|
||||||
* - amount: {number} The numerical difference in value from the previous entry.
|
|
||||||
* - direction: {string} A string indicating the direction of the trend ("up", "down", or "flat").
|
|
||||||
* - percent: {number} The percentage change from the previous entry.
|
|
||||||
*/
|
|
||||||
drawChart(balances) {
|
drawChart(balances) {
|
||||||
const data = balances.map((b) => ({
|
const data = balances.map((b) => ({
|
||||||
...b,
|
date: new Date(b.data.date + "T00:00:00"),
|
||||||
value: +b.value,
|
value: +b.data.balance,
|
||||||
date: new Date(b.date),
|
|
||||||
styles: this.trendStyles(b.trend.direction),
|
styles: this.trendStyles(b.trend.direction),
|
||||||
|
trend: b.trend,
|
||||||
formatted: {
|
formatted: {
|
||||||
value: Intl.NumberFormat("en-US", {
|
value: Intl.NumberFormat("en-US", {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: b.currency || "USD",
|
currency: b.data.currency || "USD",
|
||||||
}).format(b.value),
|
}).format(b.data.balance),
|
||||||
change: Intl.NumberFormat("en-US", {
|
change: Intl.NumberFormat("en-US", {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: b.currency || "USD",
|
currency: b.data.currency || "USD",
|
||||||
signDisplay: "always",
|
signDisplay: "always",
|
||||||
}).format(b.trend.amount),
|
}).format(b.trend.amount),
|
||||||
},
|
},
|
||||||
|
@ -87,7 +77,7 @@ export default class extends Controller {
|
||||||
])
|
])
|
||||||
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
|
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
|
||||||
|
|
||||||
const margin = { top: 20, right: 0, bottom: 30, left: 0 },
|
const margin = { top: 20, right: 1, bottom: 30, left: 1 },
|
||||||
width = +svg.attr("width") - margin.left - margin.right,
|
width = +svg.attr("width") - margin.left - margin.right,
|
||||||
height = +svg.attr("height") - margin.top - margin.bottom,
|
height = +svg.attr("height") - margin.top - margin.bottom,
|
||||||
g = svg
|
g = svg
|
||||||
|
@ -216,7 +206,7 @@ export default class extends Controller {
|
||||||
.html(
|
.html(
|
||||||
`<div style="margin-bottom: 4px; color: ${
|
`<div style="margin-bottom: 4px; color: ${
|
||||||
tailwindColors.gray[500]
|
tailwindColors.gray[500]
|
||||||
}">${d3.timeFormat("%b %Y")(d.date)}</div>
|
}">${d3.timeFormat("%b %d, %Y")(d.date)}</div>
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
<svg width="10" height="10">
|
<svg width="10" height="10">
|
||||||
<circle cx="5" cy="5" r="4" stroke="${
|
<circle cx="5" cy="5" r="4" stroke="${
|
||||||
|
|
74
app/jobs/account_balance_sync_job.rb
Normal file
74
app/jobs/account_balance_sync_job.rb
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
class AccountBalanceSyncJob < ApplicationJob
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
# Naive implementation of the perform method (will be refactored to handle transactions later)
|
||||||
|
def perform(account_id:, valuation_date:, sync_type:, sync_action:)
|
||||||
|
account = Account.find(account_id)
|
||||||
|
|
||||||
|
account.status = "SYNCING"
|
||||||
|
account.save!
|
||||||
|
|
||||||
|
case sync_type
|
||||||
|
when "valuation"
|
||||||
|
case sync_action
|
||||||
|
when "update"
|
||||||
|
handle_valuation_update(account: account, valuation_date: valuation_date)
|
||||||
|
when "destroy"
|
||||||
|
handle_valuation_destroy(account: account, valuation_date: valuation_date)
|
||||||
|
else
|
||||||
|
logger.error "Unsupported sync_action: #{sync_action} for sync_type: #{sync_type}"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
logger.error "Unsupported sync_type: #{sync_type}"
|
||||||
|
end
|
||||||
|
|
||||||
|
sync_current_account_balance(account)
|
||||||
|
|
||||||
|
account.status = "OK"
|
||||||
|
account.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def sync_current_account_balance(account)
|
||||||
|
today_balance = account.balances.find_or_initialize_by(date: Date.current)
|
||||||
|
today_balance.update(balance: account.converted_balance)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_valuation_update(account:, valuation_date:)
|
||||||
|
updated_valuation = account.valuations.find_by(date: valuation_date)
|
||||||
|
|
||||||
|
return unless updated_valuation
|
||||||
|
|
||||||
|
update_period_start = valuation_date
|
||||||
|
update_period_end = (account.valuations.where("date > ?", valuation_date).order(:date).first&.date || Date.current) - 1.day
|
||||||
|
|
||||||
|
balances_to_upsert = (update_period_start..update_period_end).map do |date|
|
||||||
|
{ date: date, balance: updated_valuation.value, created_at: Time.current, updated_at: Time.current }
|
||||||
|
end
|
||||||
|
|
||||||
|
account.balances.upsert_all(balances_to_upsert, unique_by: :index_account_balances_on_account_id_and_date)
|
||||||
|
|
||||||
|
logger.info "Upserted balances for account #{account.id} from #{update_period_start} to #{update_period_end}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_valuation_destroy(account:, valuation_date:)
|
||||||
|
prior_valuation = account.valuations.where("date < ?", valuation_date).order(:date).last
|
||||||
|
period_start = prior_valuation&.date
|
||||||
|
period_end = (account.valuations.where("date > ?", valuation_date).order(:date).first&.date || Date.current) - 1.day
|
||||||
|
|
||||||
|
if prior_valuation
|
||||||
|
balances_to_upsert = (period_start..period_end).map do |date|
|
||||||
|
{ date: date, balance: prior_valuation.value, created_at: Time.current, updated_at: Time.current }
|
||||||
|
end
|
||||||
|
|
||||||
|
account.balances.upsert_all(balances_to_upsert, unique_by: :index_account_balances_on_account_id_and_date)
|
||||||
|
logger.info "Upserted balances for account #{account.id} from #{period_start} to #{period_end}"
|
||||||
|
else
|
||||||
|
delete_count = account.balances.where(date: period_start..period_end).delete_all
|
||||||
|
logger.info "Deleted #{delete_count} balances for account #{account.id} from #{period_start} to #{period_end}"
|
||||||
|
end
|
||||||
|
rescue => e
|
||||||
|
logger.error "Sync failed after valuation destroy operation on account #{account.id} with message: #{e.message}"
|
||||||
|
end
|
||||||
|
end
|
|
@ -6,16 +6,32 @@ class Account < ApplicationRecord
|
||||||
delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy
|
delegated_type :accountable, types: Accountable::TYPES, dependent: :destroy
|
||||||
|
|
||||||
delegate :type_name, to: :accountable
|
delegate :type_name, to: :accountable
|
||||||
|
|
||||||
before_create :check_currency
|
before_create :check_currency
|
||||||
|
|
||||||
# Show all valuations in history table (no date range filtering)
|
def balance_series(period)
|
||||||
def valuations_with_trend
|
filtered_balances = balances.in_period(period).order(:date)
|
||||||
series_for(valuations, :value)
|
return nil if filtered_balances.empty?
|
||||||
|
|
||||||
|
series_data = [ nil, *filtered_balances ].each_cons(2).map do |previous, current|
|
||||||
|
trend = current&.trend(previous)
|
||||||
|
{ data: current, trend: { amount: trend&.amount, direction: trend&.direction, percent: trend&.percent } }
|
||||||
|
end
|
||||||
|
|
||||||
|
last_balance = series_data.last[:data]
|
||||||
|
|
||||||
|
{
|
||||||
|
series_data: series_data,
|
||||||
|
last_balance: last_balance.balance,
|
||||||
|
trend: last_balance.trend(series_data.first[:data])
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def balances_with_trend(date_range = default_date_range)
|
def valuation_series
|
||||||
series_for(balances, :balance, date_range)
|
series_data = [ nil, *valuations.order(:date) ].each_cons(2).map do |previous, current|
|
||||||
|
{ value: current, trend: current&.trend(previous) }
|
||||||
|
end
|
||||||
|
|
||||||
|
series_data.reverse_each
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_currency
|
def check_currency
|
||||||
|
@ -27,36 +43,4 @@ class Account < ApplicationRecord
|
||||||
self.converted_currency = self.family.currency
|
self.converted_currency = self.family.currency
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def default_date_range
|
|
||||||
{ start: 30.days.ago.to_date, end: Date.today }
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: probably a better abstraction for this in the future
|
|
||||||
def series_for(collection, value_attr, date_range = {})
|
|
||||||
collection = filtered_by_date_for(collection, date_range)
|
|
||||||
overall_trend = Trend.new(collection.last&.send(value_attr), collection.first&.send(value_attr))
|
|
||||||
|
|
||||||
collection_with_trends = [ nil, *collection ].each_cons(2).map do |previous, current|
|
|
||||||
{
|
|
||||||
current: current,
|
|
||||||
previous: previous,
|
|
||||||
date: current.date,
|
|
||||||
currency: current.currency,
|
|
||||||
value: current.send(value_attr),
|
|
||||||
trend: Trend.new(current.send(value_attr), previous&.send(value_attr))
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
{ date_range: date_range, trend: overall_trend, series: collection_with_trends }
|
|
||||||
end
|
|
||||||
|
|
||||||
def filtered_by_date_for(association, date_range)
|
|
||||||
scope = association
|
|
||||||
scope = scope.where("date >= ?", date_range[:start]) if date_range[:start]
|
|
||||||
scope = scope.where("date <= ?", date_range[:end]) if date_range[:end]
|
|
||||||
scope.order(:date).to_a
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
class AccountBalance < ApplicationRecord
|
class AccountBalance < ApplicationRecord
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
|
||||||
|
scope :in_period, ->(period) { period.date_range.nil? ? all : where(date: period.date_range) }
|
||||||
|
|
||||||
|
def trend(previous)
|
||||||
|
Trend.new(balance, previous&.balance)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
# Used for manual account value adjustments (e.g. to correct for a missing transaction)
|
|
||||||
class Adjustment < Valuation
|
|
||||||
end
|
|
|
@ -1,3 +0,0 @@
|
||||||
# Used to update the value of an account based on a manual or external appraisal (i.e. Zillow)
|
|
||||||
class Appraisal < Valuation
|
|
||||||
end
|
|
25
app/models/period.rb
Normal file
25
app/models/period.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
class Period
|
||||||
|
attr_reader :name, :date_range
|
||||||
|
|
||||||
|
def self.find_by_name(name)
|
||||||
|
INDEX[name]
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.names
|
||||||
|
INDEX.keys.sort
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(name:, date_range:)
|
||||||
|
@name = name
|
||||||
|
@date_range = date_range
|
||||||
|
end
|
||||||
|
|
||||||
|
BUILTIN = [
|
||||||
|
new(name: "all", date_range: nil),
|
||||||
|
new(name: "last_7_days", date_range: 7.days.ago.to_date..Date.current),
|
||||||
|
new(name: "last_30_days", date_range: 30.days.ago.to_date..Date.current),
|
||||||
|
new(name: "last_365_days", date_range: 365.days.ago.to_date..Date.current)
|
||||||
|
]
|
||||||
|
|
||||||
|
INDEX = BUILTIN.index_by(&:name)
|
||||||
|
end
|
|
@ -1,5 +1,20 @@
|
||||||
# STI model to represent a point-in-time "valuation" of an account's value
|
|
||||||
# Types include: Appraisal, Adjustment
|
|
||||||
class Valuation < ApplicationRecord
|
class Valuation < ApplicationRecord
|
||||||
belongs_to :account
|
belongs_to :account
|
||||||
|
|
||||||
|
after_commit :sync_account_balances, on: [ :create, :update ]
|
||||||
|
after_destroy :sync_account_balances_after_destroy
|
||||||
|
|
||||||
|
def trend(previous)
|
||||||
|
Trend.new(value, previous&.value)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def sync_account_balances_after_destroy
|
||||||
|
AccountBalanceSyncJob.perform_later(account_id: account_id, valuation_date: date, sync_type: "valuation", sync_action: "destroy")
|
||||||
|
end
|
||||||
|
|
||||||
|
def sync_account_balances
|
||||||
|
AccountBalanceSyncJob.perform_later(account_id: account_id, valuation_date: date, sync_type: "valuation", sync_action: "update")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
48
app/views/accounts/_account_valuation_list.html.erb
Normal file
48
app/views/accounts/_account_valuation_list.html.erb
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<%# locals: (valuation_series:) %>
|
||||||
|
<% valuation_series.with_index do |valuation_item, index| %>
|
||||||
|
<% valuation, trend = valuation_item.values_at(:value, :trend) %>
|
||||||
|
<% valuation_styles = trend_styles(valuation_item[:trend]) %>
|
||||||
|
<%= turbo_frame_tag dom_id(valuation) 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_currency(valuation.value) %></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-56 justify-end text-right text-sm font-medium">
|
||||||
|
<% if trend.amount == 0 %>
|
||||||
|
<span class="text-gray-500">No change</span>
|
||||||
|
<% else %>
|
||||||
|
<span class="<%= valuation_styles[:text_class] %>"><%= valuation_styles[:symbol] %><%= format_currency(trend.amount.abs) %></span>
|
||||||
|
<span class="<%= valuation_styles[:text_class] %>">(<%= lucide_icon(valuation_styles[:icon], class: "w-4 h-4 align-text-bottom inline") %> <%= trend.percent %>%)</span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="relative w-[72px]" data-controller="dropdown">
|
||||||
|
<button data-action="click->dropdown#toggleMenu" 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 class="hidden absolute min-w-[200px] z-10 top-10 right-0 bg-white p-1 rounded-sm shadow-xs border border-alpha-black-25 w-fit" data-dropdown-target="menu">
|
||||||
|
<%= link_to edit_valuation_path(valuation), 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), data: { turbo_method: :delete, turbo_confirm: "Are you sure?" }, 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.size - 1 %>
|
||||||
|
<div class="h-px bg-alpha-black-50 ml-20 mr-4"></div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
8
app/views/accounts/_sync_message.html.erb
Normal file
8
app/views/accounts/_sync_message.html.erb
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<%# locals: (is_syncing:) %>
|
||||||
|
<% if is_syncing %>
|
||||||
|
<div class="my-4 px-8 py-4 rounded-lg bg-yellow-500/10 flex items-center justify-between">
|
||||||
|
<p class="text-gray-900 text-sm">
|
||||||
|
Syncing your account balances. Please reload the page to see updated data.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
|
@ -1,5 +1,4 @@
|
||||||
<% balances = @account.balances_with_trend %>
|
<% balance_trend_styles = @balance_series.nil? ? {} : trend_styles(@balance_series[:trend]) %>
|
||||||
<% balance_styles = trend_styles(balances[:trend].direction) %>
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
|
@ -20,35 +19,43 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<%= turbo_frame_tag "sync_message" do %>
|
||||||
|
<%= render partial: "accounts/sync_message", locals: { is_syncing: @account.status == "SYNCING" } %>
|
||||||
|
<% end %>
|
||||||
<div class="bg-white shadow-xs rounded-xl border border-alpha-black-25 rounded-lg">
|
<div class="bg-white shadow-xs rounded-xl border border-alpha-black-25 rounded-lg">
|
||||||
<div class="p-4 flex justify-between">
|
<div class="p-4 flex justify-between">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<p class="text-sm text-gray-500">Total Value</p>
|
<p class="text-sm text-gray-500">Total Value</p>
|
||||||
<%# TODO: Will need a better way to split a formatted monetary value into these 3 parts %>
|
<%# TODO: Will need a better way to split a formatted monetary value into these 3 parts %>
|
||||||
<p class="text-gray-900">
|
<p class="text-gray-900">
|
||||||
<span class="text-gray-500"><%= number_to_currency(@account.converted_balance)[0] %></span>
|
<span class="text-gray-500"><%= number_to_currency(@account.original_balance)[0] %></span>
|
||||||
<span class="text-xl font-medium"><%= number_with_delimiter(@account.converted_balance.round) %></span>
|
<span class="text-xl font-medium"><%= number_with_delimiter(@account.original_balance.round) %></span>
|
||||||
<span class="text-gray-500">.<%= number_to_currency(@account.converted_balance, precision: 2)[-2, 2] %></span>
|
<span class="text-gray-500">.<%= number_to_currency(@account.original_balance, precision: 2)[-2, 2] %></span>
|
||||||
</p>
|
</p>
|
||||||
<% if balances[:trend].amount == 0 %>
|
<% if @balance_series.nil? %>
|
||||||
|
<p class="text-sm text-gray-500">Data not available for the selected period</p>
|
||||||
|
<% elsif @balance_series[:trend].amount == 0 %>
|
||||||
<p class="text-sm text-gray-500">No change vs. prior period</p>
|
<p class="text-sm text-gray-500">No change vs. prior period</p>
|
||||||
<% else %>
|
<% else %>
|
||||||
<p class="text-sm <%= balance_styles[:text_class] %>">
|
<p class="text-sm <%= balance_trend_styles[:text_class] %>">
|
||||||
<span><%= balance_styles[:symbol] %><%= number_to_currency(balances[:trend].amount.abs, precision: 2) %></span>
|
<span><%= balance_trend_styles[:symbol] %><%= number_to_currency(@balance_series[:trend].amount.abs, precision: 2) %></span>
|
||||||
<span>(<%= lucide_icon(balances[:trend].amount > 0 ? 'arrow-up' : 'arrow-down', class: "w-4 h-4 align-text-bottom inline") %> <%= balances[:trend].percent %>%)</span>
|
<span>(<%= lucide_icon(@balance_series[:trend].amount > 0 ? 'arrow-up' : 'arrow-down', class: "w-4 h-4 align-text-bottom inline") %> <%= @balance_series[:trend].percent %>%)</span>
|
||||||
<span class="text-gray-500"><%= trend_label(balances[:date_range]) %></span>
|
<span class="text-gray-500"><%= trend_label(@period) %></span>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<%= form_with url: account_path(@account), method: :get, class: "flex items-center gap-4", html: { class: "" } do |f| %>
|
||||||
<div class="flex items-center text-sm gap-2 p-2 border border-alpha-black-100 shadow-xs rounded-lg cursor-not-allowed">
|
<%= f.select :period, options_for_select([['7D', 'last_7_days'], ['1M', 'last_30_days'], ["1Y", "last_365_days"], ['All', 'all']], selected: params[:period]), {}, { class: "block w-full border border-alpha-black-100 shadow-xs rounded-lg text-sm py-2 pr-8 pl-2 cursor-pointer", onchange: "this.form.submit();" } %>
|
||||||
<span class="text-gray-900 pl-1">1M</span>
|
<% end %>
|
||||||
<%= lucide_icon("chevron-down", class: "w-5 h-5 text-gray-500") %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="h-96 flex items-center justify-center text-2xl font-bold">
|
<div class="h-96 flex items-center justify-center text-2xl font-bold">
|
||||||
<div data-controller="line-chart" id="lineChart" class="w-full h-full" data-line-chart-series-value="<%= balances[:series].map { |b| b.merge(trend: { amount: b[:trend].amount, direction: b[:trend].direction, percent: b[:trend].percent }) }.to_json %>"></div>
|
<% if @balance_series %>
|
||||||
|
<div data-controller="line-chart" id="lineChart" class="w-full h-full" data-line-chart-series-value="<%= @balance_series[:series_data].to_json %>"></div>
|
||||||
|
<% else %>
|
||||||
|
<div class="w-full h-full flex items-center justify-center">
|
||||||
|
<p class="text-gray-500">No data available for the selected period.</p>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
|
<div class="bg-white space-y-4 p-5 border border-alpha-black-25 rounded-xl shadow-xs">
|
||||||
|
@ -61,67 +68,22 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-xl bg-gray-25 p-1">
|
<div class="rounded-xl bg-gray-25 p-1">
|
||||||
<div class="flex flex-col rounded-lg space-y-1">
|
<div class="flex flex-col rounded-lg space-y-1">
|
||||||
<div class="flex justify-between gap-10 text-xs font-medium text-gray-500 uppercase py-2">
|
<div class="text-xs font-medium text-gray-500 uppercase flex items-center px-4 py-2">
|
||||||
<div class="ml-4 flex-1">DATE</div>
|
<div class="w-16">date</div>
|
||||||
<div class="flex-1 text-right">VALUE</div>
|
<div class="flex items-center justify-between grow">
|
||||||
<div class="flex-1 text-right">CHANGE</div>
|
<div></div>
|
||||||
<div class="w-12"></div>
|
<div>value</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-56 text-right">change</div>
|
||||||
|
<div class="w-[72px]"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-lg py-2 bg-white border-alpha-black-25 shadow-xs">
|
<div class="rounded-lg bg-white border-alpha-black-25 shadow-xs">
|
||||||
<% series = @account.valuations_with_trend[:series].reverse_each %>
|
<%= turbo_frame_tag dom_id(Valuation.new) %>
|
||||||
<% series.with_index do |valuation, index| %>
|
<%= turbo_frame_tag "valuations_list" do %>
|
||||||
<% valuation_styles = trend_styles(valuation[:trend].direction) %>
|
<%= render partial: "accounts/account_valuation_list", locals: { valuation_series: @valuation_series } %>
|
||||||
<%= turbo_frame_tag dom_id(valuation[:current]) do %>
|
|
||||||
<div class="p-4 flex items-center justify-between gap-10 w-full">
|
|
||||||
<div class="flex-1 flex items-center gap-4">
|
|
||||||
<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 class="text-sm">
|
|
||||||
<p><%= valuation[:date] %></p>
|
|
||||||
<%# TODO: Add descriptive name of valuation %>
|
|
||||||
<p class="text-gray-500">Manually entered</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-1 text-sm font-medium text-right"><%= format_currency(valuation[:value]) %></div>
|
|
||||||
<div class="flex-1 text-right text-sm font-medium">
|
|
||||||
<% if valuation[:trend].amount == 0 %>
|
|
||||||
<span class="text-gray-500">No change</span>
|
|
||||||
<% else %>
|
|
||||||
<span class="<%= valuation_styles[:text_class] %>"><%= valuation_styles[:symbol] %><%= format_currency(valuation[:trend].amount.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-8" data-controller="dropdown">
|
|
||||||
<button data-action="click->dropdown#toggleMenu" class="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 class="hidden absolute min-w-[200px] z-10 top-10 right-0 bg-white p-1 rounded-sm shadow-xs border border-alpha-black-25 w-fit" data-dropdown-target="menu">
|
|
||||||
<%= link_to edit_valuation_path(valuation[:current]), 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[:current]), data: { turbo_method: :delete, turbo_confirm: "Are you sure?" }, 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>
|
|
||||||
<div>
|
|
||||||
<% if index < series.size - 1 %>
|
|
||||||
<div class="h-px bg-alpha-black-50 mr-6 ml-16"></div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<%= turbo_frame_tag dom_id(Valuation.new) do %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= link_to new_account_valuation_path(@account), data: { turbo_frame: "new_valuation" }, class: "hover:bg-white w-full text-sm flex items-center justify-center gap-2 text-gray-500 px-4 py-2 rounded-md" do %>
|
|
||||||
<%= lucide_icon("plus", class: "w-4 h-4") %> New entry
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
<%#
|
<%# locals (f:, form_icon:, submit_button_text:)%>
|
||||||
Locals:
|
<div class="h-[72px] p-4 flex items-center">
|
||||||
- f: form object for valuation
|
<div class="w-16">
|
||||||
- form_icon: string representing the icon to be displayed
|
<div class="w-8 h-8 rounded-full p-1.5 flex items-center justify-center bg-gray-500/5">
|
||||||
- submit_button_text: string representing the text on the submit button
|
<%= lucide_icon(form_icon, class: "w-4 h-4 text-gray-500") %>
|
||||||
%>
|
</div>
|
||||||
<div class="p-4 flex items-center justify-between w-full">
|
</div>
|
||||||
<div class="shrink-0 w-8 h-8 rounded-full p-1.5 flex items-center justify-center bg-gray-500/5">
|
<div class="flex items-center justify-between grow">
|
||||||
<%= lucide_icon(form_icon, class: "w-4 h-4 text-gray-500") %>
|
<%= f.date_field :date, 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, step: :any, placeholder: number_to_currency(0), in: 0.00..100000000.00, required: 'required', 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>
|
||||||
<%= f.date_field :date, class: "border border-alpha-black-200 bg-white rounded-lg shadow-xs min-w-[200px] px-3 py-1.5" %>
|
|
||||||
<%= f.number_field :value, step: :any, placeholder: number_to_currency(0), in: 0.00..100000000.00, required: 'required', class: "bg-white border border-alpha-black-200 rounded-lg shadow-xs px-3 py-1.5" %>
|
|
||||||
<%= 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,3 +1,4 @@
|
||||||
<%# TODO: We need a way to determine the order the new valuation needs to be in the array, calculate the trend, and append it to the right spot %>
|
|
||||||
<%= turbo_stream.update Valuation.new, "" %>
|
<%= turbo_stream.update Valuation.new, "" %>
|
||||||
<%= turbo_stream.append "notification-tray", partial: "shared/notification", locals: { type: "success", content: "Valuation created" } %>
|
<%= turbo_stream.append "notification-tray", partial: "shared/notification", locals: { type: "success", content: "Valuation created" } %>
|
||||||
|
<%= turbo_stream.replace "valuations_list", partial: "accounts/account_valuation_list", locals: { valuation_series: @account.valuation_series } %>
|
||||||
|
<%= turbo_stream.replace "sync_message", partial: "accounts/sync_message", locals: { is_syncing: true } %>
|
||||||
|
|
|
@ -1,2 +1,4 @@
|
||||||
<%= turbo_stream.remove @valuation %>
|
<%= turbo_stream.remove @valuation %>
|
||||||
<%= turbo_stream.append "notification-tray", partial: "shared/notification", locals: { type: "success", content: "Valuation deleted" } %>
|
<%= turbo_stream.append "notification-tray", partial: "shared/notification", locals: { type: "success", content: "Valuation deleted" } %>
|
||||||
|
<%= turbo_stream.replace "valuations_list", partial: "accounts/account_valuation_list", locals: { valuation_series: @account.valuation_series } %>
|
||||||
|
<%= turbo_stream.replace "sync_message", partial: "accounts/sync_message", locals: { is_syncing: true } %>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<div>
|
<div>
|
||||||
<h1 class="font-bold text-4xl">Edit Valuation: <%= @valuation.type %></h1>
|
<h1 class="font-bold text-4xl">Edit Valuation</h1>
|
||||||
<%= turbo_frame_tag dom_id(@valuation) do %>
|
<%= turbo_frame_tag dom_id(@valuation) do %>
|
||||||
<%= form_with model: @valuation, url: valuation_path(@valuation), scope: :valuation do |f| %>
|
<%= 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" %>
|
<%= render 'form_row', f: f, form_icon: "pencil-line", submit_button_text: "Update" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<div>
|
<div>
|
||||||
<h1 class="font-bold text-4xl">Add Valuation: <%= @account.name %></h1>
|
<h1 class="font-bold text-4xl">Add Valuation: <%= @account.name %></h1>
|
||||||
<%= turbo_frame_tag dom_id(Valuation.new) do %>
|
<%= turbo_frame_tag dom_id(Valuation.new) do %>
|
||||||
<%= form_with model: [@account, @valuation], url: account_valuations_path(@account), scope: :valuation do |f| %>
|
<%= 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" %>
|
<%= render 'form_row', f: f, form_icon: "plus", submit_button_text: "Add" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<div class="h-px bg-alpha-black-50 ml-20 mr-4"></div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
5
db/migrate/20240221004818_remove_valuation_type.rb
Normal file
5
db/migrate/20240221004818_remove_valuation_type.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
class RemoveValuationType < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
remove_column :valuations, :type, :string
|
||||||
|
end
|
||||||
|
end
|
5
db/migrate/20240222144849_add_status_to_account.rb
Normal file
5
db/migrate/20240222144849_add_status_to_account.rb
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
class AddStatusToAccount < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
add_column :accounts, :status, :string, default: "OK"
|
||||||
|
end
|
||||||
|
end
|
4
db/schema.rb
generated
4
db/schema.rb
generated
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.2].define(version: 2024_02_15_201527) do
|
ActiveRecord::Schema[7.2].define(version: 2024_02_22_144849) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pgcrypto"
|
enable_extension "pgcrypto"
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -78,6 +78,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_02_15_201527) do
|
||||||
t.string "original_currency", default: "USD"
|
t.string "original_currency", default: "USD"
|
||||||
t.decimal "converted_balance", precision: 19, scale: 4, default: "0.0"
|
t.decimal "converted_balance", precision: 19, scale: 4, default: "0.0"
|
||||||
t.string "converted_currency", default: "USD"
|
t.string "converted_currency", default: "USD"
|
||||||
|
t.string "status", default: "OK"
|
||||||
t.index ["accountable_type"], name: "index_accounts_on_accountable_type"
|
t.index ["accountable_type"], name: "index_accounts_on_accountable_type"
|
||||||
t.index ["family_id"], name: "index_accounts_on_family_id"
|
t.index ["family_id"], name: "index_accounts_on_family_id"
|
||||||
end
|
end
|
||||||
|
@ -207,7 +208,6 @@ ActiveRecord::Schema[7.2].define(version: 2024_02_15_201527) do
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "valuations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
create_table "valuations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||||
t.string "type", null: false
|
|
||||||
t.uuid "account_id", null: false
|
t.uuid "account_id", null: false
|
||||||
t.date "date", null: false
|
t.date "date", null: false
|
||||||
t.decimal "value", precision: 19, scale: 4, null: false
|
t.decimal "value", precision: 19, scale: 4, null: false
|
||||||
|
|
31
db/seeds.rb
31
db/seeds.rb
|
@ -32,16 +32,15 @@ account = Account.create_or_find_by(name: "Seed Property Account", accountable:
|
||||||
puts "Account created: #{account.name}"
|
puts "Account created: #{account.name}"
|
||||||
|
|
||||||
# Represent user-defined "Valuations" at various dates
|
# Represent user-defined "Valuations" at various dates
|
||||||
appraisals = [
|
valuations = [
|
||||||
{ date: Date.today - 30, balance: 300000 },
|
{ date: Date.today - 30, value: 300000 },
|
||||||
{ date: Date.today - 22, balance: 300700 },
|
{ date: Date.today - 22, value: 300700 },
|
||||||
{ date: Date.today - 17, balance: 301400 },
|
{ date: Date.today - 17, value: 301400 },
|
||||||
{ date: Date.today - 10, balance: 300000 },
|
{ date: Date.today - 10, value: 300000 },
|
||||||
{ date: Date.today - 3, balance: 301900 }
|
{ date: Date.today - 3, value: 301900 }
|
||||||
]
|
]
|
||||||
|
|
||||||
# In prod, this would be calculated from the current balance and the appraisals with a background job
|
# Represent system-generated "Balances" at various dates, based on valuations
|
||||||
# Hardcoded for readability
|
|
||||||
balances = [
|
balances = [
|
||||||
{ date: Date.today - 30, balance: 300000 },
|
{ date: Date.today - 30, balance: 300000 },
|
||||||
{ date: Date.today - 29, balance: 300000 },
|
{ date: Date.today - 29, balance: 300000 },
|
||||||
|
@ -73,17 +72,16 @@ balances = [
|
||||||
{ date: Date.today - 3, balance: 301900 },
|
{ date: Date.today - 3, balance: 301900 },
|
||||||
{ date: Date.today - 2, balance: 301900 },
|
{ date: Date.today - 2, balance: 301900 },
|
||||||
{ date: Date.today - 1, balance: 301900 },
|
{ date: Date.today - 1, balance: 301900 },
|
||||||
{ date: Date.today, balance: 302000 }
|
{ date: Date.today, balance: current_balance }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
valuations.each do |valuation|
|
||||||
appraisals.each do |appraisal|
|
Valuation.find_or_create_by(
|
||||||
Appraisal.find_or_create_by(
|
|
||||||
account_id: account.id,
|
account_id: account.id,
|
||||||
date: appraisal[:date]
|
date: valuation[:date]
|
||||||
) do |appraisal_record|
|
) do |valuation_record|
|
||||||
appraisal_record.value = appraisal[:balance]
|
valuation_record.value = valuation[:value]
|
||||||
appraisal_record.currency = "USD"
|
valuation_record.currency = "USD"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -93,6 +91,5 @@ balances.each do |balance|
|
||||||
date: balance[:date]
|
date: balance[:date]
|
||||||
) do |balance_record|
|
) do |balance_record|
|
||||||
balance_record.balance = balance[:balance]
|
balance_record.balance = balance[:balance]
|
||||||
balance_record.currency = "USD"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
2
test/fixtures/valuations.yml
vendored
2
test/fixtures/valuations.yml
vendored
|
@ -1,13 +1,11 @@
|
||||||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||||
|
|
||||||
one:
|
one:
|
||||||
type: "Appraisal"
|
|
||||||
value: 9.99
|
value: 9.99
|
||||||
date: 2024-02-15
|
date: 2024-02-15
|
||||||
account: dylan_checking
|
account: dylan_checking
|
||||||
|
|
||||||
two:
|
two:
|
||||||
type: "Appraisal"
|
|
||||||
value: 9.99
|
value: 9.99
|
||||||
date: 2024-02-15
|
date: 2024-02-15
|
||||||
account: richards_savings
|
account: richards_savings
|
||||||
|
|
7
test/jobs/account_balance_sync_job_test.rb
Normal file
7
test/jobs/account_balance_sync_job_test.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class AccountBalanceSyncJobTest < ActiveJob::TestCase
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
Loading…
Add table
Add a link
Reference in a new issue