From 679577a7b36852827e47b1f6790a52812b2121b3 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Fri, 17 Jul 2026 11:17:16 +0000 Subject: [PATCH] Reseed Care demo tenants after each SSO login. Demo Free/Pro/Enterprise accounts wipe and reload sample clinic data on authorization-code SSO callbacks so every sales walkthrough starts clean. Co-authored-by: Cursor --- .env.example | 4 + .../Controllers/Auth/SsoLoginController.php | 1 + app/Services/Care/DemoLoginReseeder.php | 83 +++++++++++++++++++ config/demo_accounts.php | 18 ++++ tests/Feature/DemoLoginReseederTest.php | 78 +++++++++++++++++ 5 files changed, 184 insertions(+) create mode 100644 app/Services/Care/DemoLoginReseeder.php create mode 100644 config/demo_accounts.php create mode 100644 tests/Feature/DemoLoginReseederTest.php diff --git a/.env.example b/.env.example index 111e34d..94b9190 100644 --- a/.env.example +++ b/.env.example @@ -67,6 +67,10 @@ AFIA_PLATFORM_API_KEY= # Platform events webhook (user/org lifecycle from the monolith) SERVICE_EVENTS_INBOUND_SECRET= +# Suite demo accounts (sales walkthroughs) — reset Care data on each SSO login +DEMO_ACCOUNTS_ENABLED=false +DEMO_RESET_ON_LOGIN=true + # --- Inbound service API keys (sibling Ladill apps calling Care) --- CARE_API_KEY_FRONTDESK= CARE_API_KEY_CRM= diff --git a/app/Http/Controllers/Auth/SsoLoginController.php b/app/Http/Controllers/Auth/SsoLoginController.php index c914b21..5244587 100644 --- a/app/Http/Controllers/Auth/SsoLoginController.php +++ b/app/Http/Controllers/Auth/SsoLoginController.php @@ -121,6 +121,7 @@ class SsoLoginController extends Controller $request->session()->forget('sso.attempts'); app(TeamMemberProvisioner::class)->sync($user); + app(\App\Services\Care\DemoLoginReseeder::class)->maybeResetAfterLogin($user); return $this->finishCallback($request, $intended, null, app(IdentityTeamClient::class)); } diff --git a/app/Services/Care/DemoLoginReseeder.php b/app/Services/Care/DemoLoginReseeder.php new file mode 100644 index 0000000..cf6c10e --- /dev/null +++ b/app/Services/Care/DemoLoginReseeder.php @@ -0,0 +1,83 @@ +planForEmail($user->email); + if ($plan === null) { + return; + } + + $seedUser = $this->resolveSeedUser($user); + if (! $seedUser) { + return; + } + + $lockKey = 'demo-reseed:care:'.$seedUser->public_id; + if (! Cache::add($lockKey, 1, 120)) { + return; + } + + $userId = $seedUser->id; + dispatch(function () use ($userId, $plan, $lockKey) { + try { + $user = User::query()->find($userId); + if ($user) { + app(DemoTenantSeeder::class)->seed($user, $plan, reset: true); + } + } catch (\Throwable $e) { + Log::warning('demo_care_login_reseed_failed', [ + 'user_id' => $userId, + 'plan' => $plan, + 'error' => $e->getMessage(), + ]); + } finally { + Cache::forget($lockKey); + } + })->afterResponse(); + } + + private function planForEmail(?string $email): ?string + { + $email = strtolower(trim((string) $email)); + if ($email === '') { + return null; + } + + $accounts = (array) config('demo_accounts.accounts', []); + if (isset($accounts[$email])) { + return $accounts[$email]; + } + + $members = (array) config('demo_accounts.member_emails', []); + $ownerEmail = $members[$email] ?? null; + + return $ownerEmail ? ($accounts[$ownerEmail] ?? null) : null; + } + + private function resolveSeedUser(User $user): ?User + { + $email = strtolower((string) $user->email); + $ownerEmail = ((array) config('demo_accounts.member_emails', []))[$email] ?? null; + if (! $ownerEmail) { + return $user; + } + + return User::query()->where('email', $ownerEmail)->first() ?? $user; + } +} diff --git a/config/demo_accounts.php b/config/demo_accounts.php new file mode 100644 index 0000000..5adaff8 --- /dev/null +++ b/config/demo_accounts.php @@ -0,0 +1,18 @@ + (bool) env('DEMO_ACCOUNTS_ENABLED', false), + 'reset_on_login' => (bool) env('DEMO_RESET_ON_LOGIN', true), + + 'accounts' => [ + 'demo-free@ladill.com' => 'free', + 'demo-pro@ladill.com' => 'pro', + 'demo-enterprise@ladill.com' => 'enterprise', + ], + + // Org members share the enterprise owner tenant. + 'member_emails' => [ + 'demo-enterprise-member-1@ladill.com' => 'demo-enterprise@ladill.com', + 'demo-enterprise-member-2@ladill.com' => 'demo-enterprise@ladill.com', + ], +]; diff --git a/tests/Feature/DemoLoginReseederTest.php b/tests/Feature/DemoLoginReseederTest.php new file mode 100644 index 0000000..efe66f8 --- /dev/null +++ b/tests/Feature/DemoLoginReseederTest.php @@ -0,0 +1,78 @@ +withoutMiddleware(EnsurePlatformSession::class); + config([ + 'demo_accounts.enabled' => true, + 'demo_accounts.reset_on_login' => true, + ]); + Cache::flush(); + } + + public function test_it_reseeds_demo_free_user_after_login(): void + { + $user = User::create([ + 'public_id' => 'demo-free-pid', + 'name' => 'Ladill Demo (Free)', + 'email' => 'demo-free@ladill.com', + ]); + + $seeder = $this->mock(DemoTenantSeeder::class); + $seeder->shouldReceive('seed') + ->once() + ->withArgs(fn (User $u, string $plan, bool $reset) => $u->is($user) && $plan === 'free' && $reset === true) + ->andReturn(['ok' => true]); + + app(DemoLoginReseeder::class)->maybeResetAfterLogin($user); + + $this->app->terminate(); + } + + public function test_it_skips_non_demo_users(): void + { + $user = User::create([ + 'public_id' => 'regular-pid', + 'name' => 'Regular', + 'email' => 'regular@example.com', + ]); + + $seeder = $this->mock(DemoTenantSeeder::class); + $seeder->shouldReceive('seed')->never(); + + app(DemoLoginReseeder::class)->maybeResetAfterLogin($user); + $this->app->terminate(); + } + + public function test_it_skips_when_disabled(): void + { + config(['demo_accounts.enabled' => false]); + + $user = User::create([ + 'public_id' => 'demo-free-pid', + 'name' => 'Ladill Demo (Free)', + 'email' => 'demo-free@ladill.com', + ]); + + $seeder = $this->mock(DemoTenantSeeder::class); + $seeder->shouldReceive('seed')->never(); + + app(DemoLoginReseeder::class)->maybeResetAfterLogin($user); + $this->app->terminate(); + } +}