Das Altsystem hat 2009–2018 rund 126 000 Meldungen auf `rejected` gesetzt, 83 000 davon mit Aufrufen — die waren online und sind pauschal offline genommen worden. Wer nur `status = published` zählt, stellt pr4you, Blödorn und Crossover Point mit Hunderten Meldungen neben yahoo@yahoo.com. Eine Firma gilt jetzt als veröffentlicht, wenn sie eine Meldung mit `published` ODER `hits > 0` hat. Nie veröffentlicht: 23 → 7 Adressen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
208 lines
8.5 KiB
PHP
208 lines
8.5 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('a legal form with ampersand is stripped like any other', function () {
|
||
/** @var TestCase $this */
|
||
mergeCompany('info@welcon.de', 'Welcon Europe Ltd. & Co. KG');
|
||
mergeCompany('info@welcon.de', 'Welcon Europe GmbH & Co. KG');
|
||
|
||
// Im Live-Lauf vom 11.09. stand Welcon als „Agentur" — weil „& Co. KG"
|
||
// nach dem Entfernen der Satzzeichen auf keine Rechtsform mehr passte.
|
||
expect(mergeClasses()['namensvariante']['faellt_weg'])->toBe(1);
|
||
});
|
||
|
||
test('typos and small additions do not make a company an agency', function () {
|
||
/** @var TestCase $this */
|
||
mergeCompany('info@js-research.de', 'JS Research');
|
||
mergeCompany('info@js-research.de', 'JS Research UG (haftungsbeschränkt)');
|
||
mergeCompany('info@js-research.de', 'S Research UG (haftungsbeschränkt)');
|
||
mergeCompany('info@js-research.de', 'JS Research');
|
||
|
||
$classes = mergeClasses(3);
|
||
|
||
// JS Research stand mit 204 Firmen an zweiter Stelle der Agenturliste —
|
||
// eine einzelne Firma, deren Name siebzehnmal leicht anders geschrieben
|
||
// wurde. Ein Tippfehler ist kein zweiter Kunde.
|
||
expect($classes['namensvariante']['adressen'])->toBe(1)
|
||
->and($classes['sammeladresse']['adressen'])->toBe(0);
|
||
});
|
||
|
||
test('a shared three-word prefix under one address is the same entity', function () {
|
||
/** @var TestCase $this */
|
||
mergeCompany('praxis@prof-dhom.de', 'Praxis Prof. Dr. Dhom & Kollegen MVZ GmbH (Juli 2016)');
|
||
mergeCompany('praxis@prof-dhom.de', 'Praxis Prof. Dr. Dhom & Partner');
|
||
mergeCompany('praxis@prof-dhom.de', 'Praxis Prof. Dr. Dhom');
|
||
|
||
expect(mergeClasses(3)['namensvariante']['adressen'])->toBe(1);
|
||
});
|
||
|
||
test('genuinely different clients under one agency address stay distinct', function () {
|
||
/** @var TestCase $this */
|
||
foreach (['Auto Hein', 'Ihr Personalberater', 'Designwerkstatt Coburg'] as $name) {
|
||
mergeCompany('presse@agentur-zb.de', $name);
|
||
}
|
||
|
||
// Die Ähnlichkeitsregel darf echte Agenturen nicht wegnormalisieren.
|
||
expect(mergeClasses(3)['sammeladresse']['adressen'])->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 */
|
||
foreach (range(1, 30) as $i) {
|
||
mergeCompany('yahoo@yahoo.com', fake()->unique()->company().' '.fake()->unique()->word());
|
||
}
|
||
|
||
$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 */
|
||
// Echte, verschiedene Kundennamen — „Kunde 1" bis „Kunde 30" lägen
|
||
// einander so nah, dass die Ähnlichkeitsregel sie zu Recht als eine
|
||
// Firma mit Filialnummern läse.
|
||
foreach (range(1, 30) as $i) {
|
||
mergeCompany('pr@agentur.ag', fake()->unique()->company().' '.fake()->unique()->word(), 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('an agency whose releases went offline years ago still counts as published', function () {
|
||
/** @var TestCase $this */
|
||
// Der Altbestand: Das Altsystem hat 2009–2018 rund 126 000 Meldungen auf
|
||
// `rejected` gesetzt, 83 000 davon mit gezählten Aufrufen. Eine abgelehnte
|
||
// Meldung wird nie aufgerufen — die waren online. pr4you mit 65 Firmen
|
||
// und 199 solcher Meldungen aus 2008–2011 ist eine Agentur, kein Müll.
|
||
foreach (range(1, 30) as $i) {
|
||
$company = mergeCompany('presse@pr4you.de', fake()->unique()->company().' '.fake()->unique()->word());
|
||
PressRelease::factory()->create(['company_id' => $company->id, 'status' => 'rejected', 'hits' => 120]);
|
||
}
|
||
|
||
$classes = mergeClasses();
|
||
|
||
expect($classes['sammeladresse']['adressen'])->toBe(1)
|
||
->and($classes['nie_veroeffentlicht']['adressen'])->toBe(0);
|
||
});
|
||
|
||
test('a rejected release without a single view is no publication', function () {
|
||
/** @var TestCase $this */
|
||
foreach (range(1, 30) as $i) {
|
||
$company = mergeCompany('yahoo@yahoo.com', fake()->unique()->company().' '.fake()->unique()->word());
|
||
PressRelease::factory()->create(['company_id' => $company->id, 'status' => 'rejected', 'hits' => 0]);
|
||
}
|
||
|
||
expect(mergeClasses()['nie_veroeffentlicht']['adressen'])->toBe(1);
|
||
});
|
||
|
||
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(3);
|
||
|
||
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(3);
|
||
|
||
// 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();
|
||
});
|