mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-02 20:15:22 +02:00
Account:: namespace simplifications and cleanup (#2110)
* Flatten Holding model * Flatten balance model * Entries domain renames * Fix valuations reference * Fix trades stream * Fix brakeman warnings * Fix tests * Replace existing entryable type references in DB
This commit is contained in:
parent
f181ba941f
commit
e657c40d19
172 changed files with 1297 additions and 1258 deletions
|
@ -7,11 +7,11 @@ class Account < ApplicationRecord
|
|||
belongs_to :import, optional: true
|
||||
|
||||
has_many :import_mappings, as: :mappable, dependent: :destroy, class_name: "Import::Mapping"
|
||||
has_many :entries, dependent: :destroy, class_name: "Account::Entry"
|
||||
has_many :transactions, through: :entries, source: :entryable, source_type: "Account::Transaction"
|
||||
has_many :valuations, through: :entries, source: :entryable, source_type: "Account::Valuation"
|
||||
has_many :trades, through: :entries, source: :entryable, source_type: "Account::Trade"
|
||||
has_many :holdings, dependent: :destroy, class_name: "Account::Holding"
|
||||
has_many :entries, dependent: :destroy
|
||||
has_many :transactions, through: :entries, source: :entryable, source_type: "Transaction"
|
||||
has_many :valuations, through: :entries, source: :entryable, source_type: "Valuation"
|
||||
has_many :trades, through: :entries, source: :entryable, source_type: "Trade"
|
||||
has_many :holdings, dependent: :destroy
|
||||
has_many :balances, dependent: :destroy
|
||||
|
||||
monetize :balance, :cash_balance
|
||||
|
@ -43,14 +43,14 @@ class Account < ApplicationRecord
|
|||
date: Date.current,
|
||||
amount: account.balance,
|
||||
currency: account.currency,
|
||||
entryable: Account::Valuation.new
|
||||
entryable: Valuation.new
|
||||
)
|
||||
account.entries.build(
|
||||
name: "Initial Balance",
|
||||
date: 1.day.ago.to_date,
|
||||
amount: initial_balance,
|
||||
currency: account.currency,
|
||||
entryable: Account::Valuation.new
|
||||
entryable: Valuation.new
|
||||
)
|
||||
|
||||
account.save!
|
||||
|
@ -113,7 +113,7 @@ class Account < ApplicationRecord
|
|||
end
|
||||
|
||||
def update_balance!(balance)
|
||||
valuation = entries.account_valuations.find_by(date: Date.current)
|
||||
valuation = entries.valuations.find_by(date: Date.current)
|
||||
|
||||
if valuation
|
||||
valuation.update! amount: balance
|
||||
|
@ -123,7 +123,7 @@ class Account < ApplicationRecord
|
|||
name: "Balance update",
|
||||
amount: balance,
|
||||
currency: currency,
|
||||
entryable: Account::Valuation.new
|
||||
entryable: Valuation.new
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -148,7 +148,7 @@ class Account < ApplicationRecord
|
|||
end
|
||||
|
||||
def first_valuation
|
||||
entries.account_valuations.order(:date).first
|
||||
entries.valuations.order(:date).first
|
||||
end
|
||||
|
||||
def first_valuation_amount
|
||||
|
|
|
@ -7,7 +7,7 @@ module Account::Chartable
|
|||
|
||||
series_interval = interval || period.interval
|
||||
|
||||
balances = Account::Balance.find_by_sql([
|
||||
balances = Balance.find_by_sql([
|
||||
balance_series_query,
|
||||
{
|
||||
start_date: period.start_date,
|
||||
|
@ -61,7 +61,7 @@ module Account::Chartable
|
|||
COUNT(CASE WHEN accounts.currency <> :target_currency AND er.rate IS NULL THEN 1 END) as missing_rates
|
||||
FROM dates d
|
||||
LEFT JOIN accounts ON accounts.id IN (#{all.select(:id).to_sql})
|
||||
LEFT JOIN account_balances ab ON (
|
||||
LEFT JOIN balances ab ON (
|
||||
ab.date = d.date AND
|
||||
ab.currency = accounts.currency AND
|
||||
ab.account_id = accounts.id
|
||||
|
|
|
@ -2,9 +2,9 @@ module Account::Enrichable
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
def enrich_data
|
||||
total_unenriched = entries.account_transactions
|
||||
.joins("JOIN account_transactions at ON at.id = account_entries.entryable_id AND account_entries.entryable_type = 'Account::Transaction'")
|
||||
.where("account_entries.enriched_at IS NULL OR at.merchant_id IS NULL OR at.category_id IS NULL")
|
||||
total_unenriched = entries.transactions
|
||||
.joins("JOIN transactions at ON at.id = entries.entryable_id AND entries.entryable_type = 'Transaction'")
|
||||
.where("entries.enriched_at IS NULL OR at.merchant_id IS NULL OR at.category_id IS NULL")
|
||||
.count
|
||||
|
||||
if total_unenriched > 0
|
||||
|
@ -63,7 +63,7 @@ module Account::Enrichable
|
|||
transactions.active
|
||||
.includes(:merchant, :category)
|
||||
.where(
|
||||
"account_entries.enriched_at IS NULL",
|
||||
"entries.enriched_at IS NULL",
|
||||
"OR merchant_id IS NULL",
|
||||
"OR category_id IS NULL"
|
||||
)
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
class Account::Valuation < ApplicationRecord
|
||||
include Account::Entryable
|
||||
end
|
|
@ -20,7 +20,7 @@ class AccountImport < Import
|
|||
currency: row.currency,
|
||||
date: Date.current,
|
||||
name: "Imported account value",
|
||||
entryable: Account::Valuation.new
|
||||
entryable: Valuation.new
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
class Account::Balance < ApplicationRecord
|
||||
class Balance < ApplicationRecord
|
||||
include Monetizable
|
||||
|
||||
belongs_to :account
|
|
@ -1,4 +1,4 @@
|
|||
class Account::Balance::BaseCalculator
|
||||
class Balance::BaseCalculator
|
||||
attr_reader :account
|
||||
|
||||
def initialize(account)
|
||||
|
@ -13,11 +13,11 @@ class Account::Balance::BaseCalculator
|
|||
|
||||
private
|
||||
def sync_cache
|
||||
@sync_cache ||= Account::Balance::SyncCache.new(account)
|
||||
@sync_cache ||= Balance::SyncCache.new(account)
|
||||
end
|
||||
|
||||
def build_balance(date, cash_balance, holdings_value)
|
||||
Account::Balance.new(
|
||||
Balance.new(
|
||||
account_id: account.id,
|
||||
date: date,
|
||||
balance: holdings_value + cash_balance,
|
|
@ -1,4 +1,4 @@
|
|||
class Account::Balance::ForwardCalculator < Account::Balance::BaseCalculator
|
||||
class Balance::ForwardCalculator < Balance::BaseCalculator
|
||||
private
|
||||
def calculate_balances
|
||||
current_cash_balance = 0
|
|
@ -1,4 +1,4 @@
|
|||
class Account::Balance::ReverseCalculator < Account::Balance::BaseCalculator
|
||||
class Balance::ReverseCalculator < Balance::BaseCalculator
|
||||
private
|
||||
def calculate_balances
|
||||
current_cash_balance = account.cash_balance
|
|
@ -1,10 +1,10 @@
|
|||
class Account::Balance::SyncCache
|
||||
class Balance::SyncCache
|
||||
def initialize(account)
|
||||
@account = account
|
||||
end
|
||||
|
||||
def get_valuation(date)
|
||||
converted_entries.find { |e| e.date == date && e.account_valuation? }
|
||||
converted_entries.find { |e| e.date == date && e.valuation? }
|
||||
end
|
||||
|
||||
def get_holdings(date)
|
||||
|
@ -12,7 +12,7 @@ class Account::Balance::SyncCache
|
|||
end
|
||||
|
||||
def get_entries(date)
|
||||
converted_entries.select { |e| e.date == date && (e.account_transaction? || e.account_trade?) }
|
||||
converted_entries.select { |e| e.date == date && (e.transaction? || e.trade?) }
|
||||
end
|
||||
|
||||
private
|
|
@ -1,4 +1,4 @@
|
|||
class Account::Balance::Syncer
|
||||
class Balance::Syncer
|
||||
attr_reader :account, :strategy
|
||||
|
||||
def initialize(account, strategy:)
|
||||
|
@ -7,7 +7,7 @@ class Account::Balance::Syncer
|
|||
end
|
||||
|
||||
def sync_balances
|
||||
Account::Balance.transaction do
|
||||
Balance.transaction do
|
||||
sync_holdings
|
||||
calculate_balances
|
||||
|
||||
|
@ -26,7 +26,7 @@ class Account::Balance::Syncer
|
|||
|
||||
private
|
||||
def sync_holdings
|
||||
@holdings = Account::Holding::Syncer.new(account, strategy: strategy).sync_holdings
|
||||
@holdings = Holding::Syncer.new(account, strategy: strategy).sync_holdings
|
||||
end
|
||||
|
||||
def update_account_info
|
||||
|
@ -63,9 +63,9 @@ class Account::Balance::Syncer
|
|||
|
||||
def calculator
|
||||
if strategy == :reverse
|
||||
Account::Balance::ReverseCalculator.new(account)
|
||||
Balance::ReverseCalculator.new(account)
|
||||
else
|
||||
Account::Balance::ForwardCalculator.new(account)
|
||||
Balance::ForwardCalculator.new(account)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,7 +2,7 @@
|
|||
# In most cases, this is sufficient. However, for the "Activity View", we need to show intraday balances
|
||||
# to show users how each entry affects their balances. This class calculates intraday balances by
|
||||
# interpolating between end-of-day balances.
|
||||
class Account::BalanceTrendCalculator
|
||||
class Balance::TrendCalculator
|
||||
BalanceTrend = Struct.new(:trend, :cash, keyword_init: true)
|
||||
|
||||
class << self
|
||||
|
@ -48,12 +48,12 @@ class Account::BalanceTrendCalculator
|
|||
todays_entries = entries.select { |e| e.date == entry.date }
|
||||
|
||||
todays_entries.each_with_index do |e, idx|
|
||||
if e.account_valuation?
|
||||
if e.valuation?
|
||||
current_balance = e.amount
|
||||
current_cash_balance = e.amount
|
||||
else
|
||||
multiplier = e.account.liability? ? 1 : -1
|
||||
balance_change = e.account_trade? ? 0 : multiplier * e.amount
|
||||
balance_change = e.trade? ? 0 : multiplier * e.amount
|
||||
cash_change = multiplier * e.amount
|
||||
|
||||
current_balance = prior_balance + balance_change
|
|
@ -1,5 +1,5 @@
|
|||
class Category < ApplicationRecord
|
||||
has_many :transactions, dependent: :nullify, class_name: "Account::Transaction"
|
||||
has_many :transactions, dependent: :nullify, class_name: "Transaction"
|
||||
has_many :import_mappings, as: :mappable, dependent: :destroy, class_name: "Import::Mapping"
|
||||
|
||||
belongs_to :family
|
||||
|
|
|
@ -361,7 +361,7 @@ class Demo::Generator
|
|||
unknown = Security.find_by(ticker: "UNKNOWN")
|
||||
|
||||
# Buy 20 shares of the unknown stock to simulate a stock where we can't fetch security prices
|
||||
account.entries.create! date: 10.days.ago.to_date, amount: 100, currency: "USD", name: "Buy unknown stock", entryable: Account::Trade.new(qty: 20, price: 5, security: unknown, currency: "USD")
|
||||
account.entries.create! date: 10.days.ago.to_date, amount: 100, currency: "USD", name: "Buy unknown stock", entryable: Trade.new(qty: 20, price: 5, security: unknown, currency: "USD")
|
||||
|
||||
trades = [
|
||||
{ security: aapl, qty: 20 }, { security: msft, qty: 10 }, { security: aapl, qty: -5 },
|
||||
|
@ -382,7 +382,7 @@ class Demo::Generator
|
|||
amount: qty * price,
|
||||
currency: "USD",
|
||||
name: name_prefix + "#{qty} shares of #{security.ticker}",
|
||||
entryable: Account::Trade.new(qty: qty, price: price, currency: "USD", security: security)
|
||||
entryable: Trade.new(qty: qty, price: price, currency: "USD", security: security)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -450,20 +450,20 @@ class Demo::Generator
|
|||
entry_defaults = {
|
||||
date: Faker::Number.between(from: 0, to: 730).days.ago.to_date,
|
||||
currency: "USD",
|
||||
entryable: Account::Transaction.new(transaction_attributes)
|
||||
entryable: Transaction.new(transaction_attributes)
|
||||
}
|
||||
|
||||
Account::Entry.create! entry_defaults.merge(entry_attributes)
|
||||
Entry.create! entry_defaults.merge(entry_attributes)
|
||||
end
|
||||
|
||||
def create_valuation!(account, date, amount)
|
||||
Account::Entry.create! \
|
||||
Entry.create! \
|
||||
account: account,
|
||||
date: date,
|
||||
amount: amount,
|
||||
currency: "USD",
|
||||
name: "Balance update",
|
||||
entryable: Account::Valuation.new
|
||||
entryable: Valuation.new
|
||||
end
|
||||
|
||||
def random_family_record(model, family)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
class Account::Entry < ApplicationRecord
|
||||
class Entry < ApplicationRecord
|
||||
include Monetizable
|
||||
|
||||
monetize :amount
|
||||
|
@ -7,11 +7,11 @@ class Account::Entry < ApplicationRecord
|
|||
belongs_to :transfer, optional: true
|
||||
belongs_to :import, optional: true
|
||||
|
||||
delegated_type :entryable, types: Account::Entryable::TYPES, dependent: :destroy
|
||||
delegated_type :entryable, types: Entryable::TYPES, dependent: :destroy
|
||||
accepts_nested_attributes_for :entryable
|
||||
|
||||
validates :date, :name, :amount, :currency, presence: true
|
||||
validates :date, uniqueness: { scope: [ :account_id, :entryable_type ] }, if: -> { account_valuation? }
|
||||
validates :date, uniqueness: { scope: [ :account_id, :entryable_type ] }, if: -> { valuation? }
|
||||
validates :date, comparison: { greater_than: -> { min_supported_date } }
|
||||
|
||||
scope :active, -> {
|
||||
|
@ -21,7 +21,7 @@ class Account::Entry < ApplicationRecord
|
|||
scope :chronological, -> {
|
||||
order(
|
||||
date: :asc,
|
||||
Arel.sql("CASE WHEN account_entries.entryable_type = 'Account::Valuation' THEN 1 ELSE 0 END") => :asc,
|
||||
Arel.sql("CASE WHEN entries.entryable_type = 'Valuation' THEN 1 ELSE 0 END") => :asc,
|
||||
created_at: :asc
|
||||
)
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ class Account::Entry < ApplicationRecord
|
|||
scope :reverse_chronological, -> {
|
||||
order(
|
||||
date: :desc,
|
||||
Arel.sql("CASE WHEN account_entries.entryable_type = 'Account::Valuation' THEN 1 ELSE 0 END") => :desc,
|
||||
Arel.sql("CASE WHEN entries.entryable_type = 'Valuation' THEN 1 ELSE 0 END") => :desc,
|
||||
created_at: :desc
|
||||
)
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ class Account::Entry < ApplicationRecord
|
|||
end
|
||||
|
||||
def balance_trend(entries, balances)
|
||||
Account::BalanceTrendCalculator.new(self, entries, balances).trend
|
||||
Balance::TrendCalculator.new(self, entries, balances).trend
|
||||
end
|
||||
|
||||
def display_name
|
||||
|
@ -53,7 +53,7 @@ class Account::Entry < ApplicationRecord
|
|||
|
||||
class << self
|
||||
def search(params)
|
||||
Account::EntrySearch.new(params).build_query(all)
|
||||
EntrySearch.new(params).build_query(all)
|
||||
end
|
||||
|
||||
# arbitrary cutoff date to avoid expensive sync operations
|
|
@ -1,4 +1,4 @@
|
|||
class Account::EntrySearch
|
||||
class EntrySearch
|
||||
include ActiveModel::Model
|
||||
include ActiveModel::Attributes
|
||||
|
||||
|
@ -16,7 +16,7 @@ class Account::EntrySearch
|
|||
return scope if search.blank?
|
||||
|
||||
query = scope
|
||||
query = query.where("account_entries.name ILIKE :search OR account_entries.enriched_name ILIKE :search",
|
||||
query = query.where("entries.name ILIKE :search OR entries.enriched_name ILIKE :search",
|
||||
search: "%#{ActiveRecord::Base.sanitize_sql_like(search)}%"
|
||||
)
|
||||
query
|
||||
|
@ -26,8 +26,8 @@ class Account::EntrySearch
|
|||
return scope if start_date.blank? && end_date.blank?
|
||||
|
||||
query = scope
|
||||
query = query.where("account_entries.date >= ?", start_date) if start_date.present?
|
||||
query = query.where("account_entries.date <= ?", end_date) if end_date.present?
|
||||
query = query.where("entries.date >= ?", start_date) if start_date.present?
|
||||
query = query.where("entries.date <= ?", end_date) if end_date.present?
|
||||
query
|
||||
end
|
||||
|
||||
|
@ -38,11 +38,11 @@ class Account::EntrySearch
|
|||
|
||||
case amount_operator
|
||||
when "equal"
|
||||
query = query.where("ABS(ABS(account_entries.amount) - ?) <= 0.01", amount.to_f.abs)
|
||||
query = query.where("ABS(ABS(entries.amount) - ?) <= 0.01", amount.to_f.abs)
|
||||
when "less"
|
||||
query = query.where("ABS(account_entries.amount) < ?", amount.to_f.abs)
|
||||
query = query.where("ABS(entries.amount) < ?", amount.to_f.abs)
|
||||
when "greater"
|
||||
query = query.where("ABS(account_entries.amount) > ?", amount.to_f.abs)
|
||||
query = query.where("ABS(entries.amount) > ?", amount.to_f.abs)
|
||||
end
|
||||
|
||||
query
|
|
@ -1,7 +1,7 @@
|
|||
module Account::Entryable
|
||||
module Entryable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
TYPES = %w[Account::Valuation Account::Transaction Account::Trade]
|
||||
TYPES = %w[Valuation Transaction Trade]
|
||||
|
||||
def self.from_type(entryable_type)
|
||||
entryable_type.presence_in(TYPES).constantize
|
||||
|
@ -12,18 +12,18 @@ module Account::Entryable
|
|||
|
||||
scope :with_entry, -> { joins(:entry) }
|
||||
|
||||
scope :active, -> { with_entry.merge(Account::Entry.active) }
|
||||
scope :active, -> { with_entry.merge(Entry.active) }
|
||||
|
||||
scope :in_period, ->(period) {
|
||||
with_entry.where(account_entries: { date: period.start_date..period.end_date })
|
||||
with_entry.where(entries: { date: period.start_date..period.end_date })
|
||||
}
|
||||
|
||||
scope :reverse_chronological, -> {
|
||||
with_entry.merge(Account::Entry.reverse_chronological)
|
||||
with_entry.merge(Entry.reverse_chronological)
|
||||
}
|
||||
|
||||
scope :chronological, -> {
|
||||
with_entry.merge(Account::Entry.chronological)
|
||||
with_entry.merge(Entry.chronological)
|
||||
}
|
||||
end
|
||||
end
|
|
@ -1,12 +1,12 @@
|
|||
module Family::AutoTransferMatchable
|
||||
def transfer_match_candidates
|
||||
Account::Entry.select([
|
||||
Entry.select([
|
||||
"inflow_candidates.entryable_id as inflow_transaction_id",
|
||||
"outflow_candidates.entryable_id as outflow_transaction_id",
|
||||
"ABS(inflow_candidates.date - outflow_candidates.date) as date_diff"
|
||||
]).from("account_entries inflow_candidates")
|
||||
]).from("entries inflow_candidates")
|
||||
.joins("
|
||||
JOIN account_entries outflow_candidates ON (
|
||||
JOIN entries outflow_candidates ON (
|
||||
inflow_candidates.amount < 0 AND
|
||||
outflow_candidates.amount > 0 AND
|
||||
inflow_candidates.amount = -outflow_candidates.amount AND
|
||||
|
@ -29,7 +29,7 @@ module Family::AutoTransferMatchable
|
|||
.where("inflow_accounts.family_id = ? AND outflow_accounts.family_id = ?", self.id, self.id)
|
||||
.where("inflow_accounts.is_active = true")
|
||||
.where("outflow_accounts.is_active = true")
|
||||
.where("inflow_candidates.entryable_type = 'Account::Transaction' AND outflow_candidates.entryable_type = 'Account::Transaction'")
|
||||
.where("inflow_candidates.entryable_type = 'Transaction' AND outflow_candidates.entryable_type = 'Transaction'")
|
||||
.where(existing_transfers: { id: nil })
|
||||
.order("date_diff ASC") # Closest matches first
|
||||
end
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
class Account::Holding < ApplicationRecord
|
||||
class Holding < ApplicationRecord
|
||||
include Monetizable, Gapfillable
|
||||
|
||||
monetize :amount
|
||||
|
@ -27,9 +27,9 @@ class Account::Holding < ApplicationRecord
|
|||
|
||||
# Basic approximation of cost-basis
|
||||
def avg_cost
|
||||
avg_cost = account.entries.account_trades
|
||||
.joins("INNER JOIN account_trades ON account_trades.id = account_entries.entryable_id")
|
||||
.where("account_trades.security_id = ? AND account_trades.qty > 0 AND account_entries.date <= ?", security.id, date)
|
||||
avg_cost = account.entries.trades
|
||||
.joins("INNER JOIN trades ON trades.id = entries.entryable_id")
|
||||
.where("trades.security_id = ? AND trades.qty > 0 AND entries.date <= ?", security.id, date)
|
||||
.average(:price)
|
||||
|
||||
Money.new(avg_cost || price, currency)
|
|
@ -1,4 +1,4 @@
|
|||
class Account::Holding::BaseCalculator
|
||||
class Holding::BaseCalculator
|
||||
attr_reader :account
|
||||
|
||||
def initialize(account)
|
||||
|
@ -8,13 +8,13 @@ class Account::Holding::BaseCalculator
|
|||
def calculate
|
||||
Rails.logger.tagged(self.class.name) do
|
||||
holdings = calculate_holdings
|
||||
Account::Holding.gapfill(holdings)
|
||||
Holding.gapfill(holdings)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def portfolio_cache
|
||||
@portfolio_cache ||= Account::Holding::PortfolioCache.new(account)
|
||||
@portfolio_cache ||= Holding::PortfolioCache.new(account)
|
||||
end
|
||||
|
||||
def empty_portfolio
|
||||
|
@ -49,7 +49,7 @@ class Account::Holding::BaseCalculator
|
|||
next
|
||||
end
|
||||
|
||||
Account::Holding.new(
|
||||
Holding.new(
|
||||
account_id: account.id,
|
||||
security_id: security_id,
|
||||
date: date,
|
|
@ -1,7 +1,7 @@
|
|||
class Account::Holding::ForwardCalculator < Account::Holding::BaseCalculator
|
||||
class Holding::ForwardCalculator < Holding::BaseCalculator
|
||||
private
|
||||
def portfolio_cache
|
||||
@portfolio_cache ||= Account::Holding::PortfolioCache.new(account)
|
||||
@portfolio_cache ||= Holding::PortfolioCache.new(account)
|
||||
end
|
||||
|
||||
def calculate_holdings
|
|
@ -1,4 +1,4 @@
|
|||
module Account::Holding::Gapfillable
|
||||
module Holding::Gapfillable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
|
@ -19,7 +19,7 @@ module Account::Holding::Gapfillable
|
|||
previous_holding = holding
|
||||
else
|
||||
# Create a new holding based on the previous day's data
|
||||
filled_holdings << Account::Holding.new(
|
||||
filled_holdings << Holding.new(
|
||||
account: previous_holding.account,
|
||||
security: previous_holding.security,
|
||||
date: date,
|
|
@ -1,4 +1,4 @@
|
|||
class Account::Holding::PortfolioCache
|
||||
class Holding::PortfolioCache
|
||||
attr_reader :account, :use_holdings
|
||||
|
||||
class SecurityNotFound < StandardError
|
||||
|
@ -49,7 +49,7 @@ class Account::Holding::PortfolioCache
|
|||
PriceWithPriority = Data.define(:price, :priority)
|
||||
|
||||
def trades
|
||||
@trades ||= account.entries.includes(entryable: :security).account_trades.chronological.to_a
|
||||
@trades ||= account.entries.includes(entryable: :security).trades.chronological.to_a
|
||||
end
|
||||
|
||||
def holdings
|
|
@ -1,10 +1,10 @@
|
|||
class Account::Holding::ReverseCalculator < Account::Holding::BaseCalculator
|
||||
class Holding::ReverseCalculator < Holding::BaseCalculator
|
||||
private
|
||||
# Reverse calculators will use the existing holdings as a source of security ids and prices
|
||||
# since it is common for a provider to supply "current day" holdings but not all the historical
|
||||
# trades that make up those holdings.
|
||||
def portfolio_cache
|
||||
@portfolio_cache ||= Account::Holding::PortfolioCache.new(account, use_holdings: true)
|
||||
@portfolio_cache ||= Holding::PortfolioCache.new(account, use_holdings: true)
|
||||
end
|
||||
|
||||
def calculate_holdings
|
|
@ -1,4 +1,4 @@
|
|||
class Account::Holding::Syncer
|
||||
class Holding::Syncer
|
||||
def initialize(account, strategy:)
|
||||
@account = account
|
||||
@strategy = strategy
|
||||
|
@ -36,7 +36,7 @@ class Account::Holding::Syncer
|
|||
end
|
||||
|
||||
def purge_stale_holdings
|
||||
portfolio_security_ids = account.entries.account_trades.map { |entry| entry.entryable.security_id }.uniq
|
||||
portfolio_security_ids = account.entries.trades.map { |entry| entry.entryable.security_id }.uniq
|
||||
|
||||
# If there are no securities in the portfolio, delete all holdings
|
||||
if portfolio_security_ids.empty?
|
||||
|
@ -50,9 +50,9 @@ class Account::Holding::Syncer
|
|||
|
||||
def calculator
|
||||
if strategy == :reverse
|
||||
Account::Holding::ReverseCalculator.new(account)
|
||||
Holding::ReverseCalculator.new(account)
|
||||
else
|
||||
Account::Holding::ForwardCalculator.new(account)
|
||||
Holding::ForwardCalculator.new(account)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -34,7 +34,7 @@ class Import < ApplicationRecord
|
|||
has_many :rows, dependent: :destroy
|
||||
has_many :mappings, dependent: :destroy
|
||||
has_many :accounts, dependent: :destroy
|
||||
has_many :entries, dependent: :destroy, class_name: "Account::Entry"
|
||||
has_many :entries, dependent: :destroy
|
||||
|
||||
class << self
|
||||
def parse_csv_str(csv_str, col_sep: ",")
|
||||
|
|
|
@ -63,7 +63,7 @@ class Import::Row < ApplicationRecord
|
|||
return
|
||||
end
|
||||
|
||||
min_date = Account::Entry.min_supported_date
|
||||
min_date = Entry.min_supported_date
|
||||
max_date = Date.current
|
||||
|
||||
if parsed_date < min_date || parsed_date > max_date
|
||||
|
|
|
@ -11,13 +11,13 @@ module IncomeStatement::BaseQuery
|
|||
COUNT(ae.id) as transactions_count,
|
||||
BOOL_OR(ae.currency <> :target_currency AND er.rate IS NULL) as missing_exchange_rates
|
||||
FROM (#{transactions_scope.to_sql}) at
|
||||
JOIN account_entries ae ON ae.entryable_id = at.id AND ae.entryable_type = 'Account::Transaction'
|
||||
JOIN entries ae ON ae.entryable_id = at.id AND ae.entryable_type = 'Transaction'
|
||||
LEFT JOIN categories c ON c.id = at.category_id
|
||||
LEFT JOIN (
|
||||
SELECT t.*, t.id as transfer_id, a.accountable_type
|
||||
FROM transfers t
|
||||
JOIN account_entries ae ON ae.entryable_id = t.inflow_transaction_id
|
||||
AND ae.entryable_type = 'Account::Transaction'
|
||||
JOIN entries ae ON ae.entryable_id = t.inflow_transaction_id
|
||||
AND ae.entryable_type = 'Transaction'
|
||||
JOIN accounts a ON a.id = ae.account_id
|
||||
) transfer_info ON (
|
||||
transfer_info.inflow_transaction_id = at.id OR
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class Merchant < ApplicationRecord
|
||||
has_many :transactions, dependent: :nullify, class_name: "Account::Transaction"
|
||||
has_many :transactions, dependent: :nullify, class_name: "Transaction"
|
||||
belongs_to :family
|
||||
|
||||
validates :name, :color, :family, presence: true
|
||||
|
|
|
@ -35,7 +35,7 @@ class MintImport < Import
|
|||
name: row.name,
|
||||
currency: row.currency,
|
||||
notes: row.notes,
|
||||
entryable: Account::Transaction.new(category: category, tags: tags),
|
||||
entryable: Transaction.new(category: category, tags: tags),
|
||||
import: self
|
||||
|
||||
entry.save!
|
||||
|
|
|
@ -87,7 +87,7 @@ class PlaidAccount < ApplicationRecord
|
|||
t.amount = plaid_txn.amount
|
||||
t.currency = plaid_txn.iso_currency_code
|
||||
t.date = plaid_txn.date
|
||||
t.entryable = Account::Transaction.new(
|
||||
t.entryable = Transaction.new(
|
||||
category: get_category(plaid_txn.personal_finance_category.primary),
|
||||
merchant: get_merchant(plaid_txn.merchant_name)
|
||||
)
|
||||
|
@ -120,7 +120,7 @@ class PlaidAccount < ApplicationRecord
|
|||
e.amount = loan_data.origination_principal_amount
|
||||
e.currency = account.currency
|
||||
e.date = loan_data.origination_date
|
||||
e.entryable = Account::Valuation.new
|
||||
e.entryable = Valuation.new
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,7 +31,7 @@ class PlaidInvestmentSync
|
|||
t.amount = transaction.amount
|
||||
t.currency = transaction.iso_currency_code
|
||||
t.date = transaction.date
|
||||
t.entryable = Account::Transaction.new
|
||||
t.entryable = Transaction.new
|
||||
end
|
||||
else
|
||||
new_transaction = plaid_account.account.entries.find_or_create_by!(plaid_id: transaction.investment_transaction_id) do |t|
|
||||
|
@ -39,7 +39,7 @@ class PlaidInvestmentSync
|
|||
t.amount = transaction.quantity * transaction.price
|
||||
t.currency = transaction.iso_currency_code
|
||||
t.date = transaction.date
|
||||
t.entryable = Account::Trade.new(
|
||||
t.entryable = Trade.new(
|
||||
security: security,
|
||||
qty: transaction.quantity,
|
||||
price: transaction.price,
|
||||
|
|
|
@ -44,6 +44,6 @@ class Property < ApplicationRecord
|
|||
|
||||
private
|
||||
def first_valuation_amount
|
||||
account.entries.account_valuations.order(:date).first&.amount_money || account.balance_money
|
||||
account.entries.valuations.order(:date).first&.amount_money || account.balance_money
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
class RejectedTransfer < ApplicationRecord
|
||||
belongs_to :inflow_transaction, class_name: "Account::Transaction"
|
||||
belongs_to :outflow_transaction, class_name: "Account::Transaction"
|
||||
belongs_to :inflow_transaction, class_name: "Transaction"
|
||||
belongs_to :outflow_transaction, class_name: "Transaction"
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@ class Security < ApplicationRecord
|
|||
|
||||
before_save :upcase_ticker
|
||||
|
||||
has_many :trades, dependent: :nullify, class_name: "Account::Trade"
|
||||
has_many :trades, dependent: :nullify, class_name: "Trade"
|
||||
has_many :prices, dependent: :destroy
|
||||
|
||||
validates :ticker, presence: true
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
class Tag < ApplicationRecord
|
||||
belongs_to :family
|
||||
has_many :taggings, dependent: :destroy
|
||||
has_many :transactions, through: :taggings, source: :taggable, source_type: "Account::Transaction"
|
||||
has_many :transactions, through: :taggings, source: :taggable, source_type: "Transaction"
|
||||
has_many :import_mappings, as: :mappable, dependent: :destroy, class_name: "Import::Mapping"
|
||||
|
||||
validates :name, presence: true, uniqueness: { scope: :family }
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class Account::Trade < ApplicationRecord
|
||||
include Account::Entryable, Monetizable
|
||||
class Trade < ApplicationRecord
|
||||
include Entryable, Monetizable
|
||||
|
||||
monetize :price
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
class Account::TradeBuilder
|
||||
class TradeBuilder
|
||||
include ActiveModel::Model
|
||||
|
||||
attr_accessor :account, :date, :amount, :currency, :qty,
|
||||
|
@ -46,7 +46,7 @@ class Account::TradeBuilder
|
|||
date: date,
|
||||
amount: signed_amount,
|
||||
currency: currency,
|
||||
entryable: Account::Trade.new(
|
||||
entryable: Trade.new(
|
||||
qty: signed_qty,
|
||||
price: price,
|
||||
currency: currency,
|
||||
|
@ -74,7 +74,7 @@ class Account::TradeBuilder
|
|||
date: date,
|
||||
amount: signed_amount,
|
||||
currency: currency,
|
||||
entryable: Account::Transaction.new
|
||||
entryable: Transaction.new
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -85,7 +85,7 @@ class Account::TradeBuilder
|
|||
date: date,
|
||||
amount: signed_amount,
|
||||
currency: currency,
|
||||
entryable: Account::Transaction.new
|
||||
entryable: Transaction.new
|
||||
)
|
||||
end
|
||||
|
|
@ -16,12 +16,12 @@ class TradeImport < Import
|
|||
exchange_operating_mic: row.exchange_operating_mic
|
||||
)
|
||||
|
||||
Account::Trade.new(
|
||||
Trade.new(
|
||||
security: security,
|
||||
qty: row.qty,
|
||||
currency: row.currency.presence || mapped_account.currency,
|
||||
price: row.price,
|
||||
entry: Account::Entry.new(
|
||||
entry: Entry.new(
|
||||
account: mapped_account,
|
||||
date: row.date_iso,
|
||||
amount: row.signed_amount,
|
||||
|
@ -31,7 +31,7 @@ class TradeImport < Import
|
|||
),
|
||||
)
|
||||
end
|
||||
Account::Trade.import!(trades, recursive: true)
|
||||
Trade.import!(trades, recursive: true)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class Account::Transaction < ApplicationRecord
|
||||
include Account::Entryable, Transferable, Provided
|
||||
class Transaction < ApplicationRecord
|
||||
include Entryable, Transferable, Provided
|
||||
|
||||
belongs_to :category, optional: true
|
||||
belongs_to :merchant, optional: true
|
||||
|
@ -11,7 +11,7 @@ class Account::Transaction < ApplicationRecord
|
|||
|
||||
class << self
|
||||
def search(params)
|
||||
Account::TransactionSearch.new(params).build_query(all)
|
||||
Search.new(params).build_query(all)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,4 +1,4 @@
|
|||
module Account::Transaction::Provided
|
||||
module Transaction::Provided
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def fetch_enrichment_info
|
|
@ -1,4 +1,4 @@
|
|||
class Account::TransactionSearch
|
||||
class Transaction::Search
|
||||
include ActiveModel::Model
|
||||
include ActiveModel::Attributes
|
||||
|
||||
|
@ -22,10 +22,10 @@ class Account::TransactionSearch
|
|||
query = apply_type_filter(query, types)
|
||||
query = apply_merchant_filter(query, merchants)
|
||||
query = apply_tag_filter(query, tags)
|
||||
query = Account::EntrySearch.apply_search_filter(query, search)
|
||||
query = Account::EntrySearch.apply_date_filters(query, start_date, end_date)
|
||||
query = Account::EntrySearch.apply_amount_filter(query, amount, amount_operator)
|
||||
query = Account::EntrySearch.apply_accounts_filter(query, accounts, account_ids)
|
||||
query = EntrySearch.apply_search_filter(query, search)
|
||||
query = EntrySearch.apply_date_filters(query, start_date, end_date)
|
||||
query = EntrySearch.apply_amount_filter(query, amount, amount_operator)
|
||||
query = EntrySearch.apply_accounts_filter(query, accounts, account_ids)
|
||||
|
||||
query
|
||||
end
|
||||
|
@ -36,12 +36,12 @@ class Account::TransactionSearch
|
|||
LEFT JOIN (
|
||||
SELECT t.*, t.id as transfer_id, a.accountable_type
|
||||
FROM transfers t
|
||||
JOIN account_entries ae ON ae.entryable_id = t.inflow_transaction_id
|
||||
AND ae.entryable_type = 'Account::Transaction'
|
||||
JOIN entries ae ON ae.entryable_id = t.inflow_transaction_id
|
||||
AND ae.entryable_type = 'Transaction'
|
||||
JOIN accounts a ON a.id = ae.account_id
|
||||
) transfer_info ON (
|
||||
transfer_info.inflow_transaction_id = account_transactions.id OR
|
||||
transfer_info.outflow_transaction_id = account_transactions.id
|
||||
transfer_info.inflow_transaction_id = transactions.id OR
|
||||
transfer_info.outflow_transaction_id = transactions.id
|
||||
)
|
||||
SQL
|
||||
end
|
||||
|
@ -68,8 +68,8 @@ class Account::TransactionSearch
|
|||
return query if types.sort == [ "expense", "income", "transfer" ]
|
||||
|
||||
transfer_condition = "transfer_info.transfer_id IS NOT NULL"
|
||||
expense_condition = "account_entries.amount >= 0"
|
||||
income_condition = "account_entries.amount <= 0"
|
||||
expense_condition = "entries.amount >= 0"
|
||||
income_condition = "entries.amount <= 0"
|
||||
|
||||
condition = case types.sort
|
||||
when [ "transfer" ]
|
|
@ -1,4 +1,4 @@
|
|||
module Account::Transaction::Transferable
|
||||
module Transaction::Transferable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
|
@ -13,10 +13,10 @@ class TransactionImport < Import
|
|||
category = mappings.categories.mappable_for(row.category)
|
||||
tags = row.tags_list.map { |tag| mappings.tags.mappable_for(tag) }.compact
|
||||
|
||||
Account::Transaction.new(
|
||||
Transaction.new(
|
||||
category: category,
|
||||
tags: tags,
|
||||
entry: Account::Entry.new(
|
||||
entry: Entry.new(
|
||||
account: mapped_account,
|
||||
date: row.date_iso,
|
||||
amount: row.signed_amount,
|
||||
|
@ -28,7 +28,7 @@ class TransactionImport < Import
|
|||
)
|
||||
end
|
||||
|
||||
Account::Transaction.import!(transactions, recursive: true)
|
||||
Transaction.import!(transactions, recursive: true)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
class Transfer < ApplicationRecord
|
||||
belongs_to :inflow_transaction, class_name: "Account::Transaction"
|
||||
belongs_to :outflow_transaction, class_name: "Account::Transaction"
|
||||
belongs_to :inflow_transaction, class_name: "Transaction"
|
||||
belongs_to :outflow_transaction, class_name: "Transaction"
|
||||
|
||||
enum :status, { pending: "pending", confirmed: "confirmed" }
|
||||
|
||||
|
@ -23,22 +23,22 @@ class Transfer < ApplicationRecord
|
|||
end
|
||||
|
||||
new(
|
||||
inflow_transaction: Account::Transaction.new(
|
||||
inflow_transaction: Transaction.new(
|
||||
entry: to_account.entries.build(
|
||||
amount: converted_amount.amount.abs * -1,
|
||||
currency: converted_amount.currency.iso_code,
|
||||
date: date,
|
||||
name: "Transfer from #{from_account.name}",
|
||||
entryable: Account::Transaction.new
|
||||
entryable: Transaction.new
|
||||
)
|
||||
),
|
||||
outflow_transaction: Account::Transaction.new(
|
||||
outflow_transaction: Transaction.new(
|
||||
entry: from_account.entries.build(
|
||||
amount: amount.abs,
|
||||
currency: from_account.currency,
|
||||
date: date,
|
||||
name: "Transfer to #{to_account.name}",
|
||||
entryable: Account::Transaction.new
|
||||
entryable: Transaction.new
|
||||
)
|
||||
),
|
||||
status: "confirmed"
|
||||
|
|
3
app/models/valuation.rb
Normal file
3
app/models/valuation.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
class Valuation < ApplicationRecord
|
||||
include Entryable
|
||||
end
|
|
@ -31,6 +31,6 @@ class Vehicle < ApplicationRecord
|
|||
|
||||
private
|
||||
def first_valuation_amount
|
||||
account.entries.account_valuations.order(:date).first&.amount_money || account.balance_money
|
||||
account.entries.valuations.order(:date).first&.amount_money || account.balance_money
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue