From a28e8619a152e3f7fe3a29dd327fdd6530a817fb Mon Sep 17 00:00:00 2001 From: isaacclad Date: Fri, 17 Jul 2026 13:35:52 +0000 Subject: [PATCH] Wire Frontdesk demo seeder to shared DemoWorld Org name, branches, and visitors now come from DemoWorld so continuity matches Care/Queue across the suite, plus Members for DemoWorld staff with a frontdesk role. Also resolve organization by owner_ref when metadata.frontdesk.organization_id is missing, and skip reseed-on-login for DemoWorld staff emails. Co-authored-by: Cursor --- app/Console/Commands/DemoSeedCommand.php | 55 +- app/Services/Frontdesk/DemoLoginReseeder.php | 8 + .../Frontdesk/TeamMemberProvisioner.php | 7 +- app/Support/DemoWorld.php | 553 ++++++++++++++++++ tests/Feature/DemoSeedCommandTest.php | 37 ++ 5 files changed, 648 insertions(+), 12 deletions(-) create mode 100644 app/Support/DemoWorld.php diff --git a/app/Console/Commands/DemoSeedCommand.php b/app/Console/Commands/DemoSeedCommand.php index d868a52..ba2e1f3 100644 --- a/app/Console/Commands/DemoSeedCommand.php +++ b/app/Console/Commands/DemoSeedCommand.php @@ -22,6 +22,7 @@ use App\Models\Visitor; use App\Models\WatchlistEntry; use App\Models\WebhookEndpoint; use App\Services\Frontdesk\PlanService; +use App\Support\DemoWorld; use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; @@ -145,7 +146,7 @@ class DemoSeedCommand extends Command } $org->fill([ - 'name' => $plan === 'enterprise' ? 'Accra Healthcare Group Frontdesk' : 'Ladill Demo Frontdesk', + 'name' => DemoWorld::business($plan)['name'], 'timezone' => 'Africa/Accra', 'settings' => array_merge((array) ($org->settings ?? []), $settings), ]); @@ -166,6 +167,35 @@ class DemoSeedCommand extends Command return $org->fresh(); } + /** + * Local Member rows for DemoWorld staff with a frontdesk role (user_ref = + * email until SSO remaps to public_id). + * + * @param list $branches + */ + private function seedStaffMembers(string $ownerRef, Organization $organization, string $plan, array $branches): void + { + $hq = $branches[0] ?? null; + foreach (DemoWorld::staff($plan) as $staff) { + $role = $staff['roles']['frontdesk'] ?? null; + if (! is_string($role) || $role === '') { + continue; + } + + Member::query()->updateOrCreate( + [ + 'organization_id' => $organization->id, + 'user_ref' => strtolower($staff['email']), + ], + [ + 'owner_ref' => $ownerRef, + 'role' => $role, + 'branch_id' => $hq?->id, + ], + ); + } + } + private function hasDemoVisits(Organization $organization): bool { return Visit::query() @@ -239,19 +269,20 @@ class DemoSeedCommand extends Command $visitorCount = $paid ? 30 : 15; $branches = []; - $names = ['HQ', 'East Legon', 'Airport', 'Tema']; - for ($i = 0; $i < $branchCount; $i++) { + foreach (DemoWorld::branches($plan, $branchCount) as $spec) { $branches[] = Branch::query()->create([ 'owner_ref' => $ownerRef, 'organization_id' => $organization->id, - 'name' => $names[$i] ?? ('Branch '.($i + 1)), - 'code' => 'BR'.($i + 1), - 'address' => 'Accra branch address '.($i + 1), - 'phone' => '+23330200000'.($i + 1), + 'name' => $spec['name'], + 'code' => $spec['code'], + 'address' => $spec['address'], + 'phone' => $spec['phone'], 'is_active' => true, ]); } + $this->seedStaffMembers($ownerRef, $organization, $plan, $branches); + $hosts = []; $desks = []; foreach ($branches as $index => $branch) { @@ -310,15 +341,17 @@ class DemoSeedCommand extends Command ]); } + $people = DemoWorld::people(); $visitors = []; for ($v = 1; $v <= $visitorCount; $v++) { + $person = $people[($v - 1) % count($people)]; $visitors[] = Visitor::query()->create([ 'owner_ref' => $ownerRef, 'organization_id' => $organization->id, - 'full_name' => 'Visitor '.$v, - 'company' => 'Guest Co '.$v, - 'phone' => '+23355'.str_pad((string) $v, 7, '0', STR_PAD_LEFT), - 'email' => 'visitor'.$v.'@demo.ladill.com', + 'full_name' => DemoWorld::fullName($person), + 'company' => DemoWorld::companyNameForPerson($person), + 'phone' => $person['phone'], + 'email' => $person['email'], 'watchlist_status' => Visitor::WATCHLIST_ALLOWED, 'visit_count' => 0, 'is_frequent' => $v <= 3, diff --git a/app/Services/Frontdesk/DemoLoginReseeder.php b/app/Services/Frontdesk/DemoLoginReseeder.php index 7d82aad..f6a6ab0 100644 --- a/app/Services/Frontdesk/DemoLoginReseeder.php +++ b/app/Services/Frontdesk/DemoLoginReseeder.php @@ -9,6 +9,10 @@ use Illuminate\Support\Facades\Log; /** * Reseed Frontdesk demo tenants after a fresh SSO login. + * + * 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. */ class DemoLoginReseeder { @@ -19,6 +23,10 @@ class DemoLoginReseeder return; } + if (\App\Support\DemoWorld::isStaffDemoEmail($user->email)) { + return; + } + $plan = $this->planForEmail($user->email); if ($plan === null) { return; diff --git a/app/Services/Frontdesk/TeamMemberProvisioner.php b/app/Services/Frontdesk/TeamMemberProvisioner.php index ff6a664..11519e9 100644 --- a/app/Services/Frontdesk/TeamMemberProvisioner.php +++ b/app/Services/Frontdesk/TeamMemberProvisioner.php @@ -3,6 +3,7 @@ namespace App\Services\Frontdesk; use App\Models\Member; +use App\Models\Organization; use App\Models\User; use App\Services\Identity\IdentityTeamClient; @@ -35,7 +36,11 @@ class TeamMemberProvisioner } $meta = (array) data_get($grant, 'metadata.frontdesk', []); + $ownerRef = (string) ($grant['account'] ?? ''); $organizationId = (int) ($meta['organization_id'] ?? 0); + if ($organizationId <= 0 && $ownerRef !== '') { + $organizationId = (int) Organization::query()->where('owner_ref', $ownerRef)->value('id'); + } if ($organizationId <= 0) { continue; } @@ -46,7 +51,7 @@ class TeamMemberProvisioner 'user_ref' => $user->ownerRef(), ], [ - 'owner_ref' => (string) ($grant['account'] ?? $user->ownerRef()), + 'owner_ref' => $ownerRef !== '' ? $ownerRef : $user->ownerRef(), 'role' => (string) ($meta['role'] ?? 'member'), 'branch_id' => $meta['branch_id'] ?? null, ], diff --git a/app/Support/DemoWorld.php b/app/Support/DemoWorld.php new file mode 100644 index 0000000..c2bc1b6 --- /dev/null +++ b/app/Support/DemoWorld.php @@ -0,0 +1,553 @@ + + */ + public const BUSINESS = [ + 'free' => [ + 'name' => 'Ridge Family Clinic', + 'legal_name' => 'Ridge Family Clinic Ltd', + 'phone' => '+233302400100', + 'email' => 'hello@ridgeclinic.demo.ladill.com', + 'address' => '12 Castle Road, Ridge, Accra', + 'city' => 'Accra', + 'region' => 'Greater Accra', + 'timezone' => self::TIMEZONE, + ], + 'pro' => [ + 'name' => 'Ridge Medical Centre', + 'legal_name' => 'Ridge Medical Centre Ltd', + 'phone' => '+233302400200', + 'email' => 'hello@ridgemedical.demo.ladill.com', + 'address' => '45 Independence Ave, Ridge, Accra', + 'city' => 'Accra', + 'region' => 'Greater Accra', + 'timezone' => self::TIMEZONE, + ], + 'enterprise' => [ + 'name' => 'Accra Healthcare Group', + 'legal_name' => 'Accra Healthcare Group Ltd', + 'phone' => '+233302400300', + 'email' => 'hello@accrahealth.demo.ladill.com', + 'address' => '1 Liberation Road, Airport Residential, Accra', + 'city' => 'Accra', + 'region' => 'Greater Accra', + 'timezone' => self::TIMEZONE, + ], + ]; + + /** + * Shared branch catalog. Plans take the first N (free=1, pro=3, enterprise=6). + * + * @var list + */ + public const BRANCHES = [ + ['code' => 'HQ', 'name' => 'Ridge Clinic', 'phone' => '+233302400101', 'address' => '12 Castle Road, Ridge, Accra'], + ['code' => 'EL', 'name' => 'East Legon Care', 'phone' => '+233302400102', 'address' => '18 Boundary Rd, East Legon, Accra'], + ['code' => 'TM', 'name' => 'Tema Harbour Clinic', 'phone' => '+233302400103', 'address' => 'Harbour Road, Community 1, Tema'], + ['code' => 'OS', 'name' => 'Osu Medical Centre', 'phone' => '+233302400104', 'address' => 'Oxford Street, Osu, Accra'], + ['code' => 'SX', 'name' => 'Spintex Wellness', 'phone' => '+233302400105', 'address' => 'Spintex Road, Accra'], + ['code' => 'AP', 'name' => 'Airport Residential Care', 'phone' => '+233302400106', 'address' => 'Liberation Rd, Airport Residential, Accra'], + ]; + + /** + * Canonical people reused as patients / visitors / ticket guests / CRM + * contacts / invoice clients / POS customers / event attendees. + * + * @var list + */ + public const PEOPLE = [ + ['key' => 'ama-mensah', 'first_name' => 'Ama', 'last_name' => 'Mensah', 'phone' => '+233241000001', 'email' => 'ama.mensah@demo.ladill.com', 'national_id' => 'GHA-729001001', 'gender' => 'female', 'dob' => '1990-03-12', 'company_key' => 'accra-retail', 'city' => 'Accra', 'address' => '14 Ring Road Central, Accra'], + ['key' => 'kofi-asante', 'first_name' => 'Kofi', 'last_name' => 'Asante', 'phone' => '+233241000002', 'email' => 'kofi.asante@demo.ladill.com', 'national_id' => 'GHA-729001002', 'gender' => 'male', 'dob' => '1985-07-21', 'company_key' => 'kumasi-foods', 'city' => 'Accra', 'address' => '3 Labone Crescent, Accra'], + ['key' => 'abena-osei', 'first_name' => 'Abena', 'last_name' => 'Osei', 'phone' => '+233241000003', 'email' => 'abena.osei@demo.ladill.com', 'national_id' => 'GHA-729001003', 'gender' => 'female', 'dob' => '1992-11-05', 'company_key' => 'tema-logistics', 'city' => 'Tema', 'address' => 'Community 4, Tema'], + ['key' => 'kwame-boateng', 'first_name' => 'Kwame', 'last_name' => 'Boateng', 'phone' => '+233241000004', 'email' => 'kwame.boateng@demo.ladill.com', 'national_id' => 'GHA-729001004', 'gender' => 'male', 'dob' => '1978-01-30', 'company_key' => 'cape-coast-tours', 'city' => 'Accra', 'address' => '8 Cantonments Road, Accra'], + ['key' => 'akosua-owusu', 'first_name' => 'Akosua', 'last_name' => 'Owusu', 'phone' => '+233241000005', 'email' => 'akosua.owusu@demo.ladill.com', 'national_id' => 'GHA-729001005', 'gender' => 'female', 'dob' => '1995-06-18', 'company_key' => 'tamale-agro', 'city' => 'Accra', 'address' => '22 Dansoman Estates, Accra'], + ['key' => 'yaw-adjei', 'first_name' => 'Yaw', 'last_name' => 'Adjei', 'phone' => '+233241000006', 'email' => 'yaw.adjei@demo.ladill.com', 'national_id' => 'GHA-729001006', 'gender' => 'male', 'dob' => '1988-09-09', 'company_key' => 'spintex-clinics', 'city' => 'Accra', 'address' => 'Spintex Junction, Accra'], + ['key' => 'efua-appiah', 'first_name' => 'Efua', 'last_name' => 'Appiah', 'phone' => '+233241000007', 'email' => 'efua.appiah@demo.ladill.com', 'national_id' => 'GHA-729001007', 'gender' => 'female', 'dob' => '1993-02-14', 'company_key' => 'osu-cafe', 'city' => 'Accra', 'address' => 'Oxford Street, Osu, Accra'], + ['key' => 'kojo-darko', 'first_name' => 'Kojo', 'last_name' => 'Darko', 'phone' => '+233241000008', 'email' => 'kojo.darko@demo.ladill.com', 'national_id' => 'GHA-729001008', 'gender' => 'male', 'dob' => '1982-12-01', 'company_key' => 'legon-tech', 'city' => 'Accra', 'address' => 'University Area, Legon'], + ['key' => 'adwoa-agyeman', 'first_name' => 'Adwoa', 'last_name' => 'Agyeman', 'phone' => '+233241000009', 'email' => 'adwoa.agyeman@demo.ladill.com', 'national_id' => 'GHA-729001009', 'gender' => 'female', 'dob' => '1991-04-22', 'company_key' => 'airport-hotels', 'city' => 'Accra', 'address' => 'Airport Residential, Accra'], + ['key' => 'kwesi-sarpong', 'first_name' => 'Kwesi', 'last_name' => 'Sarpong', 'phone' => '+233241000010', 'email' => 'kwesi.sarpong@demo.ladill.com', 'national_id' => 'GHA-729001010', 'gender' => 'male', 'dob' => '1987-08-16', 'company_key' => 'kaneshie-traders', 'city' => 'Accra', 'address' => 'Kaneshie Market, Accra'], + ['key' => 'afia-nyarko', 'first_name' => 'Afia', 'last_name' => 'Nyarko', 'phone' => '+233241000011', 'email' => 'afia.nyarko@demo.ladill.com', 'national_id' => 'GHA-729001011', 'gender' => 'female', 'dob' => '1994-10-03', 'company_key' => 'accra-retail', 'city' => 'Accra', 'address' => 'Adabraka, Accra'], + ['key' => 'kwadwo-ankrah', 'first_name' => 'Kwadwo', 'last_name' => 'Ankrah', 'phone' => '+233241000012', 'email' => 'kwadwo.ankrah@demo.ladill.com', 'national_id' => 'GHA-729001012', 'gender' => 'male', 'dob' => '1980-05-27', 'company_key' => 'kumasi-foods', 'city' => 'Accra', 'address' => 'Mamprobi, Accra'], + ['key' => 'esi-tetteh', 'first_name' => 'Esi', 'last_name' => 'Tetteh', 'phone' => '+233241000013', 'email' => 'esi.tetteh@demo.ladill.com', 'national_id' => 'GHA-729001013', 'gender' => 'female', 'dob' => '1996-01-19', 'company_key' => 'tema-logistics', 'city' => 'Tema', 'address' => 'Community 8, Tema'], + ['key' => 'fiifi-quaye', 'first_name' => 'Fiifi', 'last_name' => 'Quaye', 'phone' => '+233241000014', 'email' => 'fiifi.quaye@demo.ladill.com', 'national_id' => 'GHA-729001014', 'gender' => 'male', 'dob' => '1989-07-07', 'company_key' => 'cape-coast-tours', 'city' => 'Accra', 'address' => 'Tesano, Accra'], + ['key' => 'mansa-addo', 'first_name' => 'Mansa', 'last_name' => 'Addo', 'phone' => '+233241000015', 'email' => 'mansa.addo@demo.ladill.com', 'national_id' => 'GHA-729001015', 'gender' => 'female', 'dob' => '1993-09-25', 'company_key' => 'tamale-agro', 'city' => 'Accra', 'address' => 'Abelemkpe, Accra'], + ['key' => 'nana-frimpong', 'first_name' => 'Nana', 'last_name' => 'Frimpong', 'phone' => '+233241000016', 'email' => 'nana.frimpong@demo.ladill.com', 'national_id' => 'GHA-729001016', 'gender' => 'male', 'dob' => '1984-03-11', 'company_key' => 'spintex-clinics', 'city' => 'Accra', 'address' => 'Teshie, Accra'], + ['key' => 'ama-boateng', 'first_name' => 'Ama', 'last_name' => 'Boateng', 'phone' => '+233241000017', 'email' => 'ama.boateng@demo.ladill.com', 'national_id' => 'GHA-729001017', 'gender' => 'female', 'dob' => '1997-12-08', 'company_key' => 'osu-cafe', 'city' => 'Accra', 'address' => 'Kokomlemle, Accra'], + ['key' => 'kofi-owusu', 'first_name' => 'Kofi', 'last_name' => 'Owusu', 'phone' => '+233241000018', 'email' => 'kofi.owusu@demo.ladill.com', 'national_id' => 'GHA-729001018', 'gender' => 'male', 'dob' => '1986-06-02', 'company_key' => 'legon-tech', 'city' => 'Accra', 'address' => 'Haatso, Accra'], + ['key' => 'abena-darko', 'first_name' => 'Abena', 'last_name' => 'Darko', 'phone' => '+233241000019', 'email' => 'abena.darko@demo.ladill.com', 'national_id' => 'GHA-729001019', 'gender' => 'female', 'dob' => '1990-08-29', 'company_key' => 'airport-hotels', 'city' => 'Accra', 'address' => 'Dzorwulu, Accra'], + ['key' => 'kwame-mensah', 'first_name' => 'Kwame', 'last_name' => 'Mensah', 'phone' => '+233241000020', 'email' => 'kwame.mensah@demo.ladill.com', 'national_id' => 'GHA-729001020', 'gender' => 'male', 'dob' => '1979-11-13', 'company_key' => 'kaneshie-traders', 'city' => 'Accra', 'address' => 'Ablekuma, Accra'], + ['key' => 'akosua-asante', 'first_name' => 'Akosua', 'last_name' => 'Asante', 'phone' => '+233241000021', 'email' => 'akosua.asante@demo.ladill.com', 'national_id' => 'GHA-729001021', 'gender' => 'female', 'dob' => '1992-02-20', 'company_key' => 'accra-retail', 'city' => 'Accra', 'address' => 'Roman Ridge, Accra'], + ['key' => 'yaw-osei', 'first_name' => 'Yaw', 'last_name' => 'Osei', 'phone' => '+233241000022', 'email' => 'yaw.osei@demo.ladill.com', 'national_id' => 'GHA-729001022', 'gender' => 'male', 'dob' => '1983-04-15', 'company_key' => 'kumasi-foods', 'city' => 'Accra', 'address' => 'Nungua, Accra'], + ['key' => 'efua-boateng', 'first_name' => 'Efua', 'last_name' => 'Boateng', 'phone' => '+233241000023', 'email' => 'efua.boateng@demo.ladill.com', 'national_id' => 'GHA-729001023', 'gender' => 'female', 'dob' => '1995-07-01', 'company_key' => 'tema-logistics', 'city' => 'Tema', 'address' => 'Community 2, Tema'], + ['key' => 'kojo-owusu', 'first_name' => 'Kojo', 'last_name' => 'Owusu', 'phone' => '+233241000024', 'email' => 'kojo.owusu@demo.ladill.com', 'national_id' => 'GHA-729001024', 'gender' => 'male', 'dob' => '1981-10-28', 'company_key' => 'cape-coast-tours', 'city' => 'Accra', 'address' => 'Madina, Accra'], + ['key' => 'adwoa-adjei', 'first_name' => 'Adwoa', 'last_name' => 'Adjei', 'phone' => '+233241000025', 'email' => 'adwoa.adjei@demo.ladill.com', 'national_id' => 'GHA-729001025', 'gender' => 'female', 'dob' => '1994-05-06', 'company_key' => 'tamale-agro', 'city' => 'Accra', 'address' => 'Ashaiman, Accra'], + ['key' => 'kwesi-appiah', 'first_name' => 'Kwesi', 'last_name' => 'Appiah', 'phone' => '+233241000026', 'email' => 'kwesi.appiah@demo.ladill.com', 'national_id' => 'GHA-729001026', 'gender' => 'male', 'dob' => '1988-12-17', 'company_key' => 'spintex-clinics', 'city' => 'Accra', 'address' => 'Baatsona, Accra'], + ['key' => 'afia-darko', 'first_name' => 'Afia', 'last_name' => 'Darko', 'phone' => '+233241000027', 'email' => 'afia.darko@demo.ladill.com', 'national_id' => 'GHA-729001027', 'gender' => 'female', 'dob' => '1991-09-23', 'company_key' => 'osu-cafe', 'city' => 'Accra', 'address' => 'Labadi, Accra'], + ['key' => 'kwadwo-agyeman', 'first_name' => 'Kwadwo', 'last_name' => 'Agyeman', 'phone' => '+233241000028', 'email' => 'kwadwo.agyeman@demo.ladill.com', 'national_id' => 'GHA-729001028', 'gender' => 'male', 'dob' => '1985-01-04', 'company_key' => 'legon-tech', 'city' => 'Accra', 'address' => 'Kwabenya, Accra'], + ['key' => 'esi-sarpong', 'first_name' => 'Esi', 'last_name' => 'Sarpong', 'phone' => '+233241000029', 'email' => 'esi.sarpong@demo.ladill.com', 'national_id' => 'GHA-729001029', 'gender' => 'female', 'dob' => '1996-03-30', 'company_key' => 'airport-hotels', 'city' => 'Accra', 'address' => 'East Legon Hills, Accra'], + ['key' => 'fiifi-nyarko', 'first_name' => 'Fiifi', 'last_name' => 'Nyarko', 'phone' => '+233241000030', 'email' => 'fiifi.nyarko@demo.ladill.com', 'national_id' => 'GHA-729001030', 'gender' => 'male', 'dob' => '1987-06-14', 'company_key' => 'kaneshie-traders', 'city' => 'Accra', 'address' => 'Odorkor, Accra'], + ['key' => 'mansa-ankrah', 'first_name' => 'Mansa', 'last_name' => 'Ankrah', 'phone' => '+233241000031', 'email' => 'mansa.ankrah@demo.ladill.com', 'national_id' => 'GHA-729001031', 'gender' => 'female', 'dob' => '1993-11-11', 'company_key' => 'accra-retail', 'city' => 'Accra', 'address' => 'Korle Bu, Accra'], + ['key' => 'nana-tetteh', 'first_name' => 'Nana', 'last_name' => 'Tetteh', 'phone' => '+233241000032', 'email' => 'nana.tetteh@demo.ladill.com', 'national_id' => 'GHA-729001032', 'gender' => 'male', 'dob' => '1982-08-08', 'company_key' => 'kumasi-foods', 'city' => 'Accra', 'address' => 'Weija, Accra'], + ['key' => 'ama-quaye', 'first_name' => 'Ama', 'last_name' => 'Quaye', 'phone' => '+233241000033', 'email' => 'ama.quaye@demo.ladill.com', 'national_id' => 'GHA-729001033', 'gender' => 'female', 'dob' => '1990-04-04', 'company_key' => 'tema-logistics', 'city' => 'Tema', 'address' => 'Community 11, Tema'], + ['key' => 'kofi-addo', 'first_name' => 'Kofi', 'last_name' => 'Addo', 'phone' => '+233241000034', 'email' => 'kofi.addo@demo.ladill.com', 'national_id' => 'GHA-729001034', 'gender' => 'male', 'dob' => '1989-02-26', 'company_key' => 'cape-coast-tours', 'city' => 'Accra', 'address' => 'Kasoa Road, Accra'], + ['key' => 'abena-frimpong', 'first_name' => 'Abena', 'last_name' => 'Frimpong', 'phone' => '+233241000035', 'email' => 'abena.frimpong@demo.ladill.com', 'national_id' => 'GHA-729001035', 'gender' => 'female', 'dob' => '1994-07-19', 'company_key' => 'tamale-agro', 'city' => 'Accra', 'address' => 'Achimota, Accra'], + ['key' => 'kwame-agyeman', 'first_name' => 'Kwame', 'last_name' => 'Agyeman', 'phone' => '+233241000036', 'email' => 'kwame.agyeman@demo.ladill.com', 'national_id' => 'GHA-729001036', 'gender' => 'male', 'dob' => '1980-09-01', 'company_key' => 'spintex-clinics', 'city' => 'Accra', 'address' => 'Sakumono, Accra'], + ]; + + /** + * @var list + */ + public const COMPANIES = [ + ['key' => 'accra-retail', 'name' => 'Accra Retail Ltd', 'email' => 'accounts@accra-retail.demo.ladill.com', 'phone' => '+233302500001', 'address' => 'Oxford Street, Accra'], + ['key' => 'kumasi-foods', 'name' => 'Kumasi Foods', 'email' => 'billing@kumasi-foods.demo.ladill.com', 'phone' => '+233302500002', 'address' => 'Kejetia Road, Kumasi'], + ['key' => 'tema-logistics', 'name' => 'Tema Logistics', 'email' => 'finance@tema-logistics.demo.ladill.com', 'phone' => '+233302500003', 'address' => 'Harbour Road, Tema'], + ['key' => 'cape-coast-tours', 'name' => 'Cape Coast Tours', 'email' => 'pay@cape-coast-tours.demo.ladill.com', 'phone' => '+233302500004', 'address' => 'Castle Road, Cape Coast'], + ['key' => 'tamale-agro', 'name' => 'Tamale Agro', 'email' => 'accounts@tamale-agro.demo.ladill.com', 'phone' => '+233302500005', 'address' => 'Salaga Road, Tamale'], + ['key' => 'spintex-clinics', 'name' => 'Spintex Clinics', 'email' => 'billing@spintex-clinics.demo.ladill.com', 'phone' => '+233302500006', 'address' => 'Spintex Road, Accra'], + ['key' => 'osu-cafe', 'name' => 'Osu Café Co', 'email' => 'hello@osu-cafe.demo.ladill.com', 'phone' => '+233302500007', 'address' => 'Oxford Street, Osu'], + ['key' => 'legon-tech', 'name' => 'Legon Tech', 'email' => 'ap@legon-tech.demo.ladill.com', 'phone' => '+233302500008', 'address' => 'University Area, Legon'], + ['key' => 'airport-hotels', 'name' => 'Airport City Hotels', 'email' => 'finance@airport-hotels.demo.ladill.com', 'phone' => '+233302500009', 'address' => 'Airport City, Accra'], + ['key' => 'kaneshie-traders', 'name' => 'Kaneshie Market Traders', 'email' => 'pay@kaneshie-traders.demo.ladill.com', 'phone' => '+233302500010', 'address' => 'Kaneshie Market, Accra'], + ]; + + /** + * Plausible shared story amounts (GHS major units) for Accounting / Invoice / POS labels. + * + * @var array{care_consult: int, care_lab: int, invoice_typical: int, pos_typical: int, crm_deal: int} + */ + public const STORY_AMOUNTS_GHS = [ + 'care_consult' => 250, + 'care_lab' => 180, + 'invoice_typical' => 1500, + 'pos_typical' => 85, + 'crm_deal' => 12000, + ]; + + /** + * Staff roster keyed by tier. Free: owner + receptionist. Pro/Enterprise: full roster. + * + * @var array, + * roles: array, + * account_role?: string + * }>> + */ + public const STAFF = [ + 'free' => [ + [ + 'key' => 'receptionist', + 'email' => 'demo-free-receptionist@ladill.com', + 'name' => 'Abena Reception (Free)', + 'apps' => ['care', 'frontdesk', 'queue'], + 'roles' => [ + 'care' => 'receptionist', + 'frontdesk' => 'receptionist', + 'queue' => 'receptionist', + ], + ], + ], + 'pro' => [ + [ + 'key' => 'doctor', + 'email' => 'demo-pro-doctor@ladill.com', + 'name' => 'Dr. Kwame Mensah (Pro)', + 'apps' => ['care'], + 'roles' => ['care' => 'doctor'], + ], + [ + 'key' => 'nurse', + 'email' => 'demo-pro-nurse@ladill.com', + 'name' => 'Akosua Nurse (Pro)', + 'apps' => ['care'], + 'roles' => ['care' => 'nurse'], + ], + [ + 'key' => 'receptionist', + 'email' => 'demo-pro-receptionist@ladill.com', + 'name' => 'Abena Reception (Pro)', + 'apps' => ['care', 'frontdesk', 'queue'], + 'roles' => [ + 'care' => 'receptionist', + 'frontdesk' => 'receptionist', + 'queue' => 'receptionist', + ], + ], + [ + 'key' => 'pharmacist', + 'email' => 'demo-pro-pharmacist@ladill.com', + 'name' => 'Yaw Pharmacist (Pro)', + 'apps' => ['care'], + 'roles' => ['care' => 'pharmacist'], + ], + [ + 'key' => 'lab', + 'email' => 'demo-pro-lab@ladill.com', + 'name' => 'Efua Lab Tech (Pro)', + 'apps' => ['care'], + 'roles' => ['care' => 'lab_technician'], + ], + [ + 'key' => 'cashier', + 'email' => 'demo-pro-cashier@ladill.com', + 'name' => 'Kojo Cashier (Pro)', + 'apps' => ['care', 'pos'], + 'roles' => [ + 'care' => 'cashier', + 'pos' => 'cashier', + ], + ], + [ + 'key' => 'accountant', + 'email' => 'demo-pro-accountant@ladill.com', + 'name' => 'Adwoa Accountant (Pro)', + 'apps' => ['accounting', 'invoice'], + 'roles' => [ + 'accounting' => 'member', + 'invoice' => 'member', + 'care' => 'accountant', + ], + ], + [ + 'key' => 'sales', + 'email' => 'demo-pro-sales@ladill.com', + 'name' => 'Kofi Sales (Pro)', + 'apps' => ['crm', 'invoice', 'meet'], + 'roles' => [ + 'crm' => 'member', + 'invoice' => 'member', + 'meet' => 'member', + ], + ], + ], + 'enterprise' => [ + [ + 'key' => 'doctor', + 'email' => 'demo-enterprise-doctor@ladill.com', + 'name' => 'Dr. Kwame Mensah', + 'apps' => ['care'], + 'roles' => ['care' => 'doctor'], + ], + [ + 'key' => 'nurse', + 'email' => 'demo-enterprise-nurse@ladill.com', + 'name' => 'Akosua Nurse', + 'apps' => ['care'], + 'roles' => ['care' => 'nurse'], + ], + [ + 'key' => 'receptionist', + 'email' => 'demo-enterprise-receptionist@ladill.com', + 'name' => 'Abena Reception', + 'apps' => ['care', 'frontdesk', 'queue'], + 'roles' => [ + 'care' => 'receptionist', + 'frontdesk' => 'receptionist', + 'queue' => 'receptionist', + ], + ], + [ + 'key' => 'pharmacist', + 'email' => 'demo-enterprise-pharmacist@ladill.com', + 'name' => 'Yaw Pharmacist', + 'apps' => ['care'], + 'roles' => ['care' => 'pharmacist'], + ], + [ + 'key' => 'lab', + 'email' => 'demo-enterprise-lab@ladill.com', + 'name' => 'Efua Lab Tech', + 'apps' => ['care'], + 'roles' => ['care' => 'lab_technician'], + ], + [ + 'key' => 'cashier', + 'email' => 'demo-enterprise-cashier@ladill.com', + 'name' => 'Kojo Cashier', + 'apps' => ['care', 'pos'], + 'roles' => [ + 'care' => 'cashier', + 'pos' => 'cashier', + ], + ], + [ + 'key' => 'accountant', + 'email' => 'demo-enterprise-accountant@ladill.com', + 'name' => 'Adwoa Accountant', + 'apps' => ['accounting', 'invoice', 'care'], + 'roles' => [ + 'accounting' => 'member', + 'invoice' => 'member', + 'care' => 'accountant', + ], + ], + [ + 'key' => 'sales', + 'email' => 'demo-enterprise-sales@ladill.com', + 'name' => 'Kofi Sales', + 'apps' => ['crm', 'invoice', 'meet', 'events'], + 'roles' => [ + 'crm' => 'member', + 'invoice' => 'member', + 'meet' => 'member', + 'events' => 'member', + ], + ], + ], + ]; + + /** @var array owner email → tier */ + public const OWNER_EMAILS = [ + 'demo-free@ladill.com' => 'free', + 'demo-pro@ladill.com' => 'pro', + 'demo-enterprise@ladill.com' => 'enterprise', + ]; + + public static function normalizeTier(string $tier): string + { + $tier = strtolower(trim($tier)); + + return in_array($tier, ['free', 'pro', 'enterprise'], true) ? $tier : 'free'; + } + + public static function ownerEmailForTier(string $tier): string + { + $tier = self::normalizeTier($tier); + + return match ($tier) { + 'pro' => 'demo-pro@ladill.com', + 'enterprise' => 'demo-enterprise@ladill.com', + default => 'demo-free@ladill.com', + }; + } + + public static function tierForOwnerEmail(?string $email): ?string + { + $email = strtolower(trim((string) $email)); + + return self::OWNER_EMAILS[$email] ?? null; + } + + /** + * @return array{name: string, legal_name: string, phone: string, email: string, address: string, city: string, region: string, timezone: string} + */ + public static function business(string $tier): array + { + return self::BUSINESS[self::normalizeTier($tier)]; + } + + /** + * @return list + */ + public static function branches(string $tier, ?int $limit = null): array + { + $tier = self::normalizeTier($tier); + $limit ??= match ($tier) { + 'enterprise' => 6, + 'pro' => 3, + default => 1, + }; + + return array_slice(self::BRANCHES, 0, max(1, $limit)); + } + + /** + * @return list + */ + public static function people(?int $limit = null): array + { + if ($limit === null) { + return self::PEOPLE; + } + + return array_slice(self::PEOPLE, 0, max(0, $limit)); + } + + /** + * @return array{key: string, first_name: string, last_name: string, phone: string, email: string, national_id: string, gender: string, dob: string, company_key: string, city: string, address: string}|null + */ + public static function personByKey(string $key): ?array + { + foreach (self::PEOPLE as $person) { + if ($person['key'] === $key) { + return $person; + } + } + + return null; + } + + public static function fullName(array $person): string + { + return trim($person['first_name'].' '.$person['last_name']); + } + + /** + * @return list + */ + public static function companies(?int $limit = null): array + { + if ($limit === null) { + return self::COMPANIES; + } + + return array_slice(self::COMPANIES, 0, max(0, $limit)); + } + + /** + * @return array{key: string, name: string, email: string, phone: string, address: string}|null + */ + public static function companyByKey(string $key): ?array + { + foreach (self::COMPANIES as $company) { + if ($company['key'] === $key) { + return $company; + } + } + + return null; + } + + public static function companyNameForPerson(array $person): string + { + return self::companyByKey($person['company_key'])['name'] ?? 'Demo Co'; + } + + /** + * @return list, roles: array, account_role?: string}> + */ + public static function staff(string $tier): array + { + return self::STAFF[self::normalizeTier($tier)] ?? []; + } + + /** + * @return array{key: string, email: string, name: string, apps: list, roles: array, account_role?: string}|null + */ + public static function staffByEmail(?string $email): ?array + { + $email = strtolower(trim((string) $email)); + if ($email === '') { + return null; + } + + foreach (self::STAFF as $roster) { + foreach ($roster as $member) { + if (strtolower($member['email']) === $email) { + return $member; + } + } + } + + return null; + } + + public static function isOwnerDemoEmail(?string $email): bool + { + return self::tierForOwnerEmail($email) !== null; + } + + public static function isStaffDemoEmail(?string $email): bool + { + return self::staffByEmail($email) !== null; + } + + /** Owner email for a staff member, or null if not staff. */ + public static function ownerEmailForStaff(?string $email): ?string + { + $email = strtolower(trim((string) $email)); + foreach (self::STAFF as $tier => $roster) { + foreach ($roster as $member) { + if (strtolower($member['email']) === $email) { + return self::ownerEmailForTier($tier); + } + } + } + + return null; + } + + /** + * Build Identity AccountTeamMember metadata from a staff roles map. + * + * @param array $roles + * @return array + */ + public static function metadataFromRoles(array $roles): array + { + $metadata = []; + foreach ($roles as $app => $role) { + $metadata[$app] = ['role' => $role]; + } + + return $metadata; + } + + public static function storyDay(): \Carbon\CarbonInterface + { + return now(self::TIMEZONE)->startOfDay()->addDays(self::STORY_DAY_OFFSET); + } + + /** Local phone form used by some apps (024####### without +233). */ + public static function phoneLocal(string $e164): string + { + if (str_starts_with($e164, '+233')) { + return '0'.substr($e164, 4); + } + + return $e164; + } +} diff --git a/tests/Feature/DemoSeedCommandTest.php b/tests/Feature/DemoSeedCommandTest.php index adff11d..f3a5ad3 100644 --- a/tests/Feature/DemoSeedCommandTest.php +++ b/tests/Feature/DemoSeedCommandTest.php @@ -5,12 +5,14 @@ namespace Tests\Feature; use App\Models\Branch; use App\Models\Device; use App\Models\Host; +use App\Models\Member; use App\Models\Organization; use App\Models\User; use App\Models\Visit; use App\Models\Visitor; use App\Models\WatchlistEntry; use App\Services\Frontdesk\PlanService; +use App\Support\DemoWorld; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Str; use Tests\TestCase; @@ -103,4 +105,39 @@ class DemoSeedCommandTest extends TestCase $this->assertSame(1, Branch::query()->where('organization_id', $fresh->id)->count()); $this->assertSame(40, Visit::query()->where('organization_id', $fresh->id)->count()); } + + public function test_demo_world_continuity_and_staff_roles(): void + { + $user = User::create([ + 'public_id' => 'demo-world-frontdesk-id', + 'name' => 'Ladill Demo (Pro)', + 'email' => 'demo-pro@ladill.com', + ]); + + $this->artisan('demo:seed', [ + 'identity' => $user->email, + '--plan' => 'pro', + ])->assertSuccessful(); + + $org = Organization::query()->where('owner_ref', $user->public_id)->firstOrFail(); + $this->assertSame(DemoWorld::business('pro')['name'], $org->name); + + $ama = DemoWorld::personByKey('ama-mensah'); + $this->assertNotNull($ama); + $visitor = Visitor::query() + ->where('organization_id', $org->id) + ->where('phone', $ama['phone']) + ->first(); + $this->assertNotNull($visitor); + $this->assertSame($ama['email'], $visitor->email); + $this->assertSame(DemoWorld::companyNameForPerson($ama), $visitor->company); + + $this->assertTrue( + Member::query() + ->where('organization_id', $org->id) + ->where('user_ref', 'demo-pro-receptionist@ladill.com') + ->where('role', 'receptionist') + ->exists() + ); + } }