*/ 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 * } */ 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 $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 */ 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)) ))); } }