From 78bd3c92676a7395424cecd7af91f6d26229aad4 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Sun, 25 Jan 2026 00:05:34 +0100 Subject: [PATCH] Allow superadmin to bypass onboarding billing --- app/Filament/Resources/TenantResource.php | 3 ++ app/Models/TenantPackage.php | 5 ++++ .../admin/mobile/lib/onboardingGuard.test.ts | 23 ++++++++++++++ .../js/admin/mobile/lib/onboardingGuard.ts | 6 ++++ resources/js/admin/router.tsx | 10 ++++++- tests/Feature/TenantPackageTest.php | 30 +++++++++++++++++++ 6 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/TenantPackageTest.php diff --git a/app/Filament/Resources/TenantResource.php b/app/Filament/Resources/TenantResource.php index 8028ee9..cf33188 100644 --- a/app/Filament/Resources/TenantResource.php +++ b/app/Filament/Resources/TenantResource.php @@ -8,6 +8,7 @@ use App\Filament\Resources\TenantResource\RelationManagers\PackagePurchasesRelat use App\Filament\Resources\TenantResource\RelationManagers\TenantPackagesRelationManager; use App\Filament\Resources\TenantResource\Schemas\TenantInfolist; use App\Jobs\AnonymizeAccount; +use App\Models\Package; use App\Models\Tenant; use App\Notifications\InactiveTenantDeletionWarning; use App\Services\Audit\SuperAdminAuditLogger; @@ -205,11 +206,13 @@ class TenantResource extends Resource Forms\Components\Textarea::make('reason')->label('Grund')->rows(3), ]) ->action(function (Tenant $record, array $data) { + $package = Package::query()->find($data['package_id']); \App\Models\TenantPackage::create([ 'tenant_id' => $record->id, 'package_id' => $data['package_id'], 'expires_at' => $data['expires_at'], 'active' => true, + 'price' => $package?->price ?? 0, 'reason' => $data['reason'] ?? null, ]); \App\Models\PackagePurchase::create([ diff --git a/app/Models/TenantPackage.php b/app/Models/TenantPackage.php index 012e380..8c92e5a 100644 --- a/app/Models/TenantPackage.php +++ b/app/Models/TenantPackage.php @@ -103,6 +103,11 @@ class TenantPackage extends Model $tenantPackage->purchased_at = now(); } + if ($tenantPackage->price === null) { + $package = $tenantPackage->package ?? Package::query()->find($tenantPackage->package_id); + $tenantPackage->price = $package?->price ?? 0; + } + $package = $tenantPackage->package; if ($package && $package->isReseller()) { diff --git a/resources/js/admin/mobile/lib/onboardingGuard.test.ts b/resources/js/admin/mobile/lib/onboardingGuard.test.ts index cbda6ec..3fb29e1 100644 --- a/resources/js/admin/mobile/lib/onboardingGuard.test.ts +++ b/resources/js/admin/mobile/lib/onboardingGuard.test.ts @@ -14,6 +14,7 @@ describe('resolveOnboardingRedirect', () => { isBillingPath: false, isOnboardingDismissed: false, isOnboardingCompleted: false, + isSuperAdmin: false, }); expect(result).toBeNull(); }); @@ -27,6 +28,7 @@ describe('resolveOnboardingRedirect', () => { isBillingPath: true, isOnboardingDismissed: false, isOnboardingCompleted: false, + isSuperAdmin: false, }); expect(result).toBeNull(); }); @@ -40,6 +42,7 @@ describe('resolveOnboardingRedirect', () => { isBillingPath: false, isOnboardingDismissed: false, isOnboardingCompleted: false, + isSuperAdmin: false, }); expect(result).toBeNull(); }); @@ -53,6 +56,7 @@ describe('resolveOnboardingRedirect', () => { isBillingPath: false, isOnboardingDismissed: false, isOnboardingCompleted: false, + isSuperAdmin: false, }); expect(result).toBe(ADMIN_BILLING_PATH); }); @@ -66,6 +70,7 @@ describe('resolveOnboardingRedirect', () => { isBillingPath: false, isOnboardingDismissed: false, isOnboardingCompleted: false, + isSuperAdmin: false, }); expect(result).toBeNull(); }); @@ -79,6 +84,7 @@ describe('resolveOnboardingRedirect', () => { isBillingPath: false, isOnboardingDismissed: false, isOnboardingCompleted: false, + isSuperAdmin: false, }); expect(result).toBeNull(); }); @@ -92,6 +98,7 @@ describe('resolveOnboardingRedirect', () => { isBillingPath: false, isOnboardingDismissed: false, isOnboardingCompleted: false, + isSuperAdmin: false, }); expect(result).toBeNull(); }); @@ -105,6 +112,7 @@ describe('resolveOnboardingRedirect', () => { isBillingPath: false, isOnboardingDismissed: true, isOnboardingCompleted: false, + isSuperAdmin: false, }); expect(result).toBeNull(); }); @@ -117,6 +125,21 @@ describe('resolveOnboardingRedirect', () => { pathname: '/event-admin/mobile/dashboard', isBillingPath: false, isOnboardingCompleted: true, + isSuperAdmin: false, + }); + expect(result).toBeNull(); + }); + + it('returns null for super admins without packages', () => { + const result = resolveOnboardingRedirect({ + hasEvents: false, + hasActivePackage: false, + remainingEvents: null, + pathname: '/event-admin/mobile/dashboard', + isBillingPath: false, + isOnboardingDismissed: false, + isOnboardingCompleted: false, + isSuperAdmin: true, }); expect(result).toBeNull(); }); diff --git a/resources/js/admin/mobile/lib/onboardingGuard.ts b/resources/js/admin/mobile/lib/onboardingGuard.ts index e878552..080c3e4 100644 --- a/resources/js/admin/mobile/lib/onboardingGuard.ts +++ b/resources/js/admin/mobile/lib/onboardingGuard.ts @@ -11,6 +11,7 @@ type OnboardingRedirectInput = { isBillingPath: boolean; isOnboardingDismissed?: boolean; isOnboardingCompleted?: boolean; + isSuperAdmin?: boolean; }; export function resolveOnboardingRedirect({ @@ -21,7 +22,12 @@ export function resolveOnboardingRedirect({ isBillingPath, isOnboardingDismissed, isOnboardingCompleted, + isSuperAdmin, }: OnboardingRedirectInput): string | null { + if (isSuperAdmin) { + return null; + } + if (isOnboardingDismissed || isOnboardingCompleted) { return null; } diff --git a/resources/js/admin/router.tsx b/resources/js/admin/router.tsx index 64fd4a1..13df1a3 100644 --- a/resources/js/admin/router.tsx +++ b/resources/js/admin/router.tsx @@ -61,8 +61,15 @@ function RequireAuth() { const isWelcomePath = location.pathname.startsWith(ADMIN_WELCOME_BASE_PATH); const isBillingPath = location.pathname.startsWith(ADMIN_BILLING_PATH); const isTenantAdmin = Boolean(user && user.role !== 'member'); + const isSuperAdmin = user?.role === 'super_admin' || user?.role === 'superadmin'; const shouldCheckPackages = - status === 'authenticated' && isTenantAdmin && !eventsLoading && !hasEvents && !isWelcomePath && !isBillingPath; + status === 'authenticated' + && isTenantAdmin + && !isSuperAdmin + && !eventsLoading + && !hasEvents + && !isWelcomePath + && !isBillingPath; const { data: packagesData, isLoading: packagesLoading } = useQuery({ queryKey: ['mobile', 'onboarding', 'packages-overview'], @@ -93,6 +100,7 @@ function RequireAuth() { isBillingPath, isOnboardingDismissed, isOnboardingCompleted, + isSuperAdmin, }); if (status === 'loading') { diff --git a/tests/Feature/TenantPackageTest.php b/tests/Feature/TenantPackageTest.php new file mode 100644 index 0000000..c42a5c2 --- /dev/null +++ b/tests/Feature/TenantPackageTest.php @@ -0,0 +1,30 @@ +create(); + $package = Package::factory()->create([ + 'price' => 123.45, + ]); + + $tenantPackage = TenantPackage::create([ + 'tenant_id' => $tenant->id, + 'package_id' => $package->id, + 'active' => true, + ])->refresh(); + + $this->assertSame('123.45', $tenantPackage->price); + } +}