we-promise/sure · error · Category::Merger::UnauthorizedCategoryError
A parent category cannot be merged into its own subcategory
Error message
A parent category cannot be merged into its own subcategory
What it means
Category::Merger#validate_hierarchy! blocks merges where the target category is a descendant of any source category. ancestor_ids_for(target) walks the parent_id chain upward; if any ancestor id matches a source id, merging would fold a parent into its own subcategory and create a cycle in the category tree.
Source
Thrown at app/models/category/merger.rb:49
family.transactions.where(category_id: source.id).update_all(category_id: target_category.id)
merge_budget_categories(source)
family.categories.where(parent_id: source.id).where.not(id: target_category.id).update_all(parent_id: target_category.id)
family.categories.find(source.id).destroy!
@merged_count += 1
end
end
def validate_category_belongs_to_family!(category, label)
return if category&.family_id == family.id
raise UnauthorizedCategoryError, "#{label} does not belong to this family"
end
def validate_hierarchy!
target_ancestor_ids = ancestor_ids_for(target_category)
return unless source_categories.any? { |source| target_ancestor_ids.include?(source.id) }
raise UnauthorizedCategoryError, "A parent category cannot be merged into its own subcategory"
end
def validate_reparenting!
return if target_category.parent_id.blank?
return unless source_categories.any? { |source| family.categories.exists?(parent_id: source.id) }
raise UnauthorizedCategoryError, "Cannot merge a category with subcategories into a subcategory"
end
def ancestor_ids_for(category)
ids = []
seen_ids = Set.new
current = category
while current&.parent_id.present? && seen_ids.exclude?(current.parent_id)
ids << current.parent_id
seen_ids << current.parent_id
current = family.categories.find_by(id: current.parent_id)View on GitHub (pinned to e69894adb9)
Solutions
- Pick a target outside every source's subtree (a sibling or an unrelated top-level category)
- Invert the merge direction: merge the subcategory into the parent instead of the parent into the subcategory
- Exclude descendants of any selected source from target candidates in the picker or a pre-merge validation layer
Defensive patterns
Strategy: validation
Validate before calling
source_ids = source_categories.map(&:id).to_set # walk target's ancestors; reject any target inside a source subtree ancestor = target_category while (pid = ancestor.parent_id) break if source_ids.include?(pid) ancestor = family.categories.find_by(id: pid) or break end # if the loop matched, the merger will raise
Prevention
- Filter target candidates to exclude descendants of any selected source
- Prefer merging children into parents (direction that cannot cycle)
- Cover both invalid directions (parent-into-child, child-with-children-into-child) in merge tests
When it happens
Trigger: Selecting a subcategory as the merge target while one of its ancestors is among source_categories — for example target 'Food > Groceries' with source 'Food'. Also hit when the target picker lists all family categories without excluding descendants of the selected sources.
Common situations: Category cleanup UIs that allow any category as target; bulk merges built from name matching that pair parents with their children; imports that auto-merge similarly named categories.
Related errors
- Cannot merge a category with subcategories into a subcategor
- #{label} does not belong to this family
- No categories available for auto-categorization
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/453c0964418f1be3.
Report an issue: GitHub.