Schreibt nichts. Der Bericht steht vor 1a.2 und 1a.3, weil für die rückwirkende Zusammenführung im Altbestand das Portal die Verantwortung trägt — dort wird nicht angelegt, sondern verschmolzen, und das ist nicht folgenlos umkehrbar. Geschlüsselt wird über die FIRMEN-E-Mail, nicht über die Pressekontaktadresse: Der Pressekontakt ist der Firma untergeordnet und wechselt, die Firmenidentität darf nicht an einer Person hängen. Der Bericht führt fünf Klassen statt der drei Signale aus dem Regelwerk. Zwei Ergänzungen, die aus den Daten kommen: Erstens die Namensvariante. "Hoppe Unternehmensberatung" und "Hoppe Unternehmensberatung GmbH" sind dieselbe Firma; ein Vergleich auf Gleichheit würde sie trennen. Die Namen werden deshalb normalisiert — Kleinschreibung, Umlaute aufgelöst, Rechtsform und Satzzeichen entfernt. Das ist der Fall "gleicher ODER ÄHNLICHER Firmenname" aus dem Regelwerk. Zweitens, und wichtiger: Adressen, die viele Namen tragen und unter denen NIE etwas veröffentlicht wurde. Im Bestand sind das 23 Adressen mit 5965 Firmen, angeführt von yahoo@yahoo.com mit 4376. Dahinter steht keine Firma und keine Agentur. Würde über sie zusammengeführt, verschmölzen tausende unbeteiligte Firmen zu einem Newsroom — der teuerste denkbare Fehler dieses Blocks. Die Namensanzahl allein trennt diese Adressen NICHT von einer großen Agentur: pr@deutsche-stadtauskunft.ag trägt 133 Namen und hat 134 Firmen mit veröffentlichten Meldungen. Trennscharf ist erst die Veröffentlichung. Der Schwellwert für Sammeladressen ist ein Parameter, kein fester Wert — E1 soll an Zahlen entschieden werden, nicht an einem Gefühl. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
133 lines
4.9 KiB
PHP
133 lines
4.9 KiB
PHP
<?php
|
|
|
|
use App\Models\Company;
|
|
use App\Models\PressRelease;
|
|
use App\Services\Company\CompanyMergeReport;
|
|
use Illuminate\Support\Collection;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Trockenlauf zur Newsroom-Konsolidierung (Roadmap 1a.1). Schreibt nichts —
|
|
* er entscheidet, was später geschrieben werden darf.
|
|
*/
|
|
function mergeCompany(string $email, string $name, bool $published = false): Company
|
|
{
|
|
$company = Company::factory()->create(['email' => $email, 'name' => $name]);
|
|
|
|
if ($published) {
|
|
PressRelease::factory()->published()->create(['company_id' => $company->id]);
|
|
}
|
|
|
|
return $company;
|
|
}
|
|
|
|
function mergeClasses(int $threshold = 3): Collection
|
|
{
|
|
return collect(app(CompanyMergeReport::class)->build($threshold)['klassen'])->keyBy('klasse');
|
|
}
|
|
|
|
test('identical names on one address are a mergeable duplicate', function () {
|
|
/** @var TestCase $this */
|
|
mergeCompany('info@muster.de', 'Muster GmbH');
|
|
mergeCompany('INFO@Muster.de', 'Muster GmbH');
|
|
|
|
$classes = mergeClasses();
|
|
|
|
// Groß-/Kleinschreibung der Adresse darf keine zweite Identität erzeugen.
|
|
expect($classes['dublette']['adressen'])->toBe(1)
|
|
->and($classes['dublette']['firmen'])->toBe(2)
|
|
->and($classes['dublette']['faellt_weg'])->toBe(1);
|
|
});
|
|
|
|
test('a differing legal form is the same company', function () {
|
|
/** @var TestCase $this */
|
|
mergeCompany('info@hoppe-net.de', 'Hoppe Unternehmensberatung');
|
|
mergeCompany('info@hoppe-net.de', 'Hoppe Unternehmensberatung GmbH');
|
|
|
|
// Genau der Fall „gleicher ODER ÄHNLICHER Firmenname" aus dem Regelwerk.
|
|
expect(mergeClasses()['namensvariante']['faellt_weg'])->toBe(1);
|
|
});
|
|
|
|
test('two genuinely different names stay below the threshold as a proposal', function () {
|
|
/** @var TestCase $this */
|
|
mergeCompany('sandra@buschgroup.com', 'Busch Group');
|
|
mergeCompany('sandra@buschgroup.com', 'Pfeiffer Vacuum');
|
|
|
|
$classes = mergeClasses(3);
|
|
|
|
// Keine automatische Aktion: Zwei Marken unter einem Pressekontakt sind
|
|
// nicht dasselbe Unternehmen, aber auch keine Agentur.
|
|
expect($classes['vorschlag']['adressen'])->toBe(1)
|
|
->and($classes['vorschlag']['faellt_weg'])->toBe(0)
|
|
->and($classes['sammeladresse']['adressen'])->toBe(0);
|
|
});
|
|
|
|
test('the threshold decides where a collection address begins', function () {
|
|
/** @var TestCase $this */
|
|
foreach (['Alpha', 'Beta', 'Gamma'] as $name) {
|
|
mergeCompany('presse@agentur.de', $name);
|
|
}
|
|
|
|
// E1 ist genau dieser Schwellwert — der Bericht macht ihn verstellbar,
|
|
// damit die Entscheidung an Zahlen und nicht an einem Gefühl hängt.
|
|
expect(mergeClasses(3)['sammeladresse']['adressen'])->toBe(1)
|
|
->and(mergeClasses(4)['sammeladresse']['adressen'])->toBe(0)
|
|
->and(mergeClasses(4)['vorschlag']['adressen'])->toBe(1);
|
|
});
|
|
|
|
test('many names without a single publication are flagged, not merged', function () {
|
|
/** @var TestCase $this */
|
|
for ($i = 1; $i <= 30; $i++) {
|
|
mergeCompany('yahoo@yahoo.com', "Firma {$i}");
|
|
}
|
|
|
|
$classes = mergeClasses();
|
|
|
|
// Der teuerste denkbare Fehler dieses Blocks: über eine Platzhalteradresse
|
|
// zusammenzuführen und tausende unbeteiligte Firmen zu verschmelzen.
|
|
expect($classes['nie_veroeffentlicht']['adressen'])->toBe(1)
|
|
->and($classes['nie_veroeffentlicht']['firmen'])->toBe(30)
|
|
->and($classes['nie_veroeffentlicht']['faellt_weg'])->toBe(0)
|
|
->and($classes['sammeladresse']['adressen'])->toBe(0);
|
|
});
|
|
|
|
test('a large but working agency is not mistaken for a placeholder', function () {
|
|
/** @var TestCase $this */
|
|
for ($i = 1; $i <= 30; $i++) {
|
|
mergeCompany('pr@agentur.ag', "Kunde {$i}", published: true);
|
|
}
|
|
|
|
// Die Namensanzahl allein trennt nicht. Erst die Veröffentlichung tut es.
|
|
$classes = mergeClasses();
|
|
|
|
expect($classes['sammeladresse']['adressen'])->toBe(1)
|
|
->and($classes['nie_veroeffentlicht']['adressen'])->toBe(0);
|
|
});
|
|
|
|
test('a single company on its own address is not part of the report', function () {
|
|
/** @var TestCase $this */
|
|
mergeCompany('allein@muster.de', 'Allein GmbH');
|
|
|
|
$data = app(CompanyMergeReport::class)->build();
|
|
|
|
expect($data['gesamt']['adressen_mehrfach'])->toBe(0);
|
|
});
|
|
|
|
test('the report writes nothing', function () {
|
|
/** @var TestCase $this */
|
|
mergeCompany('info@muster.de', 'Muster GmbH');
|
|
mergeCompany('info@muster.de', 'Muster GmbH');
|
|
|
|
app(CompanyMergeReport::class)->build();
|
|
|
|
// Für die rückwirkende Zusammenführung im Altbestand trägt das Portal die
|
|
// Verantwortung — deshalb steht der Trockenlauf davor und fasst nichts an.
|
|
expect(Company::query()->count())->toBe(2);
|
|
});
|
|
|
|
test('the command runs and reports the threshold in use', function () {
|
|
/** @var TestCase $this */
|
|
$this->artisan('companies:merge-report', ['--threshold' => 4])
|
|
->expectsOutputToContain('ab 4 abweichenden Namen')
|
|
->assertSuccessful();
|
|
});
|