Skip to content

Commit 8c12845

Browse files
simonhampclaude
andauthored
Queue new plugin notifications via a dedicated job (#356)
Move the notification fan-out from Plugin::approve() into a SendNewPluginNotifications job so the admin UI isn't blocked iterating through every opted-in user during approval. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 07f1e42 commit 8c12845

4 files changed

Lines changed: 83 additions & 22 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace App\Jobs;
4+
5+
use App\Models\Plugin;
6+
use App\Models\User;
7+
use App\Notifications\NewPluginAvailable;
8+
use Illuminate\Contracts\Queue\ShouldQueue;
9+
use Illuminate\Foundation\Queue\Queueable;
10+
use Illuminate\Support\Facades\Notification;
11+
12+
class SendNewPluginNotifications implements ShouldQueue
13+
{
14+
use Queueable;
15+
16+
public function __construct(public Plugin $plugin) {}
17+
18+
public function handle(): void
19+
{
20+
$recipients = User::query()
21+
->where('receives_new_plugin_notifications', true)
22+
->where('id', '!=', $this->plugin->user_id)
23+
->get();
24+
25+
Notification::send($recipients, new NewPluginAvailable($this->plugin));
26+
}
27+
}

app/Models/Plugin.php

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
use App\Enums\PluginTier;
88
use App\Enums\PluginType;
99
use App\Enums\PriceTier;
10-
use App\Notifications\NewPluginAvailable;
10+
use App\Jobs\SendNewPluginNotifications;
1111
use App\Notifications\PluginApproved;
1212
use App\Notifications\PluginRejected;
1313
use App\Services\PluginSyncService;
@@ -21,7 +21,6 @@
2121
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
2222
use Illuminate\Database\Eloquent\Relations\HasMany;
2323
use Illuminate\Database\Eloquent\Relations\HasOne;
24-
use Illuminate\Support\Facades\Notification;
2524

2625
class Plugin extends Model
2726
{
@@ -568,12 +567,7 @@ public function approve(int $approvedById): void
568567
$this->user->notify(new PluginApproved($this));
569568

570569
if ($isFirstApproval) {
571-
$recipients = User::query()
572-
->where('receives_new_plugin_notifications', true)
573-
->where('id', '!=', $this->user_id)
574-
->get();
575-
576-
Notification::send($recipients, new NewPluginAvailable($this));
570+
SendNewPluginNotifications::dispatch($this);
577571
}
578572

579573
resolve(PluginSyncService::class)->sync($this);
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace Tests\Feature\Jobs;
4+
5+
use App\Jobs\SendNewPluginNotifications;
6+
use App\Models\Plugin;
7+
use App\Models\User;
8+
use App\Notifications\NewPluginAvailable;
9+
use Illuminate\Foundation\Testing\RefreshDatabase;
10+
use Illuminate\Support\Facades\Notification;
11+
use Tests\TestCase;
12+
13+
class SendNewPluginNotificationsTest extends TestCase
14+
{
15+
use RefreshDatabase;
16+
17+
public function test_job_sends_notification_to_opted_in_users(): void
18+
{
19+
Notification::fake();
20+
21+
$author = User::factory()->create();
22+
$optedIn = User::factory()->create(['receives_new_plugin_notifications' => true]);
23+
$optedOut = User::factory()->create(['receives_new_plugin_notifications' => false]);
24+
25+
$plugin = Plugin::factory()->approved()->for($author)->create();
26+
27+
(new SendNewPluginNotifications($plugin))->handle();
28+
29+
Notification::assertSentTo($optedIn, NewPluginAvailable::class);
30+
Notification::assertNotSentTo($optedOut, NewPluginAvailable::class);
31+
}
32+
33+
public function test_job_does_not_notify_plugin_author(): void
34+
{
35+
Notification::fake();
36+
37+
$author = User::factory()->create(['receives_new_plugin_notifications' => true]);
38+
$plugin = Plugin::factory()->approved()->for($author)->create();
39+
40+
(new SendNewPluginNotifications($plugin))->handle();
41+
42+
Notification::assertNotSentTo($author, NewPluginAvailable::class);
43+
}
44+
}

tests/Feature/Notifications/NewPluginAvailableTest.php

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
namespace Tests\Feature\Notifications;
44

5+
use App\Jobs\SendNewPluginNotifications;
56
use App\Models\Plugin;
67
use App\Models\User;
78
use App\Notifications\NewPluginAvailable;
89
use App\Services\PluginSyncService;
910
use Illuminate\Foundation\Testing\RefreshDatabase;
10-
use Illuminate\Support\Facades\Notification;
11+
use Illuminate\Support\Facades\Bus;
1112
use Tests\TestCase;
1213

1314
class NewPluginAvailableTest extends TestCase
@@ -23,39 +24,34 @@ protected function setUp(): void
2324
});
2425
}
2526

26-
public function test_notification_is_sent_to_opted_in_users_on_first_approval(): void
27+
public function test_notification_job_is_dispatched_on_first_approval(): void
2728
{
28-
Notification::fake();
29+
Bus::fake(SendNewPluginNotifications::class);
2930

3031
$author = User::factory()->create();
31-
$optedIn = User::factory()->create(['receives_new_plugin_notifications' => true]);
32-
$optedOut = User::factory()->create(['receives_new_plugin_notifications' => false]);
33-
3432
$plugin = Plugin::factory()->pending()->for($author)->create();
3533
$admin = User::factory()->create();
3634

3735
$plugin->approve($admin->id);
3836

39-
Notification::assertSentTo($optedIn, NewPluginAvailable::class);
40-
Notification::assertNotSentTo($optedOut, NewPluginAvailable::class);
41-
Notification::assertNotSentTo($author, NewPluginAvailable::class);
37+
Bus::assertDispatched(SendNewPluginNotifications::class, function ($job) use ($plugin) {
38+
return $job->plugin->id === $plugin->id;
39+
});
4240
}
4341

44-
public function test_notification_is_not_sent_on_re_approval(): void
42+
public function test_notification_job_is_not_dispatched_on_re_approval(): void
4543
{
46-
Notification::fake();
44+
Bus::fake(SendNewPluginNotifications::class);
4745

4846
$author = User::factory()->create();
49-
$optedIn = User::factory()->create(['receives_new_plugin_notifications' => true]);
50-
5147
$plugin = Plugin::factory()->pending()->for($author)->create([
5248
'approved_at' => now()->subDay(),
5349
]);
5450
$admin = User::factory()->create();
5551

5652
$plugin->approve($admin->id);
5753

58-
Notification::assertNotSentTo($optedIn, NewPluginAvailable::class);
54+
Bus::assertNotDispatched(SendNewPluginNotifications::class);
5955
}
6056

6157
public function test_via_returns_empty_array_when_user_opted_out(): void

0 commit comments

Comments
 (0)