diff --git a/app/Filament/Resources/UserResource.php b/app/Filament/Resources/UserResource.php index 387a4b68..bb7aa4fd 100644 --- a/app/Filament/Resources/UserResource.php +++ b/app/Filament/Resources/UserResource.php @@ -119,6 +119,7 @@ public static function getRelations(): array { return [ RelationManagers\PluginLicensesRelationManager::class, + RelationManagers\ProductLicensesRelationManager::class, RelationManagers\LicensesRelationManager::class, RelationManagers\SubscriptionsRelationManager::class, ]; diff --git a/app/Filament/Resources/UserResource/RelationManagers/ProductLicensesRelationManager.php b/app/Filament/Resources/UserResource/RelationManagers/ProductLicensesRelationManager.php new file mode 100644 index 00000000..6c35a9b4 --- /dev/null +++ b/app/Filament/Resources/UserResource/RelationManagers/ProductLicensesRelationManager.php @@ -0,0 +1,72 @@ +schema([ + Forms\Components\Select::make('product_id') + ->relationship('product', 'name') + ->searchable() + ->preload() + ->required(), + Forms\Components\Toggle::make('is_comped') + ->label('Comped') + ->default(true), + Forms\Components\DateTimePicker::make('purchased_at') + ->default(now()), + ]); + } + + public function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('product.name') + ->label('Product') + ->searchable() + ->sortable() + ->fontFamily('mono'), + Tables\Columns\TextColumn::make('price_paid') + ->label('Price Paid') + ->money('usd', divideBy: 100) + ->sortable(), + Tables\Columns\IconColumn::make('is_comped') + ->label('Comped') + ->boolean(), + Tables\Columns\TextColumn::make('purchased_at') + ->dateTime() + ->sortable(), + ]) + ->defaultSort('purchased_at', 'desc') + ->filters([ + Tables\Filters\TernaryFilter::make('is_comped') + ->label('Comped'), + ]) + ->headerActions([ + Tables\Actions\CreateAction::make() + ->mutateFormDataUsing(function (array $data): array { + $data['price_paid'] = 0; + $data['currency'] = 'USD'; + + return $data; + }), + ]) + ->actions([ + Tables\Actions\DeleteAction::make(), + ]); + } +} diff --git a/app/Models/ProductLicense.php b/app/Models/ProductLicense.php index 4296ca95..883c42f5 100644 --- a/app/Models/ProductLicense.php +++ b/app/Models/ProductLicense.php @@ -53,6 +53,7 @@ protected function casts(): array { return [ 'price_paid' => 'integer', + 'is_comped' => 'boolean', 'purchased_at' => 'datetime', ]; } diff --git a/database/migrations/2026_03_19_190031_create_sales_view.php b/database/migrations/2026_03_19_190031_create_sales_view.php index 1c7efdf5..8458315d 100644 --- a/database/migrations/2026_03_19_190031_create_sales_view.php +++ b/database/migrations/2026_03_19_190031_create_sales_view.php @@ -33,7 +33,7 @@ public function up(): void NULL AS bundle_name, pdl.price_paid, pdl.currency, - 0 AS is_comped, + pdl.is_comped, pdl.purchased_at, pdl.created_at, pdl.updated_at diff --git a/database/migrations/2026_03_20_212143_add_is_comped_to_product_licenses_table.php b/database/migrations/2026_03_20_212143_add_is_comped_to_product_licenses_table.php new file mode 100644 index 00000000..f3040d56 --- /dev/null +++ b/database/migrations/2026_03_20_212143_add_is_comped_to_product_licenses_table.php @@ -0,0 +1,29 @@ +boolean('is_comped')->default(false)->after('currency')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('product_licenses', function (Blueprint $table) { + $table->dropIndex(['is_comped']); + $table->dropColumn('is_comped'); + }); + } +}; diff --git a/tests/Feature/Filament/ProductLicensesRelationManagerTest.php b/tests/Feature/Filament/ProductLicensesRelationManagerTest.php new file mode 100644 index 00000000..8937f39d --- /dev/null +++ b/tests/Feature/Filament/ProductLicensesRelationManagerTest.php @@ -0,0 +1,147 @@ +admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + + $this->user = User::factory()->create(); + } + + public function test_it_lists_product_licenses_for_user(): void + { + $licenses = ProductLicense::factory()->count(3)->create([ + 'user_id' => $this->user->id, + ]); + + Livewire::actingAs($this->admin) + ->test(ProductLicensesRelationManager::class, [ + 'ownerRecord' => $this->user, + 'pageClass' => EditUser::class, + ]) + ->assertCanSeeTableRecords($licenses) + ->assertCountTableRecords(3); + } + + public function test_it_does_not_show_other_users_licenses(): void + { + $otherUser = User::factory()->create(); + + ProductLicense::factory()->create([ + 'user_id' => $this->user->id, + ]); + + ProductLicense::factory()->create([ + 'user_id' => $otherUser->id, + ]); + + Livewire::actingAs($this->admin) + ->test(ProductLicensesRelationManager::class, [ + 'ownerRecord' => $this->user, + 'pageClass' => EditUser::class, + ]) + ->assertCountTableRecords(1); + } + + public function test_it_shows_comped_status(): void + { + ProductLicense::factory()->create([ + 'user_id' => $this->user->id, + 'is_comped' => true, + 'price_paid' => 0, + ]); + + Livewire::actingAs($this->admin) + ->test(ProductLicensesRelationManager::class, [ + 'ownerRecord' => $this->user, + 'pageClass' => EditUser::class, + ]) + ->assertCountTableRecords(1); + } + + public function test_it_can_create_a_comped_product_license(): void + { + $product = Product::factory()->create(); + + Livewire::actingAs($this->admin) + ->test(ProductLicensesRelationManager::class, [ + 'ownerRecord' => $this->user, + 'pageClass' => EditUser::class, + ]) + ->callTableAction('create', data: [ + 'product_id' => $product->id, + 'is_comped' => true, + 'purchased_at' => now()->toDateTimeString(), + ]) + ->assertHasNoTableActionErrors(); + + $this->assertDatabaseHas('product_licenses', [ + 'user_id' => $this->user->id, + 'product_id' => $product->id, + 'is_comped' => true, + 'price_paid' => 0, + 'currency' => 'USD', + ]); + } + + public function test_it_can_delete_a_product_license(): void + { + $license = ProductLicense::factory()->create([ + 'user_id' => $this->user->id, + ]); + + Livewire::actingAs($this->admin) + ->test(ProductLicensesRelationManager::class, [ + 'ownerRecord' => $this->user, + 'pageClass' => EditUser::class, + ]) + ->callTableAction('delete', $license) + ->assertHasNoTableActionErrors(); + + $this->assertDatabaseMissing('product_licenses', [ + 'id' => $license->id, + ]); + } + + public function test_it_can_filter_by_comped_status(): void + { + ProductLicense::factory()->create([ + 'user_id' => $this->user->id, + 'is_comped' => true, + ]); + + ProductLicense::factory()->create([ + 'user_id' => $this->user->id, + 'is_comped' => false, + ]); + + Livewire::actingAs($this->admin) + ->test(ProductLicensesRelationManager::class, [ + 'ownerRecord' => $this->user, + 'pageClass' => EditUser::class, + ]) + ->filterTable('is_comped', true) + ->assertCountTableRecords(1); + } +}