mirror of
https://github.com/we-promise/sure.git
synced 2026-09-07 15:44:21 +00:00
* Add parent/child category hierarchy to all category selects; add search to account select
Category selection consistency:
- DS::Select (shared component powering the main transaction form,
transaction edit, bulk-update, and transfer category pickers) now
indents subcategories with a corner-down-right icon, matching the
existing transaction-row category dropdown.
- Feed DS::Select-based category pickers with Category.alphabetically_by_hierarchy
(parent name, then parent-before-children, then own name) so children
render directly under their parent.
- Added Category::Group.select_options, a shared helper producing
parent-then-child ordered options (with an indent marker) for plain
HTML <select> elements. Used by:
- Rule builder category condition/action selects
- Bulk 'categorize transactions' select
- CSV/QIF import category mapping select
- Grouped the splits category combobox and the transaction search
category filter checklist the same way, both with the
corner-down-right indent icon used elsewhere.
Account selection:
- Added searchable: true to the account select in the new/edit
transaction form, matching the category and merchant selects next
to it.
* Fix SyntaxError: 'for' is a Ruby reserved keyword
Category::Group.select_options called for(categories) as a bare method
call, but Ruby parses a bare 'for' as the start of a for..in loop
statement, not a method invocation. Qualify it as self.for(categories)
to call the class method explicitly.
Verified with 'ruby -c' on all touched .rb files and ERB.new(...).src
on all touched .erb files.
* Add test coverage for category hierarchy and account search
Model-level:
- Category::GroupTest (new): for() grouping and the new select_options
helper (order + indent labels).
- CategoryTest: alphabetically_by_hierarchy scope ordering.
- Rule::ConditionFilter::TransactionCategoryTest (new)
- Rule::ActionExecutor::SetTransactionCategoryTest (new)
- Import::CategoryMappingTest (new): grouping + 'Add as new category'
still prepends correctly.
Controller/integration-level (asserting actual rendered HTML order):
- SplitsControllerTest: category combobox data-value ordering.
- Transactions::CategorizesControllerTest: bulk-categorize <select>
option ordering.
- TransactionsControllerTest:
- search filter checkbox ordering (q[categories][])
- new-transaction DS::Select category ordering (via trigger id +
ancestor traversal)
- new-transaction account select renders a search box
All new/modified test files verified with 'ruby -c' (syntax) and
cross-checked fixture names, family scoping, route helpers, and field
names against the actual fixtures/routes/views. Ruby/Bundler network
access to rubygems.org is unavailable in this sandbox, so the suite
itself has not been executed — run 'bin/rails test' before merging.
* Align with design-sure conventions: keep domain logic in component, not template
Per .cursor/rules/view_conventions.mdc ('keep domain logic out of the
views'), the parent/child hierarchy check for DS::Select items belongs
in the component class, not inline in the ERB template. DS::Select
already has this exact pattern for other per-item derived properties
(color_for, icon_for, logo_for) — added child? alongside them and
updated the template to call it instead of computing it inline.
Added test/components/DS/select_test.rb (ViewComponent::TestCase,
no rendering needed) covering child? directly: subcategory objects,
root-category objects, non-hierarchical objects (merchants), and the
include_blank placeholder item.
Also did a broader pass against the design-sure .cursor/rules to confirm
the rest of this branch's changes already comply:
- Uses Current.family (never current_family) throughout
- Uses the icon() helper exclusively, never lucide_icon directly
- No new/hardcoded colors; only existing semantic Tailwind tokens
already used elsewhere in these same files
- No changes to sure-design-system.css / application.css
- Extended existing components/partials rather than creating new ones
where one already existed (view_conventions.mdc component-vs-partial
guidance)
- Test additions stay in Minitest + fixtures, avoid system tests,
and test query-method output directly (testing.mdc)
* Address CodeRabbit review: sort category groups, tighten test assertion, move grouping out of view
* Address review: fix arrow leak in rule summaries, filter panel alignment, simplify splits ordering
* fix(pages): set breadcrumbs for changelog and feedback pages (#2889)
* fix(app): set breadcrumbs for changelog and feedback pages
* feat(test): add test to assert breadcrumbs
* fix(test): remove changes
* feat(app): update breadcrumbs to use semantic nav element
* feat(test): add breadcrumb assertions to changelog and feedback pages
* fix(app): replace breadcrumb nav element with div containing data-breadcrumbs attribute
* fix ci failures
* resolved failures
* Regenerate schema.rb from migrations
* Fix test
* Remove schema dump noise
---------
Signed-off-by: Shibu M <23173570+DataEnginr@users.noreply.github.com>
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
299 lines
10 KiB
Ruby
299 lines
10 KiB
Ruby
require "test_helper"
|
|
|
|
class Transactions::CategorizesControllerTest < ActionDispatch::IntegrationTest
|
|
include EntriesTestHelper
|
|
|
|
setup do
|
|
sign_in @user = users(:family_admin)
|
|
@family = @user.family
|
|
@account = accounts(:depository)
|
|
@category = categories(:food_and_drink)
|
|
# Clear entries for isolation
|
|
@family.accounts.each { |a| a.entries.delete_all }
|
|
end
|
|
|
|
# GET /transactions/categorize
|
|
|
|
test "show redirects with notice when nothing to categorize" do
|
|
get transactions_categorize_url
|
|
assert_redirected_to transactions_url
|
|
assert_match "categorized", flash[:notice]
|
|
end
|
|
|
|
test "show renders wizard when uncategorized transactions exist" do
|
|
create_transaction(account: @account, name: "Starbucks")
|
|
get transactions_categorize_url
|
|
assert_response :success
|
|
end
|
|
|
|
test "show groups subcategories immediately after their parent in the category select" do
|
|
create_transaction(account: @account, name: "Starbucks")
|
|
get transactions_categorize_url
|
|
|
|
assert_response :success
|
|
|
|
doc = Nokogiri::HTML::Document.parse(response.body)
|
|
option_values = doc.css("select[name='category_id'] option").map { |node| node["value"] }
|
|
|
|
parent_index = option_values.index(categories(:food_and_drink).id)
|
|
child_index = option_values.index(categories(:subcategory).id)
|
|
|
|
assert_not_nil parent_index
|
|
assert_not_nil child_index
|
|
assert_equal parent_index + 1, child_index
|
|
end
|
|
|
|
test "show renders full dates so multi-year lists are unambiguous" do
|
|
create_transaction(account: @account, name: "Starbucks", date: Date.new(2024, 7, 8))
|
|
|
|
get transactions_categorize_url
|
|
|
|
assert_response :success
|
|
# format_date uses the family's date_format preference, every variant of
|
|
# which includes the year; the previous :short format ("%b %d") did not,
|
|
# making rows ambiguous when the uncategorized list spans years.
|
|
expected = Date.new(2024, 7, 8).strftime(@family.date_format)
|
|
assert_match expected, response.body
|
|
end
|
|
|
|
test "show renders the first group at position 0" do
|
|
2.times { create_transaction(account: @account, name: "Netflix") }
|
|
3.times { create_transaction(account: @account, name: "Starbucks") }
|
|
|
|
get transactions_categorize_url(position: 0)
|
|
|
|
assert_response :success
|
|
assert_select "h2", text: "Starbucks"
|
|
end
|
|
|
|
test "show at position 1 skips first group" do
|
|
3.times { create_transaction(account: @account, name: "Starbucks") }
|
|
2.times { create_transaction(account: @account, name: "Netflix") }
|
|
|
|
get transactions_categorize_url(position: 1)
|
|
|
|
assert_response :success
|
|
assert_select "h2", text: "Netflix"
|
|
end
|
|
|
|
test "show redirects when position exceeds available groups" do
|
|
create_transaction(account: @account, name: "Starbucks")
|
|
|
|
get transactions_categorize_url(position: 99)
|
|
|
|
assert_redirected_to transactions_url
|
|
end
|
|
|
|
test "requires authentication" do
|
|
sign_out
|
|
get transactions_categorize_url
|
|
assert_redirected_to new_session_url
|
|
end
|
|
|
|
# Account sharing authorization
|
|
|
|
test "show only groups entries from accounts accessible to the user" do
|
|
accessible_account = accounts(:depository) # shared with family_member (full_control)
|
|
inaccessible_account = accounts(:investment) # not shared with family_member
|
|
|
|
create_transaction(account: accessible_account, name: "Starbucks")
|
|
create_transaction(account: inaccessible_account, name: "Starbucks")
|
|
|
|
sign_in users(:family_member)
|
|
get transactions_categorize_url(position: 0)
|
|
|
|
assert_response :success
|
|
# Only 1 entry should appear in the group — the inaccessible account's entry is hidden
|
|
assert_select "input[name='entry_ids[]']", count: 1
|
|
end
|
|
|
|
test "create does not categorize entries from inaccessible accounts" do
|
|
inaccessible_account = accounts(:investment) # not shared with family_member
|
|
entry = create_transaction(account: inaccessible_account, name: "Starbucks")
|
|
|
|
sign_in users(:family_member)
|
|
post transactions_categorize_url,
|
|
params: {
|
|
position: 0,
|
|
grouping_key: "Starbucks",
|
|
entry_ids: [ entry.id ],
|
|
all_entry_ids: [ entry.id ],
|
|
category_id: @category.id
|
|
},
|
|
headers: { "Accept" => "text/vnd.turbo-stream.html" }
|
|
|
|
assert_nil entry.transaction.reload.category
|
|
end
|
|
|
|
test "assign_entry does not categorize an entry from an inaccessible account" do
|
|
inaccessible_account = accounts(:investment) # not shared with family_member
|
|
entry = create_transaction(account: inaccessible_account, name: "Starbucks")
|
|
|
|
sign_in users(:family_member)
|
|
patch assign_entry_transactions_categorize_url, params: {
|
|
entry_id: entry.id,
|
|
category_id: @category.id,
|
|
position: 0,
|
|
all_entry_ids: [ entry.id ]
|
|
}
|
|
|
|
assert_response :not_found
|
|
assert_nil entry.transaction.reload.category
|
|
end
|
|
|
|
# GET /transactions/categorize/preview_rule
|
|
|
|
test "preview_rule returns matching entries for a filter" do
|
|
create_transaction(account: @account, name: "Amazon Prime")
|
|
create_transaction(account: @account, name: "Amazon Music")
|
|
create_transaction(account: @account, name: "Starbucks")
|
|
|
|
get preview_rule_transactions_categorize_url(filter: "Amazon"),
|
|
headers: { "Accept" => "text/vnd.turbo-stream.html" }
|
|
|
|
assert_response :success
|
|
assert_includes response.body, "Amazon Prime"
|
|
assert_includes response.body, "Amazon Music"
|
|
assert_not_includes response.body, "Starbucks"
|
|
end
|
|
|
|
test "preview_rule returns empty list for blank filter" do
|
|
create_transaction(account: @account, name: "Amazon")
|
|
|
|
get preview_rule_transactions_categorize_url(filter: ""),
|
|
headers: { "Accept" => "text/vnd.turbo-stream.html" }
|
|
|
|
assert_response :success
|
|
assert_not_includes response.body, "Amazon"
|
|
end
|
|
|
|
test "preview_rule requires authentication" do
|
|
sign_out
|
|
get preview_rule_transactions_categorize_url(filter: "Amazon")
|
|
assert_redirected_to new_session_url
|
|
end
|
|
|
|
private
|
|
|
|
def sign_out
|
|
@user.sessions.each { |s| delete session_path(s) }
|
|
end
|
|
|
|
# POST /transactions/categorize
|
|
|
|
test "create categorizes selected entries and returns redirect stream when all assigned" do
|
|
entry = create_transaction(account: @account, name: "Starbucks")
|
|
|
|
post transactions_categorize_url,
|
|
params: {
|
|
position: 0,
|
|
grouping_key: "Starbucks",
|
|
entry_ids: [ entry.id ],
|
|
all_entry_ids: [ entry.id ],
|
|
category_id: @category.id
|
|
},
|
|
headers: { "Accept" => "text/vnd.turbo-stream.html" }
|
|
|
|
assert_response :success
|
|
assert_equal @category, entry.transaction.reload.category
|
|
assert_includes response.body, "action=\"redirect\""
|
|
end
|
|
|
|
test "create removes assigned rows and replaces remaining when partial assignment" do
|
|
entry1 = create_transaction(account: @account, name: "Starbucks")
|
|
entry2 = create_transaction(account: @account, name: "Starbucks")
|
|
|
|
post transactions_categorize_url,
|
|
params: {
|
|
position: 0,
|
|
grouping_key: "Starbucks",
|
|
entry_ids: [ entry1.id ],
|
|
all_entry_ids: [ entry1.id, entry2.id ],
|
|
category_id: @category.id
|
|
},
|
|
headers: { "Accept" => "text/vnd.turbo-stream.html" }
|
|
|
|
assert_response :success
|
|
assert_equal @category, entry1.transaction.reload.category
|
|
assert_nil entry2.transaction.reload.category
|
|
# Remove stream for categorized entry
|
|
assert_includes response.body, "categorize_entry_#{entry1.id}"
|
|
# Replace stream for remaining entry (re-checked)
|
|
assert_includes response.body, "categorize_entry_#{entry2.id}"
|
|
# No redirect stream — still in the group
|
|
assert_not_includes response.body, "action=\"redirect\""
|
|
end
|
|
|
|
test "create with create_rule param creates rule with name and type conditions" do
|
|
entry = create_transaction(account: @account, name: "Netflix", amount: 15)
|
|
|
|
assert_difference "@family.rules.count", 1 do
|
|
post transactions_categorize_url,
|
|
params: {
|
|
position: 0,
|
|
grouping_key: "Netflix",
|
|
transaction_type: "expense",
|
|
entry_ids: [ entry.id ],
|
|
all_entry_ids: [ entry.id ],
|
|
category_id: @category.id,
|
|
create_rule: "1"
|
|
},
|
|
headers: { "Accept" => "text/vnd.turbo-stream.html" }
|
|
end
|
|
|
|
rule = @family.rules.find_by(name: "Netflix")
|
|
assert_not_nil rule
|
|
assert rule.active
|
|
assert rule.conditions.any? { |c| c.condition_type == "transaction_name" && c.value == "Netflix" }
|
|
assert rule.conditions.any? { |c| c.condition_type == "transaction_type" && c.value == "expense" }
|
|
end
|
|
|
|
test "create falls back to html redirect without turbo stream header" do
|
|
entry = create_transaction(account: @account, name: "Starbucks")
|
|
|
|
post transactions_categorize_url, params: {
|
|
position: 0,
|
|
grouping_key: "Starbucks",
|
|
entry_ids: [ entry.id ],
|
|
all_entry_ids: [ entry.id ],
|
|
category_id: @category.id
|
|
}
|
|
|
|
assert_redirected_to transactions_categorize_url(position: 0)
|
|
assert flash[:notice].present?
|
|
end
|
|
|
|
# PATCH /transactions/categorize/assign_entry
|
|
|
|
test "assign_entry categorizes single entry and returns remove stream" do
|
|
entry = create_transaction(account: @account, name: "Starbucks")
|
|
other = create_transaction(account: @account, name: "Starbucks")
|
|
|
|
patch assign_entry_transactions_categorize_url, params: {
|
|
entry_id: entry.id,
|
|
category_id: @category.id,
|
|
position: 0,
|
|
all_entry_ids: [ entry.id, other.id ]
|
|
}
|
|
|
|
assert_response :success
|
|
assert_equal @category, entry.transaction.reload.category
|
|
assert_includes response.body, "categorize_entry_#{entry.id}"
|
|
assert_not_includes response.body, "action=\"redirect\""
|
|
end
|
|
|
|
test "assign_entry returns redirect stream when last entry in group" do
|
|
entry = create_transaction(account: @account, name: "Starbucks")
|
|
|
|
patch assign_entry_transactions_categorize_url, params: {
|
|
entry_id: entry.id,
|
|
category_id: @category.id,
|
|
position: 0,
|
|
all_entry_ids: [ entry.id ]
|
|
}
|
|
|
|
assert_response :success
|
|
assert_includes response.body, "action=\"redirect\""
|
|
end
|
|
end
|