diff --git a/.env.example b/.env.example index 17c2f45..0c349d7 100644 --- a/.env.example +++ b/.env.example @@ -57,4 +57,8 @@ MERCHANT_DB_DATABASE=ladill_merchant MERCHANT_DB_USERNAME=ladill_pos MERCHANT_DB_PASSWORD= +# Suite demo accounts (sales walkthroughs) — reset POS data on each SSO login +DEMO_ACCOUNTS_ENABLED=false +DEMO_RESET_ON_LOGIN=true + VITE_APP_NAME="${APP_NAME}" diff --git a/app/Http/Controllers/Auth/SsoLoginController.php b/app/Http/Controllers/Auth/SsoLoginController.php index b1ae1e2..bf6b16d 100644 --- a/app/Http/Controllers/Auth/SsoLoginController.php +++ b/app/Http/Controllers/Auth/SsoLoginController.php @@ -124,6 +124,8 @@ class SsoLoginController extends Controller Auth::login($user, remember: true); $request->session()->regenerate(); + app(\App\Services\Pos\DemoLoginReseeder::class)->maybeResetAfterLogin($user); + return $this->finishCallback($request, $intended, null, $popup); } diff --git a/app/Services/Pos/DemoLoginReseeder.php b/app/Services/Pos/DemoLoginReseeder.php new file mode 100644 index 0000000..ab4da73 --- /dev/null +++ b/app/Services/Pos/DemoLoginReseeder.php @@ -0,0 +1,83 @@ +planForEmail($user->email); + if ($plan === null) { + return; + } + + $seedUser = $this->resolveSeedUser($user); + if (! $seedUser) { + return; + } + + $lockKey = 'demo-reseed:pos:'.$seedUser->public_id; + if (! Cache::add($lockKey, 1, 120)) { + return; + } + + $identity = (string) ($seedUser->email ?: $seedUser->public_id); + $userId = $seedUser->id; + dispatch(function () use ($identity, $plan, $lockKey, $userId) { + try { + if (User::query()->find($userId)) { + app(DemoSeeder::class)->seed($identity, $plan, true); + } + } catch (\Throwable $e) { + Log::warning('demo_pos_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..e482742 --- /dev/null +++ b/tests/Feature/DemoLoginReseederTest.php @@ -0,0 +1,81 @@ +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', + 'password' => bcrypt('password'), + ]); + + $seeder = $this->mock(DemoSeeder::class); + $seeder->shouldReceive('seed') + ->once() + ->withArgs(fn (string $identity, string $plan, bool $reset) => $identity === $user->email && $plan === 'free' && $reset === true) + ->andReturn(['user' => $user, 'plan' => 'free', 'counts' => []]); + + 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', + 'password' => bcrypt('password'), + ]); + + $seeder = $this->mock(DemoSeeder::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', + 'password' => bcrypt('password'), + ]); + + $seeder = $this->mock(DemoSeeder::class); + $seeder->shouldReceive('seed')->never(); + + app(DemoLoginReseeder::class)->maybeResetAfterLogin($user); + $this->app->terminate(); + } +}