- <% Current.family.transaction_categories.alphabetically.each do |transaction_category| %>
-
+ <% Current.family.categories.alphabetically.each do |category| %>
+
<%= form.check_box :categories,
{
multiple: true,
- checked: @q[:categories]&.include?(transaction_category.name),
+ checked: @q[:categories]&.include?(category.name),
class: "rounded-sm border-gray-300 text-indigo-600 shadow-xs focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
},
- transaction_category.name,
+ category.name,
nil %>
- <%= form.label :categories, transaction_category.name, value: transaction_category.name, class: "text-sm text-gray-900 cursor-pointer" do %>
- <%= render partial: "transactions/categories/badge", locals: { category: transaction_category } %>
+ <%= form.label :categories, category.name, value: category.name, class: "text-sm text-gray-900 cursor-pointer" do %>
+ <%= render partial: "categories/badge", locals: { category: category } %>
<% end %>
<% end %>
diff --git a/app/views/transactions/show.html.erb b/app/views/transactions/show.html.erb
index 6fc355d1..fe49b650 100644
--- a/app/views/transactions/show.html.erb
+++ b/app/views/transactions/show.html.erb
@@ -29,7 +29,7 @@
<%= f.date_field :date, label: t(".date_label"), max: Date.today, "data-auto-submit-form-target": "auto" %>
<% unless @transaction.marked_as_transfer %>
- <%= f.collection_select :category_id, Current.family.transaction_categories.alphabetically, :id, :name, { prompt: t(".category_placeholder"), label: t(".category_label"), class: "text-gray-400" }, "data-auto-submit-form-target": "auto" %>
+ <%= f.collection_select :category_id, Current.family.categories.alphabetically, :id, :name, { prompt: t(".category_placeholder"), label: t(".category_label"), class: "text-gray-400" }, "data-auto-submit-form-target": "auto" %>
<%= f.collection_select :merchant_id, Current.family.transaction_merchants.alphabetically, :id, :name, { prompt: t(".merchant_placeholder"), label: t(".merchant_label"), class: "text-gray-400" }, "data-auto-submit-form-target": "auto" %>
<% end %>
diff --git a/config/locales/views/categories/en.yml b/config/locales/views/categories/en.yml
new file mode 100644
index 00000000..9451094a
--- /dev/null
+++ b/config/locales/views/categories/en.yml
@@ -0,0 +1,43 @@
+---
+en:
+ categories:
+ create:
+ success: New transaction category created successfully
+ deletions:
+ create:
+ success: Transaction category deleted successfully
+ new:
+ category: Category
+ delete_and_leave_uncategorized: Delete "%{category_name}" and leave uncategorized
+ delete_and_recategorize: Delete "%{category_name}" and assign new category
+ delete_category: Delete category?
+ explanation: By deleting this category, every transaction that has the "%{category_name}"
+ category will be uncategorized. Instead of leaving them uncategorized, you
+ can also assign a new category below.
+ replacement_category_prompt: Select category
+ dropdowns:
+ row:
+ delete: Delete category
+ edit: Edit category
+ show:
+ add_new: Add new
+ clear: Clear
+ no_categories: No categories found
+ search_placeholder: Search
+ edit:
+ edit: Edit category
+ form:
+ create: Create category
+ update: Update
+ index:
+ categories: Categories
+ new: New
+ menu:
+ loading: Loading...
+ new:
+ new_category: New category
+ row:
+ delete: Delete category
+ edit: Edit category
+ update:
+ success: Transaction category updated successfully
diff --git a/config/locales/views/transactions/en.yml b/config/locales/views/transactions/en.yml
index 8b5e7e53..b74298d4 100644
--- a/config/locales/views/transactions/en.yml
+++ b/config/locales/views/transactions/en.yml
@@ -22,47 +22,6 @@ en:
bulk_update:
failure: Could not update transactions
success: "%{count} transactions updated"
- categories:
- create:
- success: New transaction category created successfully
- deletions:
- create:
- success: Transaction category deleted successfully
- new:
- category: Category
- delete_and_leave_uncategorized: Delete "%{category_name}" and leave uncategorized
- delete_and_recategorize: Delete "%{category_name}" and assign new category
- delete_category: Delete category?
- explanation: By deleting this category, every transaction that has the "%{category_name}"
- category will be uncategorized. Instead of leaving them uncategorized,
- you can also assign a new category below.
- replacement_category_prompt: Select category
- dropdowns:
- row:
- delete: Delete category
- edit: Edit category
- show:
- add_new: Add new
- clear: Clear
- no_categories: No categories found
- search_placeholder: Search
- edit:
- edit: Edit category
- form:
- create: Create category
- update: Update
- index:
- categories: Categories
- new: New
- menu:
- loading: Loading...
- new:
- new_category: New category
- row:
- delete: Delete category
- edit: Edit category
- update:
- success: Transaction category updated successfully
create:
success: New transaction created successfully
destroy:
diff --git a/config/routes.rb b/config/routes.rb
index 66ca561e..a1f98f7e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -41,6 +41,13 @@ Rails.application.routes.draw do
resources :deletions, only: %i[ new create ], module: :tags
end
+ resources :categories do
+ resources :deletions, only: %i[ new create ], module: :categories
+ collection do
+ resource :dropdown, only: :show, module: :categories, as: :category_dropdown
+ end
+ end
+
resources :transactions do
collection do
post "bulk_delete"
@@ -51,14 +58,6 @@ Rails.application.routes.draw do
scope module: :transactions, as: :transaction do
resources :rows, only: %i[ show update ]
-
- resources :categories do
- resources :deletions, only: %i[ new create ], module: :categories
- collection do
- resource :dropdown, only: :show, module: :categories, as: :category_dropdown
- end
- end
-
resources :rules, only: %i[ index ]
resources :merchants, only: %i[ index new create edit update destroy ]
end
diff --git a/db/migrate/20240620114307_rename_categories_table.rb b/db/migrate/20240620114307_rename_categories_table.rb
new file mode 100644
index 00000000..0a6231bd
--- /dev/null
+++ b/db/migrate/20240620114307_rename_categories_table.rb
@@ -0,0 +1,5 @@
+class RenameCategoriesTable < ActiveRecord::Migration[7.2]
+ def change
+ rename_table :transaction_categories, :categories
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 0f06cf85..1a40956f 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: 2024_06_19_125949) do
+ActiveRecord::Schema[7.2].define(version: 2024_06_20_114307) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@@ -48,7 +48,7 @@ ActiveRecord::Schema[7.2].define(version: 2024_06_19_125949) do
t.jsonb "sync_errors", default: [], null: false
t.date "last_sync_date"
t.uuid "institution_id"
- t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY ((ARRAY['Loan'::character varying, 'CreditCard'::character varying, 'OtherLiability'::character varying])::text[])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
+ t.virtual "classification", type: :string, as: "\nCASE\n WHEN ((accountable_type)::text = ANY (ARRAY[('Loan'::character varying)::text, ('CreditCard'::character varying)::text, ('OtherLiability'::character varying)::text])) THEN 'liability'::text\n ELSE 'asset'::text\nEND", stored: true
t.index ["accountable_type"], name: "index_accounts_on_accountable_type"
t.index ["family_id"], name: "index_accounts_on_family_id"
t.index ["institution_id"], name: "index_accounts_on_institution_id"
@@ -82,6 +82,16 @@ ActiveRecord::Schema[7.2].define(version: 2024_06_19_125949) do
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
end
+ create_table "categories", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
+ t.string "name", null: false
+ t.string "color", default: "#6172F3", null: false
+ t.string "internal_category"
+ t.uuid "family_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["family_id"], name: "index_categories_on_family_id"
+ end
+
create_table "credit_cards", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
@@ -274,16 +284,6 @@ ActiveRecord::Schema[7.2].define(version: 2024_06_19_125949) do
t.index ["family_id"], name: "index_tags_on_family_id"
end
- create_table "transaction_categories", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
- t.string "name", null: false
- t.string "color", default: "#6172F3", null: false
- t.string "internal_category"
- t.uuid "family_id", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.index ["family_id"], name: "index_transaction_categories_on_family_id"
- end
-
create_table "transaction_merchants", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.string "name", null: false
t.string "color", default: "#e99537", null: false
@@ -356,14 +356,14 @@ ActiveRecord::Schema[7.2].define(version: 2024_06_19_125949) do
add_foreign_key "accounts", "institutions"
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
+ add_foreign_key "categories", "families"
add_foreign_key "imports", "accounts"
add_foreign_key "institutions", "families"
add_foreign_key "taggings", "tags"
add_foreign_key "tags", "families"
- add_foreign_key "transaction_categories", "families"
add_foreign_key "transaction_merchants", "families"
add_foreign_key "transactions", "accounts", on_delete: :cascade
- add_foreign_key "transactions", "transaction_categories", column: "category_id", on_delete: :nullify
+ add_foreign_key "transactions", "categories", on_delete: :nullify
add_foreign_key "transactions", "transaction_merchants", column: "merchant_id"
add_foreign_key "transactions", "transfers"
add_foreign_key "users", "families"
diff --git a/lib/tasks/demo_data.rake b/lib/tasks/demo_data.rake
index 9893df87..8d7d6e32 100644
--- a/lib/tasks/demo_data.rake
+++ b/lib/tasks/demo_data.rake
@@ -5,10 +5,10 @@ namespace :demo_data do
family.accounts.delete_all
ExchangeRate.delete_all
- family.transaction_categories.delete_all
+ family.categories.delete_all
Tagging.delete_all
family.tags.delete_all
- Transaction::Category.create_default_categories(family)
+ Category.create_default_categories(family)
user = User.find_or_create_by(email: "user@maybe.local") do |u|
u.password = "password"
diff --git a/test/controllers/transactions/categories/deletions_controller_test.rb b/test/controllers/categories/deletions_controller_test.rb
similarity index 58%
rename from test/controllers/transactions/categories/deletions_controller_test.rb
rename to test/controllers/categories/deletions_controller_test.rb
index 7145ef5f..a9b0caa8 100644
--- a/test/controllers/transactions/categories/deletions_controller_test.rb
+++ b/test/controllers/categories/deletions_controller_test.rb
@@ -1,24 +1,24 @@
require "test_helper"
-class Transactions::Categories::DeletionsControllerTest < ActionDispatch::IntegrationTest
+class Categories::DeletionsControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in users(:family_admin)
- @category = transaction_categories(:food_and_drink)
+ @category = categories(:food_and_drink)
end
test "new" do
- get new_transaction_category_deletion_url(@category)
+ get new_category_deletion_url(@category)
assert_response :success
end
test "create with replacement" do
- replacement_category = transaction_categories(:income)
+ replacement_category = categories(:income)
assert_not_empty @category.transactions
- assert_difference "Transaction::Category.count", -1 do
+ assert_difference "Category.count", -1 do
assert_difference "replacement_category.transactions.count", @category.transactions.count do
- post transaction_category_deletions_url(@category),
+ post category_deletions_url(@category),
params: { replacement_category_id: replacement_category.id }
end
end
@@ -29,9 +29,9 @@ class Transactions::Categories::DeletionsControllerTest < ActionDispatch::Integr
test "create without replacement" do
assert_not_empty @category.transactions
- assert_difference "Transaction::Category.count", -1 do
+ assert_difference "Category.count", -1 do
assert_difference "Transaction.where(category: nil).count", @category.transactions.count do
- post transaction_category_deletions_url(@category)
+ post category_deletions_url(@category)
end
end
diff --git a/test/controllers/categories_controller_test.rb b/test/controllers/categories_controller_test.rb
new file mode 100644
index 00000000..b38fff9f
--- /dev/null
+++ b/test/controllers/categories_controller_test.rb
@@ -0,0 +1,73 @@
+require "test_helper"
+
+class CategoriesControllerTest < ActionDispatch::IntegrationTest
+ setup do
+ sign_in users(:family_admin)
+ end
+
+ test "index" do
+ get categories_url
+ assert_response :success
+ end
+
+ test "new" do
+ get new_category_url
+ assert_response :success
+ end
+
+ test "create" do
+ color = Category::COLORS.sample
+
+ assert_difference "Category.count", +1 do
+ post categories_url, params: {
+ category: {
+ name: "New Category",
+ color: color } }
+ end
+
+ new_category = Category.order(:created_at).last
+
+ assert_redirected_to transactions_url
+ assert_equal "New Category", new_category.name
+ assert_equal color, new_category.color
+ end
+
+ test "create and assign to transaction" do
+ color = Category::COLORS.sample
+
+ assert_difference "Category.count", +1 do
+ post categories_url, params: {
+ transaction_id: transactions(:checking_one).id,
+ category: {
+ name: "New Category",
+ color: color } }
+ end
+
+ new_category = Category.order(:created_at).last
+
+ assert_redirected_to transactions_url
+ assert_equal "New Category", new_category.name
+ assert_equal color, new_category.color
+ assert_equal transactions(:checking_one).reload.category, new_category
+ end
+
+ test "edit" do
+ get edit_category_url(categories(:food_and_drink))
+ assert_response :success
+ end
+
+ test "update" do
+ new_color = Category::COLORS.without(categories(:income).color).sample
+
+ assert_changes -> { categories(:income).name }, to: "New Name" do
+ assert_changes -> { categories(:income).reload.color }, to: new_color do
+ patch category_url(categories(:income)), params: {
+ category: {
+ name: "New Name",
+ color: new_color } }
+ end
+ end
+
+ assert_redirected_to transactions_url
+ end
+end
diff --git a/test/controllers/registrations_controller_test.rb b/test/controllers/registrations_controller_test.rb
index db11757d..1c3169f6 100644
--- a/test/controllers/registrations_controller_test.rb
+++ b/test/controllers/registrations_controller_test.rb
@@ -16,7 +16,7 @@ class RegistrationsControllerTest < ActionDispatch::IntegrationTest
end
test "create seeds default transaction categories" do
- assert_difference "Transaction::Category.count", Transaction::Category::DEFAULT_CATEGORIES.size do
+ assert_difference "Category.count", Category::DEFAULT_CATEGORIES.size do
post registration_url, params: { user: {
email: "john@example.com",
password: "password",
diff --git a/test/controllers/transactions/categories_controller_test.rb b/test/controllers/transactions/categories_controller_test.rb
deleted file mode 100644
index 17a52e4c..00000000
--- a/test/controllers/transactions/categories_controller_test.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-require "test_helper"
-
-class Transactions::CategoriesControllerTest < ActionDispatch::IntegrationTest
- setup do
- sign_in users(:family_admin)
- end
-
- test "index" do
- get transaction_categories_url
- assert_response :success
- end
-
- test "new" do
- get new_transaction_category_url
- assert_response :success
- end
-
- test "create" do
- color = Transaction::Category::COLORS.sample
-
- assert_difference "Transaction::Category.count", +1 do
- post transaction_categories_url, params: {
- transaction_category: {
- name: "New Category",
- color: color } }
- end
-
- new_category = Transaction::Category.order(:created_at).last
-
- assert_redirected_to transactions_url
- assert_equal "New Category", new_category.name
- assert_equal color, new_category.color
- end
-
- test "create and assign to transaction" do
- color = Transaction::Category::COLORS.sample
-
- assert_difference "Transaction::Category.count", +1 do
- post transaction_categories_url, params: {
- transaction_id: transactions(:checking_one).id,
- transaction_category: {
- name: "New Category",
- color: color } }
- end
-
- new_category = Transaction::Category.order(:created_at).last
-
- assert_redirected_to transactions_url
- assert_equal "New Category", new_category.name
- assert_equal color, new_category.color
- assert_equal transactions(:checking_one).reload.category, new_category
- end
-
- test "edit" do
- get edit_transaction_category_url(transaction_categories(:food_and_drink))
- assert_response :success
- end
-
- test "update" do
- new_color = Transaction::Category::COLORS.without(transaction_categories(:income).color).sample
-
- assert_changes -> { transaction_categories(:income).name }, to: "New Name" do
- assert_changes -> { transaction_categories(:income).reload.color }, to: new_color do
- patch transaction_category_url(transaction_categories(:income)), params: {
- transaction_category: {
- name: "New Name",
- color: new_color } }
- end
- end
-
- assert_redirected_to transactions_url
- end
-end
diff --git a/test/controllers/transactions_controller_test.rb b/test/controllers/transactions_controller_test.rb
index 57feedcc..b31fe159 100644
--- a/test/controllers/transactions_controller_test.rb
+++ b/test/controllers/transactions_controller_test.rb
@@ -159,7 +159,7 @@ class TransactionsControllerTest < ActionDispatch::IntegrationTest
transactions.each do |transaction|
transaction.update! \
excluded: false,
- category_id: Transaction::Category.first.id,
+ category_id: Category.first.id,
merchant_id: Transaction::Merchant.first.id,
notes: "Starting note"
end
@@ -169,7 +169,7 @@ class TransactionsControllerTest < ActionDispatch::IntegrationTest
date: Date.current,
transaction_ids: transactions.map(&:id),
excluded: true,
- category_id: Transaction::Category.second.id,
+ category_id: Category.second.id,
merchant_id: Transaction::Merchant.second.id,
notes: "Updated note"
}
@@ -181,7 +181,7 @@ class TransactionsControllerTest < ActionDispatch::IntegrationTest
transactions.reload.each do |transaction|
assert_equal Date.current, transaction.date
assert transaction.excluded
- assert_equal Transaction::Category.second, transaction.category
+ assert_equal Category.second, transaction.category
assert_equal Transaction::Merchant.second, transaction.merchant
assert_equal "Updated note", transaction.notes
end
diff --git a/test/fixtures/transaction/categories.yml b/test/fixtures/categories.yml
similarity index 100%
rename from test/fixtures/transaction/categories.yml
rename to test/fixtures/categories.yml
diff --git a/test/models/transaction/category_test.rb b/test/models/category_test.rb
similarity index 52%
rename from test/models/transaction/category_test.rb
rename to test/models/category_test.rb
index 4850063b..a0f920f6 100644
--- a/test/models/transaction/category_test.rb
+++ b/test/models/category_test.rb
@@ -1,50 +1,50 @@
require "test_helper"
-class Transaction::CategoryTest < ActiveSupport::TestCase
+class CategoryTest < ActiveSupport::TestCase
def setup
@family = families(:dylan_family)
end
test "create_default_categories should generate categories if none exist" do
@family.accounts.destroy_all
- @family.transaction_categories.destroy_all
- assert_difference "Transaction::Category.count", Transaction::Category::DEFAULT_CATEGORIES.size do
- Transaction::Category.create_default_categories(@family)
+ @family.categories.destroy_all
+ assert_difference "Category.count", Category::DEFAULT_CATEGORIES.size do
+ Category.create_default_categories(@family)
end
end
test "create_default_categories should raise when there are existing categories" do
assert_raises(ArgumentError) do
- Transaction::Category.create_default_categories(@family)
+ Category.create_default_categories(@family)
end
end
test "updating name should clear the internal_category field" do
- category = Transaction::Category.take
+ category = Category.take
assert_changes "category.reload.internal_category", to: nil do
category.update_attribute(:name, "new name")
end
end
test "updating other field than name should not clear the internal_category field" do
- category = Transaction::Category.take
+ category = Category.take
assert_no_changes "category.reload.internal_category" do
category.update_attribute(:color, "#000")
end
end
test "replacing and destroying" do
- transctions = transaction_categories(:food_and_drink).transactions.to_a
+ transctions = categories(:food_and_drink).transactions.to_a
- transaction_categories(:food_and_drink).replace_and_destroy!(transaction_categories(:income))
+ categories(:food_and_drink).replace_and_destroy!(categories(:income))
- assert_equal transaction_categories(:income), transactions.map { |t| t.reload.category }.uniq.first
+ assert_equal categories(:income), transactions.map { |t| t.reload.category }.uniq.first
end
test "replacing with nil should nullify the category" do
- transactions = transaction_categories(:food_and_drink).transactions.to_a
+ transactions = categories(:food_and_drink).transactions.to_a
- transaction_categories(:food_and_drink).replace_and_destroy!(nil)
+ categories(:food_and_drink).replace_and_destroy!(nil)
assert_nil transactions.map { |t| t.reload.category }.uniq.first
end
diff --git a/test/models/family_test.rb b/test/models/family_test.rb
index fc9ff284..55218957 100644
--- a/test/models/family_test.rb
+++ b/test/models/family_test.rb
@@ -33,7 +33,7 @@ class FamilyTest < ActiveSupport::TestCase
end
test "should destroy dependent transaction categories" do
- assert_difference("Transaction::Category.count", -@family.transaction_categories.count) do
+ assert_difference("Category.count", -@family.categories.count) do
@family.destroy
end
end
diff --git a/test/models/import_test.rb b/test/models/import_test.rb
index 4945b48d..02c4d127 100644
--- a/test/models/import_test.rb
+++ b/test/models/import_test.rb
@@ -46,7 +46,7 @@ class ImportTest < ActiveSupport::TestCase
# "Shopping" is a new category, but should only be created 1x during import
assert_difference \
-> { Transaction.count } => 4,
- -> { Transaction::Category.count } => 1,
+ -> { Category.count } => 1,
-> { Tagging.count } => 4,
-> { Tag.count } => 2 do
@loaded_import.publish
@@ -59,7 +59,7 @@ class ImportTest < ActiveSupport::TestCase
test "publishes a valid import with missing data" do
@empty_import.update! raw_csv_str: valid_csv_with_missing_data
- assert_difference -> { Transaction::Category.count } => 1, -> { Transaction.count } => 2 do
+ assert_difference -> { Category.count } => 1, -> { Transaction.count } => 2 do
@empty_import.publish
end
diff --git a/test/system/settings_test.rb b/test/system/settings_test.rb
index 576eb889..e9be8a86 100644
--- a/test/system/settings_test.rb
+++ b/test/system/settings_test.rb
@@ -12,7 +12,7 @@ class SettingsTest < ApplicationSystemTestCase
[ "Billing", "Billing", settings_billing_path ],
[ "Accounts", "Accounts", accounts_path ],
[ "Tags", "Tags", tags_path ],
- [ "Categories", "Categories", transaction_categories_path ],
+ [ "Categories", "Categories", categories_path ],
[ "Merchants", "Merchants", transaction_merchants_path ],
[ "Rules", "Rules", transaction_rules_path ],
[ "Imports", "Imports", imports_path ],
diff --git a/test/system/transactions_test.rb b/test/system/transactions_test.rb
index 3dd2d585..f75edd76 100644
--- a/test/system/transactions_test.rb
+++ b/test/system/transactions_test.rb
@@ -5,7 +5,7 @@ class TransactionsTest < ApplicationSystemTestCase
sign_in @user = users(:family_admin)
@latest_transactions = @user.family.transactions.ordered.limit(20).to_a
- @test_category = @user.family.transaction_categories.create! name: "System Test Category"
+ @test_category = @user.family.categories.create! name: "System Test Category"
@test_merchant = @user.family.transaction_merchants.create! name: "System Test Merchant"
@target_txn = @user.family.accounts.first.transactions.create! \
name: "Oldest transaction",