Files
fotospiel-app/app/Services/AiEditing/AiStyleAccessService.php
Codex Agent 36bed12ff9
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
feat: implement AI styling foundation and billing scope rework
2026-02-06 20:01:58 +01:00

135 lines
3.9 KiB
PHP

<?php
namespace App\Services\AiEditing;
use App\Models\AiStyle;
use App\Models\Event;
use App\Models\EventPackage;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
class AiStyleAccessService
{
public function __construct(private readonly AiStylingEntitlementService $entitlements) {}
public function canUseStyle(Event $event, AiStyle $style): bool
{
$entitlement = $this->entitlements->resolveForEvent($event);
if (! $entitlement['allowed']) {
return false;
}
$allowedSources = $this->allowedSources($style);
if (! in_array((string) $entitlement['granted_by'], $allowedSources, true)) {
return false;
}
$requiredPackageFeatures = $this->requiredPackageFeatures($style);
if ($requiredPackageFeatures === []) {
return true;
}
$packageFeatures = $this->resolveEventPackageFeatures($event);
foreach ($requiredPackageFeatures as $requiredFeature) {
if (! in_array($requiredFeature, $packageFeatures, true)) {
return false;
}
}
return true;
}
/**
* @param Collection<int, AiStyle> $styles
* @return Collection<int, AiStyle>
*/
public function filterStylesForEvent(Event $event, Collection $styles): Collection
{
return $styles
->filter(fn (AiStyle $style): bool => $this->canUseStyle($event, $style))
->values();
}
/**
* @return array<int, string>
*/
private function allowedSources(AiStyle $style): array
{
$metadataSources = $this->normalizeStringList(Arr::get($style->metadata ?? [], 'entitlements.allowed_sources', []));
if ($metadataSources !== []) {
return $metadataSources;
}
if (is_bool(Arr::get($style->metadata ?? [], 'entitlements.allow_with_addon'))) {
return Arr::get($style->metadata ?? [], 'entitlements.allow_with_addon')
? ['package', 'addon']
: ['package'];
}
return $style->is_premium ? ['package'] : ['package', 'addon'];
}
/**
* @return array<int, string>
*/
private function requiredPackageFeatures(AiStyle $style): array
{
return $this->normalizeStringList(
Arr::get($style->metadata ?? [], 'entitlements.required_package_features', [])
);
}
/**
* @return array<int, string>
*/
private function resolveEventPackageFeatures(Event $event): array
{
$eventPackage = $event->relationLoaded('eventPackage') && $event->eventPackage
? $event->eventPackage
: $event->eventPackage()->with('package')->first();
if (! $eventPackage instanceof EventPackage) {
return [];
}
$package = $eventPackage->relationLoaded('package') ? $eventPackage->package : $eventPackage->package()->first();
return $this->normalizeFeatureList($package?->features);
}
/**
* @return array<int, string>
*/
private function normalizeFeatureList(mixed $value): array
{
if (is_string($value)) {
$decoded = json_decode($value, true);
if (json_last_error() === JSON_ERROR_NONE) {
$value = $decoded;
}
}
if (! is_array($value)) {
return [];
}
if (array_is_list($value)) {
return $this->normalizeStringList($value);
}
return $this->normalizeStringList(array_keys(array_filter($value, static fn (mixed $enabled): bool => (bool) $enabled)));
}
/**
* @return array<int, string>
*/
private function normalizeStringList(array $values): array
{
return array_values(array_unique(array_filter(array_map(
static fn (mixed $value): string => trim((string) $value),
$values
))));
}
}