mirror of
https://github.com/we-promise/sure.git
synced 2026-05-10 14:15:01 +00:00
* feat(splits): add excluded attribute support for split children and improve rendering of split transactions * address coderabbitai suggestions to improve code quality * Fix split excluded coercion, DRY helpers, and clean up view partials Fix boolean coercion bug where string "false" from form params was truthy in Ruby, causing all split children to be marked excluded. Use ActiveModel::Type::Boolean for explicit casting in Entry#split!. Additional changes addressing code review feedback: - Extract duplicated in_split_group logic from TransactionsController and TransactionCategoriesController into TransactionsHelper - Remove redundant local_assigns.fetch calls in partials that already declare defaults via the Rails 7.1 locals: magic comment - Simplify ternary in _transaction.html.erb to pass grouped directly - Guard hidden_field_tag :grouped to only emit when value is "true" - Add model tests for excluded on split children (boolean and string) - Add controller test for excluded param through full HTTP stack - Add test confirming excluded children are dropped from balance queries * fix(splits): simplify excluded attribute boolean check * refactor(splits): extract truthy values constant for excluded check Extract the array of truthy values used for excluded attribute check into a private constant to improve code maintainability and avoid duplication of the magic array. * refactor: simplify split grouping link generation and add test coverage for excluded split parameters
225 lines
6.4 KiB
Ruby
225 lines
6.4 KiB
Ruby
require "test_helper"
|
|
|
|
class EntrySplitTest < ActiveSupport::TestCase
|
|
include EntriesTestHelper
|
|
|
|
setup do
|
|
@entry = create_transaction(
|
|
amount: 100,
|
|
name: "Grocery Store",
|
|
account: accounts(:depository),
|
|
category: categories(:food_and_drink)
|
|
)
|
|
end
|
|
|
|
test "split! creates child entries with correct amounts and marks parent excluded" do
|
|
splits = [
|
|
{ name: "Groceries", amount: 70, category_id: categories(:food_and_drink).id },
|
|
{ name: "Household", amount: 30, category_id: nil }
|
|
]
|
|
|
|
children = @entry.split!(splits)
|
|
|
|
assert_equal 2, children.size
|
|
assert_equal 70, children.first.amount
|
|
assert_equal 30, children.last.amount
|
|
assert @entry.reload.excluded?
|
|
assert @entry.split_parent?
|
|
end
|
|
|
|
test "split! rejects when amounts don't sum to parent" do
|
|
splits = [
|
|
{ name: "Part 1", amount: 60, category_id: nil },
|
|
{ name: "Part 2", amount: 30, category_id: nil }
|
|
]
|
|
|
|
assert_raises(ActiveRecord::RecordInvalid) do
|
|
@entry.split!(splits)
|
|
end
|
|
end
|
|
|
|
test "split! allows mixed positive and negative amounts that sum to parent" do
|
|
splits = [
|
|
{ name: "Main expense", amount: 130, category_id: nil },
|
|
{ name: "Refund", amount: -30, category_id: nil }
|
|
]
|
|
|
|
children = @entry.split!(splits)
|
|
|
|
assert_equal 2, children.size
|
|
assert_equal 130, children.first.amount
|
|
assert_equal(-30, children.last.amount)
|
|
end
|
|
|
|
test "cannot split transfers" do
|
|
transfer = create_transfer(
|
|
from_account: accounts(:depository),
|
|
to_account: accounts(:credit_card),
|
|
amount: 100
|
|
)
|
|
outflow_transaction = transfer.outflow_transaction
|
|
|
|
refute outflow_transaction.splittable?
|
|
end
|
|
|
|
test "cannot split already-split parent" do
|
|
@entry.split!([
|
|
{ name: "Part 1", amount: 50, category_id: nil },
|
|
{ name: "Part 2", amount: 50, category_id: nil }
|
|
])
|
|
|
|
refute @entry.entryable.splittable?
|
|
end
|
|
|
|
test "cannot split child entry" do
|
|
children = @entry.split!([
|
|
{ name: "Part 1", amount: 50, category_id: nil },
|
|
{ name: "Part 2", amount: 50, category_id: nil }
|
|
])
|
|
|
|
refute children.first.entryable.splittable?
|
|
end
|
|
|
|
test "unsplit! removes children and restores parent" do
|
|
@entry.split!([
|
|
{ name: "Part 1", amount: 50, category_id: nil },
|
|
{ name: "Part 2", amount: 50, category_id: nil }
|
|
])
|
|
|
|
assert @entry.reload.excluded?
|
|
assert_equal 2, @entry.child_entries.count
|
|
|
|
@entry.unsplit!
|
|
|
|
refute @entry.reload.excluded?
|
|
assert_equal 0, @entry.child_entries.count
|
|
end
|
|
|
|
test "parent deletion cascades to children" do
|
|
@entry.split!([
|
|
{ name: "Part 1", amount: 50, category_id: nil },
|
|
{ name: "Part 2", amount: 50, category_id: nil }
|
|
])
|
|
|
|
child_ids = @entry.child_entries.pluck(:id)
|
|
|
|
@entry.destroy!
|
|
|
|
assert_empty Entry.where(id: child_ids)
|
|
end
|
|
|
|
test "individual child deletion is blocked" do
|
|
children = @entry.split!([
|
|
{ name: "Part 1", amount: 50, category_id: nil },
|
|
{ name: "Part 2", amount: 50, category_id: nil }
|
|
])
|
|
|
|
refute children.first.destroy
|
|
assert children.first.persisted?
|
|
end
|
|
|
|
test "split parent cannot be un-excluded" do
|
|
@entry.split!([
|
|
{ name: "Part 1", amount: 50, category_id: nil },
|
|
{ name: "Part 2", amount: 50, category_id: nil }
|
|
])
|
|
|
|
@entry.reload
|
|
@entry.excluded = false
|
|
refute @entry.valid?
|
|
assert_includes @entry.errors[:excluded], "cannot be toggled off for a split transaction"
|
|
end
|
|
|
|
test "excluding_split_parents scope excludes parents with children" do
|
|
@entry.split!([
|
|
{ name: "Part 1", amount: 50, category_id: nil },
|
|
{ name: "Part 2", amount: 50, category_id: nil }
|
|
])
|
|
|
|
scope = Entry.excluding_split_parents.where(account: accounts(:depository))
|
|
refute_includes scope.pluck(:id), @entry.id
|
|
assert_includes scope.pluck(:id), @entry.child_entries.first.id
|
|
end
|
|
|
|
test "children inherit parent's account, date, and currency" do
|
|
children = @entry.split!([
|
|
{ name: "Part 1", amount: 50, category_id: nil },
|
|
{ name: "Part 2", amount: 50, category_id: nil }
|
|
])
|
|
|
|
children.each do |child|
|
|
assert_equal @entry.account_id, child.account_id
|
|
assert_equal @entry.date, child.date
|
|
assert_equal @entry.currency, child.currency
|
|
end
|
|
end
|
|
|
|
test "split_parent? returns true when entry has children" do
|
|
refute @entry.split_parent?
|
|
|
|
@entry.split!([
|
|
{ name: "Part 1", amount: 50, category_id: nil },
|
|
{ name: "Part 2", amount: 50, category_id: nil }
|
|
])
|
|
|
|
assert @entry.split_parent?
|
|
end
|
|
|
|
test "split_child? returns true for child entries" do
|
|
children = @entry.split!([
|
|
{ name: "Part 1", amount: 50, category_id: nil },
|
|
{ name: "Part 2", amount: 50, category_id: nil }
|
|
])
|
|
|
|
assert children.first.split_child?
|
|
refute @entry.split_child?
|
|
end
|
|
|
|
test "split! creates child entries with excluded: true when specified" do
|
|
splits = [
|
|
{ name: "Part 1", amount: 50, category_id: nil, excluded: true },
|
|
{ name: "Part 2", amount: 50, category_id: nil, excluded: false }
|
|
]
|
|
|
|
children = @entry.split!(splits)
|
|
|
|
assert_equal 2, children.size
|
|
assert children.first.excluded?
|
|
refute children.last.excluded?
|
|
end
|
|
|
|
test "split! properly casts excluded from string values" do
|
|
splits = [
|
|
{ name: "Part 1", amount: 50, category_id: nil, excluded: "true" },
|
|
{ name: "Part 2", amount: 50, category_id: nil, excluded: "false" }
|
|
]
|
|
|
|
children = @entry.split!(splits)
|
|
|
|
assert children.first.excluded?
|
|
refute children.last.excluded?
|
|
end
|
|
|
|
test "excluded split children are excluded from balance calculations" do
|
|
@entry.split!([
|
|
{ name: "Part 1", amount: 50, category_id: nil, excluded: true },
|
|
{ name: "Part 2", amount: 50, category_id: nil, excluded: false }
|
|
])
|
|
|
|
# Parent is always excluded for splits
|
|
assert @entry.reload.excluded?
|
|
|
|
# Excluded child should be filtered out by where(excluded: false)
|
|
excluded_child = @entry.child_entries.find { |c| c.name == "Part 1" }
|
|
non_excluded_child = @entry.child_entries.find { |c| c.name == "Part 2" }
|
|
|
|
assert excluded_child.excluded?
|
|
refute non_excluded_child.excluded?
|
|
|
|
# where(excluded: false) should only include the non-excluded child
|
|
visible_entries = Entry.where(id: @entry.child_entries.map(&:id)).where(excluded: false)
|
|
assert_includes visible_entries.pluck(:id), non_excluded_child.id
|
|
refute_includes visible_entries.pluck(:id), excluded_child.id
|
|
end
|
|
end
|