{"record":{"id":"01bbadc44983edaf","repo":"we-promise/sure","slug":"cannot-merge-a-category-with-subcategories-into-a","errorCode":null,"errorMessage":"Cannot merge a category with subcategories into a subcategory","messagePattern":"Cannot merge a category with subcategories into a subcategory","errorType":"exception","errorClass":"Category::Merger::UnauthorizedCategoryError","httpStatus":null,"severity":"error","filePath":"app/models/category/merger.rb","lineNumber":56,"sourceCode":"\n    def validate_category_belongs_to_family!(category, label)\n      return if category&.family_id == family.id\n\n      raise UnauthorizedCategoryError, \"#{label} does not belong to this family\"\n    end\n\n    def validate_hierarchy!\n      target_ancestor_ids = ancestor_ids_for(target_category)\n      return unless source_categories.any? { |source| target_ancestor_ids.include?(source.id) }\n\n      raise UnauthorizedCategoryError, \"A parent category cannot be merged into its own subcategory\"\n    end\n\n    def validate_reparenting!\n      return if target_category.parent_id.blank?\n      return unless source_categories.any? { |source| family.categories.exists?(parent_id: source.id) }\n\n      raise UnauthorizedCategoryError, \"Cannot merge a category with subcategories into a subcategory\"\n    end\n\n    def ancestor_ids_for(category)\n      ids = []\n      seen_ids = Set.new\n      current = category\n\n      while current&.parent_id.present? && seen_ids.exclude?(current.parent_id)\n        ids << current.parent_id\n        seen_ids << current.parent_id\n        current = family.categories.find_by(id: current.parent_id)\n      end\n\n      ids\n    end\n\n    def merge_budget_categories(source)\n      family.budget_categories.where(category_id: source.id).find_each do |source_budget_category|","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/category/merger.rb#L38-L74","documentation":"Category::Merger#validate_reparenting! blocks merges where the target is itself a subcategory (parent_id present) and at least one source category has children (family.categories.exists?(parent_id: source.id)). Reparenting a parent-with-subcategories under a subcategory would push an entire subtree below a leaf, which the merger does not support.","triggerScenarios":"Choosing a subcategory as the merge target while any selected source category still has its own subcategories: target.parent_id is set AND a category with parent_id == source.id exists in the family.","commonSituations":"Reorganizing category trees by collapsing a broad category into a more specific leaf; merge tools that allow arbitrary targets without checking source subtrees.","solutions":["Merge into a top-level (parentless) target category instead of a subcategory","First move or merge away the source's subcategories, then retry the original merge","Surface the rule in the UI: disable subcategory targets whenever any selected source has children"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"target_is_subcategory = target_category.parent_id.present?\nsources_have_children = family.categories.exists?(parent_id: source_categories.map(&:id))\nunsafe = target_is_subcategory && sources_have_children","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Default merge targets to top-level categories","Flatten or relocate a source's subcategories before merging it into a leaf","Disable subcategory targets in the picker when any source has children"],"tags":["categories","merge","hierarchy","reparenting"],"backgroundTag":"invalid-reparenting","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}