user()->tokenCan('press-releases:read'), 403); $pressReleases = PressRelease::withoutGlobalScopes() ->where('user_id', $request->user()->id) ->with(['company:id,name,portal,legacy_portal,legacy_id', 'category.translations', 'images']) ->when($request->query('status'), fn ($query, string $status) => $query->where('status', $status)) ->latest() ->paginate(min((int) $request->query('per_page', 25), 100)); return PressReleaseResource::collection($pressReleases); } public function store(StorePressReleaseRequest $request): JsonResponse { $validated = $request->validated(); $company = $this->findOwnedCompany((int) $validated['company_id'], $request); abort_unless($company !== null, 403); $pressRelease = PressRelease::withoutGlobalScopes()->create([ ...$validated, 'uuid' => (string) Str::uuid(), 'user_id' => $request->user()->id, 'portal' => $company->portal->value, 'slug' => $this->uniqueSlug( Str::slug($validated['title']), $company->portal->value, $validated['language'], ), // Über die API angelegte PMs sind immer Entwürfe. Der Übergang nach // `review` erfolgt ausschließlich über die explizite submit-Route, // damit Blacklist-/Quota-/Log-Prüfung garantiert durchlaufen werden. 'status' => PressReleaseStatus::Draft->value, ]); return PressReleaseResource::make( $pressRelease->load(['company:id,name,portal,legacy_portal,legacy_id', 'category.translations', 'images']) )->response()->setStatusCode(201); } public function show(Request $request, int $pressRelease): PressReleaseResource { abort_unless($request->user()->tokenCan('press-releases:read'), 403); $pressRelease = $this->findOwnedPressRelease($pressRelease, $request); abort_unless($pressRelease !== null, 403); return PressReleaseResource::make( $pressRelease->load(['company:id,name,portal,legacy_portal,legacy_id', 'category.translations', 'images']) ); } public function update(UpdatePressReleaseRequest $request, int $pressRelease): PressReleaseResource|JsonResponse { $pressRelease = $this->findOwnedPressRelease($pressRelease, $request); abort_unless($pressRelease !== null, 403); if (! in_array($pressRelease->status->value, ['draft', 'rejected'], true)) { return response()->json([ 'message' => 'Only draft or rejected press releases may be edited.', ], 409); } $validated = $request->validated(); $company = isset($validated['company_id']) ? $this->findOwnedCompany((int) $validated['company_id'], $request) : $this->findOwnedCompany((int) $pressRelease->company_id, $request); abort_unless($company !== null, 403); $language = $validated['language'] ?? $pressRelease->language; $title = $validated['title'] ?? $pressRelease->title; $portal = $company->portal->value; $pressRelease->fill([ ...$validated, 'portal' => $portal, ]); if ( array_key_exists('title', $validated) || array_key_exists('language', $validated) || array_key_exists('company_id', $validated) ) { $pressRelease->slug = $this->uniqueSlug(Str::slug($title), $portal, $language, $pressRelease->id); } $pressRelease->save(); // Inhaltliche Änderung einer bereits geprüften/bewerteten PM → neu prüfen. if ($pressRelease->wasChanged(['title', 'text'])) { $service = app(PressReleaseService::class); $service->reclassifyIfClassified($pressRelease); $service->rescoreIfScored($pressRelease); } return PressReleaseResource::make( $pressRelease->load(['company:id,name,portal,legacy_portal,legacy_id', 'category.translations', 'images']) ); } /** * Reicht eine PM zur Prüfung ein – der einzige API-Weg nach `review`. * * Kapselt denselben Funnel wie das Web-Formular: Blacklist-Hard-Filter, * Statuswechsel, Status-Log und Quota werden über den * `PressReleaseService` garantiert durchlaufen. `published` bleibt über die * API unerreichbar. */ public function submit(Request $request, int $pressRelease, PressReleaseService $service): PressReleaseResource|JsonResponse { abort_unless($request->user()->tokenCan('press-releases:write'), 403); $pressRelease = $this->findOwnedPressRelease($pressRelease, $request); abort_unless($pressRelease !== null, 403); if (! in_array($pressRelease->status->value, ['draft', 'rejected'], true)) { return response()->json([ 'message' => 'Only draft or rejected press releases may be submitted for review.', ], 409); } try { $service->submitForReview($pressRelease); } catch (BookingRequiredException $exception) { return response()->json([ 'message' => $exception->getMessage(), ], 402); } catch (QuotaExceededException $exception) { return response()->json([ 'message' => $exception->getMessage(), ], 422); } catch (BlacklistViolationException $exception) { return response()->json([ 'message' => $exception->getMessage(), ], 422); } return PressReleaseResource::make( $pressRelease->fresh()->load(['company:id,name,portal,legacy_portal,legacy_id', 'category.translations', 'images']) ); } public function destroy(Request $request, int $pressRelease): JsonResponse|Response { abort_unless($request->user()->tokenCan('press-releases:write'), 403); $pressRelease = $this->findOwnedPressRelease($pressRelease, $request); abort_unless($pressRelease !== null, 403); if ($pressRelease->status->value === 'published') { return response()->json([ 'message' => 'Published press releases cannot be deleted via API.', ], 409); } $pressRelease->delete(); return response()->noContent(); } private function findOwnedCompany(int $companyId, Request $request): ?Company { return Company::withoutGlobalScopes() ->whereKey($companyId) ->where(function ($query) use ($request): void { $query->where('owner_user_id', $request->user()->id) ->orWhereHas('users', fn ($users) => $users->whereKey($request->user()->id)); }) ->first(); } private function findOwnedPressRelease(int $pressReleaseId, Request $request): ?PressRelease { return PressRelease::withoutGlobalScopes() ->whereKey($pressReleaseId) ->where('user_id', $request->user()->id) ->first(); } private function uniqueSlug(string $baseSlug, string $portal, string $language, ?int $excludeId = null): string { $slug = $baseSlug !== '' ? $baseSlug : Str::random(8); $candidate = $slug; $suffix = 2; while ( PressRelease::withoutGlobalScopes() ->where('portal', $portal) ->where('language', $language) ->where('slug', $candidate) ->when($excludeId !== null, fn ($query) => $query->whereKeyNot($excludeId)) ->exists() ) { $candidate = "{$slug}-{$suffix}"; $suffix++; } return $candidate; } }