mirror of
https://github.com/we-promise/sure.git
synced 2026-04-18 11:34:13 +00:00
Quick Categorize Wizard — follow-up fixes (#1393)
* Extract Entry.uncategorized_transactions scope, remove Family#uncategorized_transaction_count Adds a single Entry.uncategorized_transactions scope containing the shared conditions (transactions join, active accounts, category nil, not transfer kinds, not excluded). All callers now use this scope: - Entry.uncategorized_matching builds on it - Transaction::Grouper::ByMerchantOrName#uncategorized_entries uses it - categorizes_controller#uncategorized_entries_for uses it (also fixes missing status/excluded filters that were silently absent before) - Both controllers replace Current.family.uncategorized_transaction_count with Current.accessible_entries.uncategorized_transactions.count so the button count and wizard count both respect account sharing Family#uncategorized_transaction_count removed as it is now unused and was family-scoped rather than user-scoped. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Scope assign_entry write to Current.accessible_entries Replaces unscoped Entry.where(id:) with Current.accessible_entries.where(id:) so the write path is consistent with the find above it. Not exploitable given the find would 404 first, but removes the pattern inconsistency. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add privacy-sensitive class to amounts in categorize wizard Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Extract uncategorized_count helper in CategorizesController Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix comment on uncategorized_transactions scope to mention draft accounts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Use uncategorized_count helper in assign_entry action Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,7 +18,7 @@ class Transactions::CategorizesController < ApplicationController
|
||||
|
||||
@group = groups.first
|
||||
@categories = Current.family.categories.alphabetically
|
||||
@total_uncategorized = Entry.uncategorized_count(Current.accessible_entries)
|
||||
@total_uncategorized = uncategorized_count
|
||||
end
|
||||
|
||||
def create
|
||||
@@ -60,7 +60,7 @@ class Transactions::CategorizesController < ApplicationController
|
||||
end
|
||||
streams << turbo_stream.replace("categorize_remaining",
|
||||
partial: "transactions/categorizes/remaining_count",
|
||||
locals: { total_uncategorized: Entry.uncategorized_count(Current.accessible_entries) })
|
||||
locals: { total_uncategorized: uncategorized_count })
|
||||
streams << turbo_stream.replace("categorize_group_summary",
|
||||
partial: "transactions/categorizes/group_summary",
|
||||
locals: { entries: remaining_entries })
|
||||
@@ -98,7 +98,7 @@ class Transactions::CategorizesController < ApplicationController
|
||||
all_entry_ids = Array.wrap(params[:all_entry_ids]).reject(&:blank?)
|
||||
remaining_ids = all_entry_ids - [ entry.id.to_s ]
|
||||
|
||||
Entry.where(id: entry.id).bulk_update!({ category_id: category.id })
|
||||
Current.accessible_entries.where(id: entry.id).bulk_update!({ category_id: category.id })
|
||||
|
||||
remaining_entries = uncategorized_entries_for(remaining_ids)
|
||||
remaining_ids = remaining_entries.map { |e| e.id.to_s }
|
||||
@@ -109,7 +109,7 @@ class Transactions::CategorizesController < ApplicationController
|
||||
else
|
||||
streams << turbo_stream.replace("categorize_remaining",
|
||||
partial: "transactions/categorizes/remaining_count",
|
||||
locals: { total_uncategorized: Entry.uncategorized_count(Current.accessible_entries) })
|
||||
locals: { total_uncategorized: uncategorized_count })
|
||||
streams << turbo_stream.replace("categorize_group_summary",
|
||||
partial: "transactions/categorizes/group_summary",
|
||||
locals: { entries: remaining_entries })
|
||||
@@ -119,13 +119,16 @@ class Transactions::CategorizesController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def uncategorized_count
|
||||
Current.accessible_entries.uncategorized_transactions.count
|
||||
end
|
||||
|
||||
def uncategorized_entries_for(ids)
|
||||
return [] if ids.blank?
|
||||
Current.accessible_entries
|
||||
.excluding_split_parents
|
||||
.where(id: ids)
|
||||
.joins("INNER JOIN transactions ON transactions.id = entries.entryable_id AND entries.entryable_type = 'Transaction'")
|
||||
.where(transactions: { category_id: nil })
|
||||
.uncategorized_transactions
|
||||
.to_a
|
||||
end
|
||||
end
|
||||
|
||||
@@ -52,7 +52,7 @@ class TransactionsController < ApplicationController
|
||||
Set.new
|
||||
end
|
||||
|
||||
@uncategorized_count = Current.family.uncategorized_transaction_count
|
||||
@uncategorized_count = Current.accessible_entries.uncategorized_transactions.count
|
||||
|
||||
# Load projected recurring transactions for next 10 days
|
||||
@projected_recurring = Current.family.recurring_transactions
|
||||
|
||||
@@ -91,19 +91,16 @@ class Entry < ApplicationRecord
|
||||
joins(:account).where(accounts: { family_id: family.id })
|
||||
end
|
||||
|
||||
# Counts uncategorized, non-transfer entries in the given scope.
|
||||
# Used by the Quick Categorize Wizard to show the remaining count.
|
||||
# @param entries [ActiveRecord::Relation] pre-scoped entries (caller controls authorization)
|
||||
def self.uncategorized_count(entries)
|
||||
entries
|
||||
.joins(:account)
|
||||
# Uncategorized, non-transfer transaction entries on draft or active accounts.
|
||||
# Caller is responsible for scoping to accessible entries before applying this scope.
|
||||
scope :uncategorized_transactions, -> {
|
||||
joins(:account)
|
||||
.joins("INNER JOIN transactions ON transactions.id = entries.entryable_id AND entries.entryable_type = 'Transaction'")
|
||||
.where(accounts: { status: %w[draft active] })
|
||||
.where(transactions: { category_id: nil })
|
||||
.where.not(transactions: { kind: Transaction::TRANSFER_KINDS })
|
||||
.where(entries: { excluded: false })
|
||||
.count
|
||||
end
|
||||
}
|
||||
|
||||
# Returns uncategorized, non-transfer entries whose name matches the given filter string.
|
||||
# Used by the Quick Categorize Wizard to preview which transactions a rule would affect.
|
||||
@@ -111,13 +108,8 @@ class Entry < ApplicationRecord
|
||||
def self.uncategorized_matching(entries, filter, transaction_type = nil)
|
||||
sanitized = sanitize_sql_like(filter.gsub(/\s+/, " ").strip)
|
||||
scope = entries
|
||||
.joins(:account)
|
||||
.joins("INNER JOIN transactions ON transactions.id = entries.entryable_id AND entries.entryable_type = 'Transaction'")
|
||||
.where(accounts: { status: %w[draft active] })
|
||||
.where(transactions: { category_id: nil })
|
||||
.where.not(transactions: { kind: Transaction::TRANSFER_KINDS })
|
||||
.where(entries: { excluded: false })
|
||||
.where("BTRIM(REGEXP_REPLACE(entries.name, '[[:space:]]+', ' ', 'g')) ILIKE ?", "%#{sanitized}%")
|
||||
.uncategorized_transactions
|
||||
.where("BTRIM(REGEXP_REPLACE(entries.name, '[[:space:]]+', ' ', 'g')) ILIKE ?", "%#{sanitized}%")
|
||||
|
||||
scope = case transaction_type
|
||||
when "income" then scope.where("entries.amount < 0")
|
||||
|
||||
@@ -144,17 +144,6 @@ class Family < ApplicationRecord
|
||||
AutoMerchantDetector.new(self, transaction_ids: transaction_ids).auto_detect
|
||||
end
|
||||
|
||||
def uncategorized_transaction_count
|
||||
Transaction
|
||||
.joins("INNER JOIN entries ON entries.entryable_id = transactions.id AND entries.entryable_type = 'Transaction'")
|
||||
.joins("INNER JOIN accounts ON accounts.id = entries.account_id")
|
||||
.where(accounts: { family_id: id, status: %w[draft active] })
|
||||
.where(transactions: { category_id: nil })
|
||||
.where.not(transactions: { kind: Transaction::TRANSFER_KINDS })
|
||||
.where(entries: { excluded: false })
|
||||
.count
|
||||
end
|
||||
|
||||
def balance_sheet(user: Current.user)
|
||||
BalanceSheet.new(self, user: user)
|
||||
end
|
||||
|
||||
@@ -22,12 +22,7 @@ class Transaction::Grouper::ByMerchantOrName < Transaction::Grouper
|
||||
|
||||
def uncategorized_entries
|
||||
entries
|
||||
.joins(:account)
|
||||
.joins("INNER JOIN transactions ON transactions.id = entries.entryable_id AND entries.entryable_type = 'Transaction'")
|
||||
.where(accounts: { status: %w[draft active] })
|
||||
.where(transactions: { category_id: nil })
|
||||
.where.not(transactions: { kind: Transaction::TRANSFER_KINDS })
|
||||
.where(entries: { excluded: false })
|
||||
.uncategorized_transactions
|
||||
.includes(entryable: :merchant)
|
||||
.order(entries: { date: :desc })
|
||||
end
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
data-action="change->categorize#uncheckRule">
|
||||
<span class="min-w-0 font-medium text-primary truncate"><%= entry.name %></span>
|
||||
<span class="text-secondary whitespace-nowrap"><%= l(entry.date, format: :short) %></span>
|
||||
<span class="font-medium text-right whitespace-nowrap <%= entry.amount.negative? ? "text-green-600" : "text-primary" %>">
|
||||
<span class="privacy-sensitive font-medium text-right whitespace-nowrap <%= entry.amount.negative? ? "text-green-600" : "text-primary" %>">
|
||||
<%= format_money(entry.amount_money.abs) %>
|
||||
</span>
|
||||
<%= select_tag "category_id",
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
<p class="text-sm text-secondary">
|
||||
<%= t("transactions.categorizes.show.transaction_count", count: entries.size) %>
|
||||
·
|
||||
<%= format_money(entries.sum { |e| e.amount_money.abs }) %>
|
||||
<span class="privacy-sensitive"><%= format_money(entries.sum { |e| e.amount_money.abs }) %></span>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<p class="text-sm text-secondary">
|
||||
<%= t(".transaction_count", count: @group.entries.size) %>
|
||||
·
|
||||
<%= format_money(@group.entries.sum { |e| e.amount_money.abs }) %>
|
||||
<span class="privacy-sensitive"><%= format_money(@group.entries.sum { |e| e.amount_money.abs }) %></span>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user