Legacy-API-Paritaet S1+S2: connektar-Flow verhaltensgleich + KI-Schalter
Eigene Compat-Controller (Api/Legacy) fuer die 5 real genutzten
/service-Endpoints (Prod-Log-Beleg): Company-/Kontakt-Create (liefen
410!), PM-Create, Bild-Upload, published-only-GET — verschachtelte
Alt-Params, 201+Location+{}, Alt-Fehlerformat, Hit-Zaehlung,
Legacy-Land-Mapping.
- Anlegen = Einreichen (Alt-Verhalten); Sofort-Publish via neuer
Permission api:immediate-publication (WS-7: connektar zuweisen)
- Admin-Schalter 'KI-Pruefung Legacy-API' (app_settings-Tabelle,
Toggle auf /admin/press-releases): an = review + KI-Routing
- Admin-PM-Liste: KI-Spalte (Ampel-Badge) + manueller Pruef-Button
ohne Routing fuer ungepruefte PMs
- Mails bei API-Einlieferern an Pressekontakt, sonst Firmen-Mail,
Fallback Einlieferer (User-Entscheidung 10.07.)
- Tests: LegacyServiceParityTest (connektar-Flow end-to-end);
Suite 1182 gruen
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
5a352425d7
commit
6a45ebf607
15 changed files with 906 additions and 24 deletions
|
|
@ -10,6 +10,7 @@ use App\Models\User;
|
|||
use App\Models\UserPaymentOption;
|
||||
use App\Services\Api\LegacyApiKeyImporter;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Laravel\Sanctum\PersonalAccessToken;
|
||||
use Tests\TestCase;
|
||||
|
|
@ -125,7 +126,9 @@ test('the legacy service paths require authentication', function () {
|
|||
|
||||
test('retired legacy service endpoints answer with a helpful 410 instead of 404', function () {
|
||||
/** @var TestCase $this */
|
||||
$this->postJson('/service/users/me/companies', ['company' => ['name' => 'Test']])
|
||||
// Company-Create ist seit 10.07. ein echter Paritaets-Endpoint (S1);
|
||||
// als weiterhin abgeschalteter Alt-Pfad dient das Einzelbild-GET.
|
||||
$this->getJson('/service/pressreleases/1/images/1')
|
||||
->assertStatus(410)
|
||||
->assertJsonStructure(['message', 'account_url', 'docs_url']);
|
||||
|
||||
|
|
@ -147,21 +150,26 @@ test('legacy service calls default the ai declaration instead of breaking old cl
|
|||
|
||||
// Store ohne KI-Felder (Alt-Client): Default "ohne generative KI erstellt"
|
||||
// (Entscheidung 10.07.2026 — das API-Kunden-Mailing weist darauf hin).
|
||||
Queue::fake();
|
||||
$response = $this->postJson('/service/pressreleases', [
|
||||
'company_id' => $company->id,
|
||||
'category_id' => $category->id,
|
||||
'language' => 'de',
|
||||
'title' => 'Alt-Client-Meldung',
|
||||
'text' => 'Inhalt aus einer bestehenden Integration.',
|
||||
'press_release' => [
|
||||
'company_id' => $company->id,
|
||||
'category_id' => $category->id,
|
||||
'language' => 'de',
|
||||
'title' => 'Alt-Client-Meldung',
|
||||
'text' => 'Inhalt aus einer bestehenden Integration.',
|
||||
],
|
||||
], ['X-ApiKey' => 'legacy-key-abc123'])->assertCreated();
|
||||
|
||||
$pressRelease = PressRelease::withoutGlobalScopes()->find($response->json('data.id'));
|
||||
$pressRelease = PressRelease::withoutGlobalScopes()->find((int) basename((string) $response->headers->get('Location')));
|
||||
expect($pressRelease->ai_text_declaration?->value)->toBe('none')
|
||||
->and($pressRelease->ai_declaration_confirmed_at)->not->toBeNull();
|
||||
|
||||
// Bild-Upload ohne KI-Feld: Default Originalfoto.
|
||||
$this->postJson("/service/pressreleases/{$pressRelease->id}/images", [
|
||||
'image' => UploadedFile::fake()->image('alt-client.jpg'),
|
||||
'press_release_image' => [
|
||||
'image' => UploadedFile::fake()->image('alt-client.jpg'),
|
||||
],
|
||||
], ['X-ApiKey' => 'legacy-key-abc123'])->assertCreated();
|
||||
|
||||
expect($pressRelease->images()->first()->ai_declaration?->value)->toBe('original');
|
||||
|
|
|
|||
222
tests/Feature/Api/LegacyServiceParityTest.php
Normal file
222
tests/Feature/Api/LegacyServiceParityTest.php
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\Portal;
|
||||
use App\Enums\PressReleaseStatus;
|
||||
use App\Http\Controllers\Api\Legacy\LegacyPressReleaseController;
|
||||
use App\Jobs\ClassifyPressRelease;
|
||||
use App\Mail\PressReleasePublished;
|
||||
use App\Models\AppSetting;
|
||||
use App\Models\Category;
|
||||
use App\Models\Company;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolesAndPermissionsSeeder;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Paritäts-Tests gegen den Alt-Vertrag (dev/release/api-legacy-paritaet.md §1):
|
||||
* der reale connektar-Flow über die 5 /service-Endpoints.
|
||||
*/
|
||||
function parityApiUser(bool $immediatePublication = true): User
|
||||
{
|
||||
test()->seed(RolesAndPermissionsSeeder::class);
|
||||
config()->set('billing.enforce_booking', false);
|
||||
|
||||
$user = User::factory()->create([
|
||||
'is_active' => true,
|
||||
'portal' => Portal::Businessportal24,
|
||||
]);
|
||||
$user->forceFill(['registration_type' => 'apiuser'])->save();
|
||||
|
||||
if ($immediatePublication) {
|
||||
$user->givePermissionTo('api:immediate-publication');
|
||||
}
|
||||
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
test('the full connektar flow works like the old api with immediate publishing', function () {
|
||||
/** @var TestCase $this */
|
||||
Queue::fake();
|
||||
Storage::fake('public');
|
||||
$user = parityApiUser();
|
||||
|
||||
// 1. Firma anlegen — verschachtelte Alt-Params, 201 + Location + {}.
|
||||
$response = $this->postJson('/service/users/me/companies', [
|
||||
'company' => [
|
||||
'name' => 'Muster Energie GmbH',
|
||||
'address' => "Musterstraße 1\n33602 Bielefeld",
|
||||
'country_id' => 177,
|
||||
'email' => 'info@muster-energie.de',
|
||||
'website' => 'https://muster-energie.de',
|
||||
],
|
||||
])->assertCreated();
|
||||
|
||||
expect($response->headers->get('Location'))->toContain('/service/users/me/companies/')
|
||||
->and($response->json())->toBe([]);
|
||||
|
||||
$company = Company::withoutGlobalScopes()->latest('id')->first();
|
||||
expect($company->country_code)->toBe('DE')
|
||||
->and($company->is_active)->toBeTrue()
|
||||
->and($company->owner_user_id)->toBe($user->id);
|
||||
|
||||
// 2. Pressekontakt anlegen — salutation_id 1 → mr.
|
||||
$this->postJson("/service/users/me/companies/{$company->id}/contacts", [
|
||||
'contact' => [
|
||||
'salutation_id' => 1,
|
||||
'first_name' => 'Max',
|
||||
'last_name' => 'Mustermann',
|
||||
'email' => 'presse@muster-energie.de',
|
||||
],
|
||||
])->assertCreated();
|
||||
|
||||
$contact = $company->contacts()->first();
|
||||
expect($contact->salutation_key)->toBe('mr');
|
||||
|
||||
// 3. PM anlegen — Anlegen IST Einreichen; mit Sofort-Publish-Recht
|
||||
// und Schalter aus geht sie sofort live (Alt-Verhalten, Log-Beleg).
|
||||
$category = Category::factory()->create(['portal' => Portal::Businessportal24, 'legacy_id' => 4242]);
|
||||
|
||||
$response = $this->postJson('/service/pressreleases', [
|
||||
'press_release' => [
|
||||
'company_id' => $company->id,
|
||||
'category_id' => 4242,
|
||||
'language' => 'de',
|
||||
'title' => 'Muster Energie eröffnet neuen Standort',
|
||||
'text' => 'Vollkommen unauffälliger Inhalt aus einer Alt-Integration.',
|
||||
'contacts_list' => [$contact->id],
|
||||
],
|
||||
])->assertCreated();
|
||||
|
||||
expect($response->json())->toBe([]);
|
||||
|
||||
$pressRelease = PressRelease::withoutGlobalScopes()->latest('id')->first();
|
||||
expect($pressRelease->status)->toBe(PressReleaseStatus::Published)
|
||||
->and($pressRelease->published_at)->not->toBeNull()
|
||||
->and($pressRelease->category_id)->toBe($category->id)
|
||||
->and($pressRelease->ai_text_declaration?->value)->toBe('none')
|
||||
->and($pressRelease->ai_declaration_confirmed_at)->not->toBeNull();
|
||||
|
||||
// 4. Bild hochladen — verschachtelt, KI-Default Originalfoto.
|
||||
$this->postJson("/service/pressreleases/{$pressRelease->id}/images", [
|
||||
'press_release_image' => [
|
||||
'image' => UploadedFile::fake()->image('pressefoto.jpg', 1200, 800),
|
||||
'copyright' => 'Muster Energie GmbH',
|
||||
],
|
||||
])->assertCreated();
|
||||
|
||||
expect($pressRelease->images()->first()->ai_declaration?->value)->toBe('original');
|
||||
|
||||
// 5. GET als Veröffentlichungs-Kontrolle: flaches Alt-Objekt, Hits zählen.
|
||||
$this->getJson("/service/pressreleases/{$pressRelease->id}")
|
||||
->assertOk()
|
||||
->assertJsonStructure(['id', 'title', 'text', 'teaser_end', 'language', 'category_id', 'view_url', 'company' => ['viewUrl', 'name']])
|
||||
->assertJsonPath('company.name', 'Muster Energie GmbH');
|
||||
|
||||
expect(PressRelease::withoutGlobalScopes()->find($pressRelease->id)->hits)->toBe(1);
|
||||
});
|
||||
|
||||
test('the admin switch routes legacy submissions through the ai review instead', function () {
|
||||
/** @var TestCase $this */
|
||||
Queue::fake();
|
||||
$user = parityApiUser();
|
||||
AppSetting::setValue(LegacyPressReleaseController::SETTING_AI_REVIEW, true);
|
||||
|
||||
$company = Company::factory()->create(['portal' => Portal::Businessportal24, 'is_active' => true]);
|
||||
$user->companies()->attach($company->id, ['role' => 'owner']);
|
||||
$category = Category::factory()->create(['portal' => Portal::Businessportal24]);
|
||||
|
||||
$this->postJson('/service/pressreleases', [
|
||||
'press_release' => [
|
||||
'company_id' => $company->id,
|
||||
'category_id' => $category->id,
|
||||
'title' => 'Wird erst KI-geprüft',
|
||||
'text' => 'Inhalt, der durch die KI-Prüfung läuft.',
|
||||
],
|
||||
])->assertCreated();
|
||||
|
||||
$pressRelease = PressRelease::withoutGlobalScopes()->latest('id')->first();
|
||||
expect($pressRelease->status)->toBe(PressReleaseStatus::Review);
|
||||
Queue::assertPushed(ClassifyPressRelease::class);
|
||||
|
||||
// Polling-Kontrolle des Alt-Clients: unveröffentlicht = 404.
|
||||
$this->getJson("/service/pressreleases/{$pressRelease->id}")->assertNotFound();
|
||||
});
|
||||
|
||||
test('without the immediate publication permission legacy posts stay in review', function () {
|
||||
/** @var TestCase $this */
|
||||
Queue::fake();
|
||||
$user = parityApiUser(immediatePublication: false);
|
||||
|
||||
$company = Company::factory()->create(['portal' => Portal::Businessportal24, 'is_active' => true]);
|
||||
$user->companies()->attach($company->id, ['role' => 'owner']);
|
||||
$category = Category::factory()->create(['portal' => Portal::Businessportal24]);
|
||||
|
||||
$this->postJson('/service/pressreleases', [
|
||||
'press_release' => [
|
||||
'company_id' => $company->id,
|
||||
'category_id' => $category->id,
|
||||
'title' => 'Ohne Sonderrecht',
|
||||
'text' => 'Inhalt ohne Sofort-Publish-Privileg.',
|
||||
],
|
||||
])->assertCreated();
|
||||
|
||||
expect(PressRelease::withoutGlobalScopes()->latest('id')->first()->status)->toBe(PressReleaseStatus::Review);
|
||||
});
|
||||
|
||||
test('legacy errors use the old global and field error format', function () {
|
||||
/** @var TestCase $this */
|
||||
parityApiUser();
|
||||
|
||||
$this->postJson('/service/pressreleases', [
|
||||
'press_release' => ['title' => 'Ohne Pflichtfelder'],
|
||||
])->assertUnprocessable()
|
||||
->assertJsonStructure(['global_errors', 'field_errors']);
|
||||
|
||||
// Fremde Firma → Alt-Wortlaut als global_error.
|
||||
$foreign = Company::factory()->create(['portal' => Portal::Businessportal24]);
|
||||
$this->postJson("/service/users/me/companies/{$foreign->id}/contacts", [
|
||||
'contact' => ['first_name' => 'Max', 'last_name' => 'Muster'],
|
||||
])->assertForbidden()
|
||||
->assertJsonPath('global_errors.0', 'You may only access companies which are related to your user-account');
|
||||
});
|
||||
|
||||
test('api submissions notify the press contact instead of the api submitter', function () {
|
||||
/** @var TestCase $this */
|
||||
Mail::fake();
|
||||
Queue::fake();
|
||||
$user = parityApiUser();
|
||||
|
||||
$company = Company::factory()->create([
|
||||
'portal' => Portal::Businessportal24,
|
||||
'is_active' => true,
|
||||
'email' => 'firma@muster.de',
|
||||
]);
|
||||
$user->companies()->attach($company->id, ['role' => 'owner']);
|
||||
$category = Category::factory()->create(['portal' => Portal::Businessportal24]);
|
||||
$contact = $company->contacts()->create([
|
||||
'portal' => Portal::Businessportal24->value,
|
||||
'first_name' => 'Max',
|
||||
'last_name' => 'Mustermann',
|
||||
'email' => 'presse@muster.de',
|
||||
]);
|
||||
|
||||
$this->postJson('/service/pressreleases', [
|
||||
'press_release' => [
|
||||
'company_id' => $company->id,
|
||||
'category_id' => $category->id,
|
||||
'title' => 'Mail geht an den Pressekontakt',
|
||||
'text' => 'Inhalt für den Mail-Routing-Test.',
|
||||
'contacts_list' => [$contact->id],
|
||||
],
|
||||
])->assertCreated();
|
||||
|
||||
Mail::assertQueued(PressReleasePublished::class, fn ($mail) => $mail->hasTo('presse@muster.de'));
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue