diff --git a/app/Console/Commands/RemoveExpiredDiscordRoles.php b/app/Console/Commands/RemoveExpiredDiscordRoles.php index c1dbf304..e9fb3993 100644 --- a/app/Console/Commands/RemoveExpiredDiscordRoles.php +++ b/app/Console/Commands/RemoveExpiredDiscordRoles.php @@ -10,7 +10,7 @@ class RemoveExpiredDiscordRoles extends Command { protected $signature = 'discord:remove-expired-roles'; - protected $description = 'Remove Discord Max role for users whose Max licenses have expired'; + protected $description = 'Remove Discord Ultra role for users whose Max licenses or Ultra subscriptions have ended'; public function handle(): int { @@ -23,8 +23,8 @@ public function handle(): int ->get(); foreach ($users as $user) { - if (! $user->hasMaxAccess()) { - $success = $discord->removeMaxRole($user->discord_id); + if (! $user->hasMaxAccess() && ! $user->hasUltraAccess()) { + $success = $discord->removeUltraRole($user->discord_id); if ($success) { $user->update([ diff --git a/app/Http/Controllers/DiscordIntegrationController.php b/app/Http/Controllers/DiscordIntegrationController.php index 23b886f8..87a2fe05 100644 --- a/app/Http/Controllers/DiscordIntegrationController.php +++ b/app/Http/Controllers/DiscordIntegrationController.php @@ -75,10 +75,10 @@ public function handleCallback(): RedirectResponse $rolesAssigned = []; - if ($user->hasMaxAccess()) { - if ($discord->assignMaxRole($discordUser['id'])) { + if ($user->hasMaxAccess() || $user->hasUltraAccess()) { + if ($discord->assignUltraRole($discordUser['id'])) { $user->update(['discord_role_granted_at' => now()]); - $rolesAssigned[] = 'Max'; + $rolesAssigned[] = 'Ultra'; } } @@ -117,7 +117,7 @@ public function disconnect(): RedirectResponse $discord = DiscordApi::make(); if ($user->discord_role_granted_at) { - $discord->removeMaxRole($user->discord_id); + $discord->removeUltraRole($user->discord_id); } if ($user->discord_early_adopter_role_granted_at) { diff --git a/app/Jobs/AssignDiscordMaxRoleJob.php b/app/Jobs/AssignDiscordUltraRoleJob.php similarity index 81% rename from app/Jobs/AssignDiscordMaxRoleJob.php rename to app/Jobs/AssignDiscordUltraRoleJob.php index 139dcc25..f58ac532 100644 --- a/app/Jobs/AssignDiscordMaxRoleJob.php +++ b/app/Jobs/AssignDiscordUltraRoleJob.php @@ -11,7 +11,7 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Log; -class AssignDiscordMaxRoleJob implements ShouldQueue +class AssignDiscordUltraRoleJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -31,8 +31,8 @@ public function handle(): void return; } - if (! $this->user->hasMaxAccess()) { - Log::info('Skipping Discord role assignment - user has no Max access', [ + if (! $this->user->hasMaxAccess() && ! $this->user->hasUltraAccess()) { + Log::info('Skipping Discord role assignment - user has no Max or Ultra access', [ 'user_id' => $this->user->id, ]); @@ -50,17 +50,17 @@ public function handle(): void return; } - $success = $discord->assignMaxRole($this->user->discord_id); + $success = $discord->assignUltraRole($this->user->discord_id); if ($success) { $this->user->update(['discord_role_granted_at' => now()]); - Log::info('Discord Max role assigned successfully', [ + Log::info('Discord Ultra role assigned successfully', [ 'user_id' => $this->user->id, 'discord_id' => $this->user->discord_id, ]); } else { - Log::error('Failed to assign Discord Max role', [ + Log::error('Failed to assign Discord Ultra role', [ 'user_id' => $this->user->id, 'discord_id' => $this->user->discord_id, ]); diff --git a/app/Jobs/RemoveDiscordMaxRoleJob.php b/app/Jobs/RemoveDiscordUltraRoleJob.php similarity index 80% rename from app/Jobs/RemoveDiscordMaxRoleJob.php rename to app/Jobs/RemoveDiscordUltraRoleJob.php index 1cce367d..92b708b6 100644 --- a/app/Jobs/RemoveDiscordMaxRoleJob.php +++ b/app/Jobs/RemoveDiscordUltraRoleJob.php @@ -11,7 +11,7 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Log; -class RemoveDiscordMaxRoleJob implements ShouldQueue +class RemoveDiscordUltraRoleJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -33,17 +33,17 @@ public function handle(): void $discord = DiscordApi::make(); - $success = $discord->removeMaxRole($this->user->discord_id); + $success = $discord->removeUltraRole($this->user->discord_id); if ($success) { $this->user->update(['discord_role_granted_at' => null]); - Log::info('Discord Max role removed successfully', [ + Log::info('Discord Ultra role removed successfully', [ 'user_id' => $this->user->id, 'discord_id' => $this->user->discord_id, ]); } else { - Log::warning('Failed to remove Discord Max role (user may not have role)', [ + Log::warning('Failed to remove Discord Ultra role (user may not have role)', [ 'user_id' => $this->user->id, 'discord_id' => $this->user->discord_id, ]); diff --git a/app/Jobs/RevokeMaxAccessJob.php b/app/Jobs/RevokeMaxAccessJob.php index b08d0686..0a77469f 100644 --- a/app/Jobs/RevokeMaxAccessJob.php +++ b/app/Jobs/RevokeMaxAccessJob.php @@ -82,17 +82,17 @@ private function revokeDiscordRole(User $user): void } $discord = DiscordApi::make(); - $success = $discord->removeMaxRole($user->discord_id); + $success = $discord->removeUltraRole($user->discord_id); if ($success) { $user->update(['discord_role_granted_at' => null]); - Log::info('Discord Max role revoked for user', [ + Log::info('Discord Ultra role revoked for user', [ 'user_id' => $user->id, 'discord_id' => $user->discord_id, ]); } else { - Log::warning('Failed to revoke Discord Max role for user', [ + Log::warning('Failed to revoke Discord Ultra role for user', [ 'user_id' => $user->id, 'discord_id' => $user->discord_id, ]); diff --git a/app/Listeners/StripeWebhookReceivedListener.php b/app/Listeners/StripeWebhookReceivedListener.php index c293fb54..4f114d11 100644 --- a/app/Listeners/StripeWebhookReceivedListener.php +++ b/app/Listeners/StripeWebhookReceivedListener.php @@ -4,7 +4,7 @@ use App\Jobs\CreateUserFromStripeCustomer; use App\Jobs\HandleInvoicePaidJob; -use App\Jobs\RemoveDiscordMaxRoleJob; +use App\Jobs\RemoveDiscordUltraRoleJob; use App\Jobs\RevokeTeamUserAccessJob; use App\Jobs\SuspendTeamJob; use App\Jobs\UnsuspendTeamJob; @@ -75,7 +75,7 @@ private function handleSubscriptionDeleted(WebhookReceived $event): void return; } - $this->removeDiscordRoleIfNoMaxLicense($user); + $this->removeDiscordRoleIfNoAccess($user); dispatch(new SuspendTeamJob($user->id)); dispatch(new RevokeTeamUserAccessJob($user->id)); @@ -101,7 +101,7 @@ private function handleSubscriptionUpdated(WebhookReceived $event): void } if (in_array($status, ['canceled', 'unpaid', 'past_due', 'incomplete_expired'])) { - $this->removeDiscordRoleIfNoMaxLicense($user); + $this->removeDiscordRoleIfNoAccess($user); dispatch(new SuspendTeamJob($user->id)); dispatch(new RevokeTeamUserAccessJob($user->id)); } @@ -112,16 +112,16 @@ private function handleSubscriptionUpdated(WebhookReceived $event): void } } - private function removeDiscordRoleIfNoMaxLicense(User $user): void + private function removeDiscordRoleIfNoAccess(User $user): void { if (! $user->discord_id) { return; } - if ($user->hasMaxAccess()) { + if ($user->hasMaxAccess() || $user->hasUltraAccess()) { return; } - dispatch(new RemoveDiscordMaxRoleJob($user)); + dispatch(new RemoveDiscordUltraRoleJob($user)); } } diff --git a/app/Livewire/DiscordAccessBanner.php b/app/Livewire/DiscordAccessBanner.php index 8e3fb501..cd2a1df0 100644 --- a/app/Livewire/DiscordAccessBanner.php +++ b/app/Livewire/DiscordAccessBanner.php @@ -10,7 +10,7 @@ class DiscordAccessBanner extends Component { public bool $inline = false; - public bool $hasMaxRole = false; + public bool $hasUltraRole = false; public bool $hasEarlyAdopterRole = false; @@ -27,7 +27,7 @@ public function checkRoleStatus(): void $user = auth()->user(); if (! $user || ! $user->discord_id) { - $this->hasMaxRole = false; + $this->hasUltraRole = false; $this->hasEarlyAdopterRole = false; $this->isGuildMember = false; @@ -41,16 +41,16 @@ public function checkRoleStatus(): void return [ 'isGuildMember' => $discord->isGuildMember($user->discord_id), - 'hasMaxRole' => $discord->hasMaxRole($user->discord_id), + 'hasUltraRole' => $discord->hasUltraRole($user->discord_id), 'hasEarlyAdopterRole' => $discord->hasEarlyAdopterRole($user->discord_id), ]; }); $this->isGuildMember = $status['isGuildMember']; - $this->hasMaxRole = $status['hasMaxRole']; + $this->hasUltraRole = $status['hasUltraRole']; $this->hasEarlyAdopterRole = $status['hasEarlyAdopterRole']; - if ($this->hasMaxRole && ! $user->discord_role_granted_at) { + if ($this->hasUltraRole && ! $user->discord_role_granted_at) { $user->update(['discord_role_granted_at' => now()]); } @@ -70,7 +70,7 @@ public function refreshStatus(): void $this->checkRoleStatus(); } - public function requestMaxRole(): void + public function requestUltraRole(): void { $user = auth()->user(); @@ -80,8 +80,8 @@ public function requestMaxRole(): void return; } - if (! $user->hasMaxAccess()) { - session()->flash('error', 'You need an active Max license to receive the Max role.'); + if (! $user->hasMaxAccess() && ! $user->hasUltraAccess()) { + session()->flash('error', 'You need an active Max license or Ultra subscription to receive the Ultra role.'); return; } @@ -94,15 +94,15 @@ public function requestMaxRole(): void return; } - $success = $discord->assignMaxRole($user->discord_id); + $success = $discord->assignUltraRole($user->discord_id); if ($success) { $user->update(['discord_role_granted_at' => now()]); Cache::forget("discord_role_status_{$user->id}"); $this->checkRoleStatus(); - session()->flash('success', 'Max role assigned successfully!'); + session()->flash('success', 'Ultra role assigned successfully!'); } else { - session()->flash('error', 'Failed to assign Max role. Please try again later.'); + session()->flash('error', 'Failed to assign Ultra role. Please try again later.'); } } diff --git a/app/Support/DiscordApi.php b/app/Support/DiscordApi.php index 35e20912..ebb835f2 100644 --- a/app/Support/DiscordApi.php +++ b/app/Support/DiscordApi.php @@ -12,7 +12,7 @@ class DiscordApi public function __construct( private ?string $botToken, private ?string $guildId, - private ?string $maxRoleId, + private ?string $ultraRoleId, private ?string $earlyAdopterRoleId ) {} @@ -21,7 +21,7 @@ public static function make(): static return new static( config('services.discord.bot_token', ''), config('services.discord.guild_id', ''), - config('services.discord.max_role_id', ''), + config('services.discord.ultra_role_id', ''), config('services.discord.early_adopter_role_id', '') ); } @@ -71,19 +71,19 @@ public function isGuildMember(string $discordUserId): bool return true; } - public function assignMaxRole(string $discordUserId): bool + public function assignUltraRole(string $discordUserId): bool { - return $this->assignRole($discordUserId, $this->maxRoleId, 'Max'); + return $this->assignRole($discordUserId, $this->ultraRoleId, 'Ultra'); } - public function removeMaxRole(string $discordUserId): bool + public function removeUltraRole(string $discordUserId): bool { - return $this->removeRole($discordUserId, $this->maxRoleId, 'Max'); + return $this->removeRole($discordUserId, $this->ultraRoleId, 'Ultra'); } - public function hasMaxRole(string $discordUserId): bool + public function hasUltraRole(string $discordUserId): bool { - return $this->hasRole($discordUserId, $this->maxRoleId); + return $this->hasRole($discordUserId, $this->ultraRoleId); } public function assignEarlyAdopterRole(string $discordUserId): bool diff --git a/config/services.php b/config/services.php index ad8ebc06..03c91a8b 100644 --- a/config/services.php +++ b/config/services.php @@ -56,7 +56,7 @@ 'redirect' => env('APP_URL').'/auth/discord/callback', 'bot_token' => env('DISCORD_BOT_TOKEN'), 'guild_id' => env('DISCORD_GUILD_ID'), - 'max_role_id' => env('DISCORD_MAX_ROLE_ID'), + 'ultra_role_id' => env('DISCORD_ULTRA_ROLE_ID'), 'early_adopter_role_id' => env('DISCORD_EARLY_ADOPTER_ROLE_ID'), ], diff --git a/resources/views/livewire/customer/integrations.blade.php b/resources/views/livewire/customer/integrations.blade.php index 4cb80b0f..584250ee 100644 --- a/resources/views/livewire/customer/integrations.blade.php +++ b/resources/views/livewire/customer/integrations.blade.php @@ -29,7 +29,7 @@

Need help? Join our Discord community. @@ -47,7 +47,7 @@ @endif - @if(auth()->user()->isEapCustomer()) + @if(auth()->user()->hasMaxAccess() || auth()->user()->hasUltraAccess() || auth()->user()->isEapCustomer()) @endif

diff --git a/resources/views/livewire/discord-access-banner.blade.php b/resources/views/livewire/discord-access-banner.blade.php index e0467d3b..58ec9f8c 100644 --- a/resources/views/livewire/discord-access-banner.blade.php +++ b/resources/views/livewire/discord-access-banner.blade.php @@ -24,13 +24,13 @@

@else
- @if($hasMaxRole) + @if($hasUltraRole) - Max Role Active + Ultra Role Active - @elseif(auth()->user()->hasMaxAccess()) + @elseif(auth()->user()->hasMaxAccess() || auth()->user()->hasUltraAccess()) - Max Eligible + Ultra Eligible @endif @@ -62,10 +62,10 @@ Checking... @else - @if(!$hasMaxRole && auth()->user()->hasMaxAccess()) - @endif @if(!$hasEarlyAdopterRole && auth()->user()->isEapCustomer()) @@ -74,7 +74,7 @@ Requesting... @endif - @if(($hasMaxRole || !auth()->user()->hasMaxAccess()) && ($hasEarlyAdopterRole || !auth()->user()->isEapCustomer())) + @if(($hasUltraRole || !(auth()->user()->hasMaxAccess() || auth()->user()->hasUltraAccess())) && ($hasEarlyAdopterRole || !auth()->user()->isEapCustomer())) Open Discord diff --git a/tests/Feature/DiscordIntegrationTest.php b/tests/Feature/DiscordIntegrationTest.php index 05ecaef6..7284fba7 100644 --- a/tests/Feature/DiscordIntegrationTest.php +++ b/tests/Feature/DiscordIntegrationTest.php @@ -22,7 +22,7 @@ protected function setUp(): void 'services.discord.client_secret' => 'test-client-secret', 'services.discord.bot_token' => 'test-bot-token', 'services.discord.guild_id' => 'test-guild-id', - 'services.discord.max_role_id' => 'max-role-id', + 'services.discord.ultra_role_id' => 'ultra-role-id', 'services.discord.early_adopter_role_id' => 'early-adopter-role-id', ]); } diff --git a/tests/Feature/DiscordUltraAccessTest.php b/tests/Feature/DiscordUltraAccessTest.php new file mode 100644 index 00000000..cd6420f3 --- /dev/null +++ b/tests/Feature/DiscordUltraAccessTest.php @@ -0,0 +1,145 @@ + self::MAX_PRICE_ID, + 'services.discord.bot_token' => 'test_token', + 'services.discord.guild_id' => self::GUILD_ID, + 'services.discord.ultra_role_id' => self::ULTRA_ROLE_ID, + ]); + + Cache::flush(); + } + + private function ultraSubscriber(array $userAttributes = []): User + { + $user = User::factory()->create(array_merge([ + 'stripe_id' => 'cus_'.uniqid(), + ], $userAttributes)); + + CashierSubscription::factory()->for($user)->active()->create([ + 'stripe_price' => self::MAX_PRICE_ID, + ]); + + return $user; + } + + public function test_ultra_subscriber_sees_discord_access_banner(): void + { + Http::fake([ + 'discord.com/api/v10/guilds/*/members/*' => Http::response([], 404), + ]); + + $user = $this->ultraSubscriber(); + + $response = $this->actingAs($user)->get('/dashboard/integrations'); + + $response->assertStatus(200); + $response->assertSeeLivewire('discord-access-banner'); + } + + public function test_user_without_max_or_ultra_does_not_see_discord_banner(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get('/dashboard/integrations'); + + $response->assertStatus(200); + $response->assertDontSeeLivewire('discord-access-banner'); + } + + public function test_ultra_subscriber_can_request_ultra_role(): void + { + Http::fake([ + 'discord.com/api/v10/guilds/*/members/discord-123/roles/*' => Http::response([], 204), + 'discord.com/api/v10/guilds/*/members/discord-123' => Http::response(['roles' => []], 200), + ]); + + $user = $this->ultraSubscriber([ + 'discord_id' => 'discord-123', + 'discord_username' => 'ultra-user', + ]); + + Livewire::actingAs($user) + ->test(DiscordAccessBanner::class) + ->call('requestUltraRole'); + + $this->assertNotNull($user->fresh()->discord_role_granted_at); + Http::assertSent(fn ($request) => str_contains($request->url(), '/members/discord-123/roles/'.self::ULTRA_ROLE_ID) + && $request->method() === 'PUT'); + } + + public function test_request_ultra_role_rejected_without_max_or_ultra(): void + { + Http::fake(); + + $user = User::factory()->create([ + 'discord_id' => 'discord-999', + 'discord_username' => 'free-user', + ]); + + Livewire::actingAs($user) + ->test(DiscordAccessBanner::class) + ->call('requestUltraRole'); + + $this->assertNull($user->fresh()->discord_role_granted_at); + Http::assertNotSent(fn ($request) => $request->method() === 'PUT' + && str_contains($request->url(), '/roles/'.self::ULTRA_ROLE_ID)); + } + + public function test_cleanup_command_retains_role_for_ultra_subscriber(): void + { + $user = $this->ultraSubscriber([ + 'discord_id' => 'discord-456', + 'discord_username' => 'ultra-user', + 'discord_role_granted_at' => now()->subDays(10), + ]); + + $this->artisan('discord:remove-expired-roles') + ->assertExitCode(0); + + $this->assertNotNull($user->fresh()->discord_role_granted_at); + } + + public function test_cleanup_command_removes_role_when_no_access(): void + { + Http::fake([ + 'discord.com/api/v10/guilds/*/members/*/roles/*' => Http::response([], 204), + ]); + + $user = User::factory()->create([ + 'discord_id' => 'discord-789', + 'discord_username' => 'former-user', + 'discord_role_granted_at' => now()->subDays(10), + ]); + + $this->artisan('discord:remove-expired-roles') + ->assertExitCode(0); + + $this->assertNull($user->fresh()->discord_role_granted_at); + } +} diff --git a/tests/Feature/Livewire/DiscordAccessBannerTest.php b/tests/Feature/Livewire/DiscordAccessBannerTest.php index 772e96b7..d4c0ff57 100644 --- a/tests/Feature/Livewire/DiscordAccessBannerTest.php +++ b/tests/Feature/Livewire/DiscordAccessBannerTest.php @@ -23,7 +23,7 @@ protected function setUp(): void config([ 'services.discord.bot_token' => 'test-bot-token', 'services.discord.guild_id' => 'test-guild-id', - 'services.discord.max_role_id' => 'max-role-id', + 'services.discord.ultra_role_id' => 'ultra-role-id', 'services.discord.early_adopter_role_id' => 'early-adopter-role-id', ]); } @@ -69,18 +69,18 @@ public function it_shows_not_in_server_when_user_is_not_a_guild_member(): void } #[Test] - public function it_shows_max_role_active_when_user_has_max_role(): void + public function it_shows_ultra_role_active_when_user_has_ultra_role(): void { $user = User::factory()->create([ 'discord_id' => '123456789', 'discord_username' => 'testuser', ]); - $this->fakeDiscordApi(isGuildMember: true, roles: ['max-role-id']); + $this->fakeDiscordApi(isGuildMember: true, roles: ['ultra-role-id']); Livewire::actingAs($user) ->test(DiscordAccessBanner::class) - ->assertSee('Max Role Active'); + ->assertSee('Ultra Role Active'); } #[Test] @@ -224,11 +224,11 @@ public function it_shows_both_roles_active_when_user_has_both(): void 'discord_username' => 'testuser', ]); - $this->fakeDiscordApi(isGuildMember: true, roles: ['max-role-id', 'early-adopter-role-id']); + $this->fakeDiscordApi(isGuildMember: true, roles: ['ultra-role-id', 'early-adopter-role-id']); Livewire::actingAs($user) ->test(DiscordAccessBanner::class) - ->assertSee('Max Role Active') + ->assertSee('Ultra Role Active') ->assertSee('Early Adopter Active'); }