API: KI-Deklaration als v1-Pflichtfeld, Legacy-Default fuer /service (Entscheidung 10.07.)

Befund: v1 hatte keine KI-Deklarationsfelder — Submit lief seit der
KI-Kennzeichnungspflicht (09.07.) immer auf 422 und haette alle
Legacy-API-Kunden blockiert (Suite uebersah es: Factory deklariert).

- v1-Submit verlangt ai_text_declaration; der Call gilt dokumentiert
  als verbindliche Bestaetigung (Art. 50 KI-VO) und schliesst die
  Archiv-Kenntnisnahme (AGB par. 13) ein
- Bild-Upload verlangt ai_declaration (original/ai_generated/ai_edited)
- Legacy-/service-Aliasse defaulten auf none/original — das
  API-Kunden-Mailing (R6.4) weist auf die Erklaerungswirkung hin
- OpenAPI v1.yml + lang-Hinweis aktualisiert; Tests erweitert
  (Submit/Image/LegacyCompatibility)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-10 12:41:24 +00:00
parent 9b2409ee54
commit 5c91e2e281
11 changed files with 213 additions and 10 deletions

View file

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api\V1;
use App\Enums\AiTextDeclaration;
use App\Enums\PressReleaseStatus;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V1\StorePressReleaseRequest;
@ -19,6 +20,7 @@ use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Http\Response;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
class PressReleaseController extends Controller
{
@ -56,6 +58,15 @@ class PressReleaseController extends Controller
// `review` erfolgt ausschließlich über die explizite submit-Route,
// damit Blacklist-/Quota-/Log-Prüfung garantiert durchlaufen werden.
'status' => PressReleaseStatus::Draft->value,
// Legacy-/service-Clients kennen die KI-Kennzeichnung (Art. 50
// KI-VO) nicht — Entscheidung 10.07.: fehlende Angabe gilt als
// „ohne generative KI erstellt"; das API-Kunden-Mailing (R6.4)
// weist darauf hin, dass die weitere Nutzung diese Erklärung
// darstellt. v1-Clients deklarieren explizit beim Submit.
...($this->isLegacyServiceRequest($request) ? [
'ai_text_declaration' => AiTextDeclaration::None->value,
'ai_declaration_confirmed_at' => now(),
] : []),
]);
return PressReleaseResource::make(
@ -133,6 +144,16 @@ class PressReleaseController extends Controller
public function submit(Request $request, int $pressRelease, PressReleaseService $service): PressReleaseResource|JsonResponse
{
abort_unless($request->user()->tokenCan('press-releases:write'), 403);
// KI-Kennzeichnungspflicht (Art. 50 KI-VO) auch via API: der
// Submit-Call trägt den deklarierten KI-Grad des Textes und gilt
// dokumentiert als verbindliche Bestätigung der Deklaration sowie
// Kenntnisnahme der dauerhaften Archivierung (AGB § 13) —
// Entscheidung 10.07.2026.
$validated = $request->validate([
'ai_text_declaration' => ['required', Rule::enum(AiTextDeclaration::class)],
]);
$pressRelease = $this->findOwnedPressRelease($pressRelease, $request);
abort_unless($pressRelease !== null, 403);
@ -142,6 +163,11 @@ class PressReleaseController extends Controller
], 409);
}
$pressRelease->update([
'ai_text_declaration' => $validated['ai_text_declaration'],
'ai_declaration_confirmed_at' => now(),
]);
try {
$service->submitForReview($pressRelease);
} catch (BookingRequiredException $exception) {
@ -211,6 +237,16 @@ class PressReleaseController extends Controller
->first();
}
/**
* Kam der Call über die Legacy-Aliasse (/service/…, routes/legacy-api.php)?
* Dort gelten Kompatibilitäts-Defaults für Felder, die die Altportale
* nicht kannten (KI-Kennzeichnung).
*/
private function isLegacyServiceRequest(Request $request): bool
{
return str_starts_with((string) $request->route()?->getName(), 'legacy-service.');
}
private function uniqueSlug(string $baseSlug, string $portal, string $language, ?int $excludeId = null): string
{
$slug = $baseSlug !== '' ? $baseSlug : Str::random(8);

View file

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Api\V1;
use App\Enums\AiImageDeclaration;
use App\Enums\PressReleaseStatus;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\V1\StorePressReleaseImageRequest;
@ -63,6 +64,10 @@ class PressReleaseImageController extends Controller
'disk' => 'public',
'path' => $stored['path'],
'variants' => $stored['variants'],
// v1 verlangt die KI-Kennzeichnung je Bild (FormRequest);
// Legacy-/service-Calls ohne Feld gelten als Originalfoto
// (Kompatibilitäts-Default, Entscheidung 10.07.2026).
'ai_declaration' => $validated['ai_declaration'] ?? AiImageDeclaration::Original->value,
'title' => $validated['title'] ?? null,
'description' => $validated['description'] ?? null,
'copyright' => $validated['copyright'] ?? null,

View file

@ -2,8 +2,10 @@
namespace App\Http\Requests\Api\V1;
use App\Enums\AiImageDeclaration;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StorePressReleaseImageRequest extends FormRequest
{
@ -18,12 +20,23 @@ class StorePressReleaseImageRequest extends FormRequest
/**
* Get the validation rules that apply to the request.
*
* KI-Kennzeichnung je Bild (Art. 50 KI-VO) ist im v1-Vertrag Pflicht;
* Legacy-/service-Clients kennen das Feld nicht — dort greift der
* Kompatibilitäts-Default „original" (Entscheidung 10.07.2026).
*
* @return array<string, ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
$isLegacyServiceCall = str_starts_with((string) $this->route()?->getName(), 'legacy-service.');
return [
'image' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:8192'],
'ai_declaration' => [
Rule::requiredIf(! $isLegacyServiceCall),
'nullable',
Rule::enum(AiImageDeclaration::class),
],
'title' => ['nullable', 'string', 'max:120'],
'description' => ['nullable', 'string', 'max:500'],
'copyright' => ['nullable', 'string', 'max:255'],