# DetectDepartmentCircularReference

## Overview

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

## Business Rules

- A department cannot be moved to become a descendant of itself
- Self-referencing is explicitly rejected: newParentId === departmentId
- Ancestor chain is traversed from the new parent upward to the root
- If the moved department's ID is found in the ancestor chain, the move is circular
- Traversal stops at root (parentDepartmentId = 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 departmentId + newParentId] --> B{newParentId === departmentId?}
    B -->|Yes| C[Return isCircular: true]
    B -->|No| D[Set ancestorId = newParentId]
    D --> E[SELECT ancestor where id = ancestorId]
    E --> F{Ancestor found?}
    F -->|No| G[Return isCircular: false]
    F -->|Yes| H[Set ancestorId = ancestor.parentDepartmentId]
    H --> I{ancestorId === departmentId?}
    I -->|Yes| C
    I -->|No| J{ancestorId is null?}
    J -->|Yes| G
    J -->|No| E
```

## External Dependencies

- None

## Error Scenarios

- **CIRCULAR_REFERENCE**: The moved department appears in the ancestor chain of the new parent — returns isCircular: true
- **SELF_REFERENCE**: Department is moved to itself as parent — returns isCircular: true

## Test Cases

- detects self-reference as circular
- detects moving department under its child as circular
- detects moving department under its grandchild as circular
- returns not circular for valid move
- returns not circular when new parent is root
- handles broken ancestor chain gracefully (not circular)
