companies: Zusammenführung im Bestand, rückabwickelbar (Roadmap 1a.3)

companies:merge führt Firmen mit derselben Firmen-E-Mail zu einem Newsroom
zusammen — nur über Adressen, die 1a.2 als Dublette oder Namensvariante
klassifiziert hat. Es bleibt die Firma mit den meisten jemals
veröffentlichten Meldungen; Meldungen, Kontakte, Nutzer- und
Zahlungsverknüpfungen wandern per company_id, jede Bewegung steht in
company_merge_moves. companies:unmerge spielt sie rückwärts ab.

77 % der Gruppen sind dieselbe Firma auf beiden Portalen (Altsystem: eine
Firma je Portal). Der Überlebende wird dann `both`; die Meldungen behalten
ihr Portal. Der alte Newsroom-Slug führt per 301 zum Überlebenden.
Slug-Kollisionen auf dem anderen Portal werden übersprungen, nicht
umbenannt. Company::slugExists() kennt jetzt `both`.

Dev-DB: 14 142 Adressen, 16 194 Firmen, 13 297 Meldungen in 2 min;
Rückabwicklung in 1:15 min exakt auf den Stand davor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-15 08:43:42 +00:00
parent 66d8398e97
commit 2aebb9d27e
17 changed files with 1508 additions and 7 deletions

View file

@ -0,0 +1,56 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Protokoll der Newsroom-Zusammenführung (Roadmap 1a.3).
*
* `company_merges` ist ein Vorgang je Adresse: welche Firma geblieben ist und
* welches Portal sie vorher trug. `company_merge_moves` hält je verschobenem
* Datensatz fest, woher er kam — das ist die gespeicherte Ursprungszuordnung,
* über die `companies:unmerge` den Vorgang exakt rückabwickelt.
*
* Bewusst zwei eigene Tabellen statt einer Spalte `merged_from_company_id` an
* `press_releases`: Die ist über 1 GB groß, ein ADD COLUMN dort nur für einen
* einmaligen Vorgang lohnt nicht, und Kontakte, Nutzer-Verknüpfungen und
* Zahlungsoptionen bräuchten dieselbe Spalte noch einmal. Hier landen alle in
* einer Liste.
*/
return new class extends Migration
{
public function up(): void
{
Schema::create('company_merges', function (Blueprint $table) {
$table->id();
$table->string('batch', 36)->index();
$table->string('email', 190)->index();
$table->foreignId('survivor_company_id')->constrained('companies')->restrictOnDelete();
$table->string('survivor_portal_before', 32);
$table->unsignedInteger('merged_companies_count');
$table->unsignedInteger('moved_press_releases_count');
$table->timestamp('reverted_at')->nullable();
$table->timestamps();
});
Schema::create('company_merge_moves', function (Blueprint $table) {
$table->id();
$table->foreignId('company_merge_id')->constrained('company_merges')->cascadeOnDelete();
$table->string('subject', 32);
$table->unsignedBigInteger('subject_id')->nullable();
$table->foreignId('from_company_id')->constrained('companies')->restrictOnDelete();
$table->string('field', 64)->nullable();
$table->text('previous_value')->nullable();
$table->boolean('dropped')->default(false);
$table->index(['subject', 'subject_id'], 'company_merge_moves_subject_idx');
});
}
public function down(): void
{
Schema::dropIfExists('company_merge_moves');
Schema::dropIfExists('company_merges');
}
};