diff --git a/app/Services/Qms/DemoLoginReseeder.php b/app/Services/Qms/DemoLoginReseeder.php index aed5819..6a36440 100644 --- a/app/Services/Qms/DemoLoginReseeder.php +++ b/app/Services/Qms/DemoLoginReseeder.php @@ -12,6 +12,10 @@ use Illuminate\Support\Facades\Log; * Only owner demo accounts trigger a full reseed. DemoWorld staff logins * (e.g. receptionist@ladill.com variants) share the owner tenant but must * never reseed mid-demo. + * + * The onboarded org shell is created synchronously before the login redirect so + * EnsureOrganizationSetup never flashes the blank onboarding form. Heavy demo + * data still refreshes afterResponse(). */ class DemoLoginReseeder { @@ -38,6 +42,29 @@ class DemoLoginReseeder $lockKey = 'demo-reseed:queue:'.$seedUser->public_id; if (! Cache::add($lockKey, 1, 120)) { + try { + app(DemoSeeder::class)->ensureOnboardedShell($seedUser, $plan); + } catch (\Throwable $e) { + Log::warning('demo_queue_login_shell_failed', [ + 'user_id' => $seedUser->id, + 'plan' => $plan, + 'error' => $e->getMessage(), + ]); + } + + return; + } + + try { + app(DemoSeeder::class)->ensureOnboardedShell($seedUser, $plan); + } catch (\Throwable $e) { + Cache::forget($lockKey); + Log::warning('demo_queue_login_shell_failed', [ + 'user_id' => $seedUser->id, + 'plan' => $plan, + 'error' => $e->getMessage(), + ]); + return; } diff --git a/app/Services/Qms/DemoSeeder.php b/app/Services/Qms/DemoSeeder.php index 4496e6f..5d049ce 100644 --- a/app/Services/Qms/DemoSeeder.php +++ b/app/Services/Qms/DemoSeeder.php @@ -71,8 +71,13 @@ class DemoSeeder $plan = $this->normalizePlan($plan); $user = $this->resolveUser($identity); + // Always materialize the onboarded org shell before any wipe so concurrent + // SSO redirects never land on the blank onboarding form. + $this->ensureOnboardedShell($user, $plan); + if ($reset) { $this->resetForOwner($user->ownerRef()); + $this->ensureOnboardedShell($user, $plan); } $organization = $this->ensureOrganization($user, $plan); @@ -160,6 +165,19 @@ class DemoSeeder ]; } + /** + * Ensure the demo tenant has an onboarded organization + owner member. + * Safe to call on the SSO request path before redirecting. + */ + public function ensureOnboardedShell(User $user, string $plan): Organization + { + $plan = $this->normalizePlan($plan); + $organization = $this->ensureOrganization($user, $plan); + $this->assignPlan($organization, $plan); + + return $organization->fresh() ?? $organization; + } + public function resolveUser(string $identity): User { $identity = trim($identity); diff --git a/app/Services/Qms/OrganizationResolver.php b/app/Services/Qms/OrganizationResolver.php index 8bc099b..30d96f0 100644 --- a/app/Services/Qms/OrganizationResolver.php +++ b/app/Services/Qms/OrganizationResolver.php @@ -18,7 +18,10 @@ class OrganizationResolver $member = Member::where('user_ref', $ref)->first(); if ($member) { - return Organization::find($member->organization_id); + $organization = Organization::query()->find($member->organization_id); + if ($organization) { + return $organization; + } } return Organization::owned($ref)->first(); diff --git a/tests/Feature/DemoLoginReseederTest.php b/tests/Feature/DemoLoginReseederTest.php index 8665bd6..2a4c705 100644 --- a/tests/Feature/DemoLoginReseederTest.php +++ b/tests/Feature/DemoLoginReseederTest.php @@ -35,6 +35,10 @@ class DemoLoginReseederTest extends TestCase ]); $seeder = $this->mock(DemoSeeder::class); + $seeder->shouldReceive('ensureOnboardedShell') + ->once() + ->withArgs(fn (User $u, string $plan) => $u->is($user) && $plan === 'free') + ->andReturn(new \App\Models\Organization); $seeder->shouldReceive('seed') ->once() ->withArgs(fn (string $identity, string $plan, bool $reset) => $identity === $user->email && $plan === 'free' && $reset === true) @@ -55,6 +59,7 @@ class DemoLoginReseederTest extends TestCase ]); $seeder = $this->mock(DemoSeeder::class); + $seeder->shouldReceive('ensureOnboardedShell')->never(); $seeder->shouldReceive('seed')->never(); app(DemoLoginReseeder::class)->maybeResetAfterLogin($user); @@ -73,9 +78,24 @@ class DemoLoginReseederTest extends TestCase ]); $seeder = $this->mock(DemoSeeder::class); + $seeder->shouldReceive('ensureOnboardedShell')->never(); $seeder->shouldReceive('seed')->never(); app(DemoLoginReseeder::class)->maybeResetAfterLogin($user); $this->app->terminate(); } + + public function test_login_shell_is_onboarded_before_async_reseed(): void + { + $user = User::create([ + 'public_id' => 'demo-shell-queue-pid', + 'name' => 'Ladill Demo (Pro)', + 'email' => 'demo-pro@ladill.com', + 'password' => bcrypt('password'), + ]); + + app(DemoLoginReseeder::class)->maybeResetAfterLogin($user); + + $this->assertTrue(app(\App\Services\Qms\OrganizationResolver::class)->isOnboarded($user)); + } }