Skip to content

Commit

Permalink
Prepare entry search for nested categories
Browse files Browse the repository at this point in the history
  • Loading branch information
zachgoll committed Dec 19, 2024
1 parent 7be6a37 commit 16d0556
Show file tree
Hide file tree
Showing 11 changed files with 122 additions and 130 deletions.
10 changes: 5 additions & 5 deletions app/controllers/transactions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ class TransactionsController < ApplicationController

def index
@q = search_params
result = Current.family.entries.account_transactions.search(@q).reverse_chronological
@pagy, @transaction_entries = pagy(result, limit: params[:per_page] || "50")
search_query = Current.family.transactions.search(@q).includes(:entryable).reverse_chronological
@pagy, @transaction_entries = pagy(search_query, limit: params[:per_page] || "50")

@totals = {
count: result.select { |t| t.currency == Current.family.currency }.count,
income: result.income_total(Current.family.currency).abs,
expense: result.expense_total(Current.family.currency)
count: search_query.select { |t| t.currency == Current.family.currency }.count,
income: search_query.income_total(Current.family.currency).abs,
expense: search_query.expense_total(Current.family.currency)
}
end

Expand Down
46 changes: 4 additions & 42 deletions app/models/account/entry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ def display_name
end

class << self
def search(params)
Account::EntrySearch.new(params).build_query(all)
end

# arbitrary cutoff date to avoid expensive sync operations
def min_supported_date
30.years.ago.to_date
Expand Down Expand Up @@ -141,49 +145,7 @@ def expense_total(currency = "USD")
Money.new(total, currency)
end

def search(params)
query = all
query = query.where("account_entries.name ILIKE ?", "%#{sanitize_sql_like(params[:search])}%") if params[:search].present?
query = query.where("account_entries.date >= ?", params[:start_date]) if params[:start_date].present?
query = query.where("account_entries.date <= ?", params[:end_date]) if params[:end_date].present?

if params[:types].present?
query = query.where(marked_as_transfer: false) unless params[:types].include?("transfer")

if params[:types].include?("income") && !params[:types].include?("expense")
query = query.where("account_entries.amount < 0")
elsif params[:types].include?("expense") && !params[:types].include?("income")
query = query.where("account_entries.amount >= 0")
end
end

if params[:amount].present? && params[:amount_operator].present?
case params[:amount_operator]
when "equal"
query = query.where("ABS(ABS(account_entries.amount) - ?) <= 0.01", params[:amount].to_f.abs)
when "less"
query = query.where("ABS(account_entries.amount) < ?", params[:amount].to_f.abs)
when "greater"
query = query.where("ABS(account_entries.amount) > ?", params[:amount].to_f.abs)
end
end

if params[:accounts].present? || params[:account_ids].present?
query = query.joins(:account)
end

query = query.where(accounts: { name: params[:accounts] }) if params[:accounts].present?
query = query.where(accounts: { id: params[:account_ids] }) if params[:account_ids].present?

# Search attributes on each entryable to further refine results
entryable_ids = entryable_search(params)
query = query.where(entryable_id: entryable_ids) unless entryable_ids.nil?

query
end

private

def entryable_search(params)
entryable_ids = []
entryable_search_performed = false
Expand Down
59 changes: 59 additions & 0 deletions app/models/account/entry_search.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
class Account::EntrySearch
include ActiveModel::Model
include ActiveModel::Attributes

attribute :search, :string
attribute :amount, :string
attribute :amount_operator, :string
attribute :types, :string
attribute :accounts, :string
attribute :account_ids, :string
attribute :start_date, :string
attribute :end_date, :string

class << self
def from_entryable_search(entryable_search)
new(entryable_search.attributes.slice(*attribute_names))
end
end

def build_query(scope)
query = scope

query = query.where("account_entries.name ILIKE :search OR account_entries.enriched_name ILIKE :search",
search: "%#{ActiveRecord::Base.sanitize_sql_like(search)}%"
) if search.present?
query = query.where("account_entries.date >= ?", start_date) if start_date.present?
query = query.where("account_entries.date <= ?", end_date) if end_date.present?

if types.present?
query = query.where(marked_as_transfer: false) unless types.include?("transfer")

if types.include?("income") && !types.include?("expense")
query = query.where("account_entries.amount < 0")
elsif types.include?("expense") && !types.include?("income")
query = query.where("account_entries.amount >= 0")
end
end

if amount.present? && amount_operator.present?
case amount_operator
when "equal"
query = query.where("ABS(ABS(account_entries.amount) - ?) <= 0.01", amount.to_f.abs)
when "less"
query = query.where("ABS(account_entries.amount) < ?", amount.to_f.abs)
when "greater"
query = query.where("ABS(account_entries.amount) > ?", amount.to_f.abs)
end
end

