api: neue Firmen dem bestehenden Newsroom zuordnen (Roadmap 1a.4)
connektar legt je Meldung eine Firma an. ApiNewsroomAssignment prüft vor dem Anlegen, ob unter derselben Firmen-E-Mail schon dieselbe Firma steht (Namensregel der Klassifizierung), und gibt dann deren ID zurück — der Alt-Vertrag (201 + Location) bleibt. Nie-veröffentlicht-Adressen werden nie zugeordnet, abweichende Namen bekommen wie bisher eine neue Firma. Liegt die Firma auf dem anderen Portal, wird sie both; bei fremd belegtem Slug bleibt es beim alten Verhalten. LegacyPressReleaseController: both ist kein Ziel-Portal für eine Meldung — dann entscheidet das Portal des API-Nutzers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4303266841
commit
8dad652d67
8 changed files with 367 additions and 34 deletions
|
|
@ -7,6 +7,7 @@ use App\Enums\Portal;
|
|||
use App\Http\Controllers\Api\Concerns\ResolvesRequestedPortal;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Company;
|
||||
use App\Services\Company\ApiNewsroomAssignment;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
|
@ -21,7 +22,7 @@ class LegacyCompanyController extends Controller
|
|||
{
|
||||
use ResolvesRequestedPortal, RespondsLikeLegacyApi;
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
public function store(Request $request, ApiNewsroomAssignment $assignment): JsonResponse
|
||||
{
|
||||
abort_unless($request->user()->tokenCan('press-releases:write'), 403);
|
||||
|
||||
|
|
@ -46,11 +47,19 @@ class LegacyCompanyController extends Controller
|
|||
// Portal aus Parameter/Domain, sonst wie bisher vom User — der
|
||||
// User-Fallback allein ließ alle Firmen (und damit deren PMs) von
|
||||
// Kunden mit Keys beider Altportale im selben Portal landen.
|
||||
$portal = $this->resolveRequestedPortal($validated['portal'] ?? null, $user->portal);
|
||||
|
||||
// Steht unter dieser Firmen-E-Mail schon dieselbe Firma, bekommt der
|
||||
// Aufrufer deren ID — statt je Meldung eines neuen Newsrooms
|
||||
// (Roadmap 1a.4). Der Vertrag bleibt: 201 + Location.
|
||||
$existing = $assignment->assign($user, $validated['email'] ?? null, $validated['name'], $portal);
|
||||
|
||||
if ($existing) {
|
||||
return $this->legacyCreated(url("/service/users/me/companies/{$existing->id}"));
|
||||
}
|
||||
|
||||
$company = new Company([
|
||||
'portal' => $this->resolveRequestedPortal(
|
||||
$validated['portal'] ?? null,
|
||||
$user->portal,
|
||||
)->value,
|
||||
'portal' => $portal->value,
|
||||
'type' => CompanyType::Company->value,
|
||||
'name' => $validated['name'],
|
||||
'address' => $validated['address'] ?? null,
|
||||
|
|
|
|||
|
|
@ -108,9 +108,11 @@ class LegacyPressReleaseController extends Controller
|
|||
]);
|
||||
$pressRelease->uuid = (string) Str::uuid();
|
||||
$pressRelease->user_id = $user->id;
|
||||
// Firmen können seit 1a.3 `both` sein — das ist kein Ziel-Portal für
|
||||
// eine Meldung. Dann entscheidet das Portal des API-Nutzers.
|
||||
$pressRelease->portal = $this->resolveRequestedPortal(
|
||||
$validated['portal'] ?? null,
|
||||
$company->portal,
|
||||
$company->portal === Portal::Both ? $user->portal : $company->portal,
|
||||
);
|
||||
$pressRelease->ai_declaration_confirmed_at = now();
|
||||
$pressRelease->slug = $pressRelease->generateUniqueSlug($pressRelease->title, [
|
||||
|
|
|
|||
|
|
@ -161,6 +161,31 @@ class Company extends Model
|
|||
return $this->gone_at !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trägt eine fremde, lebende Firma auf einem ANDEREN Portal diesen Slug?
|
||||
* Dann darf diese Firma nicht `both` werden: Die öffentliche Auflösung
|
||||
* fragt `portal IN (eigenes, both)` und fände zwei Firmen.
|
||||
*
|
||||
* Die anderen Portale werden ausdrücklich aufgezählt statt `portal != ?`
|
||||
* — nur so greift der Unique-Index (portal, slug).
|
||||
*
|
||||
* @param list<int> $exceptIds Firmen, die nicht zählen (z. B. die eigene Adressgruppe)
|
||||
*/
|
||||
public function slugTakenOnOtherPortals(array $exceptIds = []): bool
|
||||
{
|
||||
$otherPortals = array_values(array_diff(
|
||||
array_map(fn (Portal $portal): string => $portal->value, Portal::cases()),
|
||||
[$this->portal->value],
|
||||
));
|
||||
|
||||
return static::query()
|
||||
->withoutGlobalScope(PortalScope::class)
|
||||
->whereIn('portal', $otherPortals)
|
||||
->where('slug', $this->slug)
|
||||
->whereKeyNot([$this->getKey(), ...$exceptIds])
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalisierte Firmen-E-Mail — der Schlüssel der Newsroom-Konsolidierung.
|
||||
*
|
||||
|
|
|
|||
107
app/Services/Company/ApiNewsroomAssignment.php
Normal file
107
app/Services/Company/ApiNewsroomAssignment.php
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\Company;
|
||||
|
||||
use App\Enums\ActivityType;
|
||||
use App\Enums\CompanyAddressClass;
|
||||
use App\Enums\Portal;
|
||||
use App\Models\Company;
|
||||
use App\Models\CompanyEmailGroup;
|
||||
use App\Models\User;
|
||||
use App\Scopes\PortalScope;
|
||||
use App\Services\ActivityLog;
|
||||
|
||||
/**
|
||||
* Laufende Zuordnung neuer API-Firmen zu einem bestehenden Newsroom
|
||||
* (Roadmap 1a.4).
|
||||
*
|
||||
* Der Alt-Vertrag der API legt vor jeder Meldung eine Firma an — connektar
|
||||
* schickt je Meldung `POST /service/users/me/companies`. Ohne diese Klasse
|
||||
* entsteht so je Meldung ein neuer Newsroom, und die Zusammenführung 1a.3
|
||||
* müsste nachts nachlaufen. Hier wird stattdessen VOR dem Anlegen geprüft,
|
||||
* ob unter derselben Firmen-E-Mail schon eine Firma mit demselben (oder
|
||||
* normalisiert demselben) Namen steht. Dann bekommt der Aufrufer deren ID
|
||||
* zurück — der Vertrag (201 + Location) bleibt, nur die Firma ist die alte.
|
||||
*
|
||||
* Dieselben Regeln wie die Klassifizierung (1a.2): Schlüssel ist die
|
||||
* Firmen-E-Mail, „derselbe Name" ist `CompanyAddressClassifier::namesMatch()`.
|
||||
* Unter einer Sammeladresse (Agentur) wird nur die Firma mit passendem Namen
|
||||
* wiederverwendet — ein neuer Kunde der Agentur bekommt wie bisher eine neue
|
||||
* Firma. Unter einer nie-veröffentlicht-Adresse wird nie zugeordnet.
|
||||
*/
|
||||
class ApiNewsroomAssignment
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CompanyAddressClassifier $classifier,
|
||||
private readonly ActivityLog $activityLog,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Bestehende Firma für diese Anfrage — oder null, dann legt der Aufrufer
|
||||
* wie bisher eine neue an.
|
||||
*
|
||||
* Nebenwirkungen bei Treffer: Der API-Nutzer wird als Mitglied an die
|
||||
* Firma gehängt (sonst dürfte er dort keine Meldung anlegen), und liegt
|
||||
* die Firma auf dem anderen Portal, wird sie `both` — ihre Meldungen
|
||||
* behalten ihr Portal, die neue bekommt das angefragte.
|
||||
*/
|
||||
public function assign(User $user, ?string $email, string $name, Portal $portal): ?Company
|
||||
{
|
||||
$key = Company::emailKey($email);
|
||||
|
||||
if ($key === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$group = CompanyEmailGroup::forEmail($key);
|
||||
|
||||
if ($group?->effectiveClassification() === CompanyAddressClass::NeverPublished) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Schlüssel UND Rohwert abfragen: Der Index auf `email` greift so auf
|
||||
// jeder Datenbank, und die Kleinschreibung entscheidet der Filter
|
||||
// danach — nicht die Kollation.
|
||||
$matches = Company::query()
|
||||
->withoutGlobalScope(PortalScope::class)
|
||||
->whereIn('email', array_values(array_unique([$key, trim((string) $email)])))
|
||||
->whereNull('gone_at')
|
||||
->orderBy('id')
|
||||
->get()
|
||||
->filter(fn (Company $company): bool => Company::emailKey($company->email) === $key
|
||||
&& $this->classifier->namesMatch($company->name, $name));
|
||||
|
||||
if ($matches->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Mehrere Treffer gibt es nach 1a.3 nur noch als Slug-Kollision oder
|
||||
// Handentscheidung. Dann die, die auf dem angefragten Portal schon
|
||||
// erreichbar ist, sonst die älteste.
|
||||
$company = $matches->first(fn (Company $company): bool => $company->portal === $portal || $company->portal === Portal::Both)
|
||||
?? $matches->first();
|
||||
|
||||
if ($portal !== Portal::Both && $company->portal !== Portal::Both && $company->portal !== $portal) {
|
||||
if ($company->slugTakenOnOtherPortals()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$company->forceFill(['portal' => Portal::Both->value])->save();
|
||||
}
|
||||
|
||||
if (! $company->users()->whereKey($user->id)->exists() && $company->owner_user_id !== $user->id) {
|
||||
$company->users()->attach($user->id, ['role' => 'member']);
|
||||
}
|
||||
|
||||
$this->activityLog->record(
|
||||
ActivityType::Correction,
|
||||
sprintf('API-Firma „%s“ dem bestehenden Newsroom #%d „%s“ zugeordnet (%s)', $name, $company->getKey(), $company->name, $key),
|
||||
$user,
|
||||
$company,
|
||||
['api_newsroom_assignment' => true, 'requested_name' => $name, 'portal' => $portal->value],
|
||||
actorLabel: 'api',
|
||||
);
|
||||
|
||||
return $company;
|
||||
}
|
||||
}
|
||||
|
|
@ -237,6 +237,16 @@ class CompanyAddressClassifier
|
|||
return count($representatives);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sind zwei Firmennamen dieselbe Firma? Dieselbe Frage, die die
|
||||
* Klassifizierung je Adresse stellt — hier für die laufende Zuordnung
|
||||
* (1a.4) einer einzelnen neuen Firma zu einer bestehenden.
|
||||
*/
|
||||
public function namesMatch(string $a, string $b): bool
|
||||
{
|
||||
return $this->similar($this->normalise($a), $this->normalise($b));
|
||||
}
|
||||
|
||||
private function similar(string $a, string $b): bool
|
||||
{
|
||||
if ($a === $b) {
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ class CompanyMergeService
|
|||
$targetPortal = $this->targetPortal($survivor, $portals);
|
||||
|
||||
if ($targetPortal === Portal::Both && $survivor->portal !== Portal::Both
|
||||
&& $this->slugTakenElsewhere($survivor, $candidates)) {
|
||||
&& $survivor->slugTakenOnOtherPortals($candidates->modelKeys())) {
|
||||
$blocked[] = $survivor->slug;
|
||||
|
||||
continue;
|
||||
|
|
@ -294,33 +294,6 @@ class CompanyMergeService
|
|||
return $survivor->portal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trägt eine fremde, lebende Firma auf einem anderen Portal denselben
|
||||
* Slug? Dann fände die öffentliche Auflösung (`portal IN (eigenes, both)`)
|
||||
* nach dem Wechsel auf `both` zwei Firmen. Die eigenen Gruppenmitglieder
|
||||
* zählen nicht — die sind danach soft-deleted.
|
||||
*
|
||||
* @param Collection<int, Company> $group
|
||||
*/
|
||||
private function slugTakenElsewhere(Company $survivor, Collection $group): bool
|
||||
{
|
||||
// Die anderen Portale ausdrücklich aufzählen statt `portal != ?`:
|
||||
// Nur so greift der Unique-Index (portal, slug) — der Unterschied ist
|
||||
// 24 ms gegen 0 ms je Adresse, bei vierzehntausend Adressen sechs
|
||||
// Minuten gegen eine Sekunde.
|
||||
$otherPortals = array_values(array_diff(
|
||||
array_map(fn (Portal $portal): string => $portal->value, Portal::cases()),
|
||||
[$survivor->portal->value],
|
||||
));
|
||||
|
||||
return Company::query()
|
||||
->withoutGlobalScope(PortalScope::class)
|
||||
->whereIn('portal', $otherPortals)
|
||||
->where('slug', $survivor->slug)
|
||||
->whereKeyNot($group->modelKeys())
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $companyIds
|
||||
* @return Builder<PressRelease>
|
||||
|
|
|
|||
|
|
@ -852,3 +852,46 @@ Offen: die 6 Slug-Kollisionen einzeln ansehen (pizza.de, x84.de,
|
|||
2× eposso-energy, anzeiger24, togev). Möglich ist, die fremde Firma mit dem
|
||||
blockierenden Slug zu prüfen — oft ist das dieselbe Firma unter anderer
|
||||
Adresse (Vorschlag-Klasse) — und danach mit `--email=` nachzuziehen.
|
||||
|
||||
## 1a.4 — Laufende Zuordnung neuer API-Meldungen
|
||||
|
||||
Der Alt-Vertrag der API legt vor jeder Meldung eine Firma an: connektar
|
||||
schickt je Meldung `POST /service/users/me/companies` und dann die Meldung
|
||||
mit der zurückgegebenen ID. Deshalb entstand je Meldung ein Newsroom —
|
||||
`a.helf@ubc-collection.com` hatte sieben aus einer Woche.
|
||||
|
||||
**`ApiNewsroomAssignment`** prüft jetzt VOR dem Anlegen, ob unter derselben
|
||||
Firmen-E-Mail schon eine Firma mit demselben Namen steht — „derselbe Name"
|
||||
ist `CompanyAddressClassifier::namesMatch()`, also exakt die Regel der
|
||||
Klassifizierung (Rechtsform, Tippfehler, gemeinsamer Anfang). Bei Treffer
|
||||
bekommt der Aufrufer deren ID zurück. Der Vertrag bleibt: 201 + Location.
|
||||
connektar merkt nichts davon, außer dass die ID eine alte ist.
|
||||
|
||||
Regeln:
|
||||
|
||||
- **Kein Treffer bei abweichendem Namen** → neue Firma wie bisher. Das ist
|
||||
der Agenturfall: neuer Kunde unter der Sammeladresse.
|
||||
- **Nie-veröffentlicht-Adresse** (`company_email_groups`, Handentscheidung
|
||||
übersteuert) → nie zuordnen.
|
||||
- **410-entfernte und aufgegangene (soft-deleted) Firmen** werden nicht
|
||||
getroffen — der Überlebende schon.
|
||||
- **Anderes Portal:** Die Firma wird `both`, die neue Meldung bekommt das
|
||||
angefragte Portal, die alten behalten ihres. Ist der Slug auf dem anderen
|
||||
Portal fremd belegt, bleibt es beim alten Verhalten (neue Firma) — dieselbe
|
||||
Kollisionsregel wie in 1a.3, jetzt als `Company::slugTakenOnOtherPortals()`
|
||||
an einer Stelle.
|
||||
- Der API-Nutzer wird als `member` an die Firma gehängt, sonst dürfte er dort
|
||||
keine Meldung anlegen. Jede Zuordnung steht im Aktivitätsverlauf
|
||||
(„API-Firma … dem bestehenden Newsroom #… zugeordnet").
|
||||
|
||||
**Folgeänderung in `LegacyPressReleaseController`:** `both` ist kein
|
||||
Ziel-Portal für eine Meldung. Fällt die Portal-Auflösung auf das Portal der
|
||||
Firma zurück und das ist `both`, entscheidet das Portal des API-Nutzers.
|
||||
Vorher hätte eine Meldung ohne Parameter und Domain-Kontext `both` bekommen.
|
||||
|
||||
Tests: `tests/Feature/Api/ApiNewsroomAssignmentTest.php` (9).
|
||||
|
||||
**Messung:** `companies:classify-addresses` nach ein, zwei Wochen — Dublette
|
||||
und Namensvariante müssen bei den 7 aus den Slug-Kollisionen bleiben. Steigt
|
||||
die Zahl, trifft die Zuordnung nicht; `companies:merges` und der
|
||||
Aktivitätsverlauf zeigen, was zugeordnet wurde.
|
||||
|
|
|
|||
164
tests/Feature/Api/ApiNewsroomAssignmentTest.php
Normal file
164
tests/Feature/Api/ApiNewsroomAssignmentTest.php
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\CompanyAddressClass;
|
||||
use App\Enums\Portal;
|
||||
use App\Models\ActivityEvent;
|
||||
use App\Models\Category;
|
||||
use App\Models\Company;
|
||||
use App\Models\CompanyEmailGroup;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolesAndPermissionsSeeder;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Laufende Zuordnung neuer API-Firmen zum bestehenden Newsroom über die
|
||||
* Firmen-E-Mail (Roadmap 1a.4). connektar legt je Meldung eine Firma an —
|
||||
* ohne die Zuordnung entstünde je Meldung ein neuer Newsroom.
|
||||
*/
|
||||
function assignmentApiUser(Portal $portal = Portal::Businessportal24): User
|
||||
{
|
||||
test()->seed(RolesAndPermissionsSeeder::class);
|
||||
config()->set('billing.enforce_booking', false);
|
||||
config()->set('app.theme', 'portal');
|
||||
|
||||
$user = User::factory()->create(['is_active' => true, 'portal' => $portal]);
|
||||
$user->forceFill(['registration_type' => 'apiuser'])->save();
|
||||
|
||||
Sanctum::actingAs($user, ['press-releases:read', 'press-releases:write', 'companies:read']);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $overrides
|
||||
*/
|
||||
function postApiCompany(array $overrides = []): int
|
||||
{
|
||||
$response = test()->postJson('/service/users/me/companies', [
|
||||
'company' => array_merge(['name' => 'Muster GmbH', 'email' => 'info@muster.de'], $overrides),
|
||||
])->assertCreated();
|
||||
|
||||
return (int) basename((string) $response->headers->get('Location'));
|
||||
}
|
||||
|
||||
function assignmentGroup(string $email, CompanyAddressClass $class, ?CompanyAddressClass $manual = null): void
|
||||
{
|
||||
CompanyEmailGroup::query()->create([
|
||||
'email' => $email, 'classification' => $class, 'manual_classification' => $manual,
|
||||
'companies_count' => 30, 'distinct_names' => 30, 'published_companies_count' => 0, 'threshold' => 3, 'classified_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
test('the same company under the same email is reused instead of created', function () {
|
||||
/** @var TestCase $this */
|
||||
$user = assignmentApiUser();
|
||||
$existing = Company::factory()->create(['email' => 'info@muster.de', 'name' => 'Muster GmbH', 'portal' => 'businessportal24']);
|
||||
|
||||
$id = postApiCompany(['email' => 'Info@Muster.de']);
|
||||
|
||||
expect($id)->toBe($existing->id)
|
||||
->and(Company::withoutGlobalScopes()->where('email', 'like', '%muster.de')->count())->toBe(1)
|
||||
->and($existing->users()->whereKey($user->id)->wherePivot('role', 'member')->exists())->toBeTrue()
|
||||
->and(ActivityEvent::query()->where('subject_id', $existing->id)->where('description', 'like', 'API-Firma%')->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
test('a name that differs only by legal form or a typo still matches', function () {
|
||||
/** @var TestCase $this */
|
||||
assignmentApiUser();
|
||||
$existing = Company::factory()->create(['email' => 'info@muster.de', 'name' => 'Muster Unternehmensberatung', 'portal' => 'businessportal24']);
|
||||
|
||||
expect(postApiCompany(['name' => 'Muster Unternehmensberatung GmbH & Co. KG']))->toBe($existing->id)
|
||||
->and(postApiCompany(['name' => 'Muster Unternehmensbertung']))->toBe($existing->id);
|
||||
});
|
||||
|
||||
test('a different name under the same address gets its own company as before', function () {
|
||||
/** @var TestCase $this */
|
||||
assignmentApiUser();
|
||||
Company::factory()->create(['email' => 'presse@agentur.de', 'name' => 'Kunde Eins GmbH', 'portal' => 'businessportal24']);
|
||||
|
||||
$id = postApiCompany(['name' => 'Ganz Anderer Kunde AG', 'email' => 'presse@agentur.de']);
|
||||
|
||||
expect(Company::withoutGlobalScopes()->find($id)->name)->toBe('Ganz Anderer Kunde AG')
|
||||
->and(Company::withoutGlobalScopes()->where('email', 'presse@agentur.de')->count())->toBe(2);
|
||||
});
|
||||
|
||||
test('under a never-published address nothing is ever assigned', function () {
|
||||
/** @var TestCase $this */
|
||||
assignmentApiUser();
|
||||
assignmentGroup('yahoo@yahoo.com', CompanyAddressClass::NeverPublished);
|
||||
$existing = Company::factory()->create(['email' => 'yahoo@yahoo.com', 'name' => 'Muster GmbH', 'portal' => 'businessportal24']);
|
||||
|
||||
expect(postApiCompany(['email' => 'yahoo@yahoo.com']))->not->toBe($existing->id);
|
||||
});
|
||||
|
||||
test('a manual decision can turn a never-published address back into a normal one', function () {
|
||||
/** @var TestCase $this */
|
||||
assignmentApiUser();
|
||||
assignmentGroup('presse@pr4you.de', CompanyAddressClass::NeverPublished, manual: CompanyAddressClass::CollectionAddress);
|
||||
$existing = Company::factory()->create(['email' => 'presse@pr4you.de', 'name' => 'Muster GmbH', 'portal' => 'businessportal24']);
|
||||
|
||||
expect(postApiCompany(['email' => 'presse@pr4you.de']))->toBe($existing->id);
|
||||
});
|
||||
|
||||
test('a company on the other portal becomes both and the release lands on the requested portal', function () {
|
||||
/** @var TestCase $this */
|
||||
$user = assignmentApiUser(Portal::Presseecho);
|
||||
$existing = Company::factory()->create(['email' => 'info@muster.de', 'name' => 'Muster GmbH', 'portal' => 'businessportal24']);
|
||||
$category = Category::factory()->create();
|
||||
Queue::fake();
|
||||
|
||||
$id = postApiCompany();
|
||||
|
||||
expect($id)->toBe($existing->id)
|
||||
->and($existing->fresh()->portal)->toBe(Portal::Both);
|
||||
|
||||
$this->postJson('/service/pressreleases', [
|
||||
'press_release' => [
|
||||
'company_id' => $id,
|
||||
'category_id' => $category->legacy_id ?? $category->id,
|
||||
'language' => 'de',
|
||||
'title' => 'Zuordnungs-Test',
|
||||
'text' => 'Inhalt der Test-Pressemitteilung mit genug Text.',
|
||||
],
|
||||
])->assertCreated();
|
||||
|
||||
// `both` ist kein Ziel-Portal für eine Meldung — ohne Parameter und
|
||||
// Domain-Kontext entscheidet das Portal des API-Nutzers.
|
||||
$release = PressRelease::withoutGlobalScopes()->where('title', 'Zuordnungs-Test')->firstOrFail();
|
||||
expect($release->portal)->toBe(Portal::Presseecho)
|
||||
->and($release->company_id)->toBe($existing->id)
|
||||
->and($release->user_id)->toBe($user->id);
|
||||
});
|
||||
|
||||
test('a slug taken on the other portal prevents widening and falls back to a new company', function () {
|
||||
/** @var TestCase $this */
|
||||
assignmentApiUser(Portal::Presseecho);
|
||||
$existing = Company::factory()->create(['email' => 'info@muster.de', 'name' => 'Muster GmbH', 'portal' => 'businessportal24', 'slug' => 'muster-gmbh']);
|
||||
Company::factory()->create(['email' => 'anders@fremd.de', 'name' => 'Muster GmbH', 'portal' => 'presseecho', 'slug' => 'muster-gmbh']);
|
||||
|
||||
$id = postApiCompany();
|
||||
|
||||
expect($id)->not->toBe($existing->id)
|
||||
->and($existing->fresh()->portal)->toBe(Portal::Businessportal24);
|
||||
});
|
||||
|
||||
test('a merged-away company is not matched, its survivor is', function () {
|
||||
/** @var TestCase $this */
|
||||
assignmentApiUser(Portal::Presseecho);
|
||||
$survivor = Company::factory()->create(['email' => 'info@muster.de', 'name' => 'Muster GmbH', 'portal' => 'both']);
|
||||
$gone = Company::factory()->create(['email' => 'info@muster.de', 'name' => 'Muster GmbH', 'portal' => 'presseecho']);
|
||||
$gone->delete();
|
||||
|
||||
expect(postApiCompany())->toBe($survivor->id);
|
||||
});
|
||||
|
||||
test('without an email a company is created as before', function () {
|
||||
/** @var TestCase $this */
|
||||
assignmentApiUser();
|
||||
$existing = Company::factory()->create(['email' => null, 'name' => 'Muster GmbH', 'portal' => 'businessportal24']);
|
||||
|
||||
expect(postApiCompany(['email' => null]))->not->toBe($existing->id);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue