Skip to content

Commit 18deb57

Browse files
simonhampclaude
andcommitted
Add comped Ultra subscription command, config, and plugin access tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e5d7a8b commit 18deb57

7 files changed

Lines changed: 254 additions & 4 deletions

File tree

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ STRIPE_PRO_PRICE_ID_EAP=
6969
STRIPE_MAX_PRICE_ID=
7070
STRIPE_MAX_PRICE_ID_MONTHLY=
7171
STRIPE_MAX_PRICE_ID_EAP=
72+
STRIPE_ULTRA_COMP_PRICE_ID=
7273
STRIPE_EXTRA_SEAT_PRICE_ID=
7374
STRIPE_EXTRA_SEAT_PRICE_ID_MONTHLY=
7475
STRIPE_FOREVER_PRICE_ID=
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Enums\Subscription;
6+
use App\Models\User;
7+
use Illuminate\Console\Command;
8+
9+
class CompUltraSubscription extends Command
10+
{
11+
protected $signature = 'ultra:comp {email : The email address of the user to comp}';
12+
13+
protected $description = 'Create a comped Ultra subscription for a user using the dedicated $0 Stripe price';
14+
15+
public function handle(): int
16+
{
17+
$compedPriceId = config('subscriptions.plans.max.stripe_price_id_comped');
18+
19+
if (! $compedPriceId) {
20+
$this->error('STRIPE_ULTRA_COMP_PRICE_ID is not configured.');
21+
22+
return self::FAILURE;
23+
}
24+
25+
$email = $this->argument('email');
26+
$user = User::where('email', $email)->first();
27+
28+
if (! $user) {
29+
$this->error("User not found: {$email}");
30+
31+
return self::FAILURE;
32+
}
33+
34+
$existingSubscription = $user->subscription('default');
35+
36+
if ($existingSubscription && $existingSubscription->active()) {
37+
$currentPlan = 'unknown';
38+
39+
try {
40+
$currentPlan = Subscription::fromStripePriceId(
41+
$existingSubscription->items->first()?->stripe_price ?? $existingSubscription->stripe_price
42+
)->name();
43+
} catch (\Exception) {
44+
}
45+
46+
$this->error("User already has an active {$currentPlan} subscription. Cancel it first or use swap.");
47+
48+
return self::FAILURE;
49+
}
50+
51+
$user->createOrGetStripeCustomer();
52+
53+
$user->newSubscription('default', $compedPriceId)->create();
54+
55+
$this->info("Comped Ultra subscription created for {$email}.");
56+
57+
return self::SUCCESS;
58+
}
59+
}

app/Enums/Subscription.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ public static function fromStripePriceId(string $priceId): self
5858
config('subscriptions.plans.max.stripe_price_id'),
5959
config('subscriptions.plans.max.stripe_price_id_monthly'),
6060
config('subscriptions.plans.max.stripe_price_id_discounted'),
61-
config('subscriptions.plans.max.stripe_price_id_eap') => self::Max,
61+
config('subscriptions.plans.max.stripe_price_id_eap'),
62+
config('subscriptions.plans.max.stripe_price_id_comped') => self::Max,
6263
default => throw new RuntimeException("Unknown Stripe price id: {$priceId}"),
6364
};
6465
}

app/Filament/Resources/UserResource/Pages/EditUser.php

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,98 @@ protected function getHeaderActions(): array
3434
})
3535
->visible(fn (User $record) => empty($record->stripe_id)),
3636

37+
Actions\Action::make('compUltraSubscription')
38+
->label('Comp Ultra Subscription')
39+
->color('warning')
40+
->icon('heroicon-o-sparkles')
41+
->modalHeading('Comp Ultra Subscription')
42+
->modalSubmitActionLabel('Comp Ultra')
43+
->form(function (User $record): array {
44+
$existingSubscription = $record->subscription('default');
45+
$hasActiveSubscription = $existingSubscription && $existingSubscription->active();
46+
47+
$fields = [];
48+
49+
if ($hasActiveSubscription) {
50+
$currentPlan = 'their current plan';
51+
52+
try {
53+
$currentPlan = \App\Enums\Subscription::fromStripePriceId(
54+
$existingSubscription->items->first()?->stripe_price ?? $existingSubscription->stripe_price
55+
)->name();
56+
} catch (\Exception) {
57+
}
58+
59+
$fields[] = \Filament\Forms\Components\Placeholder::make('info')
60+
->label('')
61+
->content("This user has an active {$currentPlan} subscription. Choose when to switch them to the comped Ultra plan.");
62+
63+
$fields[] = \Filament\Forms\Components\Radio::make('timing')
64+
->label('When to switch')
65+
->options([
66+
'now' => 'Immediately — swap now and credit remaining value (swapAndInvoice)',
67+
'renewal' => 'At renewal — keep current plan until period ends, then switch (swap)',
68+
])
69+
->default('now')
70+
->required();
71+
} else {
72+
$fields[] = \Filament\Forms\Components\Placeholder::make('info')
73+
->label('')
74+
->content("This will create a free Ultra subscription for {$record->email}. A Stripe customer will be created if one doesn't exist.");
75+
}
76+
77+
return $fields;
78+
})
79+
->action(function (array $data, User $record): void {
80+
$compedPriceId = config('subscriptions.plans.max.stripe_price_id_comped');
81+
82+
if (! $compedPriceId) {
83+
Notification::make()
84+
->danger()
85+
->title('STRIPE_ULTRA_COMP_PRICE_ID is not configured.')
86+
->send();
87+
88+
return;
89+
}
90+
91+
$record->createOrGetStripeCustomer();
92+
93+
$existingSubscription = $record->subscription('default');
94+
95+
if ($existingSubscription && $existingSubscription->active()) {
96+
$timing = $data['timing'] ?? 'now';
97+
98+
if ($timing === 'now') {
99+
$existingSubscription->skipTrial()->swapAndInvoice($compedPriceId);
100+
$message = 'Subscription swapped to comped Ultra immediately. Remaining value has been credited.';
101+
} else {
102+
$existingSubscription->skipTrial()->swap($compedPriceId);
103+
$message = 'Subscription will switch to comped Ultra at the end of the current billing period.';
104+
}
105+
106+
Notification::make()
107+
->success()
108+
->title('Comped Ultra subscription applied.')
109+
->body($message)
110+
->send();
111+
} else {
112+
$record->newSubscription('default', $compedPriceId)->create();
113+
114+
Notification::make()
115+
->success()
116+
->title('Comped Ultra subscription created.')
117+
->body("Ultra subscription created for {$record->email}.")
118+
->send();
119+
}
120+
})
121+
->visible(function (User $record): bool {
122+
if (! config('subscriptions.plans.max.stripe_price_id_comped')) {
123+
return false;
124+
}
125+
126+
return ! $record->hasActiveUltraSubscription();
127+
}),
128+
37129
Actions\Action::make('createAnystackLicense')
38130
->label('Create Anystack License')
39131
->color('gray')

app/Models/User.php

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,19 @@ public function hasActiveUltraSubscription(): bool
197197
{
198198
$subscription = $this->subscription();
199199

200-
if (! $subscription || $subscription->is_comped) {
200+
if (! $subscription) {
201+
return false;
202+
}
203+
204+
// Comped Ultra subs use a dedicated price — always grant Ultra access
205+
$compedUltraPriceId = config('subscriptions.plans.max.stripe_price_id_comped');
206+
207+
if ($compedUltraPriceId && $this->subscribedToPrice($compedUltraPriceId)) {
208+
return true;
209+
}
210+
211+
// Legacy comped Max subs should not get Ultra access
212+
if ($subscription->is_comped) {
201213
return false;
202214
}
203215

@@ -210,8 +222,8 @@ public function hasActiveUltraSubscription(): bool
210222
}
211223

212224
/**
213-
* Check if the user has a paying (non-comped) Max subscription,
214-
* qualifying them for Ultra benefits like Teams.
225+
* Check if the user has Ultra access (paying or comped Ultra),
226+
* qualifying them for Ultra benefits like Teams and free plugins.
215227
*/
216228
public function hasUltraAccess(): bool
217229
{
@@ -221,6 +233,13 @@ public function hasUltraAccess(): bool
221233
return false;
222234
}
223235

236+
// Comped Ultra subs always get full access
237+
$compedUltraPriceId = config('subscriptions.plans.max.stripe_price_id_comped');
238+
239+
if ($compedUltraPriceId && $this->subscribedToPrice($compedUltraPriceId)) {
240+
return true;
241+
}
242+
224243
$planPriceId = $subscription->stripe_price;
225244

226245
if (! $planPriceId) {
@@ -244,6 +263,7 @@ public function hasUltraAccess(): bool
244263
return false;
245264
}
246265

266+
// Legacy comped Max subs don't get Ultra access
247267
return ! $subscription->is_comped;
248268
}
249269

config/subscriptions.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
'stripe_price_id_monthly' => env('STRIPE_MAX_PRICE_ID_MONTHLY'),
2626
'stripe_price_id_eap' => env('STRIPE_MAX_PRICE_ID_EAP'),
2727
'stripe_price_id_discounted' => env('STRIPE_MAX_PRICE_ID_DISCOUNTED'),
28+
'stripe_price_id_comped' => env('STRIPE_ULTRA_COMP_PRICE_ID'),
2829
'stripe_payment_link' => env('STRIPE_MAX_PAYMENT_LINK'),
2930
'anystack_product_id' => env('ANYSTACK_PRODUCT_ID'),
3031
'anystack_policy_id' => env('ANYSTACK_MAX_POLICY_ID'),

tests/Feature/UltraPluginAccessTest.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,36 @@ class UltraPluginAccessTest extends TestCase
1818
{
1919
use RefreshDatabase;
2020

21+
private const COMPED_ULTRA_PRICE_ID = 'price_test_ultra_comped';
22+
23+
protected function setUp(): void
24+
{
25+
parent::setUp();
26+
27+
config(['subscriptions.plans.max.stripe_price_id_comped' => self::COMPED_ULTRA_PRICE_ID]);
28+
}
29+
30+
private function createCompedUltraSubscription(User $user): \Laravel\Cashier\Subscription
31+
{
32+
$user->update(['stripe_id' => 'cus_'.uniqid()]);
33+
34+
$subscription = \Laravel\Cashier\Subscription::factory()
35+
->for($user)
36+
->active()
37+
->create([
38+
'stripe_price' => self::COMPED_ULTRA_PRICE_ID,
39+
]);
40+
41+
\Laravel\Cashier\SubscriptionItem::factory()
42+
->for($subscription, 'subscription')
43+
->create([
44+
'stripe_price' => self::COMPED_ULTRA_PRICE_ID,
45+
'quantity' => 1,
46+
]);
47+
48+
return $subscription;
49+
}
50+
2151
private function createPaidMaxSubscription(User $user): \Laravel\Cashier\Subscription
2252
{
2353
$user->update(['stripe_id' => 'cus_'.uniqid()]);
@@ -479,4 +509,50 @@ public function test_purchased_plugins_page_does_not_show_team_plugins_for_non_m
479509
$response->assertStatus(200);
480510
$response->assertDontSee('Team Plugins');
481511
}
512+
513+
// ---- Comped Ultra subscriptions ----
514+
515+
public function test_comped_ultra_user_has_active_ultra_subscription(): void
516+
{
517+
$user = User::factory()->create();
518+
$this->createCompedUltraSubscription($user);
519+
520+
$this->assertTrue($user->hasActiveUltraSubscription());
521+
}
522+
523+
public function test_comped_ultra_user_has_ultra_access(): void
524+
{
525+
$user = User::factory()->create();
526+
$this->createCompedUltraSubscription($user);
527+
528+
$this->assertTrue($user->hasUltraAccess());
529+
}
530+
531+
public function test_comped_ultra_user_gets_free_official_plugin(): void
532+
{
533+
$user = User::factory()->create();
534+
$this->createCompedUltraSubscription($user);
535+
$plugin = $this->createOfficialPlugin();
536+
537+
$bestPrice = $plugin->getBestPriceForUser($user);
538+
539+
$this->assertNotNull($bestPrice);
540+
$this->assertEquals(0, $bestPrice->amount);
541+
}
542+
543+
public function test_legacy_comped_max_does_not_have_active_ultra_subscription(): void
544+
{
545+
$user = User::factory()->create();
546+
$this->createCompedMaxSubscription($user);
547+
548+
$this->assertFalse($user->hasActiveUltraSubscription());
549+
}
550+
551+
public function test_legacy_comped_max_does_not_have_ultra_access(): void
552+
{
553+
$user = User::factory()->create();
554+
$this->createCompedMaxSubscription($user);
555+
556+
$this->assertFalse($user->hasUltraAccess());
557+
}
482558
}

0 commit comments

Comments
 (0)