feat: enhance pricing page with feature limits and trial management
- Add comprehensive feature limits enforcement middleware - Implement subscription dashboard with usage analytics - Create reusable plan card component with feature badges - Add trial configuration support with limit overrides - Fix payment controller null safety issues - Improve pricing page UI with proper feature display
This commit is contained in:
@@ -12,7 +12,7 @@ use Illuminate\Validation\ValidationException;
|
||||
class PaymentController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private PaymentOrchestrator $orchestrator
|
||||
private readonly PaymentOrchestrator $orchestrator
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -25,12 +25,55 @@ class PaymentController extends Controller
|
||||
'plan_id' => 'required|exists:plans,id',
|
||||
'provider' => 'nullable|string|in:stripe,lemon_squeezy,polar,oxapay,crypto,activation_key',
|
||||
'options' => 'nullable|array',
|
||||
'is_trial' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
$plan = Plan::findOrFail($validated['plan_id']);
|
||||
$plan = Plan::with(['planProviders', 'trialConfiguration'])->findOrFail($validated['plan_id']);
|
||||
$provider = $validated['provider'] ?? null;
|
||||
$options = $validated['options'] ?? [];
|
||||
$isTrial = $validated['is_trial'] ?? false;
|
||||
|
||||
// Validate provider support
|
||||
if ($provider && ! $plan?->supportsProvider($provider)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => "Provider '{$provider}' is not supported for this plan.",
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Validate trial requirements
|
||||
if ($isTrial) {
|
||||
if (! $plan?->hasTrial()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'This plan does not offer trials.',
|
||||
], 400);
|
||||
}
|
||||
|
||||
$trialConfig = $plan?->getTrialConfig();
|
||||
if ($trialConfig && $trialConfig->trial_requires_payment_method && ! $provider) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'Payment method is required for trial. Please specify a provider.',
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
|
||||
// Enhance options with plan-specific data
|
||||
$options = array_merge($options, [
|
||||
'is_trial' => $isTrial,
|
||||
'plan_features' => $plan?->getFeaturesWithLimits() ?? [],
|
||||
'billing_cycle' => $plan?->getBillingCycleDisplay() ?? 'Unknown',
|
||||
'plan_tier' => $plan?->planTier?->name,
|
||||
]);
|
||||
|
||||
if (! $plan) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'Plan not found.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$result = $this->orchestrator->createCheckoutSession($user, $plan, $provider, $options);
|
||||
|
||||
@@ -70,13 +113,20 @@ class PaymentController extends Controller
|
||||
$options = $validated['options'] ?? [];
|
||||
|
||||
// Only recurring providers can create subscriptions
|
||||
if (! $plan->monthly_billing) {
|
||||
if (! $plan?->monthly_billing) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'This plan does not support recurring subscriptions. Use checkout instead.',
|
||||
], 400);
|
||||
}
|
||||
|
||||
if (! $plan) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'Plan not found.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$result = $this->orchestrator->createSubscription($user, $plan, $provider, $options);
|
||||
|
||||
return response()->json([
|
||||
@@ -108,6 +158,14 @@ class PaymentController extends Controller
|
||||
]);
|
||||
|
||||
$plan = Plan::findOrFail($validated['plan_id']);
|
||||
|
||||
if (! $plan) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'Plan not found.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$providers = $this->orchestrator->getActiveProvidersForPlan($plan);
|
||||
|
||||
$methods = $providers->map(function ($provider) use ($plan) {
|
||||
@@ -117,7 +175,7 @@ class PaymentController extends Controller
|
||||
'supports_recurring' => $provider->supportsRecurring(),
|
||||
'supports_one_time' => $provider->supportsOneTime(),
|
||||
'supported_currencies' => $provider->getSupportedCurrencies(),
|
||||
'fees' => $provider->calculateFees($plan->price),
|
||||
'fees' => $provider->calculateFees($plan?->price ?? 0),
|
||||
'active' => $provider->isActive(),
|
||||
];
|
||||
})->values()->toArray();
|
||||
@@ -126,10 +184,10 @@ class PaymentController extends Controller
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'plan' => [
|
||||
'id' => $plan->id,
|
||||
'name' => $plan->name,
|
||||
'price' => $plan->price,
|
||||
'monthly_billing' => $plan->monthly_billing,
|
||||
'id' => $plan?->id,
|
||||
'name' => $plan?->name,
|
||||
'price' => $plan?->price,
|
||||
'monthly_billing' => $plan?->monthly_billing,
|
||||
],
|
||||
'payment_methods' => $methods,
|
||||
],
|
||||
@@ -237,4 +295,302 @@ class PaymentController extends Controller
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a trial subscription
|
||||
*/
|
||||
public function startTrial(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$validated = $request->validate([
|
||||
'plan_id' => 'required|exists:plans,id',
|
||||
'provider' => 'required|string|in:stripe,lemon_squeezy,polar',
|
||||
'options' => 'nullable|array',
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
$plan = Plan::with(['trialConfiguration', 'planProviders'])->findOrFail($validated['plan_id']);
|
||||
$provider = $validated['provider'];
|
||||
$options = $validated['options'] ?? [];
|
||||
|
||||
// Validate trial availability
|
||||
if (! $plan?->hasTrial()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'This plan does not offer trials.',
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Validate provider support
|
||||
if (! $plan?->supportsProvider($provider)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => "Provider '{$provider}' is not supported for this plan.",
|
||||
], 400);
|
||||
}
|
||||
|
||||
$trialConfig = $plan?->getTrialConfig();
|
||||
|
||||
// Check if user already has an active trial for this plan
|
||||
$existingTrial = $user->subscriptions()
|
||||
->where('plan_id', $plan?->id)
|
||||
->where('status', 'trialing')
|
||||
->first();
|
||||
|
||||
if ($existingTrial) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'You already have an active trial for this plan.',
|
||||
], 400);
|
||||
}
|
||||
|
||||
if (! $plan) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'Plan not found.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Create trial subscription using checkout session with trial options
|
||||
$trialOptions = array_merge($options, [
|
||||
'is_trial' => true,
|
||||
'trial_duration_days' => $trialConfig?->trial_duration_days ?? 14,
|
||||
'trial_requires_payment_method' => $trialConfig?->trial_requires_payment_method ?? true,
|
||||
]);
|
||||
|
||||
$result = $this->orchestrator->createCheckoutSession($user, $plan, $provider, $trialOptions);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $result,
|
||||
]);
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'errors' => $e->errors(),
|
||||
], 422);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plan comparison data
|
||||
*/
|
||||
public function getPlanComparison(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$validated = $request->validate([
|
||||
'plan_ids' => 'required|array|min:1|max:5',
|
||||
'plan_ids.*' => 'exists:plans,id',
|
||||
]);
|
||||
|
||||
$plans = Plan::with([
|
||||
'planFeatureLimits.planFeature',
|
||||
'planProviders',
|
||||
'trialConfiguration',
|
||||
'planTier',
|
||||
])
|
||||
->whereIn('id', $validated['plan_ids'])
|
||||
->active()
|
||||
->ordered()
|
||||
->get();
|
||||
|
||||
$comparison = $plans->map(function ($plan) {
|
||||
$trialConfig = $plan?->getTrialConfig();
|
||||
|
||||
return [
|
||||
'id' => $plan?->id,
|
||||
'name' => $plan?->name,
|
||||
'description' => $plan?->description,
|
||||
'price' => $plan?->price,
|
||||
'billing_cycle' => $plan?->getBillingCycleDisplay() ?? 'Unknown',
|
||||
'tier' => $plan?->planTier?->name,
|
||||
'providers' => $plan?->getAllowedProviders() ?? [],
|
||||
'features' => $plan?->getFeaturesWithLimits() ?? [],
|
||||
'trial' => $plan?->hasTrial() ? [
|
||||
'available' => true,
|
||||
'duration_days' => $trialConfig?->trial_duration_days ?? 0,
|
||||
'requires_payment_method' => $trialConfig?->trial_requires_payment_method ?? false,
|
||||
] : ['available' => false],
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $comparison,
|
||||
]);
|
||||
|
||||
} catch (ValidationException $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'errors' => $e->errors(),
|
||||
], 422);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upgrade paths for user's current subscription
|
||||
*/
|
||||
public function getUpgradePaths(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$user = $request->user();
|
||||
$currentSubscription = $user->subscription('default');
|
||||
|
||||
if (! $currentSubscription) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'No active subscription found.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$currentPlan = $currentSubscription->plan;
|
||||
$upgradePaths = Plan::with(['planTier', 'planFeatureLimits.planFeature'])
|
||||
->where('id', '!=', $currentPlan->id)
|
||||
->where(function ($query) use ($currentPlan) {
|
||||
$query->where('price', '>', $currentPlan->price)
|
||||
->orWhereHas('planTier', function ($q) use ($currentPlan) {
|
||||
$q->where('sort_order', '>', $currentPlan->planTier?->sort_order ?? 0);
|
||||
});
|
||||
})
|
||||
->active()
|
||||
->ordered()
|
||||
->get();
|
||||
|
||||
$paths = $upgradePaths->map(function ($plan) use ($currentPlan) {
|
||||
return [
|
||||
'plan' => [
|
||||
'id' => $plan->id,
|
||||
'name' => $plan->name,
|
||||
'price' => $plan->price,
|
||||
'billing_cycle' => $plan->getBillingCycleDisplay(),
|
||||
'tier' => $plan->planTier?->name,
|
||||
],
|
||||
'upgrade_benefits' => $this->getUpgradeBenefits($currentPlan, $plan),
|
||||
'providers' => $plan->getAllowedProviders(),
|
||||
'can_migrate' => true,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'current_plan' => [
|
||||
'id' => $currentPlan->id,
|
||||
'name' => $currentPlan->name,
|
||||
'price' => $currentPlan->price,
|
||||
],
|
||||
'upgrade_paths' => $paths,
|
||||
],
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upgrade benefits between two plans
|
||||
*/
|
||||
private function getUpgradeBenefits(Plan $currentPlan, Plan $newPlan): array
|
||||
{
|
||||
$currentFeatures = $currentPlan->getFeaturesWithLimits();
|
||||
$newFeatures = $newPlan->getFeaturesWithLimits();
|
||||
|
||||
$benefits = [];
|
||||
|
||||
foreach ($newFeatures as $featureData) {
|
||||
$feature = $featureData['feature'];
|
||||
$newLimit = $featureData['limit'];
|
||||
$currentLimit = collect($currentFeatures)
|
||||
->firstWhere('feature.id', $feature->id)['limit'] ?? null;
|
||||
|
||||
if (! $currentLimit || $this->isUpgradeBenefit($currentLimit, $newLimit)) {
|
||||
$benefits[] = [
|
||||
'feature' => $feature->display_name,
|
||||
'from' => $this->formatLimitDisplay($currentLimit),
|
||||
'to' => $this->formatLimitDisplay($newLimit),
|
||||
'improvement' => $this->getImprovementType($currentLimit, $newLimit),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $benefits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a limit change is an upgrade benefit
|
||||
*/
|
||||
private function isUpgradeBenefit($currentLimit, $newLimit): bool
|
||||
{
|
||||
if (! $currentLimit) {
|
||||
return true;
|
||||
}
|
||||
if (! $newLimit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Boolean upgrades
|
||||
if ($newLimit->limit_type === 'boolean') {
|
||||
return ! $currentLimit->limit_value && $newLimit->limit_value;
|
||||
}
|
||||
|
||||
// Numeric upgrades
|
||||
if ($newLimit->limit_type === 'numeric') {
|
||||
return ($newLimit->limit_value ?? 0) > ($currentLimit->limit_value ?? 0);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format limit value for display
|
||||
*/
|
||||
private function formatLimitDisplay($limit): string
|
||||
{
|
||||
if (! $limit) {
|
||||
return 'Not Available';
|
||||
}
|
||||
|
||||
if ($limit->limit_type === 'boolean') {
|
||||
return $limit->limit_value ? 'Enabled' : 'Disabled';
|
||||
}
|
||||
|
||||
if ($limit->limit_type === 'numeric') {
|
||||
return $limit->limit_value ? (string) $limit->limit_value : 'Unlimited';
|
||||
}
|
||||
|
||||
return 'Limited';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get improvement type for upgrade benefit
|
||||
*/
|
||||
private function getImprovementType($currentLimit, $newLimit): string
|
||||
{
|
||||
if (! $currentLimit) {
|
||||
return 'New Feature';
|
||||
}
|
||||
if ($newLimit->limit_type === 'boolean') {
|
||||
return 'Enabled';
|
||||
}
|
||||
if ($newLimit->limit_type === 'numeric') {
|
||||
return 'Increased Limit';
|
||||
}
|
||||
|
||||
return 'Improved';
|
||||
}
|
||||
}
|
||||
|
||||
289
app/Http/Middleware/EnforceFeatureLimits.php
Normal file
289
app/Http/Middleware/EnforceFeatureLimits.php
Normal file
@@ -0,0 +1,289 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\Plan;
|
||||
use App\Models\PlanUsage;
|
||||
use App\Models\Subscription;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnforceFeatureLimits
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request and enforce feature limits.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, ?string $featureName = null): Response
|
||||
{
|
||||
// Skip enforcement for guest users
|
||||
if (! Auth::check()) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Skip enforcement for admin users (configurable)
|
||||
if ($this->isAdminUser()) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Get the user's active subscription
|
||||
$subscription = $this->getActiveSubscription();
|
||||
if (! $subscription) {
|
||||
return $this->handleNoSubscription($request);
|
||||
}
|
||||
|
||||
// Get the plan with features
|
||||
$plan = $subscription->plan->load(['planFeatureLimits.planFeature']);
|
||||
if (! $plan) {
|
||||
Log::error('No plan found for subscription', ['subscription_id' => $subscription->id]);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Check if we're enforcing a specific feature
|
||||
if ($featureName) {
|
||||
return $this->enforceSpecificFeature($request, $next, $plan, $subscription, $featureName);
|
||||
}
|
||||
|
||||
// General usage tracking for common features
|
||||
$this->trackGeneralUsage($request, $plan, $subscription);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce limits for a specific feature
|
||||
*/
|
||||
private function enforceSpecificFeature(
|
||||
Request $request,
|
||||
Closure $next,
|
||||
Plan $plan,
|
||||
Subscription $subscription,
|
||||
string $featureName
|
||||
): Response {
|
||||
// Check if user can use the feature
|
||||
$currentUsage = $this->getCurrentUsage($subscription->id, $featureName);
|
||||
$isOnTrial = (bool) ($subscription?->onTrial() ?? false);
|
||||
|
||||
if (! $plan->canUseFeature($featureName, $currentUsage, $isOnTrial)) {
|
||||
return $this->handleFeatureLimitExceeded($request, $plan, $featureName, $currentUsage);
|
||||
}
|
||||
|
||||
// Track usage if this is a usage-generating request
|
||||
if ($this->isUsageGeneratingRequest($request)) {
|
||||
$this->incrementUsage($subscription->id, $featureName);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's active subscription
|
||||
*/
|
||||
private function getActiveSubscription(): ?Subscription
|
||||
{
|
||||
return Auth::user()->subscriptions()
|
||||
->whereIn('status', ['active', 'trialing'])
|
||||
->where('ends_at', '>', now())
|
||||
->with('plan')
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle requests from users with no subscription
|
||||
*/
|
||||
private function handleNoSubscription(Request $request): Response
|
||||
{
|
||||
// Allow access to non-protected routes
|
||||
$allowedRoutes = [
|
||||
'pricing',
|
||||
'checkout.*',
|
||||
'login',
|
||||
'register',
|
||||
'home',
|
||||
'dashboard.pricing',
|
||||
];
|
||||
|
||||
foreach ($allowedRoutes as $route) {
|
||||
if ($request->routeIs($route)) {
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect to pricing page for other routes
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'Subscription required',
|
||||
'message' => 'Please subscribe to access this feature.',
|
||||
'redirect_url' => route('pricing'),
|
||||
], 402);
|
||||
}
|
||||
|
||||
return redirect()->route('pricing')
|
||||
->with('error', 'Please subscribe to access this feature.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle when feature limit is exceeded
|
||||
*/
|
||||
private function handleFeatureLimitExceeded(
|
||||
Request $request,
|
||||
Plan $plan,
|
||||
string $featureName,
|
||||
float $currentUsage
|
||||
): Response {
|
||||
$feature = $plan->planFeatureLimits
|
||||
->whereHas('planFeature', function ($query) use ($featureName) {
|
||||
$query->where('name', $featureName);
|
||||
})
|
||||
->first();
|
||||
|
||||
$featureDisplayName = $feature?->planFeature->display_name ?? $featureName;
|
||||
$limit = $feature?->limit_value ?? 'Unknown';
|
||||
$remaining = max(0, ($limit ?? 0) - $currentUsage);
|
||||
|
||||
Log::info('Feature limit exceeded', [
|
||||
'user_id' => Auth::id(),
|
||||
'plan_id' => $plan->id,
|
||||
'feature' => $featureName,
|
||||
'current_usage' => $currentUsage,
|
||||
'limit' => $limit,
|
||||
]);
|
||||
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'error' => 'Feature limit exceeded',
|
||||
'message' => "You have reached your limit for {$featureDisplayName}. Current usage: {$currentUsage}, Limit: {$limit}",
|
||||
'feature' => $featureName,
|
||||
'current_usage' => $currentUsage,
|
||||
'limit' => $limit,
|
||||
'remaining' => $remaining,
|
||||
'upgrade_url' => $this->getUpgradeUrl($plan),
|
||||
], 429);
|
||||
}
|
||||
|
||||
return back()->with('error', "You have reached your limit for {$featureDisplayName}.
|
||||
Current usage: {$currentUsage}, Limit: {$limit}.
|
||||
<a href='{$this->getUpgradeUrl($plan)}' class='text-blue-600 hover:underline'>Upgrade your plan</a> to increase limits.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Track general usage for common features
|
||||
*/
|
||||
private function trackGeneralUsage(Request $request, Plan $plan, Subscription $subscription): void
|
||||
{
|
||||
// Track API calls
|
||||
if ($request->is('api/*') && $plan->hasFeature('api_access')) {
|
||||
$this->incrementUsage($subscription->id, 'api_access');
|
||||
}
|
||||
|
||||
// Track email operations
|
||||
if ($this->isEmailOperation($request) && $plan->hasFeature('email_sending')) {
|
||||
$this->incrementUsage($subscription->id, 'email_sending');
|
||||
}
|
||||
|
||||
// Track advanced filters usage
|
||||
if ($this->isFilterOperation($request) && $plan->hasFeature('advanced_filters')) {
|
||||
$this->incrementUsage($subscription->id, 'advanced_filters');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if request generates usage
|
||||
*/
|
||||
private function isUsageGeneratingRequest(Request $request): bool
|
||||
{
|
||||
// POST, PUT, PATCH requests typically generate usage
|
||||
return in_array($request->method(), ['POST', 'PUT', 'PATCH']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is an email operation
|
||||
*/
|
||||
private function isEmailOperation(Request $request): bool
|
||||
{
|
||||
return $request->is(['api/emails/*', 'emails/*']) ||
|
||||
str_contains($request->path(), 'email') ||
|
||||
$request->has('to') || $request->has('subject');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a filter operation
|
||||
*/
|
||||
private function isFilterOperation(Request $request): bool
|
||||
{
|
||||
return $request->has('filter') ||
|
||||
$request->has('filters') ||
|
||||
str_contains($request->path(), 'filter');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current usage for a feature
|
||||
*/
|
||||
private function getCurrentUsage(int $subscriptionId, string $featureName): float
|
||||
{
|
||||
$usage = PlanUsage::where('subscription_id', $subscriptionId)
|
||||
->whereHas('planFeature', function ($query) use ($featureName) {
|
||||
$query->where('name', $featureName);
|
||||
})
|
||||
->where('period_type', 'monthly')
|
||||
->whereMonth('created_at', now()->month)
|
||||
->whereYear('created_at', now()->year)
|
||||
->sum('usage_value');
|
||||
|
||||
return (float) $usage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment usage for a feature
|
||||
*/
|
||||
private function incrementUsage(int $subscriptionId, string $featureName): void
|
||||
{
|
||||
$subscription = Subscription::find($subscriptionId);
|
||||
$feature = $subscription->plan->planFeatureLimits
|
||||
->whereHas('planFeature', function ($query) use ($featureName) {
|
||||
$query->where('name', $featureName);
|
||||
})
|
||||
->first();
|
||||
|
||||
if (! $feature) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlanUsage::updateOrCreate([
|
||||
'subscription_id' => $subscriptionId,
|
||||
'plan_id' => $subscription->plan_id,
|
||||
'user_id' => Auth::id(),
|
||||
'plan_feature_id' => $feature->plan_feature_id,
|
||||
'period_type' => $feature->limit_type === 'boolean' ? 'total' : 'monthly',
|
||||
'created_at' => now()->startOfMonth(),
|
||||
], [
|
||||
'usage_value' => \DB::raw('usage_value + 1'),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is admin (bypasses limits)
|
||||
*/
|
||||
private function isAdminUser(): bool
|
||||
{
|
||||
return Auth::user() && Auth::user()->level >= 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get upgrade URL for plan
|
||||
*/
|
||||
private function getUpgradeUrl(Plan $currentPlan): string
|
||||
{
|
||||
$upgradePaths = $currentPlan->getUpgradePath();
|
||||
|
||||
return count($upgradePaths) > 0 ? route('pricing') : route('pricing');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user