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>
107 lines
4.2 KiB
PHP
107 lines
4.2 KiB
PHP
<?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;
|
|
}
|
|
}
|