From 7bc30f4160df6797ea5a7a8071a4ba41d75287d3 Mon Sep 17 00:00:00 2001 From: Srinath Sankar Date: Wed, 29 Oct 2025 10:02:44 +1100 Subject: [PATCH 1/8] remove explicit `&BackedEnum` type --- DOCUMENTATION.md | 2 +- src/CashierServiceProvider.php | 3 +-- src/Concerns/HasEntitlements.php | 5 ++--- src/Contracts/FeatureEnumContract.php | 4 +++- src/Entitlement.php | 3 +-- src/Http/Middleware/UserEntitlementCheck.php | 5 ++--- src/Support/RequiresEntitlement.php | 7 +++---- 7 files changed, 13 insertions(+), 16 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 688fe10..b155ab5 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -3101,7 +3101,7 @@ enum CustomFeature: string implements FeatureEnumContract { } ``` -As long as your enum implementation satisfies the `FeatureEnumContract&BackedEnum` type, you will +As long as your enum implementation satisfies the `FeatureEnumContract` type, you will be able to pass the feature to the main API methods like `$user->hasAccess()`; Additionally, these features and the required metadata will need to be populated in the DB. diff --git a/src/CashierServiceProvider.php b/src/CashierServiceProvider.php index a0ae8ba..c2dbdc4 100644 --- a/src/CashierServiceProvider.php +++ b/src/CashierServiceProvider.php @@ -2,7 +2,6 @@ namespace Chargebee\Cashier; -use BackedEnum; use Chargebee\Cashier\Console\FeatureEnumCommand; use Chargebee\Cashier\Console\WebhookCommand; use Chargebee\Cashier\Contracts\EntitlementAccessVerifier; @@ -153,7 +152,7 @@ protected function enableEntitlements(): void // Initialise the route macro, which binds the middleware to the route // and reads the required features from the route action. - \Illuminate\Routing\Route::macro('requiresEntitlement', function (FeatureEnumContract&BackedEnum ...$features) { + \Illuminate\Routing\Route::macro('requiresEntitlement', function (FeatureEnumContract ...$features) { /** @var \Illuminate\Routing\Route $this */ $this->middleware(UserEntitlementCheck::class); diff --git a/src/Concerns/HasEntitlements.php b/src/Concerns/HasEntitlements.php index b09b261..6ef535c 100644 --- a/src/Concerns/HasEntitlements.php +++ b/src/Concerns/HasEntitlements.php @@ -2,7 +2,6 @@ namespace Chargebee\Cashier\Concerns; -use BackedEnum; use Chargebee\Cashier\Contracts\EntitlementAccessVerifier; use Chargebee\Cashier\Contracts\FeatureEnumContract; use Chargebee\Cashier\Entitlement; @@ -95,10 +94,10 @@ public function ensureEntitlements(): void /** * Check if the user has the given entitlement * - * @param FeatureEnumContract&BackedEnum ...$features + * @param FeatureEnumContract ...$features * @return bool */ - public function hasAccess(FeatureEnumContract&BackedEnum ...$features): bool + public function hasAccess(FeatureEnumContract ...$features): bool { $featureModels = Feature::whereIn('chargebee_id', $features)->get(); $feats = collect($features); diff --git a/src/Contracts/FeatureEnumContract.php b/src/Contracts/FeatureEnumContract.php index 2fc30c8..467f83b 100644 --- a/src/Contracts/FeatureEnumContract.php +++ b/src/Contracts/FeatureEnumContract.php @@ -2,7 +2,9 @@ namespace Chargebee\Cashier\Contracts; -interface FeatureEnumContract +use BackedEnum; + +interface FeatureEnumContract extends BackedEnum { /** * Returns the chargebee id of the feature. diff --git a/src/Entitlement.php b/src/Entitlement.php index bbc1cec..8e58dfe 100644 --- a/src/Entitlement.php +++ b/src/Entitlement.php @@ -2,7 +2,6 @@ namespace Chargebee\Cashier; -use BackedEnum; use Chargebee\Cashier\Contracts\FeatureEnumContract; use Chargebee\Resources\SubscriptionEntitlement\SubscriptionEntitlement as ChargebeeSubscriptionEntitlement; use Illuminate\Contracts\Support\Arrayable; @@ -48,7 +47,7 @@ public function feature(): Feature * * @return bool */ - public function providesFeature(FeatureEnumContract&BackedEnum $feature): bool + public function providesFeature(FeatureEnumContract $feature): bool { return $this->entitlement->feature_id === $feature->id(); } diff --git a/src/Http/Middleware/UserEntitlementCheck.php b/src/Http/Middleware/UserEntitlementCheck.php index 7c5c424..f4ee335 100644 --- a/src/Http/Middleware/UserEntitlementCheck.php +++ b/src/Http/Middleware/UserEntitlementCheck.php @@ -2,7 +2,6 @@ namespace Chargebee\Cashier\Http\Middleware; -use BackedEnum; use Chargebee\Cashier\Concerns\HasEntitlements; use Chargebee\Cashier\Constants; use Chargebee\Cashier\Contracts\FeatureEnumContract; @@ -30,7 +29,7 @@ public function handle(Request $request, Closure $next) // 2) Or from route macro (closure routes) if (! $features) { - /** @var null|array $fromAction */ + /** @var null|array $fromAction */ $fromAction = $route->getAction(Constants::REQUIRED_FEATURES_KEY) ?? null; if ($fromAction) { $features = $fromAction; @@ -49,7 +48,7 @@ public function handle(Request $request, Closure $next) } /** - * @return array + * @return array */ private function featuresFromAttributes($route): array { diff --git a/src/Support/RequiresEntitlement.php b/src/Support/RequiresEntitlement.php index cb0c58d..3ade122 100644 --- a/src/Support/RequiresEntitlement.php +++ b/src/Support/RequiresEntitlement.php @@ -3,21 +3,20 @@ namespace Chargebee\Cashier\Support; use Attribute; -use BackedEnum; use Chargebee\Cashier\Contracts\FeatureEnumContract; #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class RequiresEntitlement { /** - * @var list + * @var list */ public array $features; /** - * @param FeatureEnumContract&BackedEnum ...$features + * @param FeatureEnumContract ...$features */ - public function __construct(FeatureEnumContract&BackedEnum ...$features) + public function __construct(FeatureEnumContract ...$features) { $this->features = $features; } From 8c26e97c705ff6f1524c5386eed448336ab021f9 Mon Sep 17 00:00:00 2001 From: Srinath Sankar Date: Wed, 29 Oct 2025 18:20:26 +1100 Subject: [PATCH 2/8] fix default config for entitlements --- config/cashier.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/cashier.php b/config/cashier.php index 9a2826b..01c8b30 100644 --- a/config/cashier.php +++ b/config/cashier.php @@ -98,16 +98,16 @@ 'entitlements' => [ // Enable Chargebee Entitlements for Cashier - 'enabled' => true, + 'enabled' => env('CASHIER_ENTITLEMENTS_ENABLED', false), // The class that will be used to check if the user has access to the feature. // If this is not provided, the default implementation expects fallback_access // to be provided. - 'access_verifier' => \Chargebee\Cashier\Support\DefaultEntitlementAccessVerifier::class, + 'access_verifier' => env('CASHIER_ENTITLEMENTS_ACCESS_VERIFIER'), - // Map of FeatureID => boolean which is used as a fallback to determine if + // array of FeatureID => boolean which is used as a fallback to determine if // the user has access to the feature. This is used only if a access_check // class is not provided. - 'feature_defaults' => [], + 'feature_defaults' => env('CASHIER_ENTITLEMENTS_FEATURE_DEFAULTS'), ], ]; From 9732449d9ce3fcb057f199fc4c314da56a85dedb Mon Sep 17 00:00:00 2001 From: Srinath Sankar Date: Wed, 29 Oct 2025 19:37:07 +1100 Subject: [PATCH 3/8] fix a typing bug and improve logging --- src/Concerns/HasEntitlements.php | 20 +++++++++++++++----- src/Subscription.php | 8 ++++---- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/Concerns/HasEntitlements.php b/src/Concerns/HasEntitlements.php index 6ef535c..e9ba9d5 100644 --- a/src/Concerns/HasEntitlements.php +++ b/src/Concerns/HasEntitlements.php @@ -11,6 +11,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; +use Symfony\Component\HttpKernel\Exception\HttpException; trait HasEntitlements { @@ -81,8 +82,7 @@ public function ensureEntitlements(): void $cachedEntitlements = $cacheStore->get($cacheKey); if ($cachedEntitlements) { Log::debug('Got entitlements from cache: ', ['cachedEntitlements' => $cachedEntitlements]); - // Convert the cached entitlements to an array of Entitlement objects - $this->entitlements = collect($cachedEntitlements)->map(fn ($entitlement) => Entitlement::fromArray($entitlement)); + $this->entitlements = $cachedEntitlements; } else { $entitlements = $this->getEntitlements(); Log::debug('Got entitlements from API: ', ['entitlements' => $entitlements]); @@ -101,10 +101,20 @@ public function hasAccess(FeatureEnumContract ...$features): bool { $featureModels = Feature::whereIn('chargebee_id', $features)->get(); $feats = collect($features); + + // Since we need to read the feature attributes from the DB, + // we need to ensure that all the features have been synced. If not, throw a 500 error. if ($featureModels->count() != $feats->count()) { - Log::warning(<<<'EOF' - Some features were not found in the database. Please run `php artisan cashier:generate-feature-enum` to sync. - EOF, ['missingFeatures' => $feats->diff($featureModels)->implode(', ')]); + $missingFeatureIds = $feats->reject(function ($enum) use ($featureModels) { + return $featureModels->contains(function ($model) use ($enum) { + return $enum->id() === $model->chargebee_id; + }); + }); + + Log::error(<<<'EOF' + Feature(s) missing in database. Run `php artisan cashier:generate-feature-enum` to sync. + EOF, ['missingFeatures' => $missingFeatureIds->implode(', ')]); + throw new HttpException(500, 'Error verifying your access to this resource.'); } return app(EntitlementAccessVerifier::class)::hasAccessToFeatures($this, $featureModels); diff --git a/src/Subscription.php b/src/Subscription.php index 2e95141..899c761 100644 --- a/src/Subscription.php +++ b/src/Subscription.php @@ -1025,13 +1025,13 @@ public function asChargebeeSubscription(): ChargebeeSubscription /** * Get entitlements * - * @return \Chargebee\Cashier\Entitlement[] + * @return \Illuminate\Support\Collection */ - public function getEntitlements(): array + public function getEntitlements(): Collection { Log::debug('Getting entitlements for subscription '.$this->chargebee_id); $chargebee = Cashier::chargebee(); - $entitlements = []; + $entitlements = Collection::make(); $options = []; do { @@ -1039,7 +1039,7 @@ public function getEntitlements(): array $entitlementsResponse = collect($response->list)->map(function ($entitlement) { return new Entitlement($entitlement->subscription_entitlement); }); - array_push($entitlements, ...$entitlementsResponse->toArray()); + $entitlements->push(...$entitlementsResponse); if ($response->next_offset) { $options['offset'] = $response->next_offset; } From 9eea4453dc23c61cb1a981963cd84b7674e8849e Mon Sep 17 00:00:00 2001 From: Srinath Sankar Date: Wed, 29 Oct 2025 20:27:00 +1100 Subject: [PATCH 4/8] store entitlements as json array in cache --- src/Concerns/HasEntitlements.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Concerns/HasEntitlements.php b/src/Concerns/HasEntitlements.php index e9ba9d5..ebe8345 100644 --- a/src/Concerns/HasEntitlements.php +++ b/src/Concerns/HasEntitlements.php @@ -82,12 +82,12 @@ public function ensureEntitlements(): void $cachedEntitlements = $cacheStore->get($cacheKey); if ($cachedEntitlements) { Log::debug('Got entitlements from cache: ', ['cachedEntitlements' => $cachedEntitlements]); - $this->entitlements = $cachedEntitlements; + $this->entitlements = collect($cachedEntitlements)->map(fn($entitlement) => Entitlement::fromArray($entitlement)); } else { $entitlements = $this->getEntitlements(); Log::debug('Got entitlements from API: ', ['entitlements' => $entitlements]); $cacheExpirySeconds = config('session.lifetime', 120) * 60; - $cacheStore->put($cacheKey, $entitlements, $cacheExpirySeconds); + $cacheStore->put($cacheKey, $entitlements->toArray(), $cacheExpirySeconds); } } From 143ba60a1d852a5fba9f67e892392ff758220b7c Mon Sep 17 00:00:00 2001 From: Srinath Sankar Date: Tue, 11 Nov 2025 21:16:55 +1100 Subject: [PATCH 5/8] refactor to support `EntitlementAccessVerifier::handleError` --- src/Concerns/HasEntitlements.php | 55 +++++++++++++------ src/Constants.php | 6 ++ src/Contracts/EntitlementAccessVerifier.php | 19 +++++-- src/Http/Middleware/UserEntitlementCheck.php | 12 ++-- .../DefaultEntitlementAccessVerifier.php | 33 ++++++++--- 5 files changed, 92 insertions(+), 33 deletions(-) diff --git a/src/Concerns/HasEntitlements.php b/src/Concerns/HasEntitlements.php index ebe8345..0374bdc 100644 --- a/src/Concerns/HasEntitlements.php +++ b/src/Concerns/HasEntitlements.php @@ -5,13 +5,14 @@ use Chargebee\Cashier\Contracts\EntitlementAccessVerifier; use Chargebee\Cashier\Contracts\FeatureEnumContract; use Chargebee\Cashier\Entitlement; +use Chargebee\Cashier\EntitlementErrorCode; use Chargebee\Cashier\Feature; use Chargebee\Cashier\Subscription; use Illuminate\Contracts\Cache\Repository as CacheRepository; +use Illuminate\Http\Request; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; -use Symfony\Component\HttpKernel\Exception\HttpException; trait HasEntitlements { @@ -82,7 +83,7 @@ public function ensureEntitlements(): void $cachedEntitlements = $cacheStore->get($cacheKey); if ($cachedEntitlements) { Log::debug('Got entitlements from cache: ', ['cachedEntitlements' => $cachedEntitlements]); - $this->entitlements = collect($cachedEntitlements)->map(fn($entitlement) => Entitlement::fromArray($entitlement)); + $this->entitlements = collect($cachedEntitlements)->map(fn ($entitlement) => Entitlement::fromArray($entitlement)); } else { $entitlements = $this->getEntitlements(); Log::debug('Got entitlements from API: ', ['entitlements' => $entitlements]); @@ -91,32 +92,52 @@ public function ensureEntitlements(): void } } + /** + * Check if the given features are missing in the database + * + * @param Collection $features + * @param Collection $featureModels + * @return Collection|null + */ + protected function checkMissingFeatures(Collection $features, Collection $featureModels): ?Collection + { + $missingFeatureIds = $features->reject(function ($enum) use ($featureModels) { + return $featureModels->contains(function ($model) use ($enum) { + return $enum->id() === $model->chargebee_id; + }); + }); + if ($missingFeatureIds->count() > 0) { + return $missingFeatureIds; + } + + return null; + } + /** * Check if the user has the given entitlement * - * @param FeatureEnumContract ...$features + * @param FeatureEnumContract|array $features + * @param ?Request $request * @return bool */ - public function hasAccess(FeatureEnumContract ...$features): bool + public function hasAccess($features, ?Request $request = null): bool { - $featureModels = Feature::whereIn('chargebee_id', $features)->get(); $feats = collect($features); + $featureModels = Feature::whereIn('chargebee_id', $features)->get(); + $request = $request ?? request(); + $entitlementAccessVerifier = app(EntitlementAccessVerifier::class); - // Since we need to read the feature attributes from the DB, - // we need to ensure that all the features have been synced. If not, throw a 500 error. - if ($featureModels->count() != $feats->count()) { - $missingFeatureIds = $feats->reject(function ($enum) use ($featureModels) { - return $featureModels->contains(function ($model) use ($enum) { - return $enum->id() === $model->chargebee_id; - }); - }); - + $missingFeatures = $this->checkMissingFeatures($feats, $featureModels); + if ($missingFeatures && $missingFeatures->count() > 0) { Log::error(<<<'EOF' Feature(s) missing in database. Run `php artisan cashier:generate-feature-enum` to sync. - EOF, ['missingFeatures' => $missingFeatureIds->implode(', ')]); - throw new HttpException(500, 'Error verifying your access to this resource.'); + EOF, ['missingFeatures' => $missingFeatures]); + + $entitlementAccessVerifier::handleError($request, EntitlementErrorCode::MISSING_FEATURE_IN_DB, $missingFeatures); + + return false; } - return app(EntitlementAccessVerifier::class)::hasAccessToFeatures($this, $featureModels); + return $entitlementAccessVerifier::hasAccessToFeatures($request, $featureModels); } } diff --git a/src/Constants.php b/src/Constants.php index c26ee8e..a93e5ff 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -6,3 +6,9 @@ class Constants { public const REQUIRED_FEATURES_KEY = 'chargebee.required_features'; } + +enum EntitlementErrorCode +{ + case MISSING_FEATURE_IN_DB; + case ACCESS_DENIED; +} diff --git a/src/Contracts/EntitlementAccessVerifier.php b/src/Contracts/EntitlementAccessVerifier.php index f26fda4..b172ea9 100644 --- a/src/Contracts/EntitlementAccessVerifier.php +++ b/src/Contracts/EntitlementAccessVerifier.php @@ -2,16 +2,16 @@ namespace Chargebee\Cashier\Contracts; -use Chargebee\Cashier\Concerns\HasEntitlements; +use Chargebee\Cashier\EntitlementErrorCode; use Chargebee\Cashier\Feature; -use Illuminate\Contracts\Auth\Authenticatable; +use Illuminate\Http\Request; use Illuminate\Support\Collection; interface EntitlementAccessVerifier { /** * For the given user, decide if feature is accessible to them. The entitlements - * for the user are accessible via $user->getEntitlements(). The implementation in the + * for the user are accessible via $request->user()->getEntitlements(). The implementation in the * app will need to consider variour factors like feature type, value, levels, etc. * * If multiple features are defined on the route, those are passed as an array to this method. @@ -20,9 +20,18 @@ interface EntitlementAccessVerifier * If you also track usage of these features in your app, apply the required logic to verify * if the usage is within the entitled limits. * - * @param Authenticatable&HasEntitlements $user The user to check access for + * @param Request $request * @param Collection $features * @return bool */ - public static function hasAccessToFeatures($user, Collection $features): bool; + public static function hasAccessToFeatures(Request $request, Collection $features): bool; + + /** + * When hasAccessToFeatures returns false, this method will be called to handle the access denied case. + * You can throw an exception, return a response, or redirect to a different page. + * + * @param Request $request + * @param EntitlementErrorCode $error + */ + public static function handleError(Request $request, EntitlementErrorCode $error): void; } diff --git a/src/Http/Middleware/UserEntitlementCheck.php b/src/Http/Middleware/UserEntitlementCheck.php index f4ee335..4340fa3 100644 --- a/src/Http/Middleware/UserEntitlementCheck.php +++ b/src/Http/Middleware/UserEntitlementCheck.php @@ -4,7 +4,9 @@ use Chargebee\Cashier\Concerns\HasEntitlements; use Chargebee\Cashier\Constants; +use Chargebee\Cashier\Contracts\EntitlementAccessVerifier; use Chargebee\Cashier\Contracts\FeatureEnumContract; +use Chargebee\Cashier\EntitlementErrorCode; use Chargebee\Cashier\Support\RequiresEntitlement; use Closure; use Illuminate\Contracts\Auth\Authenticatable; @@ -36,12 +38,14 @@ public function handle(Request $request, Closure $next) } } if ($features) { - $hasAccess = $user->hasAccess(...$features); + $request->attributes->set(Constants::REQUIRED_FEATURES_KEY, $features); + $hasAccess = $user->hasAccess($features, $request); + if (! $hasAccess) { - throw new HttpException(403, 'You are not authorized to access this resource.'); - } + $entitlementAccessVerifier = app(EntitlementAccessVerifier::class); - $request->attributes->set(Constants::REQUIRED_FEATURES_KEY, $features); + return $entitlementAccessVerifier::handleError($request, EntitlementErrorCode::ACCESS_DENIED); + } } return $next($request); diff --git a/src/Support/DefaultEntitlementAccessVerifier.php b/src/Support/DefaultEntitlementAccessVerifier.php index 4f176b1..8acd917 100644 --- a/src/Support/DefaultEntitlementAccessVerifier.php +++ b/src/Support/DefaultEntitlementAccessVerifier.php @@ -2,37 +2,40 @@ namespace Chargebee\Cashier\Support; -use Chargebee\Cashier\Concerns\HasEntitlements; use Chargebee\Cashier\Contracts\EntitlementAccessVerifier; use Chargebee\Cashier\Entitlement; +use Chargebee\Cashier\EntitlementErrorCode; use Chargebee\Cashier\Feature; use Chargebee\Resources\Feature\Enums\Type as FeatureType; -use Illuminate\Contracts\Auth\Authenticatable; +use Illuminate\Http\Request; use Illuminate\Support\Collection; +use Symfony\Component\HttpKernel\Exception\HttpException; final class DefaultEntitlementAccessVerifier implements EntitlementAccessVerifier { /** * Return true if the entitlements collectively provide all requested features. * - * @param Authenticatable&HasEntitlements $user The user to check access for + * @param Request $request * @param Collection $features * @return bool */ - public static function hasAccessToFeatures($user, Collection $features): bool + public static function hasAccessToFeatures(Request $request, Collection $features): bool { - $entitlements = $user->getEntitlements(); + /** @var Collection $entitlements */ + $entitlements = $request->user()?->getEntitlements() ?? collect(); $featureDefaults = config('cashier.entitlements.feature_defaults', []); // Every feature must be provided by at least one entitlement (AND logic) return $features->every(function ($feature) use ($entitlements, $featureDefaults) { return $entitlements->contains(function ($entitlement) use ($feature, $featureDefaults) { - // For the default implementation, we can only check SWITCH feature types. - // For the others, check if there is a fallback value configured. + // If the entitlement does not match the feature being checked, bail. if ($entitlement->feature_id !== $feature->chargebee_id) { return false; } + // For the default implementation, we can only check SWITCH feature types. + // For the others, check if there is a fallback value configured in the config file. $hasAccess = match (FeatureType::tryFromValue(strtolower($entitlement->feature_type))) { FeatureType::SWITCH => $entitlement->value, default => $featureDefaults[$feature->chargebee_id] ?? false, @@ -43,4 +46,20 @@ public static function hasAccessToFeatures($user, Collection $features): bool }); }); } + + /** + * Throw a 403 error when access is denied. + * + * @param Request $request + * @param EntitlementErrorCode $error + */ + public static function handleError(Request $request, EntitlementErrorCode $error): void + { + switch ($error) { + case EntitlementErrorCode::MISSING_FEATURE_IN_DB: + throw new HttpException(500, 'Error verifying your access to this resource.'); + case EntitlementErrorCode::ACCESS_DENIED: + throw new HttpException(403, 'You are not authorized to access this resource.'); + } + } } From 887093cc16c42bb89c3fe05bff99a2216e3bcf6d Mon Sep 17 00:00:00 2001 From: Srinath Sankar Date: Thu, 13 Nov 2025 13:57:55 +1100 Subject: [PATCH 6/8] add $data to handlError --- src/Contracts/EntitlementAccessVerifier.php | 4 +++- src/Support/DefaultEntitlementAccessVerifier.php | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Contracts/EntitlementAccessVerifier.php b/src/Contracts/EntitlementAccessVerifier.php index b172ea9..63a0c0a 100644 --- a/src/Contracts/EntitlementAccessVerifier.php +++ b/src/Contracts/EntitlementAccessVerifier.php @@ -32,6 +32,8 @@ public static function hasAccessToFeatures(Request $request, Collection $feature * * @param Request $request * @param EntitlementErrorCode $error + * @param mixed $data + * @return void */ - public static function handleError(Request $request, EntitlementErrorCode $error): void; + public static function handleError(Request $request, EntitlementErrorCode $error, mixed $data = null): void; } diff --git a/src/Support/DefaultEntitlementAccessVerifier.php b/src/Support/DefaultEntitlementAccessVerifier.php index 8acd917..da5b852 100644 --- a/src/Support/DefaultEntitlementAccessVerifier.php +++ b/src/Support/DefaultEntitlementAccessVerifier.php @@ -52,8 +52,10 @@ public static function hasAccessToFeatures(Request $request, Collection $feature * * @param Request $request * @param EntitlementErrorCode $error + * @param mixed $data + * @return void */ - public static function handleError(Request $request, EntitlementErrorCode $error): void + public static function handleError(Request $request, EntitlementErrorCode $error, mixed $data = null): void { switch ($error) { case EntitlementErrorCode::MISSING_FEATURE_IN_DB: From d88b880a9607605dcc818871e1f5ee2754385c91 Mon Sep 17 00:00:00 2001 From: cb-alish Date: Fri, 6 Feb 2026 16:38:56 +0530 Subject: [PATCH 7/8] Fixed test cases for feature enums --- tests/Feature/FeatureEnumCommandTest.php | 128 ++++++++++++++++++++--- 1 file changed, 112 insertions(+), 16 deletions(-) diff --git a/tests/Feature/FeatureEnumCommandTest.php b/tests/Feature/FeatureEnumCommandTest.php index 7602cc3..569ab0d 100644 --- a/tests/Feature/FeatureEnumCommandTest.php +++ b/tests/Feature/FeatureEnumCommandTest.php @@ -50,13 +50,22 @@ public function test_generate_feature_enum_should_create_enum_file_with_cases_an $this->assertNotNull($capturedPath); $this->assertNotNull($capturedPhp); - $expectedPhp = " 'feature_priority_support', ); } -}"; + + public function id(): string + { + return $this->value; + } + + /** + * @param array $featureIds + * @return array + */ + public static function fromArray(array $featureIds): array + { + return array_map(fn (string $featureId) => self::from($featureId), $featureIds); + } +} +PHP; // Verify the file path $expectedPath = base_path('app/Models/FeaturesMap.php'); @@ -124,7 +148,7 @@ public function test_should_use_default_options_when_none_provided(): void $this->artisan('cashier:generate-feature-enum', ['--force' => true]) ->assertExitCode(0); - $expectedPath = base_path('app/Models/FeaturesMap.php'); + $expectedPath = base_path('app/Models/Feature.php'); $this->assertEquals($expectedPath, $capturedPath); } @@ -150,13 +174,22 @@ public function test_should_handle_custom_class_and_namespace(): void ])->assertExitCode(0); $expectedPath = base_path('app/Enums/CustomFeatures.php'); - $expectedPhp = " 'feature_priority_support', ); } -}"; + + public function id(): string + { + return $this->value; + } + + /** + * @param array $featureIds + * @return array + */ + public static function fromArray(array $featureIds): array + { + return array_map(fn (string $featureId) => self::from($featureId), $featureIds); + } +} +PHP; $this->assertEquals($expectedPath, $capturedPath); $this->assertEquals($expectedPhp, $capturedPhp); } @@ -210,7 +258,7 @@ public function test_should_handle_path_with_trailing_slash(): void '--force' => true, ])->assertExitCode(0); - $expectedPath = base_path('app/Models/FeaturesMap.php'); + $expectedPath = base_path('app/Models/Feature.php'); $this->assertEquals($expectedPath, $capturedPath); } @@ -228,13 +276,22 @@ public function test_should_skip_features_with_invalid_names(): void $this->artisan('cashier:generate-feature-enum', ['--force' => true]) ->assertExitCode(0); - $expectedPhp = " 'feature_free_trial', ); } -}"; + + public function id(): string + { + return $this->value; + } + + /** + * @param array $featureIds + * @return array + */ + public static function fromArray(array $featureIds): array + { + return array_map(fn (string $featureId) => self::from($featureId), $featureIds); + } +} +PHP; // Should only contain the valid feature - $this->assertStringContainsString($expectedPhp, $capturedPhp); + $this->assertEquals($expectedPhp, $capturedPhp); } public function test_should_escape_special_characters_in_values(): void @@ -262,13 +334,22 @@ public function test_should_escape_special_characters_in_values(): void }); $this->artisan('cashier:generate-feature-enum', ['--force' => true]) ->assertExitCode(0); - $expectedPhp = " 'feature_priority_support', ); } -}"; - $this->assertEquals($capturedPhp, $expectedPhp); + + public function id(): string + { + return $this->value; + } + + /** + * @param array $featureIds + * @return array + */ + public static function fromArray(array $featureIds): array + { + return array_map(fn (string $featureId) => self::from($featureId), $featureIds); } } +PHP; + $this->assertEquals($capturedPhp, $expectedPhp); + } +} \ No newline at end of file From a2fc7cefdbb31e4ff954b47160383cbe3c69c772 Mon Sep 17 00:00:00 2001 From: cb-alish Date: Wed, 11 Feb 2026 14:47:38 +0530 Subject: [PATCH 8/8] Updated the CHANGELOG.md --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index de5244f..89a4ec5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +### v1.3.0-beta.2 (2025-10-30) + +--- + +* Refactored entitlements to use `FeatureEnumContract` (now extends `BackedEnum`) and simplified method signatures across routes, middleware, and attributes. +* Switched entitlements config to env-driven options (`CASHIER_ENTITLEMENTS_*`). +* Fixed entitlement cache serialization (store as array, restore via `Entitlement::fromArray`). +* Added hard failure (`HttpException(500)`) when required features are missing in DB. +* Updated `Subscription::getEntitlements()` to return `Collection`. +* Updated documentation to reflect enum contract and configuration changes. + + ### v1.3.0-beta.1 (2025-10-27) * * *