1
0
Fork 0
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:
Zach Gollwitzer 2025-04-14 11:40:34 -04:00 committed by GitHub
parent f181ba941f
commit e657c40d19
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
172 changed files with 1297 additions and 1258 deletions

View file

@ -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

View file

@ -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

View file

@ -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"
)

View file

@ -1,3 +0,0 @@
class Account::Valuation < ApplicationRecord
include Account::Entryable
end

View file

@ -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

View file

@ -1,4 +1,4 @@
class Account::Balance < ApplicationRecord
class Balance < ApplicationRecord
include Monetizable
belongs_to :account

View file

@ -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,

View file

@ -1,4 +1,4 @@
class Account::Balance::ForwardCalculator < Account::Balance::BaseCalculator
class Balance::ForwardCalculator < Balance::BaseCalculator
private
def calculate_balances
current_cash_balance = 0

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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,

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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: ",")

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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!

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 }

View file

@ -1,5 +1,5 @@
class Account::Trade < ApplicationRecord
include Account::Entryable, Monetizable
class Trade < ApplicationRecord
include Entryable, Monetizable
monetize :price

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,4 +1,4 @@
module Account::Transaction::Provided
module Transaction::Provided
extend ActiveSupport::Concern
def fetch_enrichment_info

View file

@ -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" ]

View file

@ -1,4 +1,4 @@
module Account::Transaction::Transferable
module Transaction::Transferable
extend ActiveSupport::Concern
included do

View file

@ -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

View file

@ -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
View file

@ -0,0 +1,3 @@
class Valuation < ApplicationRecord
include Entryable
end

View file

@ -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