diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb
index 2f1a1cbaa..90cb3bd9a 100644
--- a/app/controllers/categories_controller.rb
+++ b/app/controllers/categories_controller.rb
@@ -29,7 +29,10 @@ class CategoriesController < ApplicationController
@category = Current.family.categories.new(category_params)
if @category.save
- @transaction.update(category_id: @category.id) if @transaction
+ if @transaction
+ @transaction.update(category_id: @category.id)
+ @transaction.record_category_usage!
+ end
flash[:notice] = t(".success")
diff --git a/app/controllers/category/dropdowns_controller.rb b/app/controllers/category/dropdowns_controller.rb
index b8d8e69aa..98a75b7ee 100644
--- a/app/controllers/category/dropdowns_controller.rb
+++ b/app/controllers/category/dropdowns_controller.rb
@@ -3,6 +3,7 @@ class Category::DropdownsController < ApplicationController
def show
@categories = categories_scope.to_a.excluding(@selected_category).prepend(@selected_category).compact
+ @recent_categories = Category.recently_used_for(family: Current.family, excluding: @selected_category).to_a
end
private
diff --git a/app/controllers/transaction_categories_controller.rb b/app/controllers/transaction_categories_controller.rb
index d9daf436a..95c5dd731 100644
--- a/app/controllers/transaction_categories_controller.rb
+++ b/app/controllers/transaction_categories_controller.rb
@@ -9,6 +9,8 @@ class TransactionCategoriesController < ApplicationController
transaction = @entry.transaction
+ transaction.record_category_usage!
+
if needs_rule_notification?(transaction)
flash[:cta] = {
type: "category_rule",
@@ -54,6 +56,7 @@ class TransactionCategoriesController < ApplicationController
def needs_rule_notification?(transaction)
return false if Current.user.rule_prompts_disabled
+ return false if transaction.category_id.blank?
if Current.user.rule_prompt_dismissed_at.present?
time_since_last_rule_prompt = Time.current - Current.user.rule_prompt_dismissed_at
diff --git a/app/controllers/transactions_controller.rb b/app/controllers/transactions_controller.rb
index b57a10654..49a61b50b 100644
--- a/app/controllers/transactions_controller.rb
+++ b/app/controllers/transactions_controller.rb
@@ -124,6 +124,7 @@ class TransactionsController < ApplicationController
def update
if @entry.update(permitted_entry_params)
transaction = @entry.transaction
+ transaction.record_category_usage!
if needs_rule_notification?(transaction)
flash[:cta] = {
diff --git a/app/javascript/controllers/list_filter_controller.js b/app/javascript/controllers/list_filter_controller.js
index c6938a417..b012f26e5 100644
--- a/app/javascript/controllers/list_filter_controller.js
+++ b/app/javascript/controllers/list_filter_controller.js
@@ -2,7 +2,7 @@ import { Controller } from "@hotwired/stimulus";
// Basic functionality to filter a list based on a provided text attribute.
export default class extends Controller {
- static targets = ["input", "list", "emptyMessage"];
+ static targets = ["input", "list", "emptyMessage", "recentSection"];
connect() {
this.inputTarget.focus();
@@ -15,11 +15,29 @@ export default class extends Controller {
const items = this.listTarget.querySelectorAll(".filterable-item");
let noMatchFound = true;
+ // "Recent" is a pre-search shortcut only — once the user is actively
+ // searching, show canonical filtered results, not a duplicate row. Its
+ // items are hidden outright (not text-matched) so the now-ancestor-hidden
+ // section can't leave a row with display:"" that arrow-key nav would
+ // still treat as visible.
+ const recentItems = this.hasRecentSectionTarget
+ ? new Set(this.recentSectionTarget.querySelectorAll(".filterable-item"))
+ : new Set();
+
+ if (this.hasRecentSectionTarget) {
+ this.recentSectionTarget.classList.toggle("hidden", filterValue.length > 0);
+ }
+
if (this.hasEmptyMessageTarget) {
this.emptyMessageTarget.classList.add("hidden");
}
items.forEach((item) => {
+ if (filterValue.length > 0 && recentItems.has(item)) {
+ item.style.display = "none";
+ return;
+ }
+
const text = item.getAttribute("data-filter-name").toLowerCase();
const shouldDisplay = text.includes(filterValue);
item.style.display = shouldDisplay ? "" : "none";
diff --git a/app/models/category.rb b/app/models/category.rb
index 7bad10cd5..d57a8905e 100644
--- a/app/models/category.rb
+++ b/app/models/category.rb
@@ -21,6 +21,7 @@ class Category < ApplicationRecord
before_save :inherit_color_from_parent
scope :alphabetically, -> { order(:name) }
+ scope :recently_used, -> { where.not(last_used_at: nil).order(last_used_at: :desc) }
scope :alphabetically_by_hierarchy, -> {
left_joins(:parent)
.order(Arel.sql("COALESCE(parents_categories.name, categories.name)"))
@@ -148,6 +149,17 @@ class Category < ApplicationRecord
.index_with(true)
end
+ # Categories a family has manually assigned recently — a shortcut above the
+ # alphabetical list, not a replacement for it. See Transaction#record_category_usage!
+ # for where last_used_at is touched (only on a real human pick via one of the
+ # manual assignment controllers, not rule/import auto-assignment).
+ def recently_used_for(family:, excluding: [], limit: 4)
+ family.categories
+ .recently_used
+ .excluding(Array(excluding).compact)
+ .limit(limit)
+ end
+
def suggested_icon(name)
name_down = name.to_s.downcase
diff --git a/app/models/entry.rb b/app/models/entry.rb
index a29b3b2bd..44ffa15f4 100644
--- a/app/models/entry.rb
+++ b/app/models/entry.rb
@@ -487,6 +487,7 @@ class Entry < ApplicationRecord
attrs[:entryable_attributes] = attrs[:entryable_attributes].dup if attrs[:entryable_attributes].present?
attrs[:entryable_attributes][:id] = entry.entryable_id if attrs[:entryable_attributes].present?
entry.update! attrs
+ entry.transaction.record_category_usage! if entry.transaction?
changed = true
end
end
diff --git a/app/models/transaction.rb b/app/models/transaction.rb
index 1aec5ef96..0148616f6 100644
--- a/app/models/transaction.rb
+++ b/app/models/transaction.rb
@@ -144,6 +144,16 @@ class Transaction < ApplicationRecord
update!(category: category)
end
+ # Marks a category as recently used. Called explicitly from the manual
+ # category-assignment controllers (picker, edit form, categorization
+ # wizard, create-and-assign) after a successful save — not wired to a
+ # blanket after_save callback because rule and import auto-assignment
+ # also go through `category_id=`, and those shouldn't count as a "recent"
+ # pick. See Category.recently_used_for.
+ def record_category_usage!
+ category.touch(:last_used_at) if saved_change_to_category_id? && category.present?
+ end
+
def pending?
extra_data = extra.is_a?(Hash) ? extra : {}
PENDING_PROVIDERS.any? do |provider|
diff --git a/app/views/category/dropdowns/_row.html.erb b/app/views/category/dropdowns/_row.html.erb
index 8f133c14b..0a0f41e54 100644
--- a/app/views/category/dropdowns/_row.html.erb
+++ b/app/views/category/dropdowns/_row.html.erb
@@ -1,8 +1,8 @@
-<%# locals: (category:) %>
+<%# locals: (category:, id_prefix: "category_option") %>
<% is_selected = category.id === @selected_category&.id %>
<%= content_tag :div,
- id: dom_id(category, "category_option"),
+ id: dom_id(category, local_assigns.fetch(:id_prefix, "category_option")),
role: "option",
aria_selected: is_selected.to_s,
class: ["filterable-item flex justify-between items-center border-none rounded-lg px-2 py-1 group w-full hover:bg-container-inset-hover",
diff --git a/app/views/category/dropdowns/show.html.erb b/app/views/category/dropdowns/show.html.erb
index 27a272ea4..b514dc108 100644
--- a/app/views/category/dropdowns/show.html.erb
+++ b/app/views/category/dropdowns/show.html.erb
@@ -20,6 +20,15 @@
<%= t(".no_categories") %>
+ <% if @recent_categories.any? %>
+
+
<%= t(".recent") %>
+ <% @recent_categories.each do |category| %>
+ <%= render "category/dropdowns/row", category: category, id_prefix: "recent_category_option" %>
+ <% end %>
+ <%= render "shared/ruler", classes: "my-2" %>
+
+ <% end %>
<% if @categories.any? %>
<% Category::Group.for(@categories).each do |group| %>
<%= render "category/dropdowns/row", category: group.category %>
diff --git a/config/locales/views/category/dropdowns/ca.yml b/config/locales/views/category/dropdowns/ca.yml
index 6cfdb1b6c..dd79b0cb9 100644
--- a/config/locales/views/category/dropdowns/ca.yml
+++ b/config/locales/views/category/dropdowns/ca.yml
@@ -8,4 +8,5 @@ ca:
show:
clear: Esborra la categoria
no_categories: No s'han trobat categories
+ recent: Recents
search_placeholder: Cerca
diff --git a/config/locales/views/category/dropdowns/de.yml b/config/locales/views/category/dropdowns/de.yml
index 1e1d79fdd..fe77f2a78 100644
--- a/config/locales/views/category/dropdowns/de.yml
+++ b/config/locales/views/category/dropdowns/de.yml
@@ -8,4 +8,5 @@ de:
show:
clear: Kategorie löschen
no_categories: Keine Kategorien gefunden
+ recent: Zuletzt verwendet
search_placeholder: Suchen
diff --git a/config/locales/views/category/dropdowns/en.yml b/config/locales/views/category/dropdowns/en.yml
index 511e86a9a..f384041cd 100644
--- a/config/locales/views/category/dropdowns/en.yml
+++ b/config/locales/views/category/dropdowns/en.yml
@@ -8,4 +8,5 @@ en:
show:
clear: Clear category
no_categories: No categories found
+ recent: Recent
search_placeholder: Search
diff --git a/config/locales/views/category/dropdowns/es.yml b/config/locales/views/category/dropdowns/es.yml
index e7be1bd16..4f1560130 100644
--- a/config/locales/views/category/dropdowns/es.yml
+++ b/config/locales/views/category/dropdowns/es.yml
@@ -8,4 +8,5 @@ es:
show:
clear: Limpiar categoría
no_categories: No se encontraron categorías
+ recent: Recientes
search_placeholder: Buscar
diff --git a/config/locales/views/category/dropdowns/fr.yml b/config/locales/views/category/dropdowns/fr.yml
index e577feee2..12af5568a 100644
--- a/config/locales/views/category/dropdowns/fr.yml
+++ b/config/locales/views/category/dropdowns/fr.yml
@@ -8,4 +8,5 @@ fr:
show:
clear: Effacer la catégorie
no_categories: Aucune catégorie trouvée
+ recent: Récents
search_placeholder: Rechercher
diff --git a/config/locales/views/category/dropdowns/hu.yml b/config/locales/views/category/dropdowns/hu.yml
index 70be94cf5..390bf389c 100644
--- a/config/locales/views/category/dropdowns/hu.yml
+++ b/config/locales/views/category/dropdowns/hu.yml
@@ -8,4 +8,5 @@ hu:
show:
clear: Kategória törlése
no_categories: Nem található kategória
+ recent: Legutóbbiak
search_placeholder: Keresés
diff --git a/config/locales/views/category/dropdowns/it.yml b/config/locales/views/category/dropdowns/it.yml
index 9e7b7bcc2..2a29301bb 100644
--- a/config/locales/views/category/dropdowns/it.yml
+++ b/config/locales/views/category/dropdowns/it.yml
@@ -8,4 +8,5 @@ it:
show:
clear: Cancella categoria
no_categories: Nessuna categoria trovata
+ recent: Recenti
search_placeholder: Cerca
diff --git a/config/locales/views/category/dropdowns/nb.yml b/config/locales/views/category/dropdowns/nb.yml
index aa2cfa98b..5304b55b2 100644
--- a/config/locales/views/category/dropdowns/nb.yml
+++ b/config/locales/views/category/dropdowns/nb.yml
@@ -8,4 +8,5 @@ nb:
show:
clear: Fjern kategori
no_categories: Ingen kategorier funnet
+ recent: Nylige
search_placeholder: Søk
\ No newline at end of file
diff --git a/config/locales/views/category/dropdowns/nl.yml b/config/locales/views/category/dropdowns/nl.yml
index 9ae74d6a8..eb04ecb14 100644
--- a/config/locales/views/category/dropdowns/nl.yml
+++ b/config/locales/views/category/dropdowns/nl.yml
@@ -8,4 +8,5 @@ nl:
show:
clear: Categorie wissen
no_categories: Geen categorieën gevonden
+ recent: Recent
search_placeholder: Zoeken
diff --git a/config/locales/views/category/dropdowns/pl.yml b/config/locales/views/category/dropdowns/pl.yml
index 74380bb48..438283f98 100644
--- a/config/locales/views/category/dropdowns/pl.yml
+++ b/config/locales/views/category/dropdowns/pl.yml
@@ -8,4 +8,5 @@ pl:
show:
clear: Wyczyść kategorię
no_categories: Nie znaleziono kategorii
+ recent: Ostatnie
search_placeholder: Szukaj
diff --git a/config/locales/views/category/dropdowns/pt-BR.yml b/config/locales/views/category/dropdowns/pt-BR.yml
index 6cde2c64a..adb8c6fe2 100644
--- a/config/locales/views/category/dropdowns/pt-BR.yml
+++ b/config/locales/views/category/dropdowns/pt-BR.yml
@@ -8,4 +8,5 @@ pt-BR:
show:
clear: Limpar categoria
no_categories: Nenhuma categoria encontrada
+ recent: Recentes
search_placeholder: Buscar
diff --git a/config/locales/views/category/dropdowns/ro.yml b/config/locales/views/category/dropdowns/ro.yml
index 7b0a5bbf7..8617adb4b 100644
--- a/config/locales/views/category/dropdowns/ro.yml
+++ b/config/locales/views/category/dropdowns/ro.yml
@@ -8,4 +8,5 @@ ro:
show:
clear: Golește categoria
no_categories: Nu s-au găsit categorii
+ recent: Recente
search_placeholder: Caută
diff --git a/config/locales/views/category/dropdowns/ru.yml b/config/locales/views/category/dropdowns/ru.yml
index a0f008f82..cb00b95cc 100644
--- a/config/locales/views/category/dropdowns/ru.yml
+++ b/config/locales/views/category/dropdowns/ru.yml
@@ -8,4 +8,5 @@ ru:
show:
clear: Очистить категорию
no_categories: Категории не найдены
+ recent: Недавние
search_placeholder: Поиск
diff --git a/config/locales/views/category/dropdowns/tr.yml b/config/locales/views/category/dropdowns/tr.yml
index 3b645f448..0c1eea604 100644
--- a/config/locales/views/category/dropdowns/tr.yml
+++ b/config/locales/views/category/dropdowns/tr.yml
@@ -8,4 +8,5 @@ tr:
show:
clear: Kategoriyi temizle
no_categories: Hiç kategori bulunamadı
+ recent: Son kullanılanlar
search_placeholder: Ara
diff --git a/config/locales/views/category/dropdowns/vi.yml b/config/locales/views/category/dropdowns/vi.yml
index 04cf03947..11bd4d2ed 100644
--- a/config/locales/views/category/dropdowns/vi.yml
+++ b/config/locales/views/category/dropdowns/vi.yml
@@ -8,4 +8,5 @@ vi:
show:
clear: Xóa danh mục
no_categories: Không tìm thấy danh mục nào
+ recent: Gần đây
search_placeholder: Tìm kiếm
diff --git a/config/locales/views/category/dropdowns/zh-CN.yml b/config/locales/views/category/dropdowns/zh-CN.yml
index 3db41514c..b4da34709 100644
--- a/config/locales/views/category/dropdowns/zh-CN.yml
+++ b/config/locales/views/category/dropdowns/zh-CN.yml
@@ -8,4 +8,5 @@ zh-CN:
show:
clear: 清空分类
no_categories: 暂无分类
+ recent: 最近使用
search_placeholder: 搜索分类
diff --git a/config/locales/views/category/dropdowns/zh-TW.yml b/config/locales/views/category/dropdowns/zh-TW.yml
index 9970c4bd4..70c0a728e 100644
--- a/config/locales/views/category/dropdowns/zh-TW.yml
+++ b/config/locales/views/category/dropdowns/zh-TW.yml
@@ -8,4 +8,5 @@ zh-TW:
show:
clear: 清空分類
no_categories: 暫無分類
+ recent: 最近使用
search_placeholder: 搜尋分類
diff --git a/db/migrate/20260727111051_add_last_used_at_to_categories.rb b/db/migrate/20260727111051_add_last_used_at_to_categories.rb
new file mode 100644
index 000000000..35089c385
--- /dev/null
+++ b/db/migrate/20260727111051_add_last_used_at_to_categories.rb
@@ -0,0 +1,6 @@
+class AddLastUsedAtToCategories < ActiveRecord::Migration[7.2]
+ def change
+ add_column :categories, :last_used_at, :datetime
+ add_index :categories, [ :family_id, :last_used_at ]
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 7e94f1dce..c4346da7f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.2].define(version: 2026_07_25_000000) do
+ActiveRecord::Schema[7.2].define(version: 2026_07_27_111051) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@@ -394,7 +394,9 @@ ActiveRecord::Schema[7.2].define(version: 2026_07_25_000000) do
t.uuid "parent_id"
t.string "classification_unused", default: "expense", null: false
t.string "lucide_icon", default: "shapes", null: false
+ t.datetime "last_used_at"
t.index ["family_id"], name: "index_categories_on_family_id"
+ t.index ["family_id", "last_used_at"], name: "index_categories_on_family_id_and_last_used_at"
end
create_table "chats", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
diff --git a/test/controllers/category/dropdowns_controller_test.rb b/test/controllers/category/dropdowns_controller_test.rb
new file mode 100644
index 000000000..d8e1eb61f
--- /dev/null
+++ b/test/controllers/category/dropdowns_controller_test.rb
@@ -0,0 +1,50 @@
+require "test_helper"
+
+class Category::DropdownsControllerTest < ActionDispatch::IntegrationTest
+ include ActionView::RecordIdentifier
+
+ setup do
+ sign_in users(:family_admin)
+ @transaction = transactions(:one)
+ ensure_tailwind_build
+ end
+
+ test "shows a recent section for categories used recently" do
+ recent = categories(:income)
+ recent.update!(last_used_at: 1.day.ago)
+
+ get category_dropdown_url(transaction_id: @transaction.id)
+
+ assert_response :success
+ assert_select "[data-list-filter-target='recentSection']"
+ assert_select "[data-list-filter-target='recentSection']", text: /#{Regexp.escape(recent.name)}/
+ end
+
+ test "recent and canonical rows for the same category have distinct DOM ids" do
+ recent = categories(:income)
+ recent.update!(last_used_at: 1.day.ago)
+
+ get category_dropdown_url(transaction_id: @transaction.id)
+
+ assert_response :success
+ assert_select "##{dom_id(recent, 'recent_category_option')}", count: 1
+ assert_select "##{dom_id(recent, 'category_option')}", count: 1
+ end
+
+ test "excludes the currently selected category from the recent section" do
+ selected = categories(:food_and_drink)
+ selected.update!(last_used_at: 1.day.ago)
+
+ get category_dropdown_url(category_id: selected.id, transaction_id: @transaction.id)
+
+ assert_response :success
+ assert_select "[data-list-filter-target='recentSection']", false
+ end
+
+ test "omits the recent section entirely when nothing has been used yet" do
+ get category_dropdown_url(transaction_id: @transaction.id)
+
+ assert_response :success
+ assert_select "[data-list-filter-target='recentSection']", false
+ end
+end
diff --git a/test/controllers/transaction_categories_controller_test.rb b/test/controllers/transaction_categories_controller_test.rb
new file mode 100644
index 000000000..116907edc
--- /dev/null
+++ b/test/controllers/transaction_categories_controller_test.rb
@@ -0,0 +1,33 @@
+require "test_helper"
+
+class TransactionCategoriesControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ sign_in users(:family_admin)
+ @entry = entries(:transaction)
+ @transaction = transactions(:one)
+ end
+
+ test "assigning a category touches its last_used_at" do
+ category = categories(:income)
+ assert_nil category.last_used_at
+
+ patch transaction_category_url(@entry),
+ params: { entry: { entryable_type: "Transaction", entryable_attributes: { id: @transaction.id, category_id: category.id } } },
+ as: :turbo_stream
+
+ assert_not_nil category.reload.last_used_at
+ end
+
+ test "clearing a category does not touch any category's last_used_at" do
+ category = @transaction.category
+ assert_nil category.last_used_at
+
+ patch transaction_category_url(@entry),
+ params: { entry: { entryable_type: "Transaction", entryable_attributes: { id: @transaction.id, category_id: nil } } },
+ as: :turbo_stream
+
+ assert_response :success
+ assert_nil @transaction.reload.category_id
+ assert_nil category.reload.last_used_at
+ end
+end
diff --git a/test/models/category_test.rb b/test/models/category_test.rb
index a12b9790a..637546a56 100644
--- a/test/models/category_test.rb
+++ b/test/models/category_test.rb
@@ -159,4 +159,32 @@ class CategoryTest < ActiveSupport::TestCase
assert lookup.key?(category.id)
assert_not lookup.key?(0)
end
+
+ test "recently_used_for orders by last_used_at, most recent first" do
+ older = categories(:income)
+ newer = categories(:food_and_drink)
+ older.update!(last_used_at: 2.days.ago)
+ newer.update!(last_used_at: 1.day.ago)
+
+ assert_equal [ newer, older ], Category.recently_used_for(family: @family).to_a
+ end
+
+ test "recently_used_for excludes categories with no usage yet" do
+ categories(:food_and_drink).update!(last_used_at: 1.day.ago)
+
+ assert_not_includes Category.recently_used_for(family: @family).to_a, categories(:income)
+ end
+
+ test "recently_used_for excludes given categories and respects limit" do
+ a = categories(:income)
+ b = categories(:food_and_drink)
+ c = categories(:subcategory)
+ a.update!(last_used_at: 3.days.ago)
+ b.update!(last_used_at: 2.days.ago)
+ c.update!(last_used_at: 1.day.ago)
+
+ result = Category.recently_used_for(family: @family, excluding: b, limit: 1)
+
+ assert_equal [ c ], result.to_a
+ end
end
diff --git a/test/models/entry_test.rb b/test/models/entry_test.rb
index bcf820d4d..77c4b036b 100644
--- a/test/models/entry_test.rb
+++ b/test/models/entry_test.rb
@@ -22,4 +22,14 @@ class EntryTest < ActiveSupport::TestCase
assert_equal entry_ids.sort, Entry.where(id: entry_ids).chronological.pluck(:id)
assert_equal entry_ids.sort.reverse, Entry.where(id: entry_ids).reverse_chronological.pluck(:id)
end
+
+ test "bulk_update! touches the assigned category's last_used_at" do
+ entry = create_transaction(account: accounts(:depository))
+ category = categories(:income)
+ assert_nil category.last_used_at
+
+ Entry.where(id: entry.id).bulk_update!({ category_id: category.id })
+
+ assert_not_nil category.reload.last_used_at
+ end
end
diff --git a/test/models/transaction_test.rb b/test/models/transaction_test.rb
index dfbf8e96b..560d1040f 100644
--- a/test/models/transaction_test.rb
+++ b/test/models/transaction_test.rb
@@ -179,4 +179,44 @@ class TransactionTest < ActiveSupport::TestCase
assert_equal securities(:msft), transaction.activity_security
end
+
+ test "record_category_usage! touches the new category's last_used_at" do
+ transaction = transactions(:one)
+ category = categories(:income)
+ assert_nil category.last_used_at
+
+ transaction.update!(category: category)
+ transaction.record_category_usage!
+
+ assert_not_nil category.reload.last_used_at
+ end
+
+ test "record_category_usage! does nothing when category_id did not change" do
+ transaction = transactions(:one)
+ category = transaction.category
+ assert_nil category.last_used_at
+
+ transaction.reload
+ transaction.record_category_usage!
+
+ assert_nil category.reload.last_used_at
+ end
+
+ test "record_category_usage! does nothing when category is cleared" do
+ transaction = transactions(:one)
+
+ transaction.update!(category: nil)
+
+ assert_nothing_raised { transaction.record_category_usage! }
+ end
+
+ test "record_category_usage! is not invoked by rule-driven category enrichment" do
+ transaction = transactions(:one)
+ category = categories(:income)
+ assert_nil category.last_used_at
+
+ transaction.enrich_attribute(:category_id, category.id, source: "rule")
+
+ assert_nil category.reload.last_used_at
+ end
end