filled('parent_id')) { return null; } $parentId = $request->string('parent_id')->toString(); $parent = Category::query()->forSpace($space)->whereKey($parentId)->first(); if ($parent === null) { throw ValidationException::withMessages([ 'parent_id' => "No category with id {$parentId} in space {$space->id}. Call list_categories to see valid ids.", ]); } $tree = new CategoryTree; if ($moving !== null && $tree->wouldCreateCycle($moving, $parent->id)) { throw ValidationException::withMessages([ 'parent_id' => 'A category cannot be nested under itself or one of its children.', ]); } $subtreeDepth = $moving !== null ? $tree->subtreeDepth($moving) : 1; if ($tree->depth($parent) + $subtreeDepth > Category::MAX_DEPTH) { throw ValidationException::withMessages([ 'parent_id' => 'Categories can only be nested up to '.Category::MAX_DEPTH.' levels deep.', ]); } return $parent; } /** * Derive the cashflow direction the web form would compute: a child inherits * its parent's direction; a root follows its type (savings/investment always * count as an outflow, transfers keep the requested direction, everything * else is hidden). */ protected function cashflowDirectionFor(CategoryType $type, ?Category $parent, ?string $requested): CategoryCashflowDirection { if ($parent !== null) { return $parent->cashflow_direction; } return match ($type) { CategoryType::Savings, CategoryType::Investment => CategoryCashflowDirection::Outflow, CategoryType::Transfer => CategoryCashflowDirection::tryFrom((string) $requested) ?? CategoryCashflowDirection::Hidden, default => CategoryCashflowDirection::Hidden, }; } /** * Whether a sibling category (same parent) with the given name already * exists in the space, optionally ignoring one category by id (for edits). */ protected function categoryNameTaken(Space $space, string $name, ?string $parentId, ?string $ignoreId = null): bool { return Category::query() ->forSpace($space) ->where('name', $name) ->where('parent_id', $parentId) ->when($ignoreId !== null, fn ($query) => $query->whereKeyNot($ignoreId)) ->exists(); } }