namespace General_Ledger_Service.Repositories { // Shared tree logic for every self-referencing entity (Account, CostCenter — see // 01_Full_Development_Plan.md Section 4.2). LINQ/EF Core cannot express a recursive CTE, so the // calling repository loads the entity's rows once as TreeRow (a plain id/code/name/parent // projection) and these methods walk that in-memory list. Pure functions: no DbContext, no SQL, // so the same code works on any database provider. This is a shared static helper, not a // generic repository base class (CLAUDE.md Rule 2) — each repository keeps its own public // interface, method signatures, and result DTOs; only the tree-walking is shared. internal static class RecursiveTreeQueries { internal class TreeRow { public long Id { get; set; } public string Code { get; set; } = string.Empty; public string Name { get; set; } = string.Empty; public long? ParentId { get; set; } public int Depth { get; set; } } // Depth a new child of parentId would sit at: the number of nodes from parentId up to its // root, plus one. A parentId that matches no row yields 1. public static int GetAncestorDepth(IReadOnlyList rows, long parentId) { var byId = rows.ToDictionary(r => r.Id); var count = 0; var visited = new HashSet(); long? current = parentId; while (current.HasValue && byId.TryGetValue(current.Value, out var node) && visited.Add(node.Id)) { count++; current = node.ParentId; } return count + 1; } // "Root > ... > Entity" path of names, or null if the entity does not exist. public static string? GetAncestorBreadcrumb(IReadOnlyList rows, long entityId) { var byId = rows.ToDictionary(r => r.Id); var names = new List(); var visited = new HashSet(); long? current = entityId; while (current.HasValue && byId.TryGetValue(current.Value, out var node) && visited.Add(node.Id)) { names.Add(node.Name); current = node.ParentId; } if (names.Count == 0) { return null; } names.Reverse(); return string.Join(" > ", names); } // The entity plus every descendant, depth 1 for the entity itself, ordered by depth then code. public static List GetDescendantTree(IReadOnlyList rows, long entityId) { var childrenByParent = rows .Where(r => r.ParentId.HasValue) .ToLookup(r => r.ParentId!.Value); var root = rows.FirstOrDefault(r => r.Id == entityId); if (root is null) { return []; } var result = new List(); var visited = new HashSet(); var queue = new Queue<(TreeRow Node, int Depth)>(); queue.Enqueue((root, 1)); while (queue.Count > 0) { var (node, depth) = queue.Dequeue(); if (!visited.Add(node.Id)) { continue; } result.Add(new TreeRow { Id = node.Id, Code = node.Code, Name = node.Name, ParentId = node.ParentId, Depth = depth }); foreach (var child in childrenByParent[node.Id]) { queue.Enqueue((child, depth + 1)); } } return [.. result .OrderBy(r => r.Depth) .ThenBy(r => r.Code, StringComparer.Ordinal)]; } // Ids of the entity and all of its descendants; empty if the entity does not exist. public static HashSet GetDescendantIds(IReadOnlyList rows, long entityId) => [.. GetDescendantTree(rows, entityId).Select(r => r.Id)]; // True if newParentId is the entity itself or one of its descendants, i.e. making it the // entity's parent would create a loop. public static bool WouldCreateCycle(IReadOnlyList rows, long entityId, long newParentId) => GetDescendantIds(rows, entityId).Contains(newParentId); } }