- R-02/K-03: Aufrufaggregation und Merge ändern updated_at nicht mehr; neue Spalte press_releases.content_changed_at (INSTANT), contentModifiedAt() für Sitemap-lastmod, dateModified und article:modified_time - R-03: Sitemap und Legacy-view_url mit /en/ für englische Meldungen - R-04/O01: X-Pk-Ref je Empfänger per HMAC signiert; auf eine Sperre zählt — hart wie weich — nur ein gültig signierter Rückläufer, unsignierte standardmäßig nur zur Sichtung (BOUNCE_UNSIGNED_KIND=policy) - R-05/R-06: Unmerge setzt nur unveränderte Füllwerte zurück (company_merge_moves.merged_value), Canonical-Neubewertung nach Merge und Unmerge, assignAll setzt veraltete Verweise zurück - R-07 bis R-11: Crawler zählen nicht als Aufruf, Mail-Klick gedrosselt und verzögert geschrieben, Claim-Zähler erst nach Versand, email_bounces nach 395 Tagen gelöscht, Canonical-Portal über Kategorie-ID, llms-Cache geleert, Feed-Teaser ohne Doppel-Escaping, Merge-Ketten aufgelöst - N17/N18: kein falsches „gelistet seit", Rollen im Indexbericht wie Sitemap - OPT-1/OPT-2: topFeeds in SQL, purge-events löscht nach Grenz-ID Enthält aus PressRelease.php auch die Card-Bild-Helfer (B-01) und wasEverPublished()/neverPublished() (A-05), weil die Datei ungeteilt committet ist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
464 lines
22 KiB
PHP
464 lines
22 KiB
PHP
<?php
|
|
|
|
use App\Enums\CompanyAddressClass;
|
|
use App\Enums\CompanyMergeSubject;
|
|
use App\Enums\Portal;
|
|
use App\Models\Category;
|
|
use App\Models\Company;
|
|
use App\Models\CompanyEmailGroup;
|
|
use App\Models\CompanyMerge;
|
|
use App\Models\Contact;
|
|
use App\Models\PressRelease;
|
|
use App\Models\User;
|
|
use App\Services\Company\CompanyMergePlan;
|
|
use App\Services\Company\CompanyMergeService;
|
|
use App\Services\Company\CompanyMergeSkip;
|
|
use App\Services\Company\MergedNewsroomLookup;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Zusammenführung im Bestand (Roadmap 1a.3): rückabwickelbar, mit
|
|
* gespeicherter Ursprungszuordnung. Entschieden am 15.09.2026: Es bleibt die
|
|
* Firma mit den meisten Meldungen, alles wandert per company_id, die
|
|
* wegfallende Firma wird soft-deleted und ihr Slug führt per 301 weiter.
|
|
*/
|
|
function mergeGroup(string $email, CompanyAddressClass $class = CompanyAddressClass::Duplicate, ?CompanyAddressClass $manual = null): CompanyEmailGroup
|
|
{
|
|
return CompanyEmailGroup::query()->create([
|
|
'email' => Company::emailKey($email),
|
|
'classification' => $class,
|
|
'manual_classification' => $manual,
|
|
'companies_count' => 2,
|
|
'distinct_names' => 1,
|
|
'published_companies_count' => 1,
|
|
'threshold' => 3,
|
|
'classified_at' => now(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $attributes
|
|
*/
|
|
function mergeMember(string $email, Portal $portal, int $published = 0, array $attributes = []): Company
|
|
{
|
|
$company = Company::factory()->create(['email' => $email, 'portal' => $portal->value, 'name' => 'Muster GmbH'] + $attributes);
|
|
|
|
PressRelease::factory()->published()->forPortal($portal)->count($published)->create(['company_id' => $company->id]);
|
|
|
|
return $company;
|
|
}
|
|
|
|
function planFor(string $email): CompanyMergePlan|CompanyMergeSkip
|
|
{
|
|
return app(CompanyMergeService::class)->plan(CompanyEmailGroup::forEmail($email));
|
|
}
|
|
|
|
test('the company with more publications survives and becomes both when halves lie on different portals', function () {
|
|
/** @var TestCase $this */
|
|
mergeGroup('info@muster.de');
|
|
$pe = mergeMember('info@muster.de', Portal::Presseecho, 2);
|
|
$b24 = mergeMember('info@muster.de', Portal::Businessportal24, 5);
|
|
|
|
$plan = planFor('info@muster.de');
|
|
|
|
expect($plan)->toBeInstanceOf(CompanyMergePlan::class)
|
|
->and($plan->survivor->is($b24))->toBeTrue()
|
|
->and($plan->targetPortal)->toBe(Portal::Both)
|
|
->and($plan->pressReleases)->toBe(2);
|
|
|
|
$merge = app(CompanyMergeService::class)->merge($plan, 'test-batch');
|
|
|
|
// Die Meldungen behalten ihr Portal — nur die Firma wird portalübergreifend.
|
|
expect($b24->fresh()->portal)->toBe(Portal::Both)
|
|
->and(PressRelease::withoutGlobalScopes()->where('company_id', $b24->id)->count())->toBe(7)
|
|
->and(PressRelease::withoutGlobalScopes()->where('company_id', $b24->id)->where('portal', 'presseecho')->count())->toBe(2)
|
|
->and(Company::withTrashed()->find($pe->id)->trashed())->toBeTrue()
|
|
->and($merge->moved_press_releases_count)->toBe(2)
|
|
->and($merge->moves()->where('subject', CompanyMergeSubject::PressRelease->value)->count())->toBe(2)
|
|
->and($merge->moves()->where('subject', CompanyMergeSubject::Company->value)->value('subject_id'))->toBe($pe->id);
|
|
});
|
|
|
|
test('on a tie the older company survives', function () {
|
|
/** @var TestCase $this */
|
|
mergeGroup('info@muster.de');
|
|
$older = mergeMember('info@muster.de', Portal::Presseecho, 1);
|
|
mergeMember('info@muster.de', Portal::Presseecho, 1);
|
|
|
|
$plan = planFor('info@muster.de');
|
|
|
|
expect($plan)->toBeInstanceOf(CompanyMergePlan::class)
|
|
->and($plan->survivor->is($older))->toBeTrue()
|
|
->and($plan->targetPortal)->toBe(Portal::Presseecho);
|
|
});
|
|
|
|
test('releases taken offline in the legacy system count for the survivor choice', function () {
|
|
/** @var TestCase $this */
|
|
mergeGroup('info@muster.de');
|
|
$fresh = mergeMember('info@muster.de', Portal::Presseecho, 1);
|
|
$legacy = mergeMember('info@muster.de', Portal::Businessportal24);
|
|
PressRelease::factory()->count(3)->create(['company_id' => $legacy->id, 'status' => 'rejected', 'hits' => 50]);
|
|
|
|
expect(planFor('info@muster.de')->survivor->is($legacy))->toBeTrue();
|
|
});
|
|
|
|
test('empty profile fields are filled from the merged company, filled ones stay', function () {
|
|
/** @var TestCase $this */
|
|
mergeGroup('info@muster.de');
|
|
$survivor = mergeMember('info@muster.de', Portal::Presseecho, 3, ['website' => null, 'phone' => '030 1', 'logo_path' => null]);
|
|
mergeMember('info@muster.de', Portal::Businessportal24, 0, ['website' => 'https://muster.de', 'phone' => '030 2', 'logo_path' => 'logos/muster.png']);
|
|
|
|
$merge = app(CompanyMergeService::class)->merge(planFor('info@muster.de'), 'b');
|
|
|
|
$survivor->refresh();
|
|
|
|
expect($survivor->website)->toBe('https://muster.de')
|
|
->and($survivor->logo_path)->toBe('logos/muster.png')
|
|
->and($survivor->phone)->toBe('030 1')
|
|
->and($merge->moves()->where('subject', CompanyMergeSubject::Field->value)->pluck('field')->sort()->values()->all())->toBe(['logo_path', 'website']);
|
|
});
|
|
|
|
test('user links become the union without duplicates and contacts move along', function () {
|
|
/** @var TestCase $this */
|
|
mergeGroup('info@muster.de');
|
|
$survivor = mergeMember('info@muster.de', Portal::Presseecho, 1);
|
|
$loser = mergeMember('info@muster.de', Portal::Businessportal24);
|
|
$shared = User::factory()->create();
|
|
$only = User::factory()->create();
|
|
DB::table('company_user')->insert([
|
|
['company_id' => $survivor->id, 'user_id' => $shared->id, 'role' => 'owner'],
|
|
['company_id' => $loser->id, 'user_id' => $shared->id, 'role' => 'member'],
|
|
['company_id' => $loser->id, 'user_id' => $only->id, 'role' => 'member'],
|
|
]);
|
|
$contact = Contact::factory()->create(['company_id' => $loser->id]);
|
|
|
|
$merge = app(CompanyMergeService::class)->merge(planFor('info@muster.de'), 'b');
|
|
|
|
expect(DB::table('company_user')->where('company_id', $survivor->id)->pluck('user_id')->sort()->values()->all())->toBe(collect([$shared->id, $only->id])->sort()->values()->all())
|
|
->and(DB::table('company_user')->where('company_id', $loser->id)->count())->toBe(0)
|
|
->and($contact->fresh()->company_id)->toBe($survivor->id)
|
|
->and($merge->moves()->where('subject', CompanyMergeSubject::CompanyUser->value)->where('dropped', true)->value('subject_id'))->toBe($shared->id);
|
|
});
|
|
|
|
test('unmerge restores exactly what was moved and leaves what came later', function () {
|
|
/** @var TestCase $this */
|
|
mergeGroup('info@muster.de');
|
|
$survivor = mergeMember('info@muster.de', Portal::Presseecho, 1, ['website' => null]);
|
|
$loser = mergeMember('info@muster.de', Portal::Businessportal24, 2, ['website' => 'https://muster.de']);
|
|
$loserReleases = PressRelease::withoutGlobalScopes()->where('company_id', $loser->id)->pluck('id');
|
|
$shared = User::factory()->create();
|
|
DB::table('company_user')->insert([
|
|
['company_id' => $survivor->id, 'user_id' => $shared->id, 'role' => 'owner'],
|
|
['company_id' => $loser->id, 'user_id' => $shared->id, 'role' => 'member'],
|
|
]);
|
|
|
|
$service = app(CompanyMergeService::class);
|
|
$merge = $service->merge($service->plan(CompanyEmailGroup::forEmail('info@muster.de')), 'b');
|
|
|
|
// Nach der Zusammenführung entsteht eine neue Meldung beim Überlebenden.
|
|
$later = PressRelease::factory()->published()->forPortal(Portal::Presseecho)->create(['company_id' => $survivor->id]);
|
|
|
|
$service->revert($merge);
|
|
|
|
$survivor->refresh();
|
|
|
|
expect($survivor->portal)->toBe(Portal::Presseecho)
|
|
->and($survivor->website)->toBeNull()
|
|
->and(Company::withoutGlobalScopes()->find($loser->id)->trashed())->toBeFalse()
|
|
->and(PressRelease::withoutGlobalScopes()->whereIn('id', $loserReleases)->pluck('company_id')->unique()->all())->toBe([$loser->id])
|
|
->and($later->fresh()->company_id)->toBe($survivor->id)
|
|
->and(DB::table('company_user')->where('company_id', $loser->id)->where('user_id', $shared->id)->value('role'))->toBe('member')
|
|
->and(DB::table('company_user')->where('company_id', $survivor->id)->count())->toBe(1)
|
|
->and($merge->fresh()->isReverted())->toBeTrue();
|
|
|
|
// Ein zweites Mal tut nichts.
|
|
$service->revert($merge->fresh());
|
|
expect(Company::withoutGlobalScopes()->find($loser->id)->trashed())->toBeFalse();
|
|
});
|
|
|
|
test('a slug taken by a foreign company on the other portal skips the address', function () {
|
|
/** @var TestCase $this */
|
|
mergeGroup('info@muster.de');
|
|
// Beide Hälften tragen verschiedene Slugs, und jeder ist auf dem jeweils
|
|
// anderen Portal von einer fremden Firma belegt — keiner kann `both` werden.
|
|
mergeMember('info@muster.de', Portal::Presseecho, 3, ['slug' => 'muster-a']);
|
|
mergeMember('info@muster.de', Portal::Businessportal24, 1, ['slug' => 'muster-b']);
|
|
Company::factory()->create(['email' => 'anders@fremd.de', 'portal' => 'businessportal24', 'slug' => 'muster-a']);
|
|
Company::factory()->create(['email' => 'anders@fremd.de', 'portal' => 'presseecho', 'slug' => 'muster-b']);
|
|
|
|
$plan = planFor('info@muster.de');
|
|
|
|
expect($plan)->toBeInstanceOf(CompanyMergeSkip::class)
|
|
->and($plan->reason)->toBe(CompanyMergeSkip::SlugCollision)
|
|
->and(Company::withoutGlobalScopes()->where('email', 'info@muster.de')->count())->toBe(2);
|
|
});
|
|
|
|
test('when the best candidate collides but another does not, the other survives', function () {
|
|
/** @var TestCase $this */
|
|
mergeGroup('info@muster.de');
|
|
mergeMember('info@muster.de', Portal::Presseecho, 3, ['slug' => 'muster']);
|
|
$free = mergeMember('info@muster.de', Portal::Businessportal24, 1, ['slug' => 'muster-gmbh']);
|
|
// „muster" ist auf B24 fremd belegt → die PE-Hälfte kann nicht `both` werden,
|
|
// die B24-Hälfte mit „muster-gmbh" schon.
|
|
Company::factory()->create(['email' => 'anders@fremd.de', 'portal' => 'businessportal24', 'slug' => 'muster']);
|
|
|
|
$plan = planFor('info@muster.de');
|
|
|
|
expect($plan)->toBeInstanceOf(CompanyMergePlan::class)
|
|
->and($plan->survivor->is($free))->toBeTrue();
|
|
});
|
|
|
|
test('only mergeable classes are touched and a manual decision overrides', function () {
|
|
/** @var TestCase $this */
|
|
mergeGroup('agentur@sammel.de', CompanyAddressClass::CollectionAddress);
|
|
mergeGroup('doch@agentur.de', CompanyAddressClass::CollectionAddress, manual: CompanyAddressClass::Duplicate);
|
|
mergeGroup('nicht@dublette.de', CompanyAddressClass::Duplicate, manual: CompanyAddressClass::CollectionAddress);
|
|
foreach (['agentur@sammel.de', 'doch@agentur.de', 'nicht@dublette.de'] as $email) {
|
|
mergeMember($email, Portal::Presseecho, 1);
|
|
mergeMember($email, Portal::Businessportal24);
|
|
}
|
|
|
|
$this->artisan('companies:merge')->assertSuccessful();
|
|
|
|
expect(CompanyMerge::query()->pluck('email')->all())->toBe(['doch@agentur.de'])
|
|
->and(Company::withoutGlobalScopes()->whereNull('deleted_at')->where('email', 'agentur@sammel.de')->count())->toBe(2)
|
|
->and(Company::withoutGlobalScopes()->whereNull('deleted_at')->where('email', 'nicht@dublette.de')->count())->toBe(2);
|
|
});
|
|
|
|
test('companies removed with 410 stay out of the merge', function () {
|
|
/** @var TestCase $this */
|
|
mergeGroup('info@muster.de');
|
|
mergeMember('info@muster.de', Portal::Presseecho, 1);
|
|
$gone = mergeMember('info@muster.de', Portal::Businessportal24, 4, ['gone_at' => now()]);
|
|
|
|
$plan = planFor('info@muster.de');
|
|
|
|
// Nur eine lebende Firma übrig → nichts zu tun; die entfernte bleibt entfernt.
|
|
expect($plan)->toBeInstanceOf(CompanyMergeSkip::class)
|
|
->and($plan->reason)->toBe(CompanyMergeSkip::NothingToMerge)
|
|
->and($gone->fresh()->gone_at)->not->toBeNull();
|
|
});
|
|
|
|
test('dry run and limit write nothing and count correctly', function () {
|
|
/** @var TestCase $this */
|
|
foreach (['a@muster.de', 'b@muster.de', 'c@muster.de'] as $email) {
|
|
mergeGroup($email);
|
|
mergeMember($email, Portal::Presseecho, 1);
|
|
mergeMember($email, Portal::Businessportal24);
|
|
}
|
|
|
|
$this->artisan('companies:merge', ['--dry-run' => true])
|
|
->expectsOutputToContain('[Dry-Run] 3 Adressen')
|
|
->assertSuccessful();
|
|
|
|
expect(CompanyMerge::query()->count())->toBe(0)
|
|
->and(Company::withoutGlobalScopes()->whereNotNull('deleted_at')->count())->toBe(0);
|
|
|
|
$this->artisan('companies:merge', ['--limit' => 2])->assertSuccessful();
|
|
|
|
expect(CompanyMerge::query()->count())->toBe(2)
|
|
->and(CompanyMerge::query()->distinct()->count('batch'))->toBe(1);
|
|
|
|
// Der zweite Lauf holt den Rest — die erledigten Adressen zählen nicht
|
|
// gegen das Limit.
|
|
$this->artisan('companies:merge', ['--limit' => 2])->assertSuccessful();
|
|
|
|
expect(CompanyMerge::query()->count())->toBe(3);
|
|
});
|
|
|
|
test('unmerge by batch reverts every merge of that run', function () {
|
|
/** @var TestCase $this */
|
|
foreach (['a@muster.de', 'b@muster.de'] as $email) {
|
|
mergeGroup($email);
|
|
mergeMember($email, Portal::Presseecho, 1);
|
|
mergeMember($email, Portal::Businessportal24);
|
|
}
|
|
|
|
$this->artisan('companies:merge')->assertSuccessful();
|
|
$batch = CompanyMerge::query()->value('batch');
|
|
|
|
$this->artisan('companies:unmerge', ['--batch' => $batch])
|
|
->expectsOutputToContain('2 Vorgänge rückabgewickelt')
|
|
->assertSuccessful();
|
|
|
|
expect(Company::withoutGlobalScopes()->whereNotNull('deleted_at')->count())->toBe(0)
|
|
->and(CompanyMerge::query()->whereNull('reverted_at')->count())->toBe(0)
|
|
->and(Company::withoutGlobalScopes()->where('portal', 'both')->count())->toBe(0);
|
|
});
|
|
|
|
test('the old newsroom slug redirects permanently to the survivor', function () {
|
|
/** @var TestCase $this */
|
|
mergeGroup('info@muster.de');
|
|
$survivor = mergeMember('info@muster.de', Portal::Businessportal24, 2, ['slug' => 'muster-gmbh']);
|
|
$loser = mergeMember('info@muster.de', Portal::Presseecho, 1, ['slug' => 'muster']);
|
|
$release = PressRelease::withoutGlobalScopes()->where('company_id', $loser->id)->first();
|
|
$release->update(['published_at' => now()->subDay()]);
|
|
|
|
$this->get('https://presseecho.test/de/newsroom/muster')->assertOk();
|
|
|
|
$service = app(CompanyMergeService::class);
|
|
$merge = $service->merge($service->plan(CompanyEmailGroup::forEmail('info@muster.de')), 'b');
|
|
|
|
// Alte URL → 301 auf den Überlebenden, der auf diesem Portal über `both`
|
|
// erreichbar ist und die Meldung dieses Portals zeigt.
|
|
$this->get('https://presseecho.test/de/newsroom/muster')
|
|
->assertRedirect('https://presseecho.test/de/newsroom/muster-gmbh')
|
|
->assertStatus(301);
|
|
$this->get('https://presseecho.test/de/newsroom/muster-gmbh')->assertOk()->assertSeeText($release->title);
|
|
|
|
// Nach der Rückabwicklung antwortet die alte URL wieder selbst.
|
|
$service->revert($merge);
|
|
$this->get('https://presseecho.test/de/newsroom/muster')->assertOk();
|
|
$this->get('https://presseecho.test/de/newsroom/muster-gmbh')->assertNotFound();
|
|
});
|
|
|
|
test('the same slug on both portals needs no redirect', function () {
|
|
/** @var TestCase $this */
|
|
mergeGroup('info@muster.de');
|
|
mergeMember('info@muster.de', Portal::Businessportal24, 2, ['slug' => 'muster']);
|
|
$loser = mergeMember('info@muster.de', Portal::Presseecho, 1, ['slug' => 'muster']);
|
|
PressRelease::withoutGlobalScopes()->where('company_id', $loser->id)->update(['published_at' => now()->subDay()]);
|
|
|
|
$service = app(CompanyMergeService::class);
|
|
$service->merge($service->plan(CompanyEmailGroup::forEmail('info@muster.de')), 'b');
|
|
|
|
$this->get('https://presseecho.test/de/newsroom/muster')->assertOk();
|
|
$this->get('https://businessportal24.test/de/newsroom/muster')->assertOk();
|
|
});
|
|
|
|
test('the merge list shows survivor, merged companies and the URLs to check', function () {
|
|
/** @var TestCase $this */
|
|
mergeGroup('info@muster.de');
|
|
mergeMember('info@muster.de', Portal::Businessportal24, 2, ['slug' => 'muster-gmbh']);
|
|
mergeMember('info@muster.de', Portal::Presseecho, 1, ['slug' => 'muster']);
|
|
|
|
$this->artisan('companies:merge')->assertSuccessful();
|
|
$batch = CompanyMerge::query()->value('batch');
|
|
|
|
Artisan::call('companies:merges', ['--batch' => $batch, '--json' => true]);
|
|
$listed = json_decode(Artisan::output(), true)[0];
|
|
|
|
expect($listed['newsroom'])->toBe(['https://presseecho.test/de/newsroom/muster-gmbh', 'https://businessportal24.test/de/newsroom/muster-gmbh'])
|
|
->and($listed['aufgegangen'][0]['alte_url'])->toBe(['https://presseecho.test/de/newsroom/muster'])
|
|
->and($listed['meldungen'])->toBe(1);
|
|
|
|
$this->artisan('companies:merges', ['--email' => 'INFO@muster.de'])
|
|
->expectsOutputToContain('muster-gmbh')
|
|
->assertSuccessful();
|
|
});
|
|
|
|
test('merge and unmerge leave updated_at of the moved releases untouched', function () {
|
|
/** @var TestCase $this */
|
|
mergeGroup('info@muster.de');
|
|
mergeMember('info@muster.de', Portal::Presseecho, 3);
|
|
$loser = mergeMember('info@muster.de', Portal::Businessportal24, 1);
|
|
$release = PressRelease::withoutGlobalScopes()->where('company_id', $loser->id)->sole();
|
|
$before = $release->updated_at->toDateTimeString();
|
|
|
|
$this->travel(3)->days();
|
|
$service = app(CompanyMergeService::class);
|
|
$merge = $service->merge(planFor('info@muster.de'), 'b');
|
|
expect(PressRelease::withoutGlobalScopes()->find($release->id)->updated_at->toDateTimeString())->toBe($before);
|
|
|
|
$service->revert($merge);
|
|
expect(PressRelease::withoutGlobalScopes()->find($release->id))
|
|
->company_id->toBe($loser->id)
|
|
->updated_at->toDateTimeString()->toBe($before);
|
|
});
|
|
|
|
test('unmerge keeps a field the customer changed after the merge', function () {
|
|
/** @var TestCase $this */
|
|
mergeGroup('info@muster.de');
|
|
$survivor = mergeMember('info@muster.de', Portal::Presseecho, 3, ['website' => null, 'boilerplate' => null, 'phone' => null]);
|
|
mergeMember('info@muster.de', Portal::Businessportal24, 0, ['website' => 'https://muster.de', 'boilerplate' => 'Über Muster', 'phone' => '030 2']);
|
|
|
|
$service = app(CompanyMergeService::class);
|
|
$merge = $service->merge(planFor('info@muster.de'), 'b');
|
|
|
|
expect($merge->moves()->where('field', 'boilerplate')->value('merged_value'))->toBe('Über Muster');
|
|
|
|
// Der Kunde beansprucht den Newsroom und schreibt den Text neu.
|
|
$survivor->refresh()->update(['boilerplate' => 'Unser eigener Text']);
|
|
|
|
$service->revert($merge);
|
|
$survivor->refresh();
|
|
|
|
expect($survivor->boilerplate)->toBe('Unser eigener Text')
|
|
->and($survivor->website)->toBeNull()
|
|
->and($survivor->phone)->toBeNull();
|
|
});
|
|
|
|
test('unmerge of an older merge without stored fill value compares against the merged company', function () {
|
|
/** @var TestCase $this */
|
|
mergeGroup('info@muster.de');
|
|
$survivor = mergeMember('info@muster.de', Portal::Presseecho, 3, ['website' => null, 'boilerplate' => null]);
|
|
mergeMember('info@muster.de', Portal::Businessportal24, 0, ['website' => 'https://muster.de', 'boilerplate' => 'Über Muster']);
|
|
|
|
$service = app(CompanyMergeService::class);
|
|
$merge = $service->merge(planFor('info@muster.de'), 'b');
|
|
// Stand vor der Migration: kein Füllwert in der Move-Zeile.
|
|
$merge->moves()->update(['merged_value' => null]);
|
|
|
|
$survivor->refresh()->update(['boilerplate' => 'Unser eigener Text']);
|
|
$service->revert($merge);
|
|
$survivor->refresh();
|
|
|
|
expect($survivor->boilerplate)->toBe('Unser eigener Text')
|
|
->and($survivor->website)->toBeNull();
|
|
});
|
|
|
|
test('merge and unmerge re-evaluate the canonical assignment of the moved releases', function () {
|
|
/** @var TestCase $this */
|
|
config()->set('canonical.enabled', true);
|
|
config()->set('canonical.portal_by_category', ['it' => 'businessportal24']);
|
|
|
|
$category = Category::factory()->create(['portal' => Portal::Both]);
|
|
$category->translations()->create(['locale' => 'de', 'name' => 'IT', 'slug' => 'it']);
|
|
|
|
mergeGroup('info@muster.de');
|
|
$survivor = mergeMember('info@muster.de', Portal::Businessportal24, 2);
|
|
$loser = mergeMember('info@muster.de', Portal::Presseecho);
|
|
|
|
$source = PressRelease::factory()->published()->forPortal(Portal::Businessportal24)->for($category)
|
|
->create(['company_id' => $survivor->id, 'title' => 'Neuer Standort', 'text' => '<p>Gleicher Text</p>']);
|
|
$copy = PressRelease::factory()->published()->forPortal(Portal::Presseecho)->for($category)
|
|
->create(['company_id' => $loser->id, 'title' => 'Neuer Standort', 'text' => '<p>Gleicher Text</p>']);
|
|
|
|
// Verschiedene Firmen: kein Paar.
|
|
expect($copy->fresh()->canonical_press_release_id)->toBeNull();
|
|
|
|
$service = app(CompanyMergeService::class);
|
|
$merge = $service->merge(planFor('info@muster.de'), 'b');
|
|
|
|
expect($copy->fresh()->canonical_press_release_id)->toBe($source->id)
|
|
->and($source->fresh()->canonical_press_release_id)->toBeNull();
|
|
|
|
$service->revert($merge);
|
|
|
|
// Wieder verschiedene Firmen: der Verweis auf die fremde Meldung fällt.
|
|
expect($copy->fresh()->canonical_press_release_id)->toBeNull()
|
|
->and($source->fresh()->canonical_press_release_id)->toBeNull();
|
|
});
|
|
|
|
test('an old newsroom slug follows a chain of merges to the current survivor', function () {
|
|
/** @var TestCase $this */
|
|
mergeGroup('info@muster.de');
|
|
$middle = mergeMember('info@muster.de', Portal::Presseecho, 2, ['slug' => 'muster-mitte']);
|
|
mergeMember('info@muster.de', Portal::Presseecho, 1, ['slug' => 'muster-alt']);
|
|
|
|
$service = app(CompanyMergeService::class);
|
|
$service->merge(planFor('info@muster.de'), 'b1');
|
|
|
|
// Später: Die Mitte geht ihrerseits in einer weiteren Firma auf.
|
|
$final = mergeMember('info@muster.de', Portal::Presseecho, 5, ['slug' => 'muster-neu']);
|
|
PressRelease::withoutGlobalScopes()->where('company_id', $final->id)->update(['published_at' => now()->subDay()]);
|
|
$service->merge(planFor('info@muster.de'), 'b2');
|
|
|
|
expect(Company::withTrashed()->find($middle->id)->trashed())->toBeTrue()
|
|
->and(app(MergedNewsroomLookup::class)->survivorFor(['presseecho', 'both'], 'muster-alt')?->id)->toBe($final->id);
|
|
|
|
$this->get('https://presseecho.test/de/newsroom/muster-alt')
|
|
->assertRedirect('https://presseecho.test/de/newsroom/muster-neu')
|
|
->assertStatus(301);
|
|
});
|