$addonKeys * @return array{ * allowed: bool, * granted_by: 'package'|'addon'|null, * required_feature: string, * addon_keys: array * } */ public function resolveForEvent(Event $event, string $requiredFeature, array $addonKeys = []): array { $eventPackage = $this->resolveEventPackage($event); if (! $eventPackage) { return [ 'allowed' => false, 'granted_by' => null, 'required_feature' => $requiredFeature, 'addon_keys' => $addonKeys, ]; } if ($this->packageGrantsFeature($eventPackage->package, $requiredFeature)) { return [ 'allowed' => true, 'granted_by' => 'package', 'required_feature' => $requiredFeature, 'addon_keys' => $addonKeys, ]; } if ($this->addonGrantsFeature($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, ]; } 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 packageGrantsFeature(?Package $package, string $requiredFeature): bool { if (! $package) { return false; } return in_array($requiredFeature, $this->normalizeFeatureList($package->features), true); } /** * @param array $addonKeys */ private function addonGrantsFeature(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)) ))); } }