refactor: create EntitlementException hierarchy with subtypes
Create exception subclasses for fine-grained error handling: - LimitExceededException: feature usage limit exceeded - PackageNotFoundException: package code not found during provisioning - FeatureNotFoundException: feature code not found during checks - PackageSuspendedException: workspace packages suspended Update EntitlementService: - Add canOrFail() and canForNamespaceOrFail() throwing variants - Replace firstOrFail() with explicit PackageNotFoundException in provisioning - Import new exception types, remove unused ModelNotFoundException Update docs/entitlements.md with Exception Hierarchy section, API reference entries for new methods, and updated Best Practices examples. Fixes #19 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c51e4310b1
commit
2601392b8d
7 changed files with 506 additions and 13 deletions
|
|
@ -7,7 +7,23 @@ namespace Core\Tenant\Exceptions;
|
|||
use Exception;
|
||||
|
||||
/**
|
||||
* Exception thrown when an entitlement check fails.
|
||||
* Base exception for entitlement-related errors.
|
||||
*
|
||||
* This is the root of the entitlement exception hierarchy. Consumers can catch
|
||||
* this class to handle all entitlement errors, or catch specific subtypes for
|
||||
* fine-grained error handling.
|
||||
*
|
||||
* Exception hierarchy:
|
||||
* - EntitlementException (base)
|
||||
* - LimitExceededException -- feature usage limit has been exceeded
|
||||
* - PackageNotFoundException -- referenced package code does not exist
|
||||
* - FeatureNotFoundException -- referenced feature code does not exist
|
||||
* - PackageSuspendedException -- workspace packages are suspended
|
||||
*
|
||||
* @see LimitExceededException
|
||||
* @see PackageNotFoundException
|
||||
* @see FeatureNotFoundException
|
||||
* @see PackageSuspendedException
|
||||
*/
|
||||
class EntitlementException extends Exception
|
||||
{
|
||||
|
|
|
|||
53
Exceptions/FeatureNotFoundException.php
Normal file
53
Exceptions/FeatureNotFoundException.php
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Tenant\Exceptions;
|
||||
|
||||
/**
|
||||
* Exception thrown when a referenced feature does not exist.
|
||||
*
|
||||
* This exception is thrown when an entitlement check references a feature
|
||||
* code that is not defined in the features table.
|
||||
*
|
||||
* @see EntitlementException Base exception class
|
||||
*/
|
||||
class FeatureNotFoundException extends EntitlementException
|
||||
{
|
||||
public function __construct(
|
||||
string $message = 'The requested feature was not found.',
|
||||
?string $featureCode = null,
|
||||
int $code = 404,
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
parent::__construct($message, $featureCode, $code, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create exception for a specific feature code.
|
||||
*/
|
||||
public static function forCode(string $featureCode): self
|
||||
{
|
||||
return new self(
|
||||
message: "Feature '{$featureCode}' does not exist.",
|
||||
featureCode: $featureCode,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the exception as an HTTP response.
|
||||
*/
|
||||
public function render($request)
|
||||
{
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json([
|
||||
'message' => $this->getMessage(),
|
||||
'error' => 'feature_not_found',
|
||||
'feature_code' => $this->featureCode,
|
||||
], $this->getCode());
|
||||
}
|
||||
|
||||
return redirect()->back()
|
||||
->with('error', $this->getMessage());
|
||||
}
|
||||
}
|
||||
77
Exceptions/LimitExceededException.php
Normal file
77
Exceptions/LimitExceededException.php
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Tenant\Exceptions;
|
||||
|
||||
use Core\Tenant\Services\EntitlementResult;
|
||||
|
||||
/**
|
||||
* Exception thrown when a feature usage limit has been exceeded.
|
||||
*
|
||||
* This exception indicates that the workspace or namespace has consumed
|
||||
* all available capacity for a limit-based feature (e.g., pages, API calls).
|
||||
*
|
||||
* @see EntitlementException Base exception class
|
||||
*/
|
||||
class LimitExceededException extends EntitlementException
|
||||
{
|
||||
public function __construct(
|
||||
string $message = 'You have reached your limit for this feature.',
|
||||
?string $featureCode = null,
|
||||
public readonly ?int $limit = null,
|
||||
public readonly ?int $used = null,
|
||||
int $code = 403,
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
parent::__construct($message, $featureCode, $code, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create from an EntitlementResult.
|
||||
*/
|
||||
public static function fromResult(EntitlementResult $result): self
|
||||
{
|
||||
return new self(
|
||||
message: $result->getMessage() ?? 'You have reached your limit for this feature.',
|
||||
featureCode: $result->featureCode,
|
||||
limit: $result->limit,
|
||||
used: $result->used,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the feature limit that was exceeded.
|
||||
*/
|
||||
public function getLimit(): ?int
|
||||
{
|
||||
return $this->limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current usage count.
|
||||
*/
|
||||
public function getUsed(): ?int
|
||||
{
|
||||
return $this->used;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the exception as an HTTP response.
|
||||
*/
|
||||
public function render($request)
|
||||
{
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json([
|
||||
'message' => $this->getMessage(),
|
||||
'error' => 'limit_exceeded',
|
||||
'feature_code' => $this->featureCode,
|
||||
'limit' => $this->limit,
|
||||
'used' => $this->used,
|
||||
], $this->getCode());
|
||||
}
|
||||
|
||||
return redirect()->back()
|
||||
->with('error', $this->getMessage());
|
||||
}
|
||||
}
|
||||
61
Exceptions/PackageNotFoundException.php
Normal file
61
Exceptions/PackageNotFoundException.php
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Tenant\Exceptions;
|
||||
|
||||
/**
|
||||
* Exception thrown when a referenced package does not exist.
|
||||
*
|
||||
* This exception is thrown during provisioning when the requested package
|
||||
* code cannot be found in the packages table.
|
||||
*
|
||||
* @see EntitlementException Base exception class
|
||||
*/
|
||||
class PackageNotFoundException extends EntitlementException
|
||||
{
|
||||
public function __construct(
|
||||
string $message = 'The requested package was not found.',
|
||||
public readonly ?string $packageCode = null,
|
||||
int $code = 404,
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
parent::__construct($message, featureCode: null, code: $code, previous: $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create exception for a specific package code.
|
||||
*/
|
||||
public static function forCode(string $packageCode): self
|
||||
{
|
||||
return new self(
|
||||
message: "Package '{$packageCode}' not found.",
|
||||
packageCode: $packageCode,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the package code that was not found.
|
||||
*/
|
||||
public function getPackageCode(): ?string
|
||||
{
|
||||
return $this->packageCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the exception as an HTTP response.
|
||||
*/
|
||||
public function render($request)
|
||||
{
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json([
|
||||
'message' => $this->getMessage(),
|
||||
'error' => 'package_not_found',
|
||||
'package_code' => $this->packageCode,
|
||||
], $this->getCode());
|
||||
}
|
||||
|
||||
return redirect()->back()
|
||||
->with('error', $this->getMessage());
|
||||
}
|
||||
}
|
||||
63
Exceptions/PackageSuspendedException.php
Normal file
63
Exceptions/PackageSuspendedException.php
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Tenant\Exceptions;
|
||||
|
||||
/**
|
||||
* Exception thrown when an operation targets a suspended package.
|
||||
*
|
||||
* This exception indicates that the workspace's packages have been suspended
|
||||
* (e.g., for non-payment or policy violation), preventing feature access.
|
||||
*
|
||||
* @see EntitlementException Base exception class
|
||||
*/
|
||||
class PackageSuspendedException extends EntitlementException
|
||||
{
|
||||
public function __construct(
|
||||
string $message = 'Your package has been suspended.',
|
||||
?string $featureCode = null,
|
||||
public readonly ?int $workspaceId = null,
|
||||
int $code = 403,
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
parent::__construct($message, $featureCode, $code, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create exception for a specific workspace.
|
||||
*/
|
||||
public static function forWorkspace(int $workspaceId, ?string $featureCode = null): self
|
||||
{
|
||||
return new self(
|
||||
message: 'Your package has been suspended. Please contact support or update your billing details.',
|
||||
featureCode: $featureCode,
|
||||
workspaceId: $workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the workspace ID whose packages are suspended.
|
||||
*/
|
||||
public function getWorkspaceId(): ?int
|
||||
{
|
||||
return $this->workspaceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the exception as an HTTP response.
|
||||
*/
|
||||
public function render($request)
|
||||
{
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json([
|
||||
'message' => $this->getMessage(),
|
||||
'error' => 'package_suspended',
|
||||
'feature_code' => $this->featureCode,
|
||||
], $this->getCode());
|
||||
}
|
||||
|
||||
return redirect()->back()
|
||||
->with('error', $this->getMessage());
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,10 @@ declare(strict_types=1);
|
|||
namespace Core\Tenant\Services;
|
||||
|
||||
use Core\Tenant\Events\EntitlementCacheInvalidated;
|
||||
use Core\Tenant\Exceptions\EntitlementException;
|
||||
use Core\Tenant\Exceptions\FeatureNotFoundException;
|
||||
use Core\Tenant\Exceptions\LimitExceededException;
|
||||
use Core\Tenant\Exceptions\PackageNotFoundException;
|
||||
use Core\Tenant\Models\Boost;
|
||||
use Core\Tenant\Models\EntitlementLog;
|
||||
use Core\Tenant\Models\Feature;
|
||||
|
|
@ -16,7 +20,6 @@ use Core\Tenant\Models\User;
|
|||
use Core\Tenant\Models\Workspace;
|
||||
use Core\Tenant\Models\WorkspacePackage;
|
||||
use Illuminate\Cache\TaggableStore;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
|
|
@ -281,6 +284,63 @@ class EntitlementService
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a workspace can use a feature, throwing on failure.
|
||||
*
|
||||
* This is the throwing variant of `can()`. It returns the EntitlementResult
|
||||
* on success but throws a specific exception subtype on failure, allowing
|
||||
* consumers to distinguish failure modes via catch blocks.
|
||||
*
|
||||
* ## Example Usage
|
||||
*
|
||||
* ```php
|
||||
* use Core\Tenant\Exceptions\LimitExceededException;
|
||||
* use Core\Tenant\Exceptions\FeatureNotFoundException;
|
||||
*
|
||||
* try {
|
||||
* $result = $entitlementService->canOrFail($workspace, 'pages');
|
||||
* $page = $workspace->pages()->create($data);
|
||||
* $entitlementService->recordUsage($workspace, 'pages', 1, $user);
|
||||
* } catch (LimitExceededException $e) {
|
||||
* return response()->json(['error' => $e->getMessage(), 'limit' => $e->getLimit()], 403);
|
||||
* } catch (FeatureNotFoundException $e) {
|
||||
* return response()->json(['error' => 'Unknown feature'], 404);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param Workspace $workspace The workspace to check entitlements for
|
||||
* @param string $featureCode The feature code to check
|
||||
* @param int $quantity The quantity being requested (default: 1)
|
||||
* @return EntitlementResult The successful entitlement result
|
||||
*
|
||||
* @throws FeatureNotFoundException If the feature code does not exist
|
||||
* @throws LimitExceededException If the feature limit has been exceeded
|
||||
* @throws EntitlementException If the feature is not included in any package
|
||||
*/
|
||||
public function canOrFail(Workspace $workspace, string $featureCode, int $quantity = 1): EntitlementResult
|
||||
{
|
||||
$feature = $this->getFeature($featureCode);
|
||||
|
||||
if (! $feature) {
|
||||
throw FeatureNotFoundException::forCode($featureCode);
|
||||
}
|
||||
|
||||
$result = $this->can($workspace, $featureCode, $quantity);
|
||||
|
||||
if ($result->isDenied()) {
|
||||
if ($result->limit !== null && $result->used !== null) {
|
||||
throw LimitExceededException::fromResult($result);
|
||||
}
|
||||
|
||||
throw new EntitlementException(
|
||||
message: $result->getMessage() ?? 'Entitlement check failed.',
|
||||
featureCode: $featureCode,
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a namespace can use a feature.
|
||||
*
|
||||
|
|
@ -398,6 +458,61 @@ class EntitlementService
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a namespace can use a feature, throwing on failure.
|
||||
*
|
||||
* This is the throwing variant of `canForNamespace()`. It returns the
|
||||
* EntitlementResult on success but throws a specific exception subtype
|
||||
* on failure.
|
||||
*
|
||||
* ## Example Usage
|
||||
*
|
||||
* ```php
|
||||
* use Core\Tenant\Exceptions\LimitExceededException;
|
||||
* use Core\Tenant\Exceptions\FeatureNotFoundException;
|
||||
*
|
||||
* try {
|
||||
* $result = $entitlementService->canForNamespaceOrFail($namespace, 'links');
|
||||
* $link = $namespace->links()->create($data);
|
||||
* $entitlementService->recordNamespaceUsage($namespace, 'links');
|
||||
* } catch (LimitExceededException $e) {
|
||||
* return response()->json(['error' => $e->getMessage()], 403);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param Namespace_ $namespace The namespace to check entitlements for
|
||||
* @param string $featureCode The feature code to check
|
||||
* @param int $quantity The quantity being requested (default: 1)
|
||||
* @return EntitlementResult The successful entitlement result
|
||||
*
|
||||
* @throws FeatureNotFoundException If the feature code does not exist
|
||||
* @throws LimitExceededException If the feature limit has been exceeded
|
||||
* @throws EntitlementException If the feature is not included in any package
|
||||
*/
|
||||
public function canForNamespaceOrFail(Namespace_ $namespace, string $featureCode, int $quantity = 1): EntitlementResult
|
||||
{
|
||||
$feature = $this->getFeature($featureCode);
|
||||
|
||||
if (! $feature) {
|
||||
throw FeatureNotFoundException::forCode($featureCode);
|
||||
}
|
||||
|
||||
$result = $this->canForNamespace($namespace, $featureCode, $quantity);
|
||||
|
||||
if ($result->isDenied()) {
|
||||
if ($result->limit !== null && $result->used !== null) {
|
||||
throw LimitExceededException::fromResult($result);
|
||||
}
|
||||
|
||||
throw new EntitlementException(
|
||||
message: $result->getMessage() ?? 'Entitlement check failed.',
|
||||
featureCode: $featureCode,
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record usage of a feature for a namespace.
|
||||
*
|
||||
|
|
@ -590,14 +705,18 @@ class EntitlementService
|
|||
* - `metadata`: Additional data to store with the package
|
||||
* @return WorkspacePackage The created workspace package record
|
||||
*
|
||||
* @throws ModelNotFoundException If the package code does not exist
|
||||
* @throws PackageNotFoundException If the package code does not exist
|
||||
*/
|
||||
public function provisionPackage(
|
||||
Workspace $workspace,
|
||||
string $packageCode,
|
||||
array $options = []
|
||||
): WorkspacePackage {
|
||||
$package = Package::where('code', $packageCode)->firstOrFail();
|
||||
$package = Package::where('code', $packageCode)->first();
|
||||
|
||||
if (! $package) {
|
||||
throw PackageNotFoundException::forCode($packageCode);
|
||||
}
|
||||
|
||||
// Check if this is a base package and workspace already has one
|
||||
if ($package->is_base_package) {
|
||||
|
|
@ -1736,7 +1855,7 @@ class EntitlementService
|
|||
* - `metadata`: Additional data to store with the package
|
||||
* @return NamespacePackage The created namespace package record
|
||||
*
|
||||
* @throws ModelNotFoundException If the package code does not exist
|
||||
* @throws PackageNotFoundException If the package code does not exist
|
||||
*
|
||||
* @see self::provisionPackage() For workspace-level package provisioning
|
||||
*/
|
||||
|
|
@ -1745,7 +1864,11 @@ class EntitlementService
|
|||
string $packageCode,
|
||||
array $options = []
|
||||
): NamespacePackage {
|
||||
$package = Package::where('code', $packageCode)->firstOrFail();
|
||||
$package = Package::where('code', $packageCode)->first();
|
||||
|
||||
if (! $package) {
|
||||
throw PackageNotFoundException::forCode($packageCode);
|
||||
}
|
||||
|
||||
// Check if this is a base package and namespace already has one
|
||||
if ($package->is_base_package) {
|
||||
|
|
|
|||
|
|
@ -166,6 +166,20 @@ public function can(
|
|||
- `isNearLimit(): bool` (>80%)
|
||||
- `isAtLimit(): bool` (100%)
|
||||
|
||||
#### canOrFail()
|
||||
|
||||
Check if a workspace can use a feature, throwing on failure:
|
||||
|
||||
```php
|
||||
public function canOrFail(
|
||||
Workspace $workspace,
|
||||
string $featureCode,
|
||||
int $quantity = 1
|
||||
): EntitlementResult
|
||||
|
||||
// Throws: FeatureNotFoundException, LimitExceededException, EntitlementException
|
||||
```
|
||||
|
||||
#### canForNamespace()
|
||||
|
||||
Check entitlement for a namespace with cascade:
|
||||
|
|
@ -183,6 +197,20 @@ Cascade order:
|
|||
2. Workspace pool (if `namespace->workspace_id` set)
|
||||
3. User tier (if namespace owned by user)
|
||||
|
||||
#### canForNamespaceOrFail()
|
||||
|
||||
Check namespace entitlement with cascade, throwing on failure:
|
||||
|
||||
```php
|
||||
public function canForNamespaceOrFail(
|
||||
Namespace_ $namespace,
|
||||
string $featureCode,
|
||||
int $quantity = 1
|
||||
): EntitlementResult
|
||||
|
||||
// Throws: FeatureNotFoundException, LimitExceededException, EntitlementException
|
||||
```
|
||||
|
||||
#### recordUsage()
|
||||
|
||||
Record feature usage:
|
||||
|
|
@ -369,6 +397,69 @@ $isValid = $webhookService->verifySignature(
|
|||
);
|
||||
```
|
||||
|
||||
## Exception Hierarchy
|
||||
|
||||
The entitlement system provides a hierarchy of exceptions for fine-grained error handling:
|
||||
|
||||
```
|
||||
EntitlementException (base)
|
||||
├── LimitExceededException -- feature usage limit exceeded
|
||||
├── PackageNotFoundException -- referenced package code does not exist
|
||||
├── FeatureNotFoundException -- referenced feature code does not exist
|
||||
└── PackageSuspendedException -- workspace packages are suspended
|
||||
```
|
||||
|
||||
All exception classes are in the `Core\Tenant\Exceptions` namespace.
|
||||
|
||||
### Catching Specific Exceptions
|
||||
|
||||
```php
|
||||
use Core\Tenant\Exceptions\EntitlementException;
|
||||
use Core\Tenant\Exceptions\LimitExceededException;
|
||||
use Core\Tenant\Exceptions\FeatureNotFoundException;
|
||||
use Core\Tenant\Exceptions\PackageNotFoundException;
|
||||
use Core\Tenant\Exceptions\PackageSuspendedException;
|
||||
|
||||
try {
|
||||
$result = $entitlements->canOrFail($workspace, 'pages');
|
||||
$page = $workspace->pages()->create($data);
|
||||
$entitlements->recordUsage($workspace, 'pages', 1, $user);
|
||||
} catch (LimitExceededException $e) {
|
||||
// Feature limit exceeded -- $e->getLimit(), $e->getUsed()
|
||||
return response()->json(['error' => $e->getMessage()], 403);
|
||||
} catch (FeatureNotFoundException $e) {
|
||||
// Feature code does not exist -- $e->getFeatureCode()
|
||||
return response()->json(['error' => 'Unknown feature'], 404);
|
||||
} catch (EntitlementException $e) {
|
||||
// Catch-all for any entitlement error
|
||||
return response()->json(['error' => $e->getMessage()], $e->getCode());
|
||||
}
|
||||
```
|
||||
|
||||
### canOrFail() / canForNamespaceOrFail()
|
||||
|
||||
The `canOrFail()` and `canForNamespaceOrFail()` methods are throwing variants of `can()` and `canForNamespace()`. They return `EntitlementResult` on success and throw specific exception subtypes on failure:
|
||||
|
||||
```php
|
||||
// Returns EntitlementResult on success, throws on failure
|
||||
$result = $entitlements->canOrFail($workspace, 'ai.credits', quantity: 5);
|
||||
|
||||
// Namespace variant with cascade
|
||||
$result = $entitlements->canForNamespaceOrFail($namespace, 'links');
|
||||
```
|
||||
|
||||
### Provisioning Exceptions
|
||||
|
||||
Package provisioning throws `PackageNotFoundException` when the package code is invalid:
|
||||
|
||||
```php
|
||||
try {
|
||||
$entitlements->provisionPackage($workspace, 'unknown-package');
|
||||
} catch (PackageNotFoundException $e) {
|
||||
// $e->getPackageCode() returns the invalid code
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Check Before Action
|
||||
|
|
@ -383,13 +474,23 @@ if (!$workspace->can('social.accounts')->isAllowed()) {
|
|||
throw new \Exception('Limit exceeded');
|
||||
}
|
||||
|
||||
// Good: Check before action
|
||||
// Good: Check before action (non-throwing)
|
||||
$result = $workspace->can('social.accounts');
|
||||
if ($result->isDenied()) {
|
||||
throw new EntitlementException($result->reason);
|
||||
throw new LimitExceededException(
|
||||
message: $result->reason,
|
||||
featureCode: 'social.accounts',
|
||||
limit: $result->limit,
|
||||
used: $result->used,
|
||||
);
|
||||
}
|
||||
$account = SocialAccount::create([...]);
|
||||
$workspace->recordUsage('social.accounts');
|
||||
|
||||
// Good: Check before action (throwing)
|
||||
$entitlements->canOrFail($workspace, 'social.accounts');
|
||||
$account = SocialAccount::create([...]);
|
||||
$workspace->recordUsage('social.accounts');
|
||||
```
|
||||
|
||||
### Use Transactions
|
||||
|
|
@ -397,12 +498,11 @@ $workspace->recordUsage('social.accounts');
|
|||
For atomic check-and-record:
|
||||
|
||||
```php
|
||||
DB::transaction(function () use ($workspace, $user) {
|
||||
$result = $workspace->can('ai.credits', 10);
|
||||
use Core\Tenant\Exceptions\LimitExceededException;
|
||||
|
||||
if ($result->isDenied()) {
|
||||
throw new EntitlementException($result->reason);
|
||||
}
|
||||
DB::transaction(function () use ($workspace, $user, $entitlements) {
|
||||
// canOrFail throws LimitExceededException if limit reached
|
||||
$entitlements->canOrFail($workspace, 'ai.credits', quantity: 10);
|
||||
|
||||
// Perform AI generation
|
||||
$output = $aiService->generate($prompt);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue