Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
78.57% covered (warning)
78.57%
22 / 28
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
InvalidateUserSubscriptionCachesJob
78.57% covered (warning)
78.57%
22 / 28
50.00% covered (danger)
50.00%
2 / 4
6.35
0.00% covered (danger)
0.00%
0 / 1
 backoff
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 handle
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
3
 failed
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace App\Jobs;
4
5use App\Http\Models\Subscription;
6use Illuminate\Bus\Queueable;
7use Illuminate\Contracts\Queue\ShouldQueue;
8use Illuminate\Foundation\Bus\Dispatchable;
9use Illuminate\Queue\InteractsWithQueue;
10use Illuminate\Queue\SerializesModels;
11use Illuminate\Support\Facades\Cache;
12use Illuminate\Support\Facades\Log;
13
14/**
15 * Job to invalidate user subscription caches when a plan or feature changes.
16 *
17 * When an admin updates a plan or feature, this job queries subscriptions by
18 * stripe_plan to find affected users and invalidates their cached subscription
19 * data. This runs asynchronously to avoid blocking admin API responses.
20 */
21class InvalidateUserSubscriptionCachesJob implements ShouldQueue
22{
23    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
24
25    /**
26     * The number of times the job may be attempted.
27     *
28     * @var int
29     */
30    public $tries = 3;
31
32    /**
33     * The number of seconds to wait before retrying the job.
34     *
35     * @return array<int, int>
36     */
37    public function backoff(): array
38    {
39        return [10, 30, 60];
40    }
41
42    /**
43     * Cache key prefix for user subscriptions.
44     */
45    private const USER_SUBSCRIPTION_PREFIX = 'current_subscription_';
46
47    /**
48     * Batch size for processing users.
49     */
50    private const BATCH_SIZE = 100;
51
52    /**
53     * Create a new job instance.
54     *
55     * @param  string  $stripeId  The Stripe plan ID (price ID)
56     */
57    public function __construct(
58        public string $stripeId
59    ) {}
60
61    /**
62     * Execute the job.
63     */
64    public function handle(): void
65    {
66        $totalInvalidated = 0;
67        $startTime = microtime(true);
68
69        Log::info('Starting user subscription cache invalidation', [
70            'stripe_id' => $this->stripeId,
71        ]);
72
73        // Query active subscriptions for this plan in batches
74        Subscription::where('stripe_plan', $this->stripeId)
75            ->active()
76            ->select(['user_id'])
77            ->chunk(self::BATCH_SIZE, function ($subscriptions) use (&$totalInvalidated) {
78                foreach ($subscriptions as $subscription) {
79                    if ($subscription->user_id) {
80                        $cacheKey = self::USER_SUBSCRIPTION_PREFIX.$subscription->user_id;
81                        Cache::forget($cacheKey);
82                        $totalInvalidated++;
83                    }
84                }
85            });
86
87        $duration = round((microtime(true) - $startTime) * 1000, 2);
88
89        Log::info('User subscription cache invalidation completed', [
90            'stripe_id' => $this->stripeId,
91            'users_invalidated' => $totalInvalidated,
92            'duration_ms' => $duration,
93        ]);
94    }
95
96    /**
97     * Handle a job failure.
98     */
99    public function failed(\Throwable $exception): void
100    {
101        Log::error('User subscription cache invalidation job failed', [
102            'stripe_id' => $this->stripeId,
103            'error' => $exception->getMessage(),
104            'trace' => $exception->getTraceAsString(),
105        ]);
106    }
107}