Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions app/Console/Commands/ResendNewPluginNotifications.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

namespace App\Console\Commands;

use App\Models\Plugin;
use App\Models\User;
use App\Notifications\NewPluginAvailable;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Notification;

class ResendNewPluginNotifications extends Command
{
protected $signature = 'plugins:resend-new-plugin-notifications
{plugins* : Plugin names (vendor/package) to resend notifications for}
{--dry-run : Preview what would happen without sending notifications}';

protected $description = 'Resend NewPluginAvailable notifications to opted-in users for specified plugins';

public function handle(): int
{
$pluginNames = $this->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;
}
}
4 changes: 3 additions & 1 deletion app/Filament/Resources/PluginResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)')
Expand Down
107 changes: 107 additions & 0 deletions tests/Feature/ResendNewPluginNotificationsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

namespace Tests\Feature;

use App\Models\Plugin;
use App\Models\User;
use App\Notifications\NewPluginAvailable;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;

class ResendNewPluginNotificationsTest extends TestCase
{
use RefreshDatabase;

public function test_sends_notifications_to_opted_in_users(): void
{
Notification::fake();

$author = User::factory()->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();
}
}
Loading