$portalValues */ public function survivorFor(array $portalValues, string $slug): ?Company { $mergedIds = Company::onlyTrashed() ->withoutGlobalScope(PortalScope::class) ->whereIn('portal', $portalValues) ->where('slug', $slug) ->pluck('id'); if ($mergedIds->isEmpty()) { return null; } $survivorId = $this->survivorIdOf($mergedIds->all()); $visited = []; // Merge-Ketten (A → B, später B → C): Ist der Überlebende selbst // aufgegangen, weiter bis zur lebenden Firma. Zyklenschutz über die // besuchten IDs und eine feste Obergrenze (Umsetzungsaudit // 29.09.2026, R-11). while ($survivorId !== null && ! isset($visited[$survivorId]) && count($visited) < self::MaxChainLength) { $visited[$survivorId] = true; $survivor = Company::withTrashed()->withoutGlobalScope(PortalScope::class)->find($survivorId); if ($survivor === null) { return null; } if (! $survivor->trashed()) { return $survivor; } $survivorId = $this->survivorIdOf([$survivorId]); } return null; } /** * @param list $companyIds */ private function survivorIdOf(array $companyIds): ?int { $survivorId = CompanyMergeMove::query() ->where('subject', CompanyMergeSubject::Company->value) ->whereIn('subject_id', $companyIds) ->whereHas('merge', fn (Builder $query) => $query->whereNull('reverted_at')) ->with('merge:id,survivor_company_id') ->latest('id') ->first() ?->merge ?->survivor_company_id; return $survivorId !== null ? (int) $survivorId : null; } }