From 202cf340f95900875b654138cc0e9789814a5629 Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Tue, 15 Sep 2026 09:10:54 +0000 Subject: [PATCH] =?UTF-8?q?companies:=20Zusammenf=C3=BChrungen=20zum=20Nac?= =?UTF-8?q?hpr=C3=BCfen=20auflisten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit companies:merges --batch= zeigt je Vorgang Überlebenden, Newsroom-URLs auf beiden Portalen, aufgegangene Firmen und deren alte URLs (301). Der scharfe Lauf gibt die ersten 30 Vorgänge jetzt ebenfalls als Tabelle aus. Co-Authored-By: Claude Opus 5 (1M context) --- app/Console/Commands/ListCompanyMerges.php | 115 +++++++++++++++++++++ app/Console/Commands/MergeCompanies.php | 12 ++- dev/2026-09-01/umsetzung-protokoll.md | 7 ++ tests/Feature/CompanyMergeTest.php | 22 ++++ 4 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 app/Console/Commands/ListCompanyMerges.php diff --git a/app/Console/Commands/ListCompanyMerges.php b/app/Console/Commands/ListCompanyMerges.php new file mode 100644 index 00000000..13b2597a --- /dev/null +++ b/app/Console/Commands/ListCompanyMerges.php @@ -0,0 +1,115 @@ +when($this->option('batch'), fn (Builder $query, string $batch) => $query->where('batch', $batch)) + ->when($this->option('email'), fn (Builder $query, string $email) => $query->where('email', Company::emailKey($email))) + ->with([ + 'survivor', + 'moves' => fn ($query) => $query->where('subject', CompanyMergeSubject::Company->value), + ]) + ->orderByDesc('id') + ->limit(max(1, (int) $this->option('limit'))) + ->get(); + + if ($merges->isEmpty()) { + $this->warn('Keine Vorgänge gefunden.'); + + return self::SUCCESS; + } + + $losers = Company::withTrashed() + ->withoutGlobalScopes() + ->whereIn('id', $merges->flatMap(fn (CompanyMerge $merge) => $merge->moves->pluck('subject_id'))->all()) + ->get(['id', 'name', 'slug', 'portal']) + ->keyBy('id'); + + $rows = $merges->map(function (CompanyMerge $merge) use ($losers): array { + $survivor = $merge->survivor; + $gone = $merge->moves->map(fn ($move) => $losers->get($move->subject_id))->filter(); + + return [ + 'id' => $merge->getKey(), + 'batch' => $merge->batch, + 'email' => $merge->email, + 'rueckabgewickelt' => $merge->reverted_at?->toDateTimeString(), + 'bleibt' => ['id' => $survivor->getKey(), 'name' => $survivor->name, 'slug' => $survivor->slug, 'portal' => $survivor->portal->value], + 'newsroom' => $this->newsroomUrls($survivor->portal, $survivor->slug), + 'aufgegangen' => $gone->map(fn (Company $loser): array => [ + 'id' => $loser->getKey(), + 'name' => $loser->name, + 'slug' => $loser->slug, + 'portal' => $loser->portal->value, + // Nur bei abweichendem Slug gibt es eine Weiterleitung — + // bei gleichem findet die URL den Überlebenden direkt. + 'alte_url' => $loser->slug !== $survivor->slug ? $this->newsroomUrls($loser->portal, $loser->slug) : [], + ])->values()->all(), + 'meldungen' => $merge->moved_press_releases_count, + ]; + }); + + if ($this->option('json')) { + $this->line((string) json_encode($rows->values()->all(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); + + return self::SUCCESS; + } + + $this->table( + ['#', 'Adresse', 'bleibt', 'Newsroom', 'aufgegangen (alte URL → 301)', 'PM'], + $rows->map(fn (array $row): array => [ + $row['id'].($row['rueckabgewickelt'] ? ' (zurück)' : ''), + $row['email'], + sprintf('#%d %s', $row['bleibt']['id'], Str::limit($row['bleibt']['name'], 32)), + implode("\n", $row['newsroom']), + implode("\n", array_map( + fn (array $loser): string => sprintf('#%d %s%s', $loser['id'], Str::limit($loser['name'], 28), $loser['alte_url'] ? "\n ".implode("\n ", $loser['alte_url']) : ' (gleicher Slug)'), + $row['aufgegangen'], + )), + $row['meldungen'], + ])->all(), + ); + + return self::SUCCESS; + } + + /** + * Newsroom-URL je Portal, auf dem die Firma erreichbar ist. + * + * @return list + */ + private function newsroomUrls(Portal $portal, string $slug): array + { + $portals = $portal === Portal::Both ? ['presseecho', 'businessportal24'] : [$portal->value]; + + return array_map( + fn (string $key): string => rtrim((string) config("domains.domains.{$key}.url"), '/').'/de/newsroom/'.$slug, + $portals, + ); + } +} diff --git a/app/Console/Commands/MergeCompanies.php b/app/Console/Commands/MergeCompanies.php index 6c197545..4ba05c51 100644 --- a/app/Console/Commands/MergeCompanies.php +++ b/app/Console/Commands/MergeCompanies.php @@ -69,11 +69,11 @@ class MergeCompanies extends Command $stats['meldungen'] += $result->pressReleases; $stats['both'] += $result->targetPortal->value === 'both' && $result->survivor->portal->value !== 'both' ? 1 : 0; - if ($dryRun) { - if (count($plans) < 30) { - $plans[] = $result; - } + if (count($plans) < 30) { + $plans[] = $result; + } + if ($dryRun) { continue; } @@ -107,7 +107,9 @@ class MergeCompanies extends Command )); if (! $dryRun) { - $this->line(sprintf(' Batch %s — Rückabwicklung: companies:unmerge --batch=%s', $batch, $batch)); + $this->line(sprintf(' Batch %s', $batch)); + $this->line(sprintf(' Prüfen: companies:merges --batch=%s', $batch)); + $this->line(sprintf(' Rückabwicklung: companies:unmerge --batch=%s', $batch)); } foreach ($stats['uebersprungen'] as $reason => $count) { diff --git a/dev/2026-09-01/umsetzung-protokoll.md b/dev/2026-09-01/umsetzung-protokoll.md index 45e1ee01..2d1efff1 100644 --- a/dev/2026-09-01/umsetzung-protokoll.md +++ b/dev/2026-09-01/umsetzung-protokoll.md @@ -797,6 +797,13 @@ Unique-Index (portal, slug), 0 ms. Und wegfallende Firmen werden per Query-Builder soft-deleted, nicht per `$company->delete()`: Der Observer würde je Firma den Admin-Cache leeren; der Lauf leert einmal am Ende. +**Prüfen:** `companies:merges --batch=` listet je Vorgang den +Überlebenden mit seinen Newsroom-URLs auf beiden Portalen, die aufgegangenen +Firmen und deren alte URLs, die per 301 weiterführen. Nachgerüstet nach dem +ersten Live-Lauf mit `--limit=50` — der scharfe Lauf zeigte nur die Batch-ID, +und die Frage „welche 50 sind das?" war berechtigt. Der scharfe Lauf zeigt +seither auch die ersten 30 Vorgänge als Tabelle. + **Deploy:** `migrate --force`, dann `companies:merge --dry-run`, dann `companies:merge --limit=50` und die 50 im Admin ansehen, dann ohne Limit. Erledigte Adressen zählen beim zweiten Lauf nicht gegen das Limit. Danach diff --git a/tests/Feature/CompanyMergeTest.php b/tests/Feature/CompanyMergeTest.php index d1c14028..00f1ae7d 100644 --- a/tests/Feature/CompanyMergeTest.php +++ b/tests/Feature/CompanyMergeTest.php @@ -12,6 +12,7 @@ use App\Models\User; use App\Services\Company\CompanyMergePlan; use App\Services\Company\CompanyMergeService; use App\Services\Company\CompanyMergeSkip; +use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\DB; use Tests\TestCase; @@ -323,3 +324,24 @@ test('the same slug on both portals needs no redirect', function () { $this->get('https://presseecho.test/de/newsroom/muster')->assertOk(); $this->get('https://businessportal24.test/de/newsroom/muster')->assertOk(); }); + +test('the merge list shows survivor, merged companies and the URLs to check', function () { + /** @var TestCase $this */ + mergeGroup('info@muster.de'); + mergeMember('info@muster.de', Portal::Businessportal24, 2, ['slug' => 'muster-gmbh']); + mergeMember('info@muster.de', Portal::Presseecho, 1, ['slug' => 'muster']); + + $this->artisan('companies:merge')->assertSuccessful(); + $batch = CompanyMerge::query()->value('batch'); + + Artisan::call('companies:merges', ['--batch' => $batch, '--json' => true]); + $listed = json_decode(Artisan::output(), true)[0]; + + expect($listed['newsroom'])->toBe(['https://presseecho.test/de/newsroom/muster-gmbh', 'https://businessportal24.test/de/newsroom/muster-gmbh']) + ->and($listed['aufgegangen'][0]['alte_url'])->toBe(['https://presseecho.test/de/newsroom/muster']) + ->and($listed['meldungen'])->toBe(1); + + $this->artisan('companies:merges', ['--email' => 'INFO@muster.de']) + ->expectsOutputToContain('muster-gmbh') + ->assertSuccessful(); +});