feat: implement AI styling foundation and billing scope rework
This commit is contained in:
209
app/Services/AiEditing/AiStylingEntitlementService.php
Normal file
209
app/Services/AiEditing/AiStylingEntitlementService.php
Normal file
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\AiEditing;
|
||||
|
||||
use App\Models\Event;
|
||||
use App\Models\EventPackage;
|
||||
use App\Models\EventPackageAddon;
|
||||
use App\Models\Package;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class AiStylingEntitlementService
|
||||
{
|
||||
public function packageFeatureKey(): string
|
||||
{
|
||||
$featureKey = trim((string) config('ai-editing.entitlements.package_feature', 'ai_styling'));
|
||||
|
||||
return $featureKey !== '' ? $featureKey : 'ai_styling';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function addonKeys(): array
|
||||
{
|
||||
return array_values(array_filter(array_map(
|
||||
static fn (mixed $key): string => trim((string) $key),
|
||||
(array) config('ai-editing.entitlements.addon_keys', ['ai_styling_unlock'])
|
||||
)));
|
||||
}
|
||||
|
||||
public function lockedMessage(): string
|
||||
{
|
||||
$message = trim((string) config('ai-editing.entitlements.locked_message', ''));
|
||||
|
||||
if ($message !== '') {
|
||||
return $message;
|
||||
}
|
||||
|
||||
return 'AI editing requires the Premium package or the AI Styling add-on.';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* allowed: bool,
|
||||
* granted_by: 'package'|'addon'|null,
|
||||
* required_feature: string,
|
||||
* addon_keys: array<int, string>
|
||||
* }
|
||||
*/
|
||||
public function resolveForEvent(Event $event): array
|
||||
{
|
||||
$requiredFeature = $this->packageFeatureKey();
|
||||
$addonKeys = $this->addonKeys();
|
||||
$eventPackage = $this->resolveEventPackage($event);
|
||||
|
||||
if (! $eventPackage) {
|
||||
return [
|
||||
'allowed' => false,
|
||||
'granted_by' => null,
|
||||
'required_feature' => $requiredFeature,
|
||||
'addon_keys' => $addonKeys,
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->packageGrantsAccess($eventPackage->package, $requiredFeature)) {
|
||||
return [
|
||||
'allowed' => true,
|
||||
'granted_by' => 'package',
|
||||
'required_feature' => $requiredFeature,
|
||||
'addon_keys' => $addonKeys,
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->addonGrantsAccess($eventPackage, $addonKeys, $requiredFeature)) {
|
||||
return [
|
||||
'allowed' => true,
|
||||
'granted_by' => 'addon',
|
||||
'required_feature' => $requiredFeature,
|
||||
'addon_keys' => $addonKeys,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'allowed' => false,
|
||||
'granted_by' => null,
|
||||
'required_feature' => $requiredFeature,
|
||||
'addon_keys' => $addonKeys,
|
||||
];
|
||||
}
|
||||
|
||||
public function hasAccessForEvent(Event $event): bool
|
||||
{
|
||||
return (bool) $this->resolveForEvent($event)['allowed'];
|
||||
}
|
||||
|
||||
private function resolveEventPackage(Event $event): ?EventPackage
|
||||
{
|
||||
$event->loadMissing('eventPackage.package', 'eventPackage.addons');
|
||||
|
||||
if ($event->eventPackage) {
|
||||
return $event->eventPackage;
|
||||
}
|
||||
|
||||
return $event->eventPackages()
|
||||
->with(['package', 'addons'])
|
||||
->orderByDesc('purchased_at')
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
}
|
||||
|
||||
private function packageGrantsAccess(?Package $package, string $requiredFeature): bool
|
||||
{
|
||||
if (! $package) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($requiredFeature, $this->normalizeFeatureList($package->features), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $addonKeys
|
||||
*/
|
||||
private function addonGrantsAccess(EventPackage $eventPackage, array $addonKeys, string $requiredFeature): bool
|
||||
{
|
||||
$addons = $eventPackage->relationLoaded('addons')
|
||||
? $eventPackage->addons
|
||||
: $eventPackage->addons()
|
||||
->where('status', 'completed')
|
||||
->get();
|
||||
|
||||
return $addons->contains(function (EventPackageAddon $addon) use ($addonKeys, $requiredFeature): bool {
|
||||
if (! $this->addonIsActive($addon)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($addonKeys !== [] && in_array((string) $addon->addon_key, $addonKeys, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$metadataFeatures = $this->normalizeFeatureList(
|
||||
Arr::get($addon->metadata ?? [], 'entitlements.features', Arr::get($addon->metadata ?? [], 'features', []))
|
||||
);
|
||||
|
||||
return in_array($requiredFeature, $metadataFeatures, true);
|
||||
});
|
||||
}
|
||||
|
||||
private function addonIsActive(EventPackageAddon $addon): bool
|
||||
{
|
||||
if ($addon->status !== 'completed') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$expiryCandidates = [
|
||||
Arr::get($addon->metadata ?? [], 'entitlements.expires_at'),
|
||||
Arr::get($addon->metadata ?? [], 'expires_at'),
|
||||
Arr::get($addon->metadata ?? [], 'valid_until'),
|
||||
];
|
||||
|
||||
foreach ($expiryCandidates as $candidate) {
|
||||
if (! is_string($candidate) || trim($candidate) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$expiresAt = CarbonImmutable::parse($candidate);
|
||||
} catch (\Throwable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($expiresAt->isPast()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 array_values(array_filter(array_map(
|
||||
static fn (mixed $feature): string => trim((string) $feature),
|
||||
$value
|
||||
)));
|
||||
}
|
||||
|
||||
return array_values(array_filter(array_map(
|
||||
static fn (mixed $feature): string => trim((string) $feature),
|
||||
array_keys(array_filter($value, static fn (mixed $enabled): bool => (bool) $enabled))
|
||||
)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user