if accounts.present? || account_ids.present?
query = query.joins(:account)
end

query = query.where(accounts: { name: accounts }) if accounts.present?
query = query.where(accounts: { id: account_ids }) if account_ids.present?

query
end
end
20 changes: 1 addition & 19 deletions app/models/account/trade.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,8 @@ class Account::Trade < ApplicationRecord
validates :qty, presence: true
validates :price, :currency, presence: true

class << self
def search(_params)
all
end

def requires_search?(_params)
false
end
end

def sell?
qty < 0
end

def buy?
qty > 0
end

def unrealized_gain_loss
return nil if sell?
return nil if qty.negative?
current_price = security.current_price
return nil if current_price.nil?

Expand Down
47 changes: 1 addition & 46 deletions app/models/account/transaction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,52 +12,7 @@ class Account::Transaction < ApplicationRecord

class << self
def search(params)
query = all
if params[:categories].present?
if params[:categories].exclude?("Uncategorized")
query = query
.joins(:category)
.where(categories: { name: params[:categories] })
else
query = query
.left_joins(:category)
.where(categories: { name: params[:categories] })
.or(query.where(category_id: nil))
end
end

query = query.joins(:merchant).where(merchants: { name: params[:merchants] }) if params[:merchants].present?

if params[:tags].present?
query = query.joins(:tags)
.where(tags: { name: params[:tags] })
.distinct
end

query
Account::TransactionSearch.new(params).build_query(all)
end

def requires_search?(params)
searchable_keys.any? { |key| params.key?(key) }
end

private

def searchable_keys
%i[categories merchants tags]
end
end

def eod_balance
entry.amount_money
end

private
def account
entry.account
end

def daily_transactions
account.entries.account_transactions
end
end
42 changes: 42 additions & 0 deletions app/models/account/transaction_search.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
class Account::TransactionSearch
include ActiveModel::Model
include ActiveModel::Attributes

attribute :search, :string
attribute :amount, :string
attribute :amount_operator, :string
attribute :types, array: true
attribute :accounts, array: true
attribute :account_ids, array: true
attribute :start_date, :string
attribute :end_date, :string
attribute :categories, array: true
attribute :merchants, array: true
attribute :tags, array: true

# Returns array of Account::Entry objects to stay consistent with partials, which only deal with Account::Entry
def build_query(scope)
query = scope

if categories.present?
if categories.exclude?("Uncategorized")
query = query
.joins(:category)
.where(categories: { name: categories })
else
query = query
.left_joins(:category)
.where(categories: { name: categories })
.or(query.where(category_id: nil))
end
end

query = query.joins(:merchant).where(merchants: { name: merchants }) if merchants.present?

query = query.joins(:tags).where(tags: { name: tags }) if tags.present?

entries_scope = Account::Entry.account_transactions.where(entryable_id: query.select(:id))

Account::EntrySearch.from_entryable_search(self).build_query(entries_scope)
end
end
10 changes: 0 additions & 10 deletions app/models/account/valuation.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,3 @@
class Account::Valuation < ApplicationRecord
include Account::Entryable

class << self
def search(_params)
all
end

def requires_search?(_params)
false
end
end
end
4 changes: 2 additions & 2 deletions app/views/account/trades/_header.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
<dd class="text-gray-900"><%= trade.security.ticker %></dd>
</div>

<% if trade.buy? %>
<% if trade.qty.positive? %>
<div class="flex items-center justify-between text-sm">
<dt class="text-gray-500"><%= t(".purchase_qty_label") %></dt>
<dd class="text-gray-900"><%= trade.qty.abs %></dd>
Expand All @@ -53,7 +53,7 @@
</div>
<% end %>

<% if trade.buy? && trade.unrealized_gain_loss.present? %>
<% if trade.qty.positive? && trade.unrealized_gain_loss.present? %>
<div class="flex items-center justify-between text-sm">
<dt class="text-gray-500"><%= t(".total_return_label") %></dt>
<dd style="color: <%= trade.unrealized_gain_loss.color %>;">
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20241219174803_add_parent_category.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddParentCategory < ActiveRecord::Migration[7.2]
def change
add_column :categories, :parent_category_id, :uuid
end
end
5 changes: 3 additions & 2 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 0 additions & 4 deletions test/models/account/entry_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,6 @@ class Account::EntryTest < ActiveSupport::TestCase

assert_equal 2, family.entries.search(params).size

params = params.merge(categories: [ category.name ], merchants: [ merchant.name ]) # transaction specific search param

assert_equal 1, family.entries.search(params).size

params = { search: "%" }
assert_equal 0, family.entries.search(params).size
end
Expand Down

0 comments on commit 16d0556

Please sign in to comment.