Files
sure/app/controllers/transactions/categorizes_controller.rb
Mikael Møller 5cb474d61c 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>
2026-04-07 13:17:37 +02:00

135 lines
5.2 KiB
Ruby

class Transactions::CategorizesController < ApplicationController
def show
@breadcrumbs = [
[ t("breadcrumbs.home"), root_path ],
[ t("breadcrumbs.transactions"), transactions_path ],
[ t("breadcrumbs.categorize"), nil ]
]
@position = [ params[:position].to_i, 0 ].max
groups = Transaction::Grouper.strategy.call(
Current.accessible_entries,
limit: 1,
offset: @position
)
if groups.empty?
redirect_to transactions_path, notice: t(".all_done") and return
end
@group = groups.first
@categories = Current.family.categories.alphabetically
@total_uncategorized = uncategorized_count
end
def create
@position = params[:position].to_i
entry_ids = Array.wrap(params[:entry_ids]).reject(&:blank?)
all_entry_ids = Array.wrap(params[:all_entry_ids]).reject(&:blank?)
remaining_ids = all_entry_ids - entry_ids
category = Current.family.categories.find(params[:category_id])
entries = Current.accessible_entries.excluding_split_parents.where(id: entry_ids)
count = entries.bulk_update!({ category_id: category.id })
if params[:create_rule] == "1"
rule = Rule.create_from_grouping(
Current.family,
params[:grouping_key],
category,
transaction_type: params[:transaction_type]
)
flash[:alert] = t(".rule_creation_failed") if rule.nil?
end
respond_to do |format|
format.turbo_stream do
remaining_entries = uncategorized_entries_for(remaining_ids)
remaining_ids = remaining_entries.map { |e| e.id.to_s }
if remaining_ids.empty?
render turbo_stream: turbo_stream.action(:redirect, transactions_categorize_path(position: @position))
else
@categories = Current.family.categories.alphabetically
streams = entry_ids.map { |id| turbo_stream.remove("categorize_entry_#{id}") }
remaining_entries.each do |entry|
streams << turbo_stream.replace(
"categorize_entry_#{entry.id}",
partial: "transactions/categorizes/entry_row",
locals: { entry: entry, categories: @categories }
)
end
streams << turbo_stream.replace("categorize_remaining",
partial: "transactions/categorizes/remaining_count",
locals: { total_uncategorized: uncategorized_count })
streams << turbo_stream.replace("categorize_group_summary",
partial: "transactions/categorizes/group_summary",
locals: { entries: remaining_entries })
streams.concat(flash_notification_stream_items)
render turbo_stream: streams
end
end
format.html { redirect_to transactions_categorize_path(position: @position), notice: t(".categorized", count: count) }
end
end
def preview_rule
filter = params[:filter].to_s.strip
transaction_type = params[:transaction_type].presence
entries = filter.present? ? Entry.uncategorized_matching(Current.accessible_entries, filter, transaction_type) : []
@categories = Current.family.categories.alphabetically
render turbo_stream: [
turbo_stream.replace("categorize_group_title",
partial: "transactions/categorizes/group_title",
locals: { display_name: filter.presence || "", color: "#737373", transaction_type: transaction_type }),
turbo_stream.replace("categorize_group_summary",
partial: "transactions/categorizes/group_summary",
locals: { entries: entries }),
turbo_stream.replace("categorize_transaction_list",
partial: "transactions/categorizes/transaction_list",
locals: { entries: entries, categories: @categories })
]
end
def assign_entry
entry = Current.accessible_entries.excluding_split_parents.find(params[:entry_id])
category = Current.family.categories.find(params[:category_id])
position = params[:position].to_i
all_entry_ids = Array.wrap(params[:all_entry_ids]).reject(&:blank?)
remaining_ids = all_entry_ids - [ entry.id.to_s ]
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 }
streams = [ turbo_stream.remove("categorize_entry_#{entry.id}") ]
if remaining_ids.empty?
streams << turbo_stream.action(:redirect, transactions_categorize_path(position: position))
else
streams << turbo_stream.replace("categorize_remaining",
partial: "transactions/categorizes/remaining_count",
locals: { total_uncategorized: uncategorized_count })
streams << turbo_stream.replace("categorize_group_summary",
partial: "transactions/categorizes/group_summary",
locals: { entries: remaining_entries })
end
render turbo_stream: streams
end
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)
.uncategorized_transactions
.to_a
end
end