Skip to content

Commit 3567970

Browse files
authored
Add license suspension (#146)
1 parent 449530d commit 3567970

17 files changed

+469
-142
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace App\Actions\Licenses;
4+
5+
use App\Models\License;
6+
use App\Services\Anystack\Anystack;
7+
8+
class SuspendLicense
9+
{
10+
public function __construct(
11+
protected Anystack $anystack
12+
) {}
13+
14+
/**
15+
* Handle the suspension of a license.
16+
*/
17+
public function handle(License $license): License
18+
{
19+
$this->anystack->suspendLicense($license->anystack_product_id, $license->anystack_id);
20+
21+
$license->update([
22+
'is_suspended' => true,
23+
]);
24+
25+
return $license;
26+
}
27+
}

app/Filament/Resources/LicenseResource.php

Lines changed: 66 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
namespace App\Filament\Resources;
44

55
use App\Filament\Resources\LicenseResource\Pages;
6+
use App\Filament\Resources\LicenseResource\RelationManagers;
67
use App\Models\License;
78
use Filament\Forms;
89
use Filament\Forms\Form;
9-
use Filament\Infolists\Components;
10+
use Filament\Infolists;
1011
use Filament\Infolists\Infolist;
1112
use Filament\Resources\Resource;
1213
use Filament\Tables;
@@ -24,24 +25,21 @@ public static function form(Form $form): Form
2425
->schema([
2526
Forms\Components\Section::make('License Information')
2627
->schema([
27-
Forms\Components\TextInput::make('id')
28-
->disabled(),
28+
Forms\Components\TextInput::make('id'),
2929
Forms\Components\TextInput::make('anystack_id')
30-
->maxLength(36)
31-
->disabled(),
30+
->maxLength(36),
3231
Forms\Components\Select::make('user_id')
33-
->relationship('user', 'email')
34-
->disabled(),
32+
->relationship('user', 'email'),
3533
Forms\Components\TextInput::make('policy_name')
36-
->label('Plan')
37-
->disabled(),
38-
Forms\Components\TextInput::make('key')
39-
->disabled(),
40-
Forms\Components\DateTimePicker::make('expires_at')
41-
->disabled(),
42-
Forms\Components\DateTimePicker::make('created_at')
43-
->disabled(),
44-
])->columns(2),
34+
->label('Plan'),
35+
Forms\Components\TextInput::make('key'),
36+
Forms\Components\DateTimePicker::make('expires_at'),
37+
Forms\Components\DateTimePicker::make('created_at'),
38+
Forms\Components\Toggle::make('is_suspended')
39+
->label('Suspended'),
40+
])
41+
->columns(2)
42+
->disabled(),
4543
]);
4644
}
4745

@@ -54,10 +52,11 @@ public static function table(Table $table): Table
5452
->searchable(),
5553
Tables\Columns\TextColumn::make('user.email')
5654
->searchable()
57-
->sortable()
58-
->copyable()
59-
->url(fn (\App\Models\License $record): string => route('filament.admin.resources.users.edit', ['record' => $record->user_id]))
60-
->openUrlInNewTab(),
55+
->sortable(),
56+
Tables\Columns\TextColumn::make('subscription_item_id')
57+
->label('Subscription Item')
58+
->searchable()
59+
->sortable(),
6160
Tables\Columns\TextColumn::make('key')
6261
->searchable()
6362
->copyable(),
@@ -70,138 +69,77 @@ public static function table(Table $table): Table
7069
Tables\Columns\TextColumn::make('created_at')
7170
->dateTime()
7271
->sortable(),
72+
Tables\Columns\IconColumn::make('is_suspended')
73+
->boolean()
74+
->label('Suspended')
75+
->sortable(),
7376
])
7477
->filters([
7578
//
7679
])
7780
->actions([
78-
Tables\Actions\ViewAction::make()
79-
->slideOver()
80-
->modalHeading('License Details')
81-
->modalWidth('7xl')
82-
->extraModalFooterActions([
83-
Tables\Actions\Action::make('viewUser')
84-
->label('View User')
85-
->icon('heroicon-o-user')
86-
->color('primary')
87-
->url(fn (License $record) => route('filament.admin.resources.users.edit', ['record' => $record->user_id]))
88-
->openUrlInNewTab()
89-
->visible(fn (License $record) => $record->user_id !== null),
90-
]),
81+
Tables\Actions\ActionGroup::make([
82+
Tables\Actions\EditAction::make(),
83+
Tables\Actions\Action::make('viewUser')
84+
->label('View User')
85+
->icon('heroicon-o-user')
86+
->color('primary')
87+
->url(fn (License $record) => route('filament.admin.resources.users.edit', ['record' => $record->user_id]))
88+
->openUrlInNewTab()
89+
->visible(fn (License $record) => $record->user_id !== null),
90+
])
91+
->label('Actions')
92+
->icon('heroicon-m-ellipsis-vertical'),
9193
])
9294
->defaultPaginationPageOption(25)
9395
->bulkActions([]);
9496
}
9597

96-
public static function getRelations(): array
97-
{
98-
return [
99-
//
100-
];
101-
}
102-
10398
public static function infolist(Infolist $infolist): Infolist
10499
{
105100
return $infolist
106101
->schema([
107-
Components\Section::make('License Information')
102+
Infolists\Components\Section::make('License Information')
108103
->schema([
109-
Components\TextEntry::make('id'),
110-
Components\TextEntry::make('key')
104+
Infolists\Components\TextEntry::make('id')
111105
->copyable(),
112-
Components\TextEntry::make('policy_name')
113-
->label('Plan'),
114-
Components\TextEntry::make('expires_at')
115-
->dateTime(),
116-
Components\TextEntry::make('created_at')
117-
->dateTime(),
118-
Components\TextEntry::make('anystack_id')
106+
Infolists\Components\TextEntry::make('anystack_id')
119107
->copyable(),
120-
])->columns(2),
121-
122-
Components\Section::make('User Information')
123-
->schema([
124-
Components\TextEntry::make('user.id')
125-
->label('User ID')
126-
->url(fn ($record) => route('filament.admin.resources.users.edit', ['record' => $record->user_id]))
127-
->openUrlInNewTab(),
128-
Components\TextEntry::make('user.email')
129-
->label('Email')
130-
->copyable()
131-
->url(fn ($record) => route('filament.admin.resources.users.edit', ['record' => $record->user_id]))
132-
->openUrlInNewTab(),
133-
Components\TextEntry::make('user.name')
134-
->label('Name'),
135-
Components\TextEntry::make('user.first_name')
136-
->label('First Name'),
137-
Components\TextEntry::make('user.last_name')
138-
->label('Last Name'),
139-
Components\TextEntry::make('user.stripe_id')
140-
->label('Stripe ID')
141-
->copyable()
142-
->visible(fn ($record) => filled($record->user->stripe_id))
143-
->url(fn ($record) => filled($record->user->stripe_id)
144-
? "https://dashboard.stripe.com/customers/{$record->user->stripe_id}"
145-
: null)
146-
->openUrlInNewTab(),
147-
Components\TextEntry::make('user.anystack_contact_id')
148-
->label('Anystack Contact ID')
149-
->copyable()
150-
->visible(fn ($record) => filled($record->user->anystack_contact_id))
151-
->url(fn ($record) => filled($record->user->anystack_contact_id)
152-
? "https://app.anystack.sh/contacts/{$record->user->anystack_contact_id}"
153-
: null)
154-
->openUrlInNewTab(),
155-
])->columns(2),
156-
157-
Components\Section::make('Subscription Information')
158-
->schema([
159-
Components\TextEntry::make('subscriptionItem.id')
160-
->label('Subscription Item ID')
161-
->visible(fn ($record) => $record->subscription_item_id !== null),
162-
Components\TextEntry::make('subscriptionItem.stripe_id')
163-
->label('Stripe Subscription Item ID')
164-
->copyable()
165-
->visible(fn ($record) => $record->subscription_item_id !== null),
166-
Components\TextEntry::make('subscriptionItem.subscription.stripe_id')
167-
->label('Stripe Subscription ID')
168-
->copyable()
169-
->visible(fn ($record) => $record->subscription_item_id !== null)
170-
->url(fn ($record) => $record->subscription_item_id !== null && filled($record->subscriptionItem?->subscription?->stripe_id)
171-
? "https://dashboard.stripe.com/subscriptions/{$record->subscriptionItem->subscription->stripe_id}"
172-
: null)
173-
->openUrlInNewTab(),
174-
Components\TextEntry::make('subscriptionItem.stripe_price')
175-
->label('Stripe Price ID')
176-
->copyable()
177-
->visible(fn ($record) => $record->subscription_item_id !== null),
178-
Components\TextEntry::make('subscriptionItem.stripe_product')
179-
->label('Stripe Product ID')
180-
->copyable()
181-
->visible(fn ($record) => $record->subscription_item_id !== null),
182-
Components\TextEntry::make('subscriptionItem.subscription.stripe_status')
183-
->label('Subscription Status')
184-
->badge()
185-
->color(fn ($state): string => match ($state) {
186-
'active' => 'success',
187-
'canceled' => 'danger',
188-
'incomplete' => 'warning',
189-
'incomplete_expired' => 'danger',
190-
'past_due' => 'warning',
191-
'trialing' => 'info',
192-
'unpaid' => 'danger',
193-
default => 'gray',
194-
})
195-
->visible(fn ($record) => $record->subscription_item_id !== null),
196-
])->columns(2)
197-
->visible(fn ($record) => $record->subscription_item_id !== null),
108+
Infolists\Components\TextEntry::make('user.email')
109+
->label('User')
110+
->copyable(),
111+
Infolists\Components\TextEntry::make('policy_name')
112+
->label('Plan')
113+
->copyable(),
114+
Infolists\Components\TextEntry::make('key')
115+
->copyable(),
116+
Infolists\Components\TextEntry::make('expires_at')
117+
->dateTime()
118+
->copyable(),
119+
Infolists\Components\TextEntry::make('created_at')
120+
->dateTime()
121+
->copyable(),
122+
Infolists\Components\IconEntry::make('is_suspended')
123+
->label('Suspended')
124+
->boolean(),
125+
])
126+
->columns(2),
198127
]);
199128
}
200129

130+
public static function getRelations(): array
131+
{
132+
return [
133+
RelationManagers\UserRelationManager::class,
134+
RelationManagers\SubscriptionItemRelationManager::class,
135+
];
136+
}
137+
201138
public static function getPages(): array
202139
{
203140
return [
204141
'index' => Pages\ListLicenses::route('/'),
142+
'view' => Pages\ViewLicense::route('/{record}'),
205143
];
206144
}
207145
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\LicenseResource\Pages;
4+
5+
use App\Actions\Licenses\SuspendLicense;
6+
use App\Filament\Resources\LicenseResource;
7+
use App\Models\License;
8+
use Filament\Actions;
9+
use Filament\Notifications\Notification;
10+
use Filament\Resources\Pages\ViewRecord;
11+
12+
/**
13+
* @property ?License $record
14+
*/
15+
class ViewLicense extends ViewRecord
16+
{
17+
protected static string $resource = LicenseResource::class;
18+
19+
protected function getHeaderActions(): array
20+
{
21+
return [
22+
Actions\ActionGroup::make([
23+
Actions\Action::make('suspend')
24+
->label('Suspend License')
25+
->icon('heroicon-o-archive-box-x-mark')
26+
->color('danger')
27+
->requiresConfirmation()
28+
->modalHeading('Suspend License')
29+
->modalDescription('Are you sure you want to suspend this license? This will prevent the user from using the software.')
30+
->modalSubmitActionLabel('Yes, suspend license')
31+
->visible(fn () => ! $this->record->is_suspended)
32+
->action(function () {
33+
app(SuspendLicense::class)->handle($this->record);
34+
35+
Notification::make()
36+
->title('License suspended successfully')
37+
->success()
38+
->send();
39+
}),
40+
Actions\DeleteAction::make(),
41+
])
42+
->label('Actions')
43+
->icon('heroicon-m-ellipsis-vertical'),
44+
];
45+
}
46+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\LicenseResource\RelationManagers;
4+
5+
use App\Filament\Resources\SubscriptionItemResource;
6+
use Filament\Forms\Form;
7+
use Filament\Resources\RelationManagers\RelationManager;
8+
use Filament\Tables\Table;
9+
10+
class SubscriptionItemRelationManager extends RelationManager
11+
{
12+
protected static string $relationship = 'subscriptionItem';
13+
14+
protected static bool $hasAssociateAction = false;
15+
16+
protected static bool $hasDissociateAction = false;
17+
18+
protected static bool $hasCreateAction = false;
19+
20+
protected static bool $hasDeleteAction = false;
21+
22+
protected static bool $hasEditAction = false;
23+
24+
public function form(Form $form): Form
25+
{
26+
return SubscriptionItemResource::form($form);
27+
}
28+
29+
public function table(Table $table): Table
30+
{
31+
return SubscriptionItemResource::table($table);
32+
}
33+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace App\Filament\Resources\LicenseResource\RelationManagers;
4+
5+
use App\Filament\Resources\UserResource;
6+
use Filament\Forms\Form;
7+
use Filament\Resources\RelationManagers\RelationManager;
8+
use Filament\Tables\Table;
9+
10+
class UserRelationManager extends RelationManager
11+
{
12+
protected static string $relationship = 'user';
13+
14+
protected static ?string $recordTitleAttribute = 'email';
15+
16+
protected static bool $hasAssociateAction = false;
17+
18+
protected static bool $hasDissociateAction = false;
19+
20+
protected static bool $hasCreateAction = false;
21+
22+
protected static bool $hasDeleteAction = false;
23+
24+
protected static bool $hasEditAction = false;
25+
26+
public function form(Form $form): Form
27+
{
28+
return UserResource::form($form);
29+
}
30+
31+
public function table(Table $table): Table
32+
{
33+
return UserResource::table($table);
34+
}
35+
}

0 commit comments

Comments
 (0)