create(['email' => $email, 'name' => $name]); } test('the classification is written per address and readable by email', function () { /** @var TestCase $this */ classifiedCompany('info@muster.de', 'Muster GmbH'); classifiedCompany('INFO@Muster.de', 'Muster GmbH'); $this->artisan('companies:classify-addresses')->assertSuccessful(); $group = CompanyEmailGroup::forEmail('Info@Muster.DE'); // Eine Zeile je Adresse, gefunden über dieselbe Normalisierung, die auch // die Firma benutzt — sonst finden sich Firma und Gruppe nicht. expect($group)->not->toBeNull() ->and($group->effectiveClassification())->toBe(CompanyAddressClass::Duplicate) ->and($group->isMergeable())->toBeTrue() ->and($group->companies_count)->toBe(2) ->and($group->threshold)->toBe(3); }); test('a collection address is marked and excluded from merging', function () { /** @var TestCase $this */ foreach (['Auto Hein', 'Ihr Personalberater', 'Designwerkstatt Coburg'] as $name) { classifiedCompany('presse@agentur-zb.de', $name); } $this->artisan('companies:classify-addresses')->assertSuccessful(); $group = CompanyEmailGroup::forEmail('presse@agentur-zb.de'); // Genau das ist 1a.2: markiert und von der automatischen Zusammenführung // ausgenommen. expect($group->effectiveClassification())->toBe(CompanyAddressClass::CollectionAddress) ->and($group->isMergeable())->toBeFalse(); }); test('a manual classification survives recomputation', function () { /** @var TestCase $this */ classifiedCompany('presse@pr4you.de', 'Kunde A'); classifiedCompany('presse@pr4you.de', 'Kunde B Beratung'); classifiedCompany('presse@pr4you.de', 'Kunde C Verlag'); $this->artisan('companies:classify-addresses')->assertSuccessful(); // Der Mensch entscheidet: Diese Adresse ist KEINE Agentur, sondern ein // Kunde mit drei Marken — wird als Vorschlag geführt. CompanyEmailGroup::forEmail('presse@pr4you.de') ->update(['manual_classification' => CompanyAddressClass::Proposal]); $this->artisan('companies:classify-addresses') ->expectsOutputToContain('1 von Hand gesetzte Klassen unverändert übernommen.') ->assertSuccessful(); $group = CompanyEmailGroup::forEmail('presse@pr4you.de'); // Berechnet bleibt Sammeladresse, wirksam ist die Entscheidung. expect($group->classification)->toBe(CompanyAddressClass::CollectionAddress) ->and($group->effectiveClassification())->toBe(CompanyAddressClass::Proposal); }); test('an address that no longer holds several companies drops out', function () { /** @var TestCase $this */ $a = classifiedCompany('info@einmal.de', 'Einmal GmbH'); classifiedCompany('info@einmal.de', 'Einmal GmbH'); $this->artisan('companies:classify-addresses')->assertSuccessful(); expect(CompanyEmailGroup::forEmail('info@einmal.de'))->not->toBeNull(); $a->forceDelete(); $this->artisan('companies:classify-addresses')->assertSuccessful(); // Wiederholbar: Was keine Gruppe mehr ist, bleibt nicht als Karteileiche // stehen und würde sonst später eine Zusammenführung mit sich selbst // auslösen. expect(CompanyEmailGroup::forEmail('info@einmal.de'))->toBeNull(); }); test('a manually classified address stays even when it drops below two companies', function () { /** @var TestCase $this */ $a = classifiedCompany('info@bleibt.de', 'Bleibt GmbH'); classifiedCompany('info@bleibt.de', 'Bleibt AG Holding'); $this->artisan('companies:classify-addresses')->assertSuccessful(); CompanyEmailGroup::forEmail('info@bleibt.de') ->update(['manual_classification' => CompanyAddressClass::CollectionAddress]); $a->forceDelete(); $this->artisan('companies:classify-addresses')->assertSuccessful(); // Die Entscheidung könnte für die nächste Firma unter dieser Adresse // wieder gelten — sie wird nicht mit weggeräumt. expect(CompanyEmailGroup::forEmail('info@bleibt.de')?->effectiveClassification()) ->toBe(CompanyAddressClass::CollectionAddress); }); test('the dry run writes nothing', function () { /** @var TestCase $this */ classifiedCompany('info@muster.de', 'Muster GmbH'); classifiedCompany('info@muster.de', 'Muster GmbH'); $this->artisan('companies:classify-addresses', ['--dry-run' => true]) ->expectsOutputToContain('[Dry-Run] 1 Adressen') ->assertSuccessful(); expect(CompanyEmailGroup::query()->count())->toBe(0); }); test('the threshold comes from the configuration by default', function () { /** @var TestCase $this */ config()->set('newsroom.merge.collection_threshold', 4); classifiedCompany('a@b.de', 'Alpha'); classifiedCompany('a@b.de', 'Beta Consulting'); classifiedCompany('a@b.de', 'Gamma Verlag'); $this->artisan('companies:classify-addresses')->assertSuccessful(); // Drei Namen sind bei Schwellwert 4 noch ein Vorschlag, keine Agentur. expect(CompanyEmailGroup::forEmail('a@b.de')->effectiveClassification())->toBe(CompanyAddressClass::Proposal) ->and(CompanyEmailGroup::forEmail('a@b.de')->threshold)->toBe(4); });