diff --git a/app/Filament/Resources/PluginResource.php b/app/Filament/Resources/PluginResource.php index 4167ba03..ae704395 100644 --- a/app/Filament/Resources/PluginResource.php +++ b/app/Filament/Resources/PluginResource.php @@ -7,6 +7,7 @@ use App\Enums\PluginType; use App\Filament\Resources\PluginResource\Pages; use App\Filament\Resources\PluginResource\RelationManagers; +use App\Jobs\SyncPluginReleases; use App\Models\Plugin; use App\Models\PluginLicense; use App\Models\User; @@ -297,6 +298,36 @@ public static function table(Table $table): Table ->modalDescription(fn (Plugin $record) => "Grant '{$record->name}' to a user for free.") ->modalSubmitActionLabel('Grant'), + Tables\Actions\Action::make('convertToPaid') + ->label('Convert to Paid') + ->icon('heroicon-o-currency-dollar') + ->color('success') + ->visible(fn (Plugin $record) => $record->isFree()) + ->form([ + Forms\Components\Select::make('tier') + ->label('Pricing Tier') + ->options(PluginTier::class) + ->required() + ->helperText('This sets the pricing for the plugin.'), + ]) + ->action(function (Plugin $record, array $data): void { + $record->update([ + 'type' => PluginType::Paid, + 'tier' => $data['tier'], + ]); + + SyncPluginReleases::dispatch($record); + + Notification::make() + ->title("Converted '{$record->name}' to paid") + ->body('Plugin type updated, prices synced, and Satis ingestion queued.') + ->success() + ->send(); + }) + ->modalHeading('Convert Plugin to Paid') + ->modalDescription(fn (Plugin $record) => "This will convert '{$record->name}' from free to paid, set up pricing, and trigger a Satis build so it's available via Composer.") + ->modalSubmitActionLabel('Convert & Ingest'), + Tables\Actions\ViewAction::make(), ]) ->label('More') diff --git a/tests/Feature/Filament/ConvertPluginToPaidTest.php b/tests/Feature/Filament/ConvertPluginToPaidTest.php new file mode 100644 index 00000000..73fbb219 --- /dev/null +++ b/tests/Feature/Filament/ConvertPluginToPaidTest.php @@ -0,0 +1,71 @@ +admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + } + + public function test_convert_to_paid_changes_type_and_dispatches_satis(): void + { + Bus::fake([SyncPluginReleases::class]); + + $plugin = Plugin::factory()->free()->approved()->create(); + + Livewire::actingAs($this->admin) + ->test(ListPlugins::class) + ->callTableAction('convertToPaid', $plugin, data: [ + 'tier' => PluginTier::Silver->value, + ]) + ->assertNotified(); + + $plugin->refresh(); + + $this->assertEquals(PluginType::Paid, $plugin->type); + $this->assertEquals(PluginTier::Silver, $plugin->tier); + $this->assertTrue($plugin->prices()->exists()); + + Bus::assertDispatched(SyncPluginReleases::class, function ($job) use ($plugin) { + return $job->plugin->is($plugin); + }); + } + + public function test_convert_to_paid_is_not_visible_on_paid_plugins(): void + { + $plugin = Plugin::factory()->paid()->approved()->create(); + + Livewire::actingAs($this->admin) + ->test(ListPlugins::class) + ->assertTableActionHidden('convertToPaid', $plugin); + } + + public function test_convert_to_paid_is_visible_on_free_plugins(): void + { + $plugin = Plugin::factory()->free()->approved()->create(); + + Livewire::actingAs($this->admin) + ->test(ListPlugins::class) + ->assertTableActionVisible('convertToPaid', $plugin); + } +}