presseportale/tests/Feature/CompanyMergeReportTest.php
Kevin Adametz 77318dcb79 companies: Aufrufe als Veröffentlichungsnachweis im Altbestand werten
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>
2026-09-11 10:20:57 +00:00

208 lines
8.5 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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();
});