Skip to content

Commit d16b505

Browse files
simonhampclaude
andcommitted
Rename plugin-sales to sales and include all product types
Replace PluginSalesResource with SalesResource backed by a database view that unions plugin_licenses and product_licenses. The "Plugin" column becomes "Product", the "Bundle" column is now shown inline as a description, and "Grandfathered" is renamed to "Comped". The slug changes from plugin-sales to sales. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5999bdb commit d16b505

7 files changed

Lines changed: 184 additions & 67 deletions

File tree

app/Filament/Resources/PluginSalesResource/Pages/ListPluginSales.php

Lines changed: 0 additions & 22 deletions
This file was deleted.

app/Filament/Resources/PluginSalesResource.php renamed to app/Filament/Resources/SalesResource.php

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@
22

33
namespace App\Filament\Resources;
44

5-
use App\Filament\Resources\PluginSalesResource\Pages;
6-
use App\Filament\Resources\PluginSalesResource\Widgets\PluginSalesStats;
7-
use App\Models\PluginLicense;
5+
use App\Filament\Resources\SalesResource\Pages;
6+
use App\Filament\Resources\SalesResource\Widgets\SalesStats;
7+
use App\Models\Sale;
88
use Filament\Forms\Form;
99
use Filament\Resources\Resource;
1010
use Filament\Tables;
1111
use Filament\Tables\Table;
1212
use Illuminate\Database\Eloquent\Builder;
1313

14-
class PluginSalesResource extends Resource
14+
class SalesResource extends Resource
1515
{
16-
protected static ?string $model = PluginLicense::class;
16+
protected static ?string $model = Sale::class;
1717

1818
protected static ?string $navigationIcon = 'heroicon-o-banknotes';
1919

@@ -27,7 +27,7 @@ class PluginSalesResource extends Resource
2727

2828
protected static ?string $pluralModelLabel = 'Sales';
2929

30-
protected static ?string $slug = 'plugin-sales';
30+
protected static ?string $slug = 'sales';
3131

3232
public static function form(Form $form): Form
3333
{
@@ -48,24 +48,20 @@ public static function table(Table $table): Table
4848
->searchable()
4949
->sortable(),
5050

51-
Tables\Columns\TextColumn::make('plugin.name')
52-
->label('Plugin')
51+
Tables\Columns\TextColumn::make('product_name')
52+
->label('Product')
53+
->description(fn (Sale $record): ?string => $record->bundle_name ? "Bundle: {$record->bundle_name}" : null)
5354
->searchable()
5455
->sortable()
5556
->fontFamily('mono'),
5657

57-
Tables\Columns\TextColumn::make('pluginBundle.name')
58-
->label('Bundle')
59-
->placeholder('-')
60-
->sortable(),
61-
6258
Tables\Columns\TextColumn::make('price_paid')
6359
->label('Amount')
64-
->formatStateUsing(fn (int $state, PluginLicense $record): string => '$'.number_format($state / 100, 2).' '.$record->currency)
60+
->formatStateUsing(fn (int $state, Sale $record): string => '$'.number_format($state / 100, 2).' '.$record->currency)
6561
->sortable(),
6662

67-
Tables\Columns\IconColumn::make('is_grandfathered')
68-
->label('Grandfathered')
63+
Tables\Columns\IconColumn::make('is_comped')
64+
->label('Comped')
6965
->boolean()
7066
->sortable(),
7167
])
@@ -76,20 +72,17 @@ public static function table(Table $table): Table
7672
->searchable()
7773
->preload(),
7874

79-
Tables\Filters\SelectFilter::make('plugin_id')
80-
->label('Plugin')
81-
->relationship('plugin', 'name', fn (Builder $query) => $query->whereNotNull('name'))
82-
->searchable()
83-
->preload(),
84-
85-
Tables\Filters\SelectFilter::make('plugin_bundle_id')
86-
->label('Bundle')
87-
->relationship('pluginBundle', 'name', fn (Builder $query) => $query->whereNotNull('name'))
88-
->searchable()
89-
->preload(),
90-
91-
Tables\Filters\TernaryFilter::make('is_grandfathered')
92-
->label('Grandfathered'),
75+
Tables\Filters\SelectFilter::make('product_name')
76+
->label('Product')
77+
->options(fn (): array => Sale::query()
78+
->whereNotNull('product_name')
79+
->distinct()
80+
->pluck('product_name', 'product_name')
81+
->toArray())
82+
->searchable(),
83+
84+
Tables\Filters\TernaryFilter::make('is_comped')
85+
->label('Comped'),
9386
])
9487
->actions([])
9588
->bulkActions([])
@@ -104,14 +97,14 @@ public static function getRelations(): array
10497
public static function getWidgets(): array
10598
{
10699
return [
107-
PluginSalesStats::class,
100+
SalesStats::class,
108101
];
109102
}
110103

111104
public static function getPages(): array
112105
{
113106
return [
114-
'index' => Pages\ListPluginSales::route('/'),
107+
'index' => Pages\ListSales::route('/'),
115108
];
116109
}
117110

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\SalesResource\Pages;
4+
5+
use App\Filament\Resources\SalesResource;
6+
use App\Filament\Resources\SalesResource\Widgets\SalesStats;
7+
use Filament\Pages\Concerns\ExposesTableToWidgets;
8+
use Filament\Resources\Pages\ListRecords;
9+
10+
class ListSales extends ListRecords
11+
{
12+
use ExposesTableToWidgets;
13+
14+
protected static string $resource = SalesResource::class;
15+
16+
protected function getHeaderWidgets(): array
17+
{
18+
return [
19+
SalesStats::class,
20+
];
21+
}
22+
}

app/Filament/Resources/PluginSalesResource/Widgets/PluginSalesStats.php renamed to app/Filament/Resources/SalesResource/Widgets/SalesStats.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
11
<?php
22

3-
namespace App\Filament\Resources\PluginSalesResource\Widgets;
3+
namespace App\Filament\Resources\SalesResource\Widgets;
44

5-
use App\Filament\Resources\PluginSalesResource\Pages\ListPluginSales;
6-
use App\Models\PluginLicense;
5+
use App\Filament\Resources\SalesResource\Pages\ListSales;
6+
use App\Models\Sale;
77
use Filament\Widgets\Concerns\InteractsWithPageTable;
88
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
99
use Filament\Widgets\StatsOverviewWidget\Stat;
1010

