From 88f84a4f969fff6240c2c755d764370a9eda79cd Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sat, 18 Jul 2026 05:34:45 +0000 Subject: [PATCH] Assign every demo waiter to a doctor and sync Queue tickets. Link DemoWorld doctor staff to practitioners, clear unresolved routing, distribute staff across branches, and restore scoped consultation/pharmacy ticket sync so demos are not left Unassigned. Co-authored-by: Cursor --- app/Services/Care/DemoTenantSeeder.php | 137 +++++++++++++++++++++++-- tests/Feature/DemoSeedCommandTest.php | 10 ++ 2 files changed, 141 insertions(+), 6 deletions(-) diff --git a/app/Services/Care/DemoTenantSeeder.php b/app/Services/Care/DemoTenantSeeder.php index 36ef2de..0816975 100644 --- a/app/Services/Care/DemoTenantSeeder.php +++ b/app/Services/Care/DemoTenantSeeder.php @@ -114,6 +114,7 @@ class DemoTenantSeeder $departments = $this->departmentsByBranch($branches, $ownerRef); } $practitioners = $this->seedPractitioners($organization, $branches, $departments, $ownerRef, $volumes); + $this->linkStaffDoctorsToPractitioners($organization, $ownerRef, $plan, $branches, $practitioners); $patients = $this->seedPatients($organization, $branches, $ownerRef, $volumes); $appointmentCount = $this->seedAppointmentsAndClinical( $organization, @@ -124,6 +125,7 @@ class DemoTenantSeeder $ownerRef, $volumes, ); + $this->assignAllDemoWaiters($organization, $ownerRef, $branches, $practitioners); if (in_array($plan, ['pro', 'enterprise'], true)) { $appointmentCount += $this->seedSpecialtyDemoData( $organization->fresh(), @@ -173,10 +175,126 @@ class DemoTenantSeeder */ private function syncQueueTicketsForDemo(Organization $organization, array $branches): void { - // Intentionally no-op during demo seed. Queue HTTP (branch × context) can - // take many minutes on Enterprise volumes and hung production reseeds. - // Specialty/care waiting lists still come from local appointments; Queue - // tickets backfill lazily on first Ops / specialty page visit. + if (! data_get($organization->settings, 'queue_integration_enabled')) { + return; + } + + try { + $bridge = app(CareQueueBridge::class); + if (! $bridge->isEnabled($organization)) { + return; + } + + // Scoped per branch/context (not full-org) so demos finish quickly while + // still leaving waiters with real tickets instead of "Unassigned". + $contexts = [ + CareQueueContexts::CONSULTATION, + CareQueueContexts::PHARMACY, + CareQueueContexts::LABORATORY, + CareQueueContexts::BILLING, + ]; + + foreach ($branches as $branch) { + foreach ($contexts as $context) { + try { + $bridge->syncMissingTickets($organization, $context, (int) $branch->id, 50); + } catch (\Throwable) { + // Continue — page load will retry for that context. + } + } + } + } catch (\Throwable) { + // Demo seed should not fail if Queue API is unreachable. + } + } + + /** + * Point DemoWorld doctor staff at a real practitioner so Call next / rooms work. + * + * @param list $branches + * @param list $practitioners + */ + private function linkStaffDoctorsToPractitioners( + Organization $organization, + string $ownerRef, + string $plan, + array $branches, + array $practitioners, + ): void { + $doctorEmail = null; + $doctorName = null; + foreach (DemoWorld::staff($plan) as $staff) { + if (($staff['roles']['care'] ?? null) === 'doctor') { + $doctorEmail = strtolower((string) $staff['email']); + $doctorName = (string) $staff['name']; + break; + } + } + if ($doctorEmail === null || $practitioners === []) { + return; + } + + $member = Member::query() + ->where('organization_id', $organization->id) + ->where('user_ref', $doctorEmail) + ->first(); + + $byBranch = []; + foreach ($practitioners as $practitioner) { + $byBranch[(int) $practitioner->branch_id] ??= $practitioner; + } + + foreach ($branches as $branch) { + $practitioner = $byBranch[(int) $branch->id] ?? null; + if (! $practitioner) { + continue; + } + $practitioner->forceFill([ + 'user_ref' => $doctorEmail, + 'member_id' => $member?->id, + 'name' => $doctorName !== '' ? $doctorName : $practitioner->name, + ])->save(); + } + } + + /** + * Every waiting / checked-in demo appointment gets a doctor and a clean routing + * state so Queue sync can issue tickets (no amber Unassigned leftovers). + * + * @param list $branches + * @param list $practitioners + */ + private function assignAllDemoWaiters( + Organization $organization, + string $ownerRef, + array $branches, + array $practitioners, + ): void { + if ($practitioners === []) { + return; + } + + $byBranch = []; + foreach ($practitioners as $practitioner) { + $byBranch[(int) $practitioner->branch_id][] = $practitioner; + } + + $waiters = Appointment::withTrashed() + ->owned($ownerRef) + ->where('organization_id', $organization->id) + ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) + ->get(); + + foreach ($waiters as $index => $appointment) { + $branchPracs = $byBranch[(int) $appointment->branch_id] ?? $practitioners; + $practitioner = $branchPracs[$index % max(1, count($branchPracs))] ?? $practitioners[0]; + + $appointment->forceFill([ + 'practitioner_id' => $practitioner->id, + 'queue_routing_status' => null, + 'deleted_at' => null, + ])->save(); + } } /** @@ -473,18 +591,25 @@ class DemoTenantSeeder /** * Local Member rows for DemoWorld staff (user_ref = email until SSO remaps to public_id). + * Multi-branch demos spread staff across branches so each site has a role roster. * * @param list $branches */ private function seedStaffMembers(Organization $organization, string $ownerRef, string $plan, array $branches): void { $hq = $branches[0] ?? null; - foreach (DemoWorld::staff($plan) as $staff) { + $staffList = DemoWorld::staff($plan); + foreach ($staffList as $index => $staff) { $role = $staff['roles']['care'] ?? null; if (! is_string($role) || $role === '') { continue; } + $branch = $hq; + if ($role !== 'hospital_admin' && $branches !== []) { + $branch = $branches[$index % count($branches)]; + } + Member::query()->updateOrCreate( [ 'organization_id' => $organization->id, @@ -493,7 +618,7 @@ class DemoTenantSeeder [ 'owner_ref' => $ownerRef, 'role' => $role, - 'branch_id' => in_array($role, ['hospital_admin'], true) ? null : $hq?->id, + 'branch_id' => $role === 'hospital_admin' ? null : $branch?->id, ], ); } diff --git a/tests/Feature/DemoSeedCommandTest.php b/tests/Feature/DemoSeedCommandTest.php index b48f99f..ade561d 100644 --- a/tests/Feature/DemoSeedCommandTest.php +++ b/tests/Feature/DemoSeedCommandTest.php @@ -342,5 +342,15 @@ class DemoSeedCommandTest extends TestCase ->where('role', 'receptionist') ->exists() ); + + $waiters = Appointment::query() + ->where('owner_ref', $user->public_id) + ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) + ->get(); + $this->assertNotEmpty($waiters); + foreach ($waiters as $waiter) { + $this->assertNotNull($waiter->practitioner_id, 'Demo waiters must all have a doctor assigned'); + $this->assertNotSame('unresolved', $waiter->queue_routing_status); + } } }