diff --git a/app/Console/Commands/ResendNewPluginNotifications.php b/app/Console/Commands/ResendNewPluginNotifications.php new file mode 100644 index 00000000..14d0c941 --- /dev/null +++ b/app/Console/Commands/ResendNewPluginNotifications.php @@ -0,0 +1,72 @@ +argument('plugins'); + $dryRun = $this->option('dry-run'); + + $plugins = Plugin::query() + ->whereIn('name', $pluginNames) + ->where('status', 'approved') + ->get(); + + $missingNames = collect($pluginNames)->diff($plugins->pluck('name')); + + if ($missingNames->isNotEmpty()) { + foreach ($missingNames as $name) { + $this->error("Plugin not found or not approved: {$name}"); + } + + return Command::FAILURE; + } + + $recipients = User::query() + ->where('receives_new_plugin_notifications', true) + ->whereNotIn('id', $plugins->pluck('user_id')) + ->get(); + + if ($recipients->isEmpty()) { + $this->warn('No opted-in users found to notify.'); + + return Command::SUCCESS; + } + + $this->info("Plugins: {$plugins->pluck('name')->implode(', ')}"); + $this->info("Recipients: {$recipients->count()} opted-in users"); + + if ($dryRun) { + $this->warn('[DRY RUN] No notifications will be sent.'); + + return Command::SUCCESS; + } + + foreach ($plugins as $plugin) { + $pluginRecipients = $recipients->where('id', '!=', $plugin->user_id); + + Notification::send($pluginRecipients, new NewPluginAvailable($plugin)); + + $this->info("Sent NewPluginAvailable for {$plugin->name} to {$pluginRecipients->count()} users."); + } + + $this->newLine(); + $this->info('Done. All notifications queued.'); + + return Command::SUCCESS; + } +} diff --git a/app/Filament/Resources/PluginResource.php b/app/Filament/Resources/PluginResource.php index 550d99ca..e99eeae2 100644 --- a/app/Filament/Resources/PluginResource.php +++ b/app/Filament/Resources/PluginResource.php @@ -75,7 +75,9 @@ public static function form(Schema $schema): Schema ->suffixIconColor('gray'), Forms\Components\Select::make('status') - ->options(PluginStatus::class), + ->options(PluginStatus::class) + ->disabled() + ->helperText('Use the Approve/Reject actions to change status'), Forms\Components\Toggle::make('is_official') ->label('Official (First-Party)') diff --git a/tests/Feature/ResendNewPluginNotificationsTest.php b/tests/Feature/ResendNewPluginNotificationsTest.php new file mode 100644 index 00000000..b97171d9 --- /dev/null +++ b/tests/Feature/ResendNewPluginNotificationsTest.php @@ -0,0 +1,107 @@ +create(); + $optedIn = User::factory()->create(['receives_new_plugin_notifications' => true]); + $optedOut = User::factory()->create(['receives_new_plugin_notifications' => false]); + + $plugin = Plugin::factory()->approved()->for($author)->create(); + + $this->artisan('plugins:resend-new-plugin-notifications', [ + 'plugins' => [$plugin->name], + ])->assertSuccessful(); + + Notification::assertSentTo($optedIn, NewPluginAvailable::class); + Notification::assertNotSentTo($optedOut, NewPluginAvailable::class); + } + + public function test_does_not_send_to_plugin_author(): void + { + Notification::fake(); + + $author = User::factory()->create(['receives_new_plugin_notifications' => true]); + $plugin = Plugin::factory()->approved()->for($author)->create(); + + $this->artisan('plugins:resend-new-plugin-notifications', [ + 'plugins' => [$plugin->name], + ])->assertSuccessful(); + + Notification::assertNotSentTo($author, NewPluginAvailable::class); + } + + public function test_fails_when_plugin_not_found(): void + { + $this->artisan('plugins:resend-new-plugin-notifications', [ + 'plugins' => ['nonexistent/plugin'], + ])->assertFailed(); + } + + public function test_fails_when_plugin_is_not_approved(): void + { + $plugin = Plugin::factory()->pending()->create(); + + $this->artisan('plugins:resend-new-plugin-notifications', [ + 'plugins' => [$plugin->name], + ])->assertFailed(); + } + + public function test_dry_run_does_not_send_notifications(): void + { + Notification::fake(); + + $user = User::factory()->create(['receives_new_plugin_notifications' => true]); + $plugin = Plugin::factory()->approved()->create(); + + $this->artisan('plugins:resend-new-plugin-notifications', [ + 'plugins' => [$plugin->name], + '--dry-run' => true, + ])->assertSuccessful(); + + Notification::assertNothingSent(); + } + + public function test_handles_multiple_plugins(): void + { + Notification::fake(); + + $user = User::factory()->create(['receives_new_plugin_notifications' => true]); + $plugin1 = Plugin::factory()->approved()->create(); + $plugin2 = Plugin::factory()->approved()->create(); + + $this->artisan('plugins:resend-new-plugin-notifications', [ + 'plugins' => [$plugin1->name, $plugin2->name], + ])->assertSuccessful(); + + Notification::assertSentTo($user, NewPluginAvailable::class, 2); + } + + public function test_succeeds_with_no_opted_in_users(): void + { + Notification::fake(); + + $plugin = Plugin::factory()->approved()->create(); + User::query()->update(['receives_new_plugin_notifications' => false]); + + $this->artisan('plugins:resend-new-plugin-notifications', [ + 'plugins' => [$plugin->name], + ])->assertSuccessful(); + + Notification::assertNothingSent(); + } +}