2024-02-23 21:34:33 -05:00
class Transaction < ApplicationRecord
2024-03-18 11:21:00 -04:00
include Monetizable
2024-05-30 20:55:18 -04:00
monetize :amount
2024-02-23 21:34:33 -05:00
belongs_to :account
2024-06-19 06:52:08 -04:00
belongs_to :transfer , optional : true
2024-03-07 19:15:50 +01:00
belongs_to :category , optional : true
2024-04-29 21:17:28 +02:00
belongs_to :merchant , optional : true
2024-05-23 08:09:33 -04:00
has_many :taggings , as : :taggable , dependent : :destroy
has_many :tags , through : :taggings
2024-05-30 20:55:18 -04:00
accepts_nested_attributes_for :taggings , allow_destroy : true
2024-05-23 08:09:33 -04:00
2024-03-15 12:21:59 -07:00
validates :name , :date , :amount , :account , presence : true
2024-05-30 20:55:18 -04:00
scope :ordered , - > { order ( date : :desc ) }
scope :active , - > { where ( excluded : false ) }
2024-05-17 17:50:49 -04:00
scope :inflows , - > { where ( " amount <= 0 " ) }
scope :outflows , - > { where ( " amount > 0 " ) }
2024-05-30 20:55:18 -04:00
scope :by_name , - > ( name ) { where ( " transactions.name ILIKE ? " , " % #{ name } % " ) }
2024-06-20 08:15:09 -04:00
scope :with_categories , - > ( categories ) { joins ( :category ) . where ( categories : { name : categories } ) }
2024-05-30 20:55:18 -04:00
scope :with_accounts , - > ( accounts ) { joins ( :account ) . where ( accounts : { name : accounts } ) }
scope :with_account_ids , - > ( account_ids ) { joins ( :account ) . where ( accounts : { id : account_ids } ) }
scope :with_merchants , - > ( merchants ) { joins ( :merchant ) . where ( transaction_merchants : { name : merchants } ) }
scope :on_or_after_date , - > ( date ) { where ( " transactions.date >= ? " , date ) }
scope :on_or_before_date , - > ( date ) { where ( " transactions.date <= ? " , date ) }
2024-04-24 13:34:50 +01:00
scope :with_converted_amount , - > ( currency = Current . family . currency ) {
# Join with exchange rates to convert the amount to the given currency
# If no rate is available, exclude the transaction from the results
select (
" transactions.* " ,
" transactions.amount * COALESCE(er.rate, 1) AS converted_amount "
)
. joins ( sanitize_sql_array ( [ " LEFT JOIN exchange_rates er ON transactions.date = er.date AND transactions.currency = er.base_currency AND er.converted_currency = ? " , currency ] ) )
. where ( " er.rate IS NOT NULL OR transactions.currency = ? " , currency )
}
2024-05-30 20:55:18 -04:00
def inflow?
amount < = 0
2024-04-24 13:34:50 +01:00
end
2024-05-30 20:55:18 -04:00
def outflow?
amount > 0
2024-04-24 13:34:50 +01:00
end
2024-03-18 11:21:00 -04:00
2024-06-19 06:52:08 -04:00
def transfer?
marked_as_transfer
end
2024-05-30 20:55:18 -04:00
def sync_account_later
if destroyed?
sync_start_date = previous_transaction_date
else
sync_start_date = [ date_previously_was , date ] . compact . min
end
2024-03-11 14:51:16 +02:00
2024-05-30 20:55:18 -04:00
account . sync_later ( sync_start_date )
2024-03-11 14:51:16 +02:00
end
2024-05-30 20:55:18 -04:00
class << self
2024-06-19 06:52:08 -04:00
def income_total ( currency = " USD " )
inflows . reject ( & :transfer? ) . select { | t | t . currency == currency } . sum ( & :amount_money )
end
def expense_total ( currency = " USD " )
outflows . reject ( & :transfer? ) . select { | t | t . currency == currency } . sum ( & :amount_money )
end
def mark_transfers!
update_all marked_as_transfer : true
# Attempt to "auto match" and save a transfer if 2 transactions selected
Transfer . new ( transactions : all ) . save if all . count == 2
end
2024-05-30 20:55:18 -04:00
def daily_totals ( transactions , period : Period . last_30_days , currency : Current . family . currency )
# Sum spending and income for each day in the period with the given currency
select (
" gs.date " ,
" COALESCE(SUM(converted_amount) FILTER (WHERE converted_amount > 0), 0) AS spending " ,
" COALESCE(SUM(-converted_amount) FILTER (WHERE converted_amount < 0), 0) AS income "
)
2024-06-19 06:52:08 -04:00
. from ( transactions . with_converted_amount ( currency ) . where ( marked_as_transfer : false ) , :t )
2024-05-30 20:55:18 -04:00
. joins ( sanitize_sql ( [ " RIGHT JOIN generate_series(?, ?, interval '1 day') AS gs(date) ON t.date = gs.date " , period . date_range . first , period . date_range . last ] ) )
. group ( " gs.date " )
end
2024-03-28 13:23:54 -04:00
2024-05-30 20:55:18 -04:00
def daily_rolling_totals ( transactions , period : Period . last_30_days , currency : Current . family . currency )
# Extend the period to include the rolling window
period_with_rolling = period . extend_backward ( period . date_range . count . days )
# Aggregate the rolling sum of spending and income based on daily totals
rolling_totals = from ( daily_totals ( transactions , period : period_with_rolling , currency : currency ) )
. select (
" * " ,
sanitize_sql_array ( [ " SUM(spending) OVER (ORDER BY date RANGE BETWEEN INTERVAL ? PRECEDING AND CURRENT ROW) as rolling_spend " , " #{ period . date_range . count } days " ] ) ,
sanitize_sql_array ( [ " SUM(income) OVER (ORDER BY date RANGE BETWEEN INTERVAL ? PRECEDING AND CURRENT ROW) as rolling_income " , " #{ period . date_range . count } days " ] )
)
. order ( " date " )
# Trim the results to the original period
select ( " * " ) . from ( rolling_totals ) . where ( " date >= ? " , period . date_range . first )
end
2024-04-19 12:03:16 -04:00
2024-05-30 20:55:18 -04:00
def search ( params )
2024-06-19 06:52:08 -04:00
query = all . includes ( :transfer )
2024-05-30 20:55:18 -04:00
query = query . by_name ( params [ :search ] ) if params [ :search ] . present?
query = query . with_categories ( params [ :categories ] ) if params [ :categories ] . present?
query = query . with_accounts ( params [ :accounts ] ) if params [ :accounts ] . present?
query = query . with_account_ids ( params [ :account_ids ] ) if params [ :account_ids ] . present?
query = query . with_merchants ( params [ :merchants ] ) if params [ :merchants ] . present?
query = query . on_or_after_date ( params [ :start_date ] ) if params [ :start_date ] . present?
query = query . on_or_before_date ( params [ :end_date ] ) if params [ :end_date ] . present?
query
end
end
2024-03-28 13:23:54 -04:00
2024-05-30 20:55:18 -04:00
private
def previous_transaction_date
self . account
. transactions
. where ( " date < ? " , date )
. order ( date : :desc )
. first & . date
2024-03-28 13:23:54 -04:00
end
2024-02-23 21:34:33 -05:00
end