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) <noreply@anthropic.com>
180 lines
7.2 KiB
PHP
180 lines
7.2 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Enums\CompanyAddressClass;
|
|
use App\Models\Company;
|
|
use App\Models\CompanyEmailGroup;
|
|
use App\Services\Company\CompanyMergePlan;
|
|
use App\Services\Company\CompanyMergeService;
|
|
use App\Services\Company\CompanyMergeSkip;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Str;
|
|
|
|
/**
|
|
* Zusammenführung im Bestand (Roadmap 1a.3).
|
|
*
|
|
* Läuft über `company_email_groups` und fasst jede zusammenführbare Adresse
|
|
* an — Dublette und Namensvariante, eine Handentscheidung übersteuert. Jede
|
|
* Adresse ist eine eigene Transaktion mit protokollierten Bewegungen;
|
|
* `companies:unmerge` kehrt sie um.
|
|
*
|
|
* `--limit` ist für den ersten scharfen Lauf gedacht: erst 50 Adressen, dann
|
|
* ansehen, dann der Rest. Alle Vorgänge eines Laufs teilen eine Batch-ID.
|
|
*/
|
|
class MergeCompanies extends Command
|
|
{
|
|
protected $signature = 'companies:merge
|
|
{--dry-run : Nur zeigen, was geschähe}
|
|
{--limit= : Höchstens so viele Adressen anfassen}
|
|
{--email= : Nur diese eine Adresse}
|
|
{--json : Ausgabe als JSON}';
|
|
|
|
protected $description = 'Führt Firmen mit derselben Firmen-E-Mail zu einem Newsroom zusammen (nur zusammenführbare Adressen).';
|
|
|
|
public function handle(CompanyMergeService $service): int
|
|
{
|
|
$dryRun = (bool) $this->option('dry-run');
|
|
$batch = (string) Str::uuid();
|
|
$limit = $this->option('limit') !== null ? max(1, (int) $this->option('limit')) : null;
|
|
|
|
$groups = $this->groups();
|
|
|
|
$stats = ['adressen' => 0, 'zusammengefuehrt' => 0, 'firmen_weg' => 0, 'meldungen' => 0, 'both' => 0, 'uebersprungen' => []];
|
|
$plans = [];
|
|
$skips = [];
|
|
|
|
foreach ($groups->lazy() as $group) {
|
|
if ($limit !== null && $stats['adressen'] >= $limit) {
|
|
break;
|
|
}
|
|
|
|
$result = $service->plan($group);
|
|
|
|
if ($result instanceof CompanyMergeSkip) {
|
|
// Eine Adresse, unter der nichts (mehr) zusammenzuführen ist,
|
|
// zählt nicht gegen das Limit — sonst füllt sie beim zweiten
|
|
// Lauf die ersten 50 Plätze.
|
|
if ($result->reason !== CompanyMergeSkip::NothingToMerge) {
|
|
$skips[] = $result;
|
|
}
|
|
$stats['uebersprungen'][$result->reason] = ($stats['uebersprungen'][$result->reason] ?? 0) + 1;
|
|
|
|
continue;
|
|
}
|
|
|
|
$stats['adressen']++;
|
|
$stats['firmen_weg'] += count($result->losers);
|
|
$stats['meldungen'] += $result->pressReleases;
|
|
$stats['both'] += $result->targetPortal->value === 'both' && $result->survivor->portal->value !== 'both' ? 1 : 0;
|
|
|
|
if (count($plans) < 30) {
|
|
$plans[] = $result;
|
|
}
|
|
|
|
if ($dryRun) {
|
|
continue;
|
|
}
|
|
|
|
$service->merge($result, $batch, flushCaches: false);
|
|
$stats['zusammengefuehrt']++;
|
|
}
|
|
|
|
if (! $dryRun && $stats['zusammengefuehrt'] > 0) {
|
|
$service->flushCaches();
|
|
}
|
|
|
|
if ($this->option('json')) {
|
|
$this->line((string) json_encode([
|
|
'dry_run' => $dryRun,
|
|
'batch' => $dryRun ? null : $batch,
|
|
'statistik' => $stats,
|
|
'plaene' => array_map(fn (CompanyMergePlan $plan): array => $this->describe($plan), $plans),
|
|
'uebersprungen' => array_map(fn (CompanyMergeSkip $skip): array => ['email' => $skip->email, 'grund' => $skip->reason, 'details' => $skip->details], $skips),
|
|
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$this->info(sprintf(
|
|
'%s%d Adressen, %d Firmen gehen im Überlebenden auf, %d Meldungen wandern, %d Firmen werden portalübergreifend (both).',
|
|
$dryRun ? '[Dry-Run] ' : '',
|
|
$stats['adressen'],
|
|
$stats['firmen_weg'],
|
|
$stats['meldungen'],
|
|
$stats['both'],
|
|
));
|
|
|
|
if (! $dryRun) {
|
|
$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) {
|
|
$this->line(sprintf(' übersprungen (%s): %d', $reason, $count));
|
|
}
|
|
|
|
if ($plans !== []) {
|
|
$this->newLine();
|
|
$this->table(
|
|
['Adresse', 'bleibt', 'Portal', 'geht auf', 'Meldungen'],
|
|
array_map(fn (CompanyMergePlan $plan): array => [
|
|
$plan->email,
|
|
sprintf('#%d %s', $plan->survivor->getKey(), Str::limit($plan->survivor->name, 40)),
|
|
$plan->targetPortal->value,
|
|
implode(', ', array_map(fn (Company $loser): string => sprintf('#%d %s', $loser->getKey(), Str::limit($loser->name, 30)), $plan->losers)),
|
|
$plan->pressReleases,
|
|
], $plans),
|
|
);
|
|
}
|
|
|
|
if ($skips !== []) {
|
|
$this->newLine();
|
|
$this->table(
|
|
['übersprungen', 'Grund', 'Details'],
|
|
array_map(fn (CompanyMergeSkip $skip): array => [$skip->email, $skip->reason, implode(', ', $skip->details)], array_slice($skips, 0, 50)),
|
|
);
|
|
}
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* Zusammenführbare Adressen: berechnete Klasse, sofern keine
|
|
* Handentscheidung dagegen steht — oder Handentscheidung dafür.
|
|
*
|
|
* @return Builder<CompanyEmailGroup>
|
|
*/
|
|
private function groups(): Builder
|
|
{
|
|
$mergeable = array_map(
|
|
fn (CompanyAddressClass $class): string => $class->value,
|
|
array_filter(CompanyAddressClass::cases(), fn (CompanyAddressClass $class): bool => $class->isMergeable()),
|
|
);
|
|
|
|
return CompanyEmailGroup::query()
|
|
->when($this->option('email'), fn (Builder $query, string $email) => $query->where('email', Company::emailKey($email)))
|
|
->where(function (Builder $query) use ($mergeable): void {
|
|
$query->whereIn('manual_classification', $mergeable)
|
|
->orWhere(fn (Builder $computed) => $computed->whereNull('manual_classification')->whereIn('classification', $mergeable));
|
|
})
|
|
->orderBy('id');
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function describe(CompanyMergePlan $plan): array
|
|
{
|
|
return [
|
|
'email' => $plan->email,
|
|
'bleibt' => ['id' => $plan->survivor->getKey(), 'name' => $plan->survivor->name, 'portal' => $plan->survivor->portal->value, 'slug' => $plan->survivor->slug],
|
|
'portal_danach' => $plan->targetPortal->value,
|
|
'geht_auf' => array_map(fn (Company $loser): array => ['id' => $loser->getKey(), 'name' => $loser->name, 'portal' => $loser->portal->value, 'slug' => $loser->slug], $plan->losers),
|
|
'ausgenommen_410' => array_map(fn (Company $company): int => $company->getKey(), $plan->excluded),
|
|
'meldungen' => $plan->pressReleases,
|
|
];
|
|
}
|
|
}
|