{"record":{"id":"453c0964418f1be3","repo":"we-promise/sure","slug":"a-parent-category-cannot-be-merged-into-its-own-su","errorCode":null,"errorMessage":"A parent category cannot be merged into its own subcategory","messagePattern":"A parent category cannot be merged into its own subcategory","errorType":"exception","errorClass":"Category::Merger::UnauthorizedCategoryError","httpStatus":null,"severity":"error","filePath":"app/models/category/merger.rb","lineNumber":49,"sourceCode":"        family.transactions.where(category_id: source.id).update_all(category_id: target_category.id)\n        merge_budget_categories(source)\n        family.categories.where(parent_id: source.id).where.not(id: target_category.id).update_all(parent_id: target_category.id)\n        family.categories.find(source.id).destroy!\n        @merged_count += 1\n      end\n    end\n\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)","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/category/merger.rb#L31-L67","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"source_ids = source_categories.map(&:id).to_set\n# walk target's ancestors; reject any target inside a source subtree\nancestor = target_category\nwhile (pid = ancestor.parent_id)\n  break if source_ids.include?(pid)\n  ancestor = family.categories.find_by(id: pid) or break\nend\n# if the loop matched, the merger will raise","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["categories","merge","hierarchy","cycle-detection"],"backgroundTag":"cyclic-hierarchy","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}