Compare commits
1 commit
dev
...
feat/fix-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef7ed4ca77 |
1 changed files with 372 additions and 72 deletions
|
|
@ -19,6 +19,7 @@ use Illuminate\Cache\TaggableStore;
|
||||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core service for managing feature entitlements, usage tracking, and package provisioning.
|
* Core service for managing feature entitlements, usage tracking, and package provisioning.
|
||||||
|
|
@ -529,6 +530,193 @@ class EntitlementService
|
||||||
return $record;
|
return $record;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomically check entitlement and record usage in a single transaction.
|
||||||
|
*
|
||||||
|
* This method prevents the race condition where two concurrent requests both
|
||||||
|
* read the same usage value, both pass the limit check, and both record usage,
|
||||||
|
* resulting in over-consumption. It uses `lockForUpdate()` on usage records to
|
||||||
|
* serialise concurrent access.
|
||||||
|
*
|
||||||
|
* ## Example Usage
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* // Atomic check + record in one operation
|
||||||
|
* $result = $entitlementService->checkAndRecordUsage($workspace, 'pages', 1, $user);
|
||||||
|
* if ($result->isDenied()) {
|
||||||
|
* return response()->json(['error' => $result->getMessage()], 403);
|
||||||
|
* }
|
||||||
|
* // Usage was already recorded — no need to call recordUsage() separately
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param Workspace $workspace The workspace to check and record for
|
||||||
|
* @param string $featureCode The feature code to check
|
||||||
|
* @param int $quantity The amount to consume (default: 1)
|
||||||
|
* @param User|null $user Optional user who triggered the usage
|
||||||
|
* @param array<string, mixed>|null $metadata Optional metadata for audit/debugging
|
||||||
|
* @return EntitlementResult Contains allowed/denied status. If allowed, usage has already been recorded.
|
||||||
|
*/
|
||||||
|
public function checkAndRecordUsage(
|
||||||
|
Workspace $workspace,
|
||||||
|
string $featureCode,
|
||||||
|
int $quantity = 1,
|
||||||
|
?User $user = null,
|
||||||
|
?array $metadata = null
|
||||||
|
): EntitlementResult {
|
||||||
|
$feature = $this->getFeature($featureCode);
|
||||||
|
|
||||||
|
if (! $feature) {
|
||||||
|
return EntitlementResult::denied(
|
||||||
|
reason: "Feature '{$featureCode}' does not exist.",
|
||||||
|
featureCode: $featureCode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$poolFeatureCode = $feature->getPoolFeatureCode();
|
||||||
|
$totalLimit = $this->getTotalLimit($workspace, $poolFeatureCode);
|
||||||
|
|
||||||
|
if ($totalLimit === null) {
|
||||||
|
return EntitlementResult::denied(
|
||||||
|
reason: "Your plan does not include {$feature->name}.",
|
||||||
|
featureCode: $featureCode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($totalLimit === -1) {
|
||||||
|
// Unlimited — still record usage for tracking, but no need for locking
|
||||||
|
$this->recordUsage($workspace, $featureCode, $quantity, $user, $metadata);
|
||||||
|
|
||||||
|
return EntitlementResult::unlimited($featureCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($feature->isBoolean()) {
|
||||||
|
return EntitlementResult::allowed(featureCode: $featureCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Atomic check-and-record within a transaction with row-level locking
|
||||||
|
return DB::transaction(function () use ($workspace, $feature, $poolFeatureCode, $totalLimit, $quantity, $user, $metadata, $featureCode) {
|
||||||
|
// Lock existing usage records to prevent concurrent reads from seeing
|
||||||
|
// the same count. This serialises concurrent usage recording.
|
||||||
|
$currentUsage = $this->getLockedCurrentUsage($workspace, $poolFeatureCode, $feature);
|
||||||
|
|
||||||
|
if ($currentUsage + $quantity > $totalLimit) {
|
||||||
|
return EntitlementResult::denied(
|
||||||
|
reason: "You've reached your {$feature->name} limit ({$totalLimit}).",
|
||||||
|
limit: $totalLimit,
|
||||||
|
used: $currentUsage,
|
||||||
|
featureCode: $featureCode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
UsageRecord::create([
|
||||||
|
'workspace_id' => $workspace->id,
|
||||||
|
'feature_code' => $poolFeatureCode,
|
||||||
|
'quantity' => $quantity,
|
||||||
|
'user_id' => $user?->id,
|
||||||
|
'metadata' => $metadata,
|
||||||
|
'recorded_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->invalidateUsageCache($workspace, $poolFeatureCode);
|
||||||
|
|
||||||
|
return EntitlementResult::allowed(
|
||||||
|
limit: $totalLimit,
|
||||||
|
used: $currentUsage + $quantity,
|
||||||
|
featureCode: $featureCode
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomically check namespace entitlement and record usage in a single transaction.
|
||||||
|
*
|
||||||
|
* Namespace equivalent of `checkAndRecordUsage()`. Prevents race conditions
|
||||||
|
* for namespace-scoped usage by using row-level locking within a transaction.
|
||||||
|
*
|
||||||
|
* @param Namespace_ $namespace The namespace to check and record for
|
||||||
|
* @param string $featureCode The feature code to check
|
||||||
|
* @param int $quantity The amount to consume (default: 1)
|
||||||
|
* @param User|null $user Optional user who triggered the usage
|
||||||
|
* @param array<string, mixed>|null $metadata Optional metadata for audit/debugging
|
||||||
|
* @return EntitlementResult Contains allowed/denied status. If allowed, usage has already been recorded.
|
||||||
|
*/
|
||||||
|
public function checkAndRecordNamespaceUsage(
|
||||||
|
Namespace_ $namespace,
|
||||||
|
string $featureCode,
|
||||||
|
int $quantity = 1,
|
||||||
|
?User $user = null,
|
||||||
|
?array $metadata = null
|
||||||
|
): EntitlementResult {
|
||||||
|
$feature = $this->getFeature($featureCode);
|
||||||
|
|
||||||
|
if (! $feature) {
|
||||||
|
return EntitlementResult::denied(
|
||||||
|
reason: "Feature '{$featureCode}' does not exist.",
|
||||||
|
featureCode: $featureCode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$poolFeatureCode = $feature->getPoolFeatureCode();
|
||||||
|
|
||||||
|
// Resolve the effective limit using the namespace cascade
|
||||||
|
$totalLimit = $this->getNamespaceTotalLimit($namespace, $poolFeatureCode);
|
||||||
|
|
||||||
|
if ($totalLimit === null && $namespace->workspace_id) {
|
||||||
|
$workspace = $namespace->workspace;
|
||||||
|
if ($workspace) {
|
||||||
|
$totalLimit = $this->getTotalLimit($workspace, $poolFeatureCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($totalLimit === null) {
|
||||||
|
return EntitlementResult::denied(
|
||||||
|
reason: "Your plan does not include {$feature->name}.",
|
||||||
|
featureCode: $featureCode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($totalLimit === -1) {
|
||||||
|
$this->recordNamespaceUsage($namespace, $featureCode, $quantity, $user, $metadata);
|
||||||
|
|
||||||
|
return EntitlementResult::unlimited($featureCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($feature->isBoolean()) {
|
||||||
|
return EntitlementResult::allowed(featureCode: $featureCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($namespace, $feature, $poolFeatureCode, $totalLimit, $quantity, $user, $metadata, $featureCode) {
|
||||||
|
$currentUsage = $this->getLockedNamespaceCurrentUsage($namespace, $poolFeatureCode, $feature);
|
||||||
|
|
||||||
|
if ($currentUsage + $quantity > $totalLimit) {
|
||||||
|
return EntitlementResult::denied(
|
||||||
|
reason: "You've reached your {$feature->name} limit ({$totalLimit}).",
|
||||||
|
limit: $totalLimit,
|
||||||
|
used: $currentUsage,
|
||||||
|
featureCode: $featureCode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
UsageRecord::create([
|
||||||
|
'namespace_id' => $namespace->id,
|
||||||
|
'workspace_id' => $namespace->workspace_id,
|
||||||
|
'feature_code' => $poolFeatureCode,
|
||||||
|
'quantity' => $quantity,
|
||||||
|
'user_id' => $user?->id,
|
||||||
|
'metadata' => $metadata,
|
||||||
|
'recorded_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->invalidateNamespaceUsageCache($namespace, $poolFeatureCode);
|
||||||
|
|
||||||
|
return EntitlementResult::allowed(
|
||||||
|
limit: $totalLimit,
|
||||||
|
used: $currentUsage + $quantity,
|
||||||
|
featureCode: $featureCode
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provision a package for a workspace.
|
* Provision a package for a workspace.
|
||||||
*
|
*
|
||||||
|
|
@ -599,37 +787,40 @@ class EntitlementService
|
||||||
): WorkspacePackage {
|
): WorkspacePackage {
|
||||||
$package = Package::where('code', $packageCode)->firstOrFail();
|
$package = Package::where('code', $packageCode)->firstOrFail();
|
||||||
|
|
||||||
// Check if this is a base package and workspace already has one
|
$workspacePackage = DB::transaction(function () use ($workspace, $package, $options) {
|
||||||
if ($package->is_base_package) {
|
// Use lockForUpdate() to prevent concurrent provisioning from creating
|
||||||
$existingBase = $workspace->workspacePackages()
|
// duplicate active base packages (TOCTOU race condition).
|
||||||
->whereHas('package', fn ($q) => $q->where('is_base_package', true))
|
if ($package->is_base_package) {
|
||||||
->active()
|
$existingBase = $workspace->workspacePackages()
|
||||||
->first();
|
->whereHas('package', fn ($q) => $q->where('is_base_package', true))
|
||||||
|
->active()
|
||||||
|
->lockForUpdate()
|
||||||
|
->first();
|
||||||
|
|
||||||
if ($existingBase) {
|
if ($existingBase) {
|
||||||
// Cancel existing base package
|
$existingBase->cancel(now());
|
||||||
$existingBase->cancel(now());
|
|
||||||
|
|
||||||
EntitlementLog::logPackageAction(
|
EntitlementLog::logPackageAction(
|
||||||
$workspace,
|
$workspace,
|
||||||
EntitlementLog::ACTION_PACKAGE_CANCELLED,
|
EntitlementLog::ACTION_PACKAGE_CANCELLED,
|
||||||
$existingBase,
|
$existingBase,
|
||||||
source: $options['source'] ?? EntitlementLog::SOURCE_SYSTEM,
|
source: $options['source'] ?? EntitlementLog::SOURCE_SYSTEM,
|
||||||
metadata: ['reason' => 'Replaced by new base package']
|
metadata: ['reason' => 'Replaced by new base package']
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$workspacePackage = WorkspacePackage::create([
|
return WorkspacePackage::create([
|
||||||
'workspace_id' => $workspace->id,
|
'workspace_id' => $workspace->id,
|
||||||
'package_id' => $package->id,
|
'package_id' => $package->id,
|
||||||
'status' => WorkspacePackage::STATUS_ACTIVE,
|
'status' => WorkspacePackage::STATUS_ACTIVE,
|
||||||
'starts_at' => $options['starts_at'] ?? now(),
|
'starts_at' => $options['starts_at'] ?? now(),
|
||||||
'expires_at' => $options['expires_at'] ?? null,
|
'expires_at' => $options['expires_at'] ?? null,
|
||||||
'billing_cycle_anchor' => $options['billing_cycle_anchor'] ?? now(),
|
'billing_cycle_anchor' => $options['billing_cycle_anchor'] ?? now(),
|
||||||
'blesta_service_id' => $options['blesta_service_id'] ?? null,
|
'blesta_service_id' => $options['blesta_service_id'] ?? null,
|
||||||
'metadata' => $options['metadata'] ?? null,
|
'metadata' => $options['metadata'] ?? null,
|
||||||
]);
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
EntitlementLog::logPackageAction(
|
EntitlementLog::logPackageAction(
|
||||||
$workspace,
|
$workspace,
|
||||||
|
|
@ -723,19 +914,42 @@ class EntitlementService
|
||||||
string $featureCode,
|
string $featureCode,
|
||||||
array $options = []
|
array $options = []
|
||||||
): Boost {
|
): Boost {
|
||||||
$boost = Boost::create([
|
$boost = DB::transaction(function () use ($workspace, $featureCode, $options) {
|
||||||
'workspace_id' => $workspace->id,
|
// Lock existing active boosts for this workspace+feature to prevent
|
||||||
'feature_code' => $featureCode,
|
// duplicate provisioning from concurrent requests.
|
||||||
'boost_type' => $options['boost_type'] ?? Boost::BOOST_TYPE_ADD_LIMIT,
|
$existingBoosts = $workspace->boosts()
|
||||||
'duration_type' => $options['duration_type'] ?? Boost::DURATION_CYCLE_BOUND,
|
->forFeature($featureCode)
|
||||||
'limit_value' => $options['limit_value'] ?? null,
|
->usable()
|
||||||
'consumed_quantity' => 0,
|
->lockForUpdate()
|
||||||
'status' => Boost::STATUS_ACTIVE,
|
->get();
|
||||||
'starts_at' => $options['starts_at'] ?? now(),
|
|
||||||
'expires_at' => $options['expires_at'] ?? null,
|
// If a blesta_addon_id is provided, check for duplicates to prevent
|
||||||
'blesta_addon_id' => $options['blesta_addon_id'] ?? null,
|
// the same external addon from being provisioned twice.
|
||||||
'metadata' => $options['metadata'] ?? null,
|
$blestaAddonId = $options['blesta_addon_id'] ?? null;
|
||||||
]);
|
if ($blestaAddonId !== null) {
|
||||||
|
$duplicate = $existingBoosts->first(
|
||||||
|
fn (Boost $b) => $b->blesta_addon_id === $blestaAddonId
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($duplicate) {
|
||||||
|
return $duplicate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Boost::create([
|
||||||
|
'workspace_id' => $workspace->id,
|
||||||
|
'feature_code' => $featureCode,
|
||||||
|
'boost_type' => $options['boost_type'] ?? Boost::BOOST_TYPE_ADD_LIMIT,
|
||||||
|
'duration_type' => $options['duration_type'] ?? Boost::DURATION_CYCLE_BOUND,
|
||||||
|
'limit_value' => $options['limit_value'] ?? null,
|
||||||
|
'consumed_quantity' => 0,
|
||||||
|
'status' => Boost::STATUS_ACTIVE,
|
||||||
|
'starts_at' => $options['starts_at'] ?? now(),
|
||||||
|
'expires_at' => $options['expires_at'] ?? null,
|
||||||
|
'blesta_addon_id' => $blestaAddonId,
|
||||||
|
'metadata' => $options['metadata'] ?? null,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
EntitlementLog::logBoostAction(
|
EntitlementLog::logBoostAction(
|
||||||
$workspace,
|
$workspace,
|
||||||
|
|
@ -1224,6 +1438,79 @@ class EntitlementService
|
||||||
return Cache::remember($cacheKey, self::USAGE_CACHE_TTL, $callback);
|
return Cache::remember($cacheKey, self::USAGE_CACHE_TTL, $callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current usage with a pessimistic lock for atomic check-and-record.
|
||||||
|
*
|
||||||
|
* This bypasses the cache and queries the database directly with `lockForUpdate()`
|
||||||
|
* to ensure that concurrent transactions see a consistent usage count. Must be
|
||||||
|
* called within a DB::transaction().
|
||||||
|
*
|
||||||
|
* @param Workspace $workspace The workspace to get locked usage for
|
||||||
|
* @param string $featureCode The feature code to get usage for
|
||||||
|
* @param Feature $feature The feature model (for reset configuration)
|
||||||
|
* @return int The current usage count (under lock)
|
||||||
|
*/
|
||||||
|
protected function getLockedCurrentUsage(Workspace $workspace, string $featureCode, Feature $feature): int
|
||||||
|
{
|
||||||
|
$query = UsageRecord::where('workspace_id', $workspace->id)
|
||||||
|
->where('feature_code', $featureCode)
|
||||||
|
->lockForUpdate();
|
||||||
|
|
||||||
|
if ($feature->resetsMonthly()) {
|
||||||
|
$primaryPackage = $workspace->workspacePackages()
|
||||||
|
->whereHas('package', fn ($q) => $q->where('is_base_package', true))
|
||||||
|
->active()
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$cycleStart = $primaryPackage
|
||||||
|
? $primaryPackage->getCurrentCycleStart()
|
||||||
|
: now()->startOfMonth();
|
||||||
|
|
||||||
|
$query->where('recorded_at', '>=', $cycleStart);
|
||||||
|
} elseif ($feature->resetsRolling()) {
|
||||||
|
$days = $feature->rolling_window_days ?? 30;
|
||||||
|
$query->where('recorded_at', '>=', now()->subDays($days));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $query->sum('quantity');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current namespace usage with a pessimistic lock for atomic check-and-record.
|
||||||
|
*
|
||||||
|
* Namespace equivalent of `getLockedCurrentUsage()`. Must be called within
|
||||||
|
* a DB::transaction().
|
||||||
|
*
|
||||||
|
* @param Namespace_ $namespace The namespace to get locked usage for
|
||||||
|
* @param string $featureCode The feature code to get usage for
|
||||||
|
* @param Feature $feature The feature model (for reset configuration)
|
||||||
|
* @return int The current usage count (under lock)
|
||||||
|
*/
|
||||||
|
protected function getLockedNamespaceCurrentUsage(Namespace_ $namespace, string $featureCode, Feature $feature): int
|
||||||
|
{
|
||||||
|
$query = UsageRecord::where('namespace_id', $namespace->id)
|
||||||
|
->where('feature_code', $featureCode)
|
||||||
|
->lockForUpdate();
|
||||||
|
|
||||||
|
if ($feature->resetsMonthly()) {
|
||||||
|
$primaryPackage = $namespace->namespacePackages()
|
||||||
|
->whereHas('package', fn ($q) => $q->where('is_base_package', true))
|
||||||
|
->active()
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$cycleStart = $primaryPackage
|
||||||
|
? $primaryPackage->getCurrentCycleStart()
|
||||||
|
: now()->startOfMonth();
|
||||||
|
|
||||||
|
$query->where('recorded_at', '>=', $cycleStart);
|
||||||
|
} elseif ($feature->resetsRolling()) {
|
||||||
|
$days = $feature->rolling_window_days ?? 30;
|
||||||
|
$query->where('recorded_at', '>=', now()->subDays($days));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $query->sum('quantity');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a feature by its unique code.
|
* Get a feature by its unique code.
|
||||||
*
|
*
|
||||||
|
|
@ -1747,28 +2034,31 @@ class EntitlementService
|
||||||
): NamespacePackage {
|
): NamespacePackage {
|
||||||
$package = Package::where('code', $packageCode)->firstOrFail();
|
$package = Package::where('code', $packageCode)->firstOrFail();
|
||||||
|
|
||||||
// Check if this is a base package and namespace already has one
|
$namespacePackage = DB::transaction(function () use ($namespace, $package, $options) {
|
||||||
if ($package->is_base_package) {
|
// Use lockForUpdate() to prevent concurrent provisioning from creating
|
||||||
$existingBase = $namespace->namespacePackages()
|
// duplicate active base packages (TOCTOU race condition).
|
||||||
->whereHas('package', fn ($q) => $q->where('is_base_package', true))
|
if ($package->is_base_package) {
|
||||||
->active()
|
$existingBase = $namespace->namespacePackages()
|
||||||
->first();
|
->whereHas('package', fn ($q) => $q->where('is_base_package', true))
|
||||||
|
->active()
|
||||||
|
->lockForUpdate()
|
||||||
|
->first();
|
||||||
|
|
||||||
if ($existingBase) {
|
if ($existingBase) {
|
||||||
// Cancel existing base package
|
$existingBase->cancel(now());
|
||||||
$existingBase->cancel(now());
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$namespacePackage = NamespacePackage::create([
|
return NamespacePackage::create([
|
||||||
'namespace_id' => $namespace->id,
|
'namespace_id' => $namespace->id,
|
||||||
'package_id' => $package->id,
|
'package_id' => $package->id,
|
||||||
'status' => NamespacePackage::STATUS_ACTIVE,
|
'status' => NamespacePackage::STATUS_ACTIVE,
|
||||||
'starts_at' => $options['starts_at'] ?? now(),
|
'starts_at' => $options['starts_at'] ?? now(),
|
||||||
'expires_at' => $options['expires_at'] ?? null,
|
'expires_at' => $options['expires_at'] ?? null,
|
||||||
'billing_cycle_anchor' => $options['billing_cycle_anchor'] ?? now(),
|
'billing_cycle_anchor' => $options['billing_cycle_anchor'] ?? now(),
|
||||||
'metadata' => $options['metadata'] ?? null,
|
'metadata' => $options['metadata'] ?? null,
|
||||||
]);
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
$this->invalidateNamespaceCache($namespace);
|
$this->invalidateNamespaceCache($namespace);
|
||||||
|
|
||||||
|
|
@ -1846,19 +2136,29 @@ class EntitlementService
|
||||||
string $featureCode,
|
string $featureCode,
|
||||||
array $options = []
|
array $options = []
|
||||||
): Boost {
|
): Boost {
|
||||||
$boost = Boost::create([
|
$boost = DB::transaction(function () use ($namespace, $featureCode, $options) {
|
||||||
'namespace_id' => $namespace->id,
|
// Lock existing active boosts for this namespace+feature to prevent
|
||||||
'workspace_id' => $namespace->workspace_id,
|
// duplicate provisioning from concurrent requests.
|
||||||
'feature_code' => $featureCode,
|
$namespace->boosts()
|
||||||
'boost_type' => $options['boost_type'] ?? Boost::BOOST_TYPE_ADD_LIMIT,
|
->forFeature($featureCode)
|
||||||
'duration_type' => $options['duration_type'] ?? Boost::DURATION_CYCLE_BOUND,
|
->usable()
|
||||||
'limit_value' => $options['limit_value'] ?? null,
|
->lockForUpdate()
|
||||||
'consumed_quantity' => 0,
|
->get();
|
||||||
'status' => Boost::STATUS_ACTIVE,
|
|
||||||
'starts_at' => $options['starts_at'] ?? now(),
|
return Boost::create([
|
||||||
'expires_at' => $options['expires_at'] ?? null,
|
'namespace_id' => $namespace->id,
|
||||||
'metadata' => $options['metadata'] ?? null,
|
'workspace_id' => $namespace->workspace_id,
|
||||||
]);
|
'feature_code' => $featureCode,
|
||||||
|
'boost_type' => $options['boost_type'] ?? Boost::BOOST_TYPE_ADD_LIMIT,
|
||||||
|
'duration_type' => $options['duration_type'] ?? Boost::DURATION_CYCLE_BOUND,
|
||||||
|
'limit_value' => $options['limit_value'] ?? null,
|
||||||
|
'consumed_quantity' => 0,
|
||||||
|
'status' => Boost::STATUS_ACTIVE,
|
||||||
|
'starts_at' => $options['starts_at'] ?? now(),
|
||||||
|
'expires_at' => $options['expires_at'] ?? null,
|
||||||
|
'metadata' => $options['metadata'] ?? null,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
$this->invalidateNamespaceCache($namespace);
|
$this->invalidateNamespaceCache($namespace);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue