# DetectCategoryCircularReference

## Overview

DetectCategoryCircularReference validates that reparenting a product category would not create a circular reference in the tree hierarchy. It walks up the ancestor chain from the proposed new parent, checking whether the category being moved appears as an ancestor. If the moved category is found in the ancestor chain, the move would create a cycle.

## Business Rules

- A category cannot be moved to become a descendant of itself
- Self-referencing is explicitly rejected: newParentId === categoryId
- Ancestor chain is traversed from the new parent upward to the root
- If the moved category's ID is found in the ancestor chain, the move is circular
- Traversal stops at root (parentId = null) or broken chain (parent not found)
- Only relevant when newParentId is non-null (promoting to root cannot create a cycle)

## Process Flow

```mermaid
flowchart TD
    A[Receive categoryId + newParentId] --> B[SELECT category where id = categoryId]
    B --> C{Category found?}
    C -->|No| D[Return error: CATEGORY_NOT_FOUND]
    C -->|Yes| E{newParentId === categoryId?}
    E -->|Yes| F[Return isCircular: true]
    E -->|No| G[Set ancestorId = newParentId]
    G --> H[SELECT ancestor where id = ancestorId]
    H --> I{Ancestor found?}
    I -->|No| J[Return isCircular: false]
    I -->|Yes| K[Set ancestorId = ancestor.parentId]
    K --> L{ancestorId === categoryId?}
    L -->|Yes| F
    L -->|No| M{ancestorId is null?}
    M -->|Yes| J
    M -->|No| H
```

## External Dependencies

- None

## Error Scenarios

- **CATEGORY_NOT_FOUND**: Specified category ID does not exist

## Test Cases

- returns CategoryNotFoundError when category does not exist
- detects self-reference as circular
- detects circular reference when moving node under its descendant
- returns not circular for valid move
- returns not circular when ancestor chain is broken