11-
class PluginSalesStats extends BaseWidget
11+
class SalesStats extends BaseWidget
1212
{
1313
use InteractsWithPageTable;
1414

1515
protected static ?string $pollingInterval = null;
1616

1717
protected function getTablePage(): string
1818
{
19-
return ListPluginSales::class;
19+
return ListSales::class;
2020
}
2121

2222
protected function getStats(): array
2323
{
2424
$filteredQuery = $this->getPageTableQuery();
2525

26-
$grandTotalRevenue = PluginLicense::sum('price_paid');
27-
$grandTotalSales = PluginLicense::count();
26+
$grandTotalRevenue = Sale::sum('price_paid');
27+
$grandTotalSales = Sale::count();
2828

2929
$filteredRevenue = (clone $filteredQuery)->sum('price_paid');
3030
$filteredSales = (clone $filteredQuery)->count();

app/Models/Sale.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace App\Models;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
7+
8+
class Sale extends Model
9+
{
10+
protected $table = 'sales_view';
11+
12+
public $incrementing = false;
13+
14+
protected $keyType = 'string';
15+
16+
public $timestamps = false;
17+
18+
/**
19+
* @return BelongsTo<User, Sale>
20+
*/
21+
public function user(): BelongsTo
22+
{
23+
return $this->belongsTo(User::class);
24+
}
25+
26+
protected function casts(): array
27+
{
28+
return [
29+
'price_paid' => 'integer',
30+
'is_comped' => 'boolean',
31+
'purchased_at' => 'datetime',
32+
];
33+
}
34+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Support\Facades\DB;
5+
6+
return new class extends Migration
7+
{
8+
public function up(): void
9+
{
10+
DB::statement("
11+
CREATE VIEW sales_view AS
12+
SELECT
13+
CONCAT('pl_', pl.id) AS id,
14+
pl.user_id,
15+
p.name AS product_name,
16+
pb.name AS bundle_name,
17+
pl.price_paid,
18+
pl.currency,
19+
pl.is_grandfathered AS is_comped,
20+
pl.purchased_at,
21+
pl.created_at,
22+
pl.updated_at
23+
FROM plugin_licenses pl
24+
LEFT JOIN plugins p ON p.id = pl.plugin_id
25+
LEFT JOIN plugin_bundles pb ON pb.id = pl.plugin_bundle_id
26+
27+
UNION ALL
28+
29+
SELECT
30+
CONCAT('pr_', pdl.id) AS id,
31+
pdl.user_id,
32+
pd.name AS product_name,
33+
NULL AS bundle_name,
34+
pdl.price_paid,
35+
pdl.currency,
36+
0 AS is_comped,
37+
pdl.purchased_at,
38+
pdl.created_at,
39+
pdl.updated_at
40+
FROM product_licenses pdl
41+
LEFT JOIN products pd ON pd.id = pdl.product_id
42+
");
43+
}
44+
45+
public function down(): void
46+
{
47+
DB::statement('DROP VIEW IF EXISTS sales_view');
48+
}
49+
};

tests/Feature/Filament/PluginSalesResourceTest.php

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
namespace Tests\Feature\Filament;
44

5-
use App\Filament\Resources\PluginSalesResource\Pages\ListPluginSales;
5+
use App\Filament\Resources\SalesResource\Pages\ListSales;
66
use App\Models\Plugin;
7+
use App\Models\PluginBundle;
78
use App\Models\PluginLicense;
9+
use App\Models\ProductLicense;
810
use App\Models\User;
911
use Illuminate\Foundation\Testing\RefreshDatabase;
1012
use Livewire\Livewire;
@@ -24,7 +26,16 @@ protected function setUp(): void
2426
config(['filament.users' => ['admin@test.com']]);
2527
}
2628

27-
public function test_plugin_sales_page_renders_when_plugin_has_null_name(): void
29+
public function test_sales_page_renders_successfully(): void
30+
{
31+
PluginLicense::factory()->count(3)->create();
32+
33+
Livewire::actingAs($this->admin)
34+
->test(ListSales::class)
35+
->assertSuccessful();
36+
}
37+
38+
public function test_sales_page_renders_when_plugin_has_null_name(): void
2839
{
2940
$pluginWithName = Plugin::factory()->approved()->create(['name' => 'acme/camera-123']);
3041
$pluginWithoutName = Plugin::factory()->approved()->create(['name' => null]);
@@ -33,16 +44,46 @@ public function test_plugin_sales_page_renders_when_plugin_has_null_name(): void
3344
PluginLicense::factory()->create(['plugin_id' => $pluginWithoutName->id]);
3445

3546
Livewire::actingAs($this->admin)
36-
->test(ListPluginSales::class)
47+
->test(ListSales::class)
3748
->assertSuccessful();
3849
}
3950

40-
public function test_plugin_sales_page_renders_successfully(): void
51+
public function test_sales_page_shows_product_license_sales(): void
4152
{
42-
PluginLicense::factory()->count(3)->create();
53+
ProductLicense::factory()->count(2)->create();
54+
55+
Livewire::actingAs($this->admin)
56+
->test(ListSales::class)
57+
->assertSuccessful();
58+
}
59+
60+
public function test_sales_page_shows_both_plugin_and_product_sales(): void
61+
{
62+
PluginLicense::factory()->count(2)->create();
63+
ProductLicense::factory()->count(2)->create();
64+
65+
Livewire::actingAs($this->admin)
66+
->test(ListSales::class)
67+
->assertSuccessful();
68+
}
69+
70+
public function test_sales_page_shows_bundle_name_for_plugin_sales(): void
71+
{
72+
$bundle = PluginBundle::factory()->create(['name' => 'Pro Bundle']);
73+
PluginLicense::factory()->create(['plugin_bundle_id' => $bundle->id]);
74+
75+
Livewire::actingAs($this->admin)
76+
->test(ListSales::class)
77+
->assertSuccessful()
78+
->assertSee('Pro Bundle');
79+
}
80+
81+
public function test_sales_page_shows_comped_column(): void
82+
{
83+
PluginLicense::factory()->grandfathered()->create();
4384

4485
Livewire::actingAs($this->admin)
45-
->test(ListPluginSales::class)
86+
->test(ListSales::class)
4687
->assertSuccessful();
4788
}
4889
}

0 commit comments

Comments
 (0)