*/ private const FIRST_NAMES = [ 'Ama', 'Kofi', 'Abena', 'Kwame', 'Akosua', 'Yaw', 'Efua', 'Kojo', 'Adwoa', 'Kwesi', 'Afia', 'Kwadwo', 'Esi', 'Fiifi', 'Mansa', 'Nana', ]; /** @var list */ private const LAST_NAMES = [ 'Mensah', 'Asante', 'Osei', 'Boateng', 'Owusu', 'Adjei', 'Appiah', 'Darko', 'Agyeman', 'Sarpong', 'Nyarko', 'Ankrah', 'Tetteh', 'Quaye', 'Addo', 'Frimpong', ]; /** @var list */ private const BRANCH_NAMES = [ 'Ridge Clinic', 'Osu Medical Centre', 'East Legon Care', 'Tema Harbour Clinic', 'Kaneshie Health Hub', 'Spintex Wellness', 'Madina Family Clinic', 'Airport Residential Care', ]; /** @var list */ private const CORE_DEPARTMENTS = [ ['name' => 'Outpatient', 'type' => 'outpatient'], ['name' => 'General Practice', 'type' => 'general'], ['name' => 'Nursing', 'type' => 'nursing'], ['name' => 'Specialist Clinic', 'type' => 'specialist'], ]; /** @var list */ private const PAID_DEPARTMENTS = [ ['name' => 'Laboratory', 'type' => 'laboratory'], ['name' => 'Pharmacy', 'type' => 'pharmacy'], ]; /** @var list */ private const SPECIALTIES = [ 'General Practice', 'Internal Medicine', 'Paediatrics', 'Obstetrics', 'Emergency', 'Nursing', 'Laboratory', 'Pharmacy', ]; /** * @return array{ * organization: Organization, * branches: int, * patients: int, * appointments: int, * plan: string * } */ public function seed(User $user, string $plan, bool $reset = false): array { $plan = $this->normalizePlan($plan); $ownerRef = $user->ownerRef(); $volumes = $this->volumes($plan); // 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->resetTenant($ownerRef); $this->ensureOnboardedShell($user, $plan); } // No outer transaction — Enterprise volumes can hold row locks for minutes // and 500 concurrent settings/login requests (messaging credentials, org updates). $organization = $this->upsertOrganization($user, $ownerRef, $plan, $volumes['branches']); $this->upsertOwnerMember($organization, $ownerRef); $branches = $this->seedBranches($organization, $ownerRef, $volumes); // Lab catalog early — survives if later clinical seeding fails mid-run. $investigationTypes = $volumes['paid_modules'] ? $this->seedInvestigationCatalog($organization, $ownerRef) : []; $this->seedStaffMembers($organization, $ownerRef, $plan, $branches); $departments = $this->seedDepartments($branches, $ownerRef, $volumes); if (in_array($plan, ['pro', 'enterprise'], true)) { $this->seedSpecialtyModules($organization, $ownerRef); // Keep $departments as core/paid only. Specialty departments must not feed // general GP desks or appointments — those waiters live in seedSpecialtyDemoData. } $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, $branches, $departments, $practitioners, $patients, $ownerRef, $volumes, ); if (in_array($plan, ['pro', 'enterprise'], true)) { $appointmentCount += $this->seedSpecialtyDemoData( $organization->fresh(), $branches, $practitioners, $patients, $ownerRef, $plan, ); } $this->assignAllDemoWaiters($organization, $ownerRef, $branches, $practitioners); $this->renumberWaitingQueuePositions($organization, $ownerRef, $branches); // Catalog first so a later lab-request failure never leaves Lab → Catalog empty. if ($volumes['paid_modules']) { if ($investigationTypes === []) { $investigationTypes = $this->seedInvestigationCatalog($organization, $ownerRef); } $this->seedPaidModules( $organization, $branches, $practitioners, $patients, $ownerRef, $volumes, $investigationTypes, ); $this->seedDevices($organization, $branches, $ownerRef); } if ($volumes['assessments'] > 0) { $this->seedAssessments( $organization, $branches, $practitioners, $patients, $ownerRef, $volumes['assessments'], ); } $this->syncQueueTicketsForDemo($organization->fresh(), $branches); return [ 'organization' => $organization->fresh(), 'branches' => count($branches), 'patients' => count($patients), 'appointments' => $appointmentCount, 'plan' => $plan, ]; } /** * @param list $branches */ private function syncQueueTicketsForDemo(Organization $organization, array $branches): void { if (! data_get($organization->settings, 'queue_integration_enabled')) { return; } try { $bridge = app(CareQueueBridge::class); if (! $bridge->isEnabled($organization)) { return; } // Scoped per branch (not full-org) so demos finish quickly while // still leaving waiters with real tickets instead of local positions only. foreach ($branches as $branch) { try { $bridge->syncMissingAppointmentTickets($organization, (int) $branch->id, 100); } catch (\Throwable) { // Continue — page load will retry. } foreach ([ CareQueueContexts::PHARMACY, CareQueueContexts::LABORATORY, CareQueueContexts::BILLING, ] 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 = $this->findStaffMemberByEmail($organization, $doctorEmail); // Link the demo doctor to a single desk on their home branch (not every site). $homeBranchId = (int) ($member?->branch_id ?: ($branches[0]->id ?? 0)); $practitioner = collect($practitioners)->first( fn (Practitioner $p) => (int) $p->branch_id === $homeBranchId, ) ?? ($practitioners[0] ?? null); if (! $practitioner) { return; } $practitioner->forceFill([ 'user_ref' => $member?->user_ref ?: $doctorEmail, 'member_id' => $member?->id, 'name' => $doctorName !== '' ? $doctorName : $practitioner->name, ])->save(); if ($homeBranchId > 0) { $practitioner->syncAssignedBranches([$homeBranchId]); } } /** * 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]) ->where(function ($q) { $q->whereNull('practitioner_id')->orWhere('practitioner_id', 0); }) ->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(); } } /** * Give each waiting/checked-in appointment a unique 1..n position per branch. * * @param list $branches */ private function renumberWaitingQueuePositions( Organization $organization, string $ownerRef, array $branches, ): void { foreach ($branches as $branch) { $waiters = Appointment::withTrashed() ->owned($ownerRef) ->where('organization_id', $organization->id) ->where('branch_id', $branch->id) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->orderByRaw('COALESCE(waiting_at, checked_in_at, scheduled_at) asc') ->orderBy('id') ->get(); foreach ($waiters as $index => $appointment) { $position = $index + 1; if ((int) $appointment->queue_position === $position) { continue; } $appointment->forceFill([ 'queue_position' => $position, 'deleted_at' => null, ])->save(); } } } /** * 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); $ownerRef = $user->ownerRef(); $volumes = $this->volumes($plan); return DB::transaction(function () use ($user, $ownerRef, $plan, $volumes) { $organization = $this->upsertOrganization($user, $ownerRef, $plan, $volumes['branches']); $this->upsertOwnerMember($organization, $ownerRef); return $organization; }); } public function resetTenant(string $ownerRef): void { $orgIds = DB::table('care_organizations')->where('owner_ref', $ownerRef)->pluck('id'); if (Schema::hasTable('care_financial_obligations')) { DB::table('care_financial_obligations')->where('owner_ref', $ownerRef)->delete(); DB::table('care_visit_stage_advances')->where('owner_ref', $ownerRef)->delete(); $workflowIds = DB::table('care_facility_workflows')->where('owner_ref', $ownerRef)->pluck('id'); if ($workflowIds->isNotEmpty()) { DB::table('care_workflow_stages')->whereIn('workflow_id', $workflowIds)->delete(); DB::table('care_facility_workflows')->whereIn('id', $workflowIds)->delete(); } } DB::table('care_assessment_answers')->where('owner_ref', $ownerRef)->delete(); DB::table('care_assessment_scores')->where('owner_ref', $ownerRef)->delete(); DB::table('care_assessments')->where('owner_ref', $ownerRef)->update(['patient_pathway_id' => null]); DB::table('care_patient_pathways')->where('owner_ref', $ownerRef)->delete(); DB::table('care_assessments')->where('owner_ref', $ownerRef)->delete(); if ($orgIds->isNotEmpty()) { $templateIds = DB::table('care_assessment_templates') ->whereIn('organization_id', $orgIds) ->pluck('id'); if ($templateIds->isNotEmpty()) { DB::table('care_assessment_questions')->whereIn('template_id', $templateIds)->delete(); DB::table('care_assessment_templates')->whereIn('id', $templateIds)->delete(); } } // Orphaned demo templates left behind by nullOnDelete on prior resets. $orphanIds = DB::table('care_assessment_templates') ->where('code', self::DEMO_TEMPLATE_CODE) ->whereNull('organization_id') ->pluck('id'); if ($orphanIds->isNotEmpty()) { DB::table('care_assessment_questions')->whereIn('template_id', $orphanIds)->delete(); DB::table('care_assessment_templates')->whereIn('id', $orphanIds)->delete(); } DB::table('care_dispensing_records')->where('owner_ref', $ownerRef)->delete(); DB::table('care_payments')->where('owner_ref', $ownerRef)->delete(); DB::table('care_bill_line_items')->where('owner_ref', $ownerRef)->delete(); DB::table('care_bills')->where('owner_ref', $ownerRef)->delete(); DB::table('care_investigation_result_values')->where('owner_ref', $ownerRef)->delete(); DB::table('care_investigation_attachments')->where('owner_ref', $ownerRef)->delete(); DB::table('care_investigation_results')->where('owner_ref', $ownerRef)->delete(); DB::table('care_investigation_requests')->where('owner_ref', $ownerRef)->delete(); DB::table('care_prescription_items')->where('owner_ref', $ownerRef)->delete(); DB::table('care_prescriptions')->where('owner_ref', $ownerRef)->delete(); DB::table('care_drug_batches')->where('owner_ref', $ownerRef)->delete(); DB::table('care_drugs')->where('owner_ref', $ownerRef)->delete(); DB::table('care_investigation_types')->where('owner_ref', $ownerRef)->delete(); DB::table('care_consultation_documents')->where('owner_ref', $ownerRef)->delete(); DB::table('care_diagnoses')->where('owner_ref', $ownerRef)->delete(); DB::table('care_vital_signs')->where('owner_ref', $ownerRef)->delete(); DB::table('care_consultations')->where('owner_ref', $ownerRef)->delete(); DB::table('care_devices')->where('owner_ref', $ownerRef)->delete(); DB::table('care_appointments')->where('owner_ref', $ownerRef)->delete(); DB::table('care_visits')->where('owner_ref', $ownerRef)->delete(); DB::table('care_patient_allergies')->where('owner_ref', $ownerRef)->delete(); DB::table('care_patient_conditions')->where('owner_ref', $ownerRef)->delete(); DB::table('care_patient_family_history')->where('owner_ref', $ownerRef)->delete(); DB::table('care_emergency_contacts')->where('owner_ref', $ownerRef)->delete(); DB::table('care_insurance_policies')->where('owner_ref', $ownerRef)->delete(); DB::table('care_patient_attachments')->where('owner_ref', $ownerRef)->delete(); DB::table('care_patients')->where('owner_ref', $ownerRef)->delete(); DB::table('care_practitioner_schedules')->where('owner_ref', $ownerRef)->delete(); if (Schema::hasTable('care_practitioner_branch')) { $practitionerIds = DB::table('care_practitioners')->where('owner_ref', $ownerRef)->pluck('id'); if ($practitionerIds->isNotEmpty()) { DB::table('care_practitioner_branch')->whereIn('practitioner_id', $practitionerIds)->delete(); } } DB::table('care_practitioners')->where('owner_ref', $ownerRef)->delete(); // Keep the owner member + organization so SSO redirects never see // "not onboarded" while afterResponse() reseeding is still running. DB::table('care_members') ->where('owner_ref', $ownerRef) ->where('user_ref', '!=', $ownerRef) ->delete(); DB::table('care_departments')->where('owner_ref', $ownerRef)->delete(); DB::table('care_branches')->where('owner_ref', $ownerRef)->delete(); DB::table('care_messaging_credentials')->where('owner_ref', $ownerRef)->delete(); DB::table('care_payment_gateway_settings')->where('owner_ref', $ownerRef)->delete(); DB::table('care_audit_logs')->where('owner_ref', $ownerRef)->delete(); $organization = Organization::withTrashed()->where('owner_ref', $ownerRef)->first(); if ($organization) { if ($organization->trashed()) { $organization->restore(); } $settings = $organization->settings ?? []; $settings['onboarded'] = true; $organization->forceFill(['settings' => $settings])->save(); } } /** * @return array{ * branches: int, * departments_per_branch: int, * practitioners: int, * patients: int, * appointments: int, * paid_modules: bool, * assessments: int, * lab_requests: int, * prescriptions: int, * bills: int * } */ public function volumes(string $plan): array { $testing = app()->runningUnitTests(); return match ($this->normalizePlan($plan)) { 'free' => [ 'branches' => 1, 'departments_per_branch' => 2, 'practitioners' => 3, 'patients' => $testing ? 12 : 30, 'appointments' => $testing ? 20 : 50, 'paid_modules' => false, 'assessments' => 0, 'lab_requests' => 0, 'prescriptions' => 0, 'bills' => 0, ], 'pro' => [ 'branches' => 3, 'departments_per_branch' => 3, 'practitioners' => 8, 'patients' => $testing ? 24 : 200, 'appointments' => $testing ? 40 : 300, 'paid_modules' => true, 'assessments' => 0, 'lab_requests' => $testing ? 8 : 40, 'prescriptions' => $testing ? 8 : 40, 'bills' => $testing ? 10 : 60, ], 'enterprise' => [ 'branches' => 6, 'departments_per_branch' => 4, 'practitioners' => 16, 'patients' => $testing ? 36 : 500, 'appointments' => $testing ? 60 : 800, 'paid_modules' => true, 'assessments' => $testing ? 12 : 80, 'lab_requests' => $testing ? 12 : 80, 'prescriptions' => $testing ? 12 : 80, 'bills' => $testing ? 15 : 120, ], default => throw new \InvalidArgumentException("Unsupported plan [{$plan}]"), }; } private function normalizePlan(string $plan): string { $plan = strtolower(trim($plan)); return in_array($plan, ['free', 'pro', 'enterprise'], true) ? $plan : 'free'; } private function upsertOrganization(User $user, string $ownerRef, string $plan, int $billedBranches): Organization { $slug = 'demo-'.Str::slug(Str::limit($user->email ?: $ownerRef, 40, '')); if ($slug === 'demo-' || $slug === 'demo') { $slug = 'demo-'.Str::lower(Str::substr($ownerRef, 0, 8)); } $name = DemoWorld::business($plan)['name']; $organization = Organization::withTrashed() ->where('owner_ref', $ownerRef) ->first(); $facilityType = $plan === 'enterprise' ? 'hospital' : 'clinic'; $settings = array_merge($organization?->settings ?? [], [ 'onboarded' => true, 'facility_type' => $facilityType, 'facility_category' => $facilityType, 'plan' => $plan, 'plan_expires_at' => now()->addYear()->toIso8601String(), 'billed_branches' => max(1, $billedBranches), 'demo' => true, // Pro/Enterprise demos: Queue on natural role pages + every specialty module. // Free / non-demo tenants leave specialty_modules empty (opt-in via Settings). 'queue_integration_enabled' => in_array($plan, ['pro', 'enterprise'], true), 'specialty_modules' => in_array($plan, ['pro', 'enterprise'], true) ? $this->allSpecialtyFlagsEnabled() : [], // Demo seed uses the legacy clinical path. Explicitly clear leftover // onboarding rollout so a failed race that created a real workflow // shell cannot leave an empty gated tenant. 'rollout' => [ CareFeatures::WORKFLOW_ENGINE => false, CareFeatures::FINANCIAL_GATES => false, ], ]); unset($settings['workflow_template']); if ($organization) { if ($organization->trashed()) { $organization->restore(); } $organization->update([ 'name' => $name, 'slug' => $slug, 'timezone' => 'Africa/Accra', 'settings' => $settings, ]); return $organization->fresh(); } return Organization::create([ 'owner_ref' => $ownerRef, 'name' => $name, 'slug' => $slug, 'timezone' => 'Africa/Accra', 'settings' => $settings, ]); } private function upsertOwnerMember(Organization $organization, string $ownerRef): Member { return Member::query()->updateOrCreate( [ 'organization_id' => $organization->id, 'user_ref' => $ownerRef, ], [ 'owner_ref' => $ownerRef, 'role' => 'hospital_admin', 'branch_id' => null, ], ); } /** * @param array $volumes * @return list */ private function seedBranches(Organization $organization, string $ownerRef, array $volumes): array { $catalog = DemoWorld::branches($this->normalizePlan( (string) data_get($organization->settings, 'plan', 'free') ), $volumes['branches']); $branches = []; foreach ($catalog as $i => $spec) { $code = $spec['code'] !== '' ? $spec['code'] : sprintf('DEMO-BR-%02d', $i + 1); $branches[] = Branch::withTrashed()->updateOrCreate( [ 'organization_id' => $organization->id, 'code' => $code, ], [ 'owner_ref' => $ownerRef, 'name' => $spec['name'], 'address' => $spec['address'], 'phone' => $spec['phone'], 'is_active' => true, 'deleted_at' => null, ], ); } return $branches; } /** * Find a demo staff Member by login email, including SSO-remapped public_id user_ref. */ private function findStaffMemberByEmail(Organization $organization, string $email): ?Member { $email = strtolower(trim($email)); if ($email === '') { return null; } $publicId = User::query() ->whereRaw('LOWER(email) = ?', [$email]) ->value('public_id'); return Member::query() ->where('organization_id', $organization->id) ->where(function ($query) use ($email, $publicId) { $query->where('user_ref', $email); if (is_string($publicId) && $publicId !== '') { $query->orWhere('user_ref', $publicId); } }) ->orderByRaw('CASE WHEN user_ref = ? THEN 0 ELSE 1 END', [ is_string($publicId) && $publicId !== '' ? $publicId : $email, ]) ->first(); } /** * 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; $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 !== []) { // Cashiers always staff HQ so demo unpaid invoices on branch[0] are collectible. $branch = $role === 'cashier' ? $hq : $branches[$index % count($branches)]; } $email = strtolower((string) $staff['email']); // Prefer remapped public_id when the staff user already exists locally, // so reseeds do not orphan SSO sessions keyed by public_id. $publicId = User::query() ->whereRaw('LOWER(email) = ?', [$email]) ->value('public_id'); $userRef = is_string($publicId) && $publicId !== '' ? $publicId : $email; $existing = Member::query() ->where('organization_id', $organization->id) ->where(function ($query) use ($email, $publicId) { $query->where('user_ref', $email); if (is_string($publicId) && $publicId !== '') { $query->orWhere('user_ref', $publicId); } }) ->orderByRaw('CASE WHEN user_ref = ? THEN 0 ELSE 1 END', [$userRef]) ->first(); if ($existing) { $existing->forceFill([ 'user_ref' => $userRef, 'owner_ref' => $ownerRef, 'role' => $role, 'branch_id' => $role === 'hospital_admin' ? null : $branch?->id, ])->save(); Member::query() ->where('organization_id', $organization->id) ->where('id', '!=', $existing->id) ->where(function ($query) use ($email, $publicId) { $query->where('user_ref', $email); if (is_string($publicId) && $publicId !== '') { $query->orWhere('user_ref', $publicId); } }) ->delete(); continue; } Member::query()->create([ 'organization_id' => $organization->id, 'user_ref' => $userRef, 'owner_ref' => $ownerRef, 'role' => $role, 'branch_id' => $role === 'hospital_admin' ? null : $branch?->id, ]); } } /** * @param list $branches * @param array $volumes * @return array> keyed by branch id */ private function seedDepartments(array $branches, string $ownerRef, array $volumes): array { $catalog = self::CORE_DEPARTMENTS; if ($volumes['paid_modules']) { $catalog = array_merge($catalog, self::PAID_DEPARTMENTS); } $byBranch = []; foreach ($branches as $branch) { $byBranch[$branch->id] = []; $count = min($volumes['departments_per_branch'], count($catalog)); for ($i = 0; $i < $count; $i++) { $spec = $catalog[$i]; $byBranch[$branch->id][] = Department::withTrashed()->updateOrCreate( [ 'branch_id' => $branch->id, 'name' => $spec['name'], ], [ 'owner_ref' => $ownerRef, 'type' => $spec['type'], 'is_active' => true, 'deleted_at' => null, ], ); } } return $byBranch; } /** * @return array */ private function allSpecialtyFlagsEnabled(): array { $flags = []; foreach (array_keys(config('care.specialty_modules', [])) as $key) { $flags[$key] = true; } return $flags; } private function seedSpecialtyModules(Organization $organization, string $ownerRef): void { $organization = $organization->fresh(); $queueEnabled = (bool) data_get($organization->settings, 'queue_integration_enabled'); $service = app(SpecialtyModuleService::class); foreach (array_keys($service->catalog()) as $key) { try { $service->activate($organization->fresh(), $ownerRef, $key); } catch (\Throwable) { // Demo seed should not fail if activation hits an unexpected error. } } if ($queueEnabled) { try { app(CareQueueProvisioner::class)->provisionOrganization($organization->fresh()); } catch (\Throwable) { // Demo seed continues; pages lazy-provision on use. } } } /** * @param list $branches * @return array> keyed by branch id */ private function departmentsByBranch(array $branches, string $ownerRef): array { $byBranch = []; foreach ($branches as $branch) { $byBranch[$branch->id] = Department::withTrashed() ->owned($ownerRef) ->where('branch_id', $branch->id) ->where('is_active', true) ->orderBy('id') ->get() ->all(); } return $byBranch; } /** * Waiting / completed specialty appointments so each module page has demo traffic. * * @param list $branches * @param list $practitioners * @param list $patients */ private function seedSpecialtyDemoData( Organization $organization, array $branches, array $practitioners, array $patients, string $ownerRef, string $plan = 'pro', ): int { if ($branches === [] || $patients === [] || $practitioners === []) { return 0; } $reasons = [ 'emergency' => ['Chest pain', 'Trauma review', 'Acute fever'], 'blood_bank' => ['Cross-match request', 'Transfusion review', 'Blood product issue'], 'dentistry' => ['Toothache', 'Dental cleaning', 'Extraction review'], 'ophthalmology' => ['Blurry vision', 'Eye exam', 'Contact lens fitting'], 'physiotherapy' => ['Back pain rehab', 'Post-injury physio', 'Mobility session'], 'maternity' => ['Antenatal visit', 'Pregnancy check', 'Postnatal review'], 'radiology' => ['X-ray referral', 'Ultrasound', 'Imaging review'], 'cardiology' => ['Chest pain review', 'ECG follow-up', 'Hypertension clinic'], 'psychiatry' => ['Mental health review', 'Counselling session', 'Medication review'], 'pediatrics' => ['Well-child visit', 'Fever in child', 'Growth check'], 'orthopedics' => ['Fracture follow-up', 'Joint pain', 'Cast review'], 'ent' => ['Ear infection', 'Sore throat', 'Sinus review'], 'oncology' => ['Chemo day-care', 'Oncology review', 'Symptom management'], 'renal' => ['Dialysis session', 'Nephrology review', 'Fluid assessment'], 'surgery' => ['Pre-op assessment', 'Surgical review', 'Wound check'], 'vaccination' => ['Routine immunization', 'Catch-up vaccine', 'Travel vaccine'], 'pathology' => ['Biopsy review', 'Histology result', 'Cytology sample'], 'infusion' => ['IV infusion', 'Iron infusion', 'Day-care therapy'], 'dermatology' => ['Skin rash', 'Mole review', 'Dermatology procedure'], 'podiatry' => ['Diabetic foot care', 'Nail procedure', 'Foot pain'], 'fertility' => ['Fertility consult', 'Cycle review', 'IVF follow-up'], 'child_welfare' => ['Growth monitoring', 'Well-child CWC', 'Nutrition follow-up'], 'ambulance' => ['Chest pain response', 'Trauma pickup', 'Facility transfer'], ]; $count = 0; $catalog = config('care.specialty_modules', []); /** @var array $nextPositionByBranch */ $nextPositionByBranch = []; /** @var array $moduleMembers */ $moduleMembers = []; foreach ($catalog as $key => $definition) { $type = (string) ($definition['department_type'] ?? ''); if ($type === '') { continue; } $moduleReasons = $reasons[$key] ?? ['Specialty visit']; $staffEmail = sprintf('demo-%s-%s@ladill.com', $plan, $key); // Resolve by email or SSO-remapped public_id (seedStaffMembers prefers public_id). $moduleMembers[$key] = $this->findStaffMemberByEmail($organization, $staffEmail); foreach ($branches as $branchIndex => $branch) { $department = Department::owned($ownerRef) ->where('branch_id', $branch->id) ->where('type', $type) ->where('is_active', true) ->first(); if (! $department) { continue; } $member = $moduleMembers[$key]; $staff = $member ? \App\Support\DemoWorld::staffByEmail($staffEmail) : null; $homeBranchId = (int) ($member?->branch_id ?? 0); // One doctor = one branch. Only link the specialty desk on their home site. $assignMember = $member && $homeBranchId > 0 && (int) $branch->id === $homeBranchId; $stableRef = sprintf('demo-specialty-%s-%s', $key, $branch->id); $doctorName = (string) (\App\Support\DemoWorld::SPECIALTY_DOCTORS[$key] ?? ('Dr. '.($definition['label'] ?? ucfirst($key)))); if ($assignMember && is_array($staff) && filled($staff['name'] ?? null)) { $practitionerName = (string) $staff['name']; } elseif ($assignMember) { $practitionerName = $doctorName; } else { // Cover desk for demo traffic at other sites — still a clinician, never a "Clinic" entity. $practitionerName = $doctorName.' · '.$branch->name; } $practitioner = Practitioner::withTrashed()->updateOrCreate( [ 'organization_id' => $organization->id, 'user_ref' => $stableRef, ], [ 'owner_ref' => $ownerRef, 'branch_id' => $branch->id, 'department_id' => $department->id, 'member_id' => $assignMember ? $member->id : null, 'name' => $practitionerName, 'specialty' => (string) ($definition['label'] ?? $key), 'room' => 'Bay '.(($branchIndex % 4) + 1), 'is_active' => true, 'deleted_at' => null, ], ); $practitioner->syncAssignedBranches([(int) $branch->id]); foreach ([Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN, Appointment::STATUS_COMPLETED] as $slot => $status) { $patient = $patients[($branchIndex + $slot + $count) % count($patients)]; $scheduledAt = now()->subHours(($branchIndex * 3) + $slot + 1); $token = "specialty|{$key}|{$branch->id}|{$slot}"; $visit = null; if (in_array($status, [Appointment::STATUS_CHECKED_IN, Appointment::STATUS_WAITING, Appointment::STATUS_COMPLETED], true)) { $visit = Visit::withTrashed()->updateOrCreate( ['uuid' => $this->demoUuid("visit|{$ownerRef}|{$token}")], [ 'owner_ref' => $ownerRef, 'organization_id' => $organization->id, 'branch_id' => $branch->id, 'patient_id' => $patient->id, 'status' => $status === Appointment::STATUS_COMPLETED ? Visit::STATUS_COMPLETED : Visit::STATUS_IN_PROGRESS, 'checked_in_at' => $scheduledAt->copy()->addMinutes(5), 'completed_at' => $status === Appointment::STATUS_COMPLETED ? $scheduledAt->copy()->addHour() : null, 'checked_in_by' => $ownerRef, 'deleted_at' => null, ], ); } $inQueue = in_array($status, [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN], true); $queuePosition = null; if ($inQueue) { $nextPositionByBranch[$branch->id] = ($nextPositionByBranch[$branch->id] ?? 0) + 1; $queuePosition = $nextPositionByBranch[$branch->id]; } Appointment::withTrashed()->updateOrCreate( ['uuid' => $this->demoUuid("appt|{$ownerRef}|{$token}")], [ 'owner_ref' => $ownerRef, 'organization_id' => $organization->id, 'branch_id' => $branch->id, 'patient_id' => $patient->id, 'practitioner_id' => $practitioner->id, 'department_id' => $department->id, 'visit_id' => $visit?->id, 'type' => Appointment::TYPE_SCHEDULED, 'status' => $status, 'scheduled_at' => $scheduledAt, 'checked_in_at' => $visit?->checked_in_at, 'waiting_at' => $inQueue ? $scheduledAt->copy()->addMinutes(5) : null, 'completed_at' => $status === Appointment::STATUS_COMPLETED ? $scheduledAt->copy()->addHour() : null, 'cancelled_at' => null, 'queue_position' => $queuePosition, 'reason' => $moduleReasons[$slot % count($moduleReasons)], 'created_by' => $ownerRef, 'deleted_at' => null, ], ); $count++; } } } $this->seedDentistryClinicalDemo($organization, $ownerRef); $this->seedBloodBankInventoryDemo($organization, $ownerRef); $this->seedOphthalmologyClinicalDemo($organization, $ownerRef); $this->seedPhysiotherapyClinicalDemo($organization, $ownerRef); $this->seedMaternityClinicalDemo($organization, $ownerRef); $this->seedRadiologyClinicalDemo($organization, $ownerRef); $this->seedCardiologyClinicalDemo($organization, $ownerRef); $this->seedPsychiatryClinicalDemo($organization, $ownerRef); $this->seedPediatricsClinicalDemo($organization, $ownerRef); $this->seedOrthopedicsClinicalDemo($organization, $ownerRef); $this->seedEntClinicalDemo($organization, $ownerRef); $this->seedOncologyClinicalDemo($organization, $ownerRef); $this->seedRenalClinicalDemo($organization, $ownerRef); $this->seedSurgeryClinicalDemo($organization, $ownerRef); $this->seedVaccinationClinicalDemo($organization, $ownerRef); $this->seedPathologyClinicalDemo($organization, $ownerRef); $this->seedInfusionClinicalDemo($organization, $ownerRef); $this->seedDermatologyClinicalDemo($organization, $ownerRef); $this->seedPodiatryClinicalDemo($organization, $ownerRef); $this->seedFertilityClinicalDemo($organization, $ownerRef); $this->seedChildWelfareClinicalDemo($organization, $ownerRef); $this->seedAmbulanceClinicalDemo($organization, $ownerRef); return $count; } /** * Visit-scoped facility stock for Blood Bank Inventory tab / reports. * Seeds inventory_note on each open BB visit so findForVisit shows stock without manual entry. */ private function seedBloodBankInventoryDemo(Organization $organization, string $ownerRef): void { // Specialty departments are provisioned without organization_id; scope via branches. $branchIds = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->pluck('id'); if ($branchIds->isEmpty()) { return; } $departmentIds = Department::owned($ownerRef) ->whereIn('branch_id', $branchIds) ->where('type', 'blood_bank') ->where('is_active', true) ->pluck('id'); if ($departmentIds->isEmpty()) { return; } $appointments = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->whereNotNull('visit_id') ->with('visit') ->orderBy('branch_id') ->orderBy('id') ->get(); if ($appointments->isEmpty()) { return; } $clinical = app(SpecialtyClinicalRecordService::class); /** @var array $branchOrder */ $branchOrder = []; foreach ($appointments as $appointment) { $visit = $appointment->visit; if (! $visit) { continue; } $branchId = (int) $appointment->branch_id; if (! array_key_exists($branchId, $branchOrder)) { $branchOrder[$branchId] = count($branchOrder); } $clinical->upsert( $organization, $visit, 'blood_bank', 'inventory_note', $this->demoBloodBankInventoryPayload($branchOrder[$branchId]), $ownerRef, $ownerRef, ); } } /** * @return array */ private function demoBloodBankInventoryPayload(int $branchIndex): array { // First branch: low whole-blood + alert flag so Inventory / reports demo alerts. if ($branchIndex === 0) { return [ 'whole_blood_units' => 1, 'packed_rbc_units' => 14, 'platelet_units' => 7, 'ffp_units' => 9, 'cryo_units' => 4, 'low_stock_alert' => true, 'notes' => 'Demo morning stock count — whole blood critically low; replenishment requested.', ]; } return [ 'whole_blood_units' => 8 + ($branchIndex * 2), 'packed_rbc_units' => 18 + $branchIndex, 'platelet_units' => 10 + $branchIndex, 'ffp_units' => 12 + $branchIndex, 'cryo_units' => 5, 'low_stock_alert' => false, 'notes' => 'Demo facility stock count for blood bank.', ]; } /** * Seed refraction + eye exam (+ optional plan) on open ophthalmology visits. */ private function seedOphthalmologyClinicalDemo(Organization $organization, string $ownerRef): void { $branchIds = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->pluck('id'); if ($branchIds->isEmpty()) { return; } $departmentIds = Department::owned($ownerRef) ->whereIn('branch_id', $branchIds) ->where('type', 'ophthalmology') ->where('is_active', true) ->pluck('id'); if ($departmentIds->isEmpty()) { return; } $appointments = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->whereNotNull('visit_id') ->with('visit') ->orderBy('branch_id') ->orderBy('id') ->get(); if ($appointments->isEmpty()) { return; } $clinical = app(SpecialtyClinicalRecordService::class); $index = 0; foreach ($appointments as $appointment) { $visit = $appointment->visit; if (! $visit) { continue; } $elevated = $index === 0; $clinical->upsert( $organization, $visit, 'ophthalmology', 'refraction', [ 'va_od' => $elevated ? '6/60' : '6/9', 'va_os' => $elevated ? '6/36' : '6/6', 'va_od_near' => 'N8', 'va_os_near' => 'N6', 'sphere_od' => $elevated ? '-2.50' : '-0.75', 'cylinder_od' => '-0.50', 'axis_od' => '90', 'sphere_os' => $elevated ? '-1.75' : '-0.50', 'cylinder_os' => '-0.25', 'axis_os' => '85', 'add' => '+1.50', 'pd' => 62, 'notes' => 'Demo refraction for eye care suite.', ], $ownerRef, $ownerRef, 'refraction', ); $clinical->upsert( $organization, $visit, 'ophthalmology', 'eye_exam', [ 'chief_complaint' => $elevated ? 'Blurry vision and headache' : 'Routine eye exam', 'history' => $elevated ? 'Gradual reduction in vision over 2 weeks; intermittent haloes.' : 'Annual review; no acute symptoms.', 'va_od' => $elevated ? '6/60' : '6/9', 'va_os' => $elevated ? '6/36' : '6/6', 'iop_od' => $elevated ? 32 : 16, 'iop_os' => $elevated ? 28 : 15, 'pupils' => 'Equal, reactive', 'extraocular' => 'Full', 'anterior_segment' => $elevated ? 'Quiet; possible early cataract OD' : 'Quiet', 'fundus' => $elevated ? 'Cup:disc elevated OD; review for glaucoma' : 'Healthy discs', 'findings' => $elevated ? 'Elevated IOP with reduced acuity — urgent glaucoma work-up.' : 'Normal examination; refraction updated.', ], $ownerRef, $ownerRef, 'exam', ); if ($elevated) { $clinical->upsert( $organization, $visit, 'ophthalmology', 'eye_plan', [ 'diagnosis' => 'Suspected primary open-angle glaucoma', 'laterality' => 'OU', 'plan' => 'Start topical IOP-lowering drops; arrange OCT and visual fields.', 'medications' => 'Latanoprost 0.005% OU nocte', 'procedure_planned' => false, 'follow_up' => '2 weeks', 'advice' => 'Return sooner if vision worsens or severe eye pain.', ], $ownerRef, $ownerRef, 'plan', ); } if (! $visit->specialty_stage) { $visit->update(['specialty_stage' => $elevated ? 'plan' : 'exam']); } $index++; } } /** * Seed assessment + plan (+ optional session) on open physiotherapy visits. */ private function seedPhysiotherapyClinicalDemo(Organization $organization, string $ownerRef): void { $branchIds = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->pluck('id'); if ($branchIds->isEmpty()) { return; } $departmentIds = Department::owned($ownerRef) ->whereIn('branch_id', $branchIds) ->where('type', 'physiotherapy') ->where('is_active', true) ->pluck('id'); if ($departmentIds->isEmpty()) { return; } $appointments = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->whereNotNull('visit_id') ->with('visit') ->orderBy('branch_id') ->orderBy('id') ->get(); if ($appointments->isEmpty()) { return; } $clinical = app(SpecialtyClinicalRecordService::class); $index = 0; foreach ($appointments as $appointment) { $visit = $appointment->visit; if (! $visit) { continue; } $highPain = $index === 0; $clinical->upsert( $organization, $visit, 'physiotherapy', 'pt_assessment', [ 'chief_complaint' => $highPain ? 'Severe low back pain after lifting' : 'Knee stiffness post-injury', 'onset' => $highPain ? 'Acute — 3 days ago' : 'Subacute — 3 weeks', 'pain_score' => $highPain ? 9 : 4, 'body_region' => $highPain ? 'Back / lumbar' : 'Knee', 'rom' => $highPain ? 'Lumbar flexion limited, guarded' : 'Knee flexion 90°, extension -5°', 'strength' => $highPain ? 'Core activation poor' : 'Quadriceps 4/5', 'special_tests' => $highPain ? 'SLR positive right' : 'McMurray negative', 'red_flags' => $highPain ? 'Night pain; no saddle anaesthesia' : '', 'goals' => $highPain ? 'Reduce pain and restore safe lifting' : 'Restore full ROM and return to sport', ], $ownerRef, $ownerRef, 'assessment', ); $clinical->upsert( $organization, $visit, 'physiotherapy', 'pt_plan', [ 'diagnosis' => $highPain ? 'Acute lumbar strain' : 'Post-traumatic knee stiffness', 'frequency' => '2–3× / week', 'duration_weeks' => $highPain ? 4 : 6, 'interventions' => $highPain ? 'Manual therapy, core activation, graded activity' : 'ROM drills, strengthening, proprioception', 'precautions' => $highPain ? 'Avoid heavy flexion under load' : 'No pivoting until week 4', 'goals' => 'Pain <3/10 and functional mobility', 'outcome_measures' => 'VAS, Oswestry / LEFS', ], $ownerRef, $ownerRef, 'treatment_plan', ); if ($highPain) { $clinical->upsert( $organization, $visit, 'physiotherapy', 'pt_session', [ 'session_number' => 1, 'modalities' => 'Heat, soft-tissue mobilisation', 'exercises' => 'Pelvic tilts, bird-dog, walking', 'pain_before' => 9, 'pain_after' => 6, 'response' => 'Tolerated well; pain eased post-session', 'outcome' => 'Completed — continue plan', 'next_review' => '2 days', 'notes' => 'Demo physio session.', ], $ownerRef, $ownerRef, 'session', ); } if (! $visit->specialty_stage) { $visit->update(['specialty_stage' => $highPain ? 'session' : 'treatment_plan']); } $index++; } } /** * Seed ANC history + exam (+ optional plan) on open maternity visits. */ private function seedMaternityClinicalDemo(Organization $organization, string $ownerRef): void { $branchIds = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->pluck('id'); if ($branchIds->isEmpty()) { return; } $departmentIds = Department::owned($ownerRef) ->whereIn('branch_id', $branchIds) ->where('type', 'maternity') ->where('is_active', true) ->pluck('id'); if ($departmentIds->isEmpty()) { return; } $appointments = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->whereNotNull('visit_id') ->with('visit') ->orderBy('branch_id') ->orderBy('id') ->get(); if ($appointments->isEmpty()) { return; } $clinical = app(SpecialtyClinicalRecordService::class); $index = 0; foreach ($appointments as $appointment) { $visit = $appointment->visit; if (! $visit) { continue; } $highRisk = $index === 0; $clinical->upsert( $organization, $visit, 'maternity', 'anc_history', [ 'gravida' => $highRisk ? 3 : 1, 'para' => $highRisk ? 1 : 0, 'lmp' => '12 Jan 2026', 'edd' => '19 Oct 2026', 'gestational_age_weeks' => $highRisk ? 34 : 22, 'anc_visit_number' => $highRisk ? 6 : 3, 'obstetric_history' => $highRisk ? 'Previous CS; one miscarriage' : 'Primigravida', 'medical_history' => $highRisk ? 'Hypertension in pregnancy' : 'None significant', 'allergies' => 'None known', 'medications' => 'IFAS, calcium', 'social_history' => 'Lives with partner; support available', ], $ownerRef, $ownerRef, 'history', ); $clinical->upsert( $organization, $visit, 'maternity', 'obstetric_exam', [ 'bp' => $highRisk ? '158/102' : '118/74', 'pulse' => $highRisk ? 92 : 78, 'weight_kg' => 72, 'fundal_height' => $highRisk ? 33 : 22, 'presentation' => 'Cephalic', 'lie' => 'Longitudinal', 'engagement' => $highRisk ? '3/5' : 'Free', 'oedema' => $highRisk ? 'Moderate' : 'None', 'urine' => $highRisk ? 'Protein +1' : 'NAD', 'danger_signs' => $highRisk ? 'Headache; visual spots' : '', 'findings' => $highRisk ? 'Hypertension with oedema — escalate high-risk ANC' : 'Normal ANC examination', ], $ownerRef, $ownerRef, 'exam', ); $clinical->upsert( $organization, $visit, 'maternity', 'fetal_notes', [ 'fetal_heart_rate' => $highRisk ? 168 : 142, 'fetal_movements' => $highRisk ? 'Reduced' : 'Normal', 'presentation' => 'Cephalic', 'liquor' => 'Adequate clinically', 'ctg_summary' => $highRisk ? 'Baseline raised; observe' : 'Not indicated', 'notes' => 'Demo fetal notes for maternity suite.', ], $ownerRef, $ownerRef, 'exam', ); if ($highRisk) { $clinical->upsert( $organization, $visit, 'maternity', 'mat_plan', [ 'risk_category' => 'High risk', 'plan' => 'Urgent senior review; BP control; fetal monitoring; consider admission.', 'medications' => 'Continue antihypertensives as prescribed', 'education' => 'Return immediately if headache, bleeding, or reduced movements', 'postnatal_needed' => false, 'follow_up' => 'Same day review', 'referral' => 'Obstetric high-risk clinic', ], $ownerRef, $ownerRef, 'plan', ); } if (! $visit->specialty_stage) { $visit->update(['specialty_stage' => $highRisk ? 'plan' : 'exam']); } $index++; } } private function seedRadiologyClinicalDemo(Organization $organization, string $ownerRef): void { $branchIds = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->pluck('id'); if ($branchIds->isEmpty()) { return; } $departmentIds = Department::owned($ownerRef) ->whereIn('branch_id', $branchIds) ->where('type', 'radiology') ->where('is_active', true) ->pluck('id'); if ($departmentIds->isEmpty()) { return; } $appointments = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->whereNotNull('visit_id') ->with('visit') ->orderBy('branch_id') ->orderBy('id') ->get(); if ($appointments->isEmpty()) { return; } $clinical = app(SpecialtyClinicalRecordService::class); $index = 0; foreach ($appointments as $appointment) { $visit = $appointment->visit; if (! $visit) { continue; } $urgent = $index === 0; $clinical->upsert( $organization, $visit, 'radiology', 'imaging_request', [ 'modality' => $urgent ? 'CT' : 'X-ray', 'body_part' => $urgent ? 'Chest' : 'Left ankle', 'clinical_info' => $urgent ? 'Shortness of breath; rule out PE / infiltrates' : 'Trauma — query fracture after fall', 'urgency' => $urgent ? 'STAT' : 'Routine', 'contrast' => $urgent ? 'IV' : 'None', 'protocol' => $urgent ? 'CTPA protocol' : 'AP / lateral', 'referrer' => 'Demo ER / Ortho', ], $ownerRef, $ownerRef, 'protocol', ); $clinical->upsert( $organization, $visit, 'radiology', 'imaging_acquisition', [ 'technologist' => 'Demo Tech', 'equipment' => $urgent ? 'CT 1' : 'XR Bay A', 'acquisition_status' => 'Acquired', 'contrast_given' => $urgent, 'dose_notes' => $urgent ? 'Standard CTPA dose' : 'Low dose extremity', 'quality' => 'Diagnostic', 'notes' => 'Demo acquisition for radiology suite.', ], $ownerRef, $ownerRef, 'acquisition', ); if ($urgent) { $clinical->upsert( $organization, $visit, 'radiology', 'imaging_report', [ 'technique' => 'CT pulmonary angiogram with IV contrast', 'comparison' => 'None available', 'findings' => 'No filling defect in main or segmental pulmonary arteries. Mild basilar atelectasis.', 'impression' => 'No CT evidence of pulmonary embolism.', 'recommendations' => 'Clinical correlation; consider alternative causes of dyspnoea.', 'critical_result' => false, ], $ownerRef, $ownerRef, 'reporting', ); } if (! $visit->specialty_stage) { $visit->update(['specialty_stage' => $urgent ? 'reporting' : 'acquisition']); } $index++; } } private function seedCardiologyClinicalDemo(Organization $organization, string $ownerRef): void { $branchIds = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->pluck('id'); if ($branchIds->isEmpty()) { return; } $departmentIds = Department::owned($ownerRef) ->whereIn('branch_id', $branchIds) ->where('type', 'cardiology') ->where('is_active', true) ->pluck('id'); if ($departmentIds->isEmpty()) { return; } $appointments = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->whereNotNull('visit_id') ->with('visit') ->orderBy('branch_id') ->orderBy('id') ->get(); if ($appointments->isEmpty()) { return; } $clinical = app(SpecialtyClinicalRecordService::class); $index = 0; foreach ($appointments as $appointment) { $visit = $appointment->visit; if (! $visit) { continue; } $highRisk = $index === 0; $clinical->upsert( $organization, $visit, 'cardiology', 'cardio_history', [ 'history' => $highRisk ? 'Progressive dyspnoea and orthopnoea for 2 weeks' : 'Palpitations intermittent for 1 month', 'past_cardiac' => $highRisk ? 'Prior MI 2019' : 'None', 'medications' => $highRisk ? 'Aspirin, atorvastatin, bisoprolol' : 'None', 'allergies' => 'NKDA', 'risk_factors' => $highRisk ? 'Hypertension, diabetes, smoking history' : 'Family history of AF', 'family_history' => $highRisk ? 'Father CAD' : 'Mother AF', ], $ownerRef, $ownerRef, 'history', ); $clinical->upsert( $organization, $visit, 'cardiology', 'cardiac_exam', [ 'chief_complaint' => $highRisk ? 'Dyspnoea / heart failure symptoms' : 'Palpitations', 'nyha_class' => $highRisk ? 'III' : 'I', 'bp' => $highRisk ? '168/98' : '124/78', 'hr' => $highRisk ? 104 : 72, 'heart_sounds' => $highRisk ? 'S3; soft pansystolic murmur' : 'Normal S1 S2; no murmur', 'ecg_findings' => $highRisk ? 'Sinus tach; Q waves inferiorly; no acute STEMI' : 'Sinus rhythm; occasional PACs', 'exam_findings' => $highRisk ? 'Bilateral basal crackles; mild ankle oedema' : 'CVD exam normal', ], $ownerRef, $ownerRef, 'exam', ); if ($highRisk) { $clinical->upsert( $organization, $visit, 'cardiology', 'cardio_plan', [ 'diagnosis' => 'Decompensated heart failure (NYHA III)', 'plan' => 'Diuresis; echo; review meds; admit if not improving', 'medications' => 'Increase loop diuretic; continue GDMT', 'procedure_planned' => false, 'follow_up' => '48-hour review or sooner if worse', 'advice' => 'Daily weights; fluid restriction counselling', ], $ownerRef, $ownerRef, 'plan', ); } if (! $visit->specialty_stage) { $visit->update(['specialty_stage' => $highRisk ? 'plan' : 'exam']); } $index++; } } private function seedPsychiatryClinicalDemo(Organization $organization, string $ownerRef): void { $branchIds = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->pluck('id'); if ($branchIds->isEmpty()) { return; } $departmentIds = Department::owned($ownerRef) ->whereIn('branch_id', $branchIds) ->where('type', 'psychiatry') ->where('is_active', true) ->pluck('id'); if ($departmentIds->isEmpty()) { return; } $appointments = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->whereNotNull('visit_id') ->with('visit') ->orderBy('branch_id') ->orderBy('id') ->get(); if ($appointments->isEmpty()) { return; } $clinical = app(SpecialtyClinicalRecordService::class); $index = 0; foreach ($appointments as $appointment) { $visit = $appointment->visit; if (! $visit) { continue; } $highRisk = $index === 0; $clinical->upsert( $organization, $visit, 'psychiatry', 'mental_status', [ 'chief_complaint' => $highRisk ? 'Low mood with suicidal ideation' : 'Anxiety and poor sleep', 'history' => $highRisk ? '2-week worsening depression; sleep and appetite reduced' : 'Generalised worry for 3 months; work stress', 'appearance' => $highRisk ? 'Dishevelled; limited eye contact' : 'Well kempt; cooperative', 'mood_affect' => $highRisk ? 'Depressed / flat' : 'Anxious / congruent', 'thought' => $highRisk ? 'Hopeless themes; no psychosis' : 'Worried ruminations; no delusions', 'perception' => 'No hallucinations', 'cognition' => 'Oriented ×3', 'insight' => $highRisk ? 'Partial' : 'Good', 'mse_summary' => $highRisk ? 'Depressed MSE with active suicidal ideation — high vigilance' : 'Anxious MSE without psychosis or self-harm intent', ], $ownerRef, $ownerRef, 'history', ); $clinical->upsert( $organization, $visit, 'psychiatry', 'risk_assessment', [ 'overall_risk' => $highRisk ? 'High' : 'Low', 'self_harm' => $highRisk ? 'Ideation' : 'None identified', 'harm_others' => 'None identified', 'safeguarding' => $highRisk ? 'Lives alone; limited support tonight' : 'Supportive partner', 'protective_factors' => $highRisk ? 'Engaged with clinic; no prior attempts' : 'Good insight; coping strategies', 'risk_summary' => $highRisk ? 'High risk of self-harm — safety planning and close follow-up required' : 'Low risk; outpatient management appropriate', 'immediate_actions' => $highRisk ? 'Safety plan; remove means; same-day senior review' : 'Psychoeducation; follow-up in clinic', ], $ownerRef, $ownerRef, 'risk', ); if ($highRisk) { $clinical->upsert( $organization, $visit, 'psychiatry', 'psych_plan', [ 'formulation' => 'Major depressive episode with high self-harm risk in context of isolation', 'diagnosis' => 'Major depressive disorder — severe', 'plan' => 'Crisis safety plan; consider short admission vs intensive outpatient; start/adjust antidepressant', 'medications' => 'Review current AD; discuss options', 'therapy' => 'CBT referral when risk settles', 'follow_up' => '24–48 hours or sooner if crisis', 'crisis_plan' => 'Emergency contacts; crisis line; return to ED if ideation intensifies', ], $ownerRef, $ownerRef, 'formulation', ); } if (! $visit->specialty_stage) { $visit->update(['specialty_stage' => $highRisk ? 'formulation' : 'risk']); } $index++; } } private function seedPediatricsClinicalDemo(Organization $organization, string $ownerRef): void { $branchIds = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->pluck('id'); if ($branchIds->isEmpty()) { return; } $departmentIds = Department::owned($ownerRef) ->whereIn('branch_id', $branchIds) ->where('type', 'pediatrics') ->where('is_active', true) ->pluck('id'); if ($departmentIds->isEmpty()) { return; } $appointments = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->whereNotNull('visit_id') ->with('visit') ->orderBy('branch_id') ->orderBy('id') ->get(); if ($appointments->isEmpty()) { return; } $clinical = app(SpecialtyClinicalRecordService::class); $index = 0; foreach ($appointments as $appointment) { $visit = $appointment->visit; if (! $visit) { continue; } $highRisk = $index === 0; $clinical->upsert( $organization, $visit, 'pediatrics', 'ped_history', [ 'history' => $highRisk ? 'Fever and poor feeding for 3 days; reduced urine output' : 'Mild cough for 1 week; eating well', 'birth_history' => $highRisk ? 'Term; birth weight 2.8 kg' : 'Term; uneventful', 'past_medical' => $highRisk ? 'Prior admission for pneumonia' : 'None', 'medications' => 'None', 'allergies' => 'NKDA', 'feeding' => $highRisk ? 'Breast + formula; intake reduced' : 'Age-appropriate diet', 'social' => 'Lives with parents', ], $ownerRef, $ownerRef, 'history', ); $clinical->upsert( $organization, $visit, 'pediatrics', 'pediatric_exam', [ 'chief_complaint' => $highRisk ? 'Fever / possible sepsis' : 'Cough review', 'age_months' => $highRisk ? 18 : 48, 'weight_kg' => $highRisk ? 8.2 : 16.5, 'height_cm' => $highRisk ? 76 : 102, 'head_circumference_cm' => $highRisk ? 45 : null, 'temperature_c' => $highRisk ? 39.2 : 36.8, 'growth_concern' => $highRisk ? 'Underweight' : 'None', 'developmental' => $highRisk ? 'Walks; few words' : 'Age-appropriate', 'immunization_status' => $highRisk ? 'Up to date except measles' : 'Up to date', 'examination' => $highRisk ? 'Irritable; mild dehydration; clear chest; no rash' : 'Well; mild rhinitis; chest clear', ], $ownerRef, $ownerRef, 'exam', ); if ($highRisk) { $clinical->upsert( $organization, $visit, 'pediatrics', 'ped_plan', [ 'diagnosis' => 'Febrile illness — rule out serious bacterial infection', 'plan' => 'Labs; oral fluids; antipyretic; review same day if worse', 'medications' => 'Paracetamol PRN', 'treatment_planned' => true, 'follow_up' => '24 hours or sooner if red flags', 'advice' => 'Hydration; fever advice; return if lethargy or poor feeding', ], $ownerRef, $ownerRef, 'plan', ); } if (! $visit->specialty_stage) { $visit->update(['specialty_stage' => $highRisk ? 'plan' : 'exam']); } $index++; } } private function seedOrthopedicsClinicalDemo(Organization $organization, string $ownerRef): void { $branchIds = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->pluck('id'); if ($branchIds->isEmpty()) { return; } $departmentIds = Department::owned($ownerRef) ->whereIn('branch_id', $branchIds) ->where('type', 'orthopedics') ->where('is_active', true) ->pluck('id'); if ($departmentIds->isEmpty()) { return; } $appointments = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->whereNotNull('visit_id') ->with('visit') ->orderBy('branch_id') ->orderBy('id') ->get(); if ($appointments->isEmpty()) { return; } $clinical = app(SpecialtyClinicalRecordService::class); $index = 0; foreach ($appointments as $appointment) { $visit = $appointment->visit; if (! $visit) { continue; } $highRisk = $index === 0; $clinical->upsert( $organization, $visit, 'orthopedics', 'ortho_history', [ 'history' => $highRisk ? 'Fall from height; right wrist pain and deformity' : 'Left knee pain after football twist', 'mechanism' => $highRisk ? 'FOOSH from ladder' : 'Twisting injury', 'past_ortho' => $highRisk ? 'Prior ankle sprain' : 'None', 'medications' => 'None', 'allergies' => 'NKDA', 'comorbidities' => $highRisk ? 'Diabetes' : 'None', ], $ownerRef, $ownerRef, 'history', ); $clinical->upsert( $organization, $visit, 'orthopedics', 'ortho_exam', [ 'chief_complaint' => $highRisk ? 'Wrist deformity after fall' : 'Knee pain', 'side' => $highRisk ? 'Right' : 'Left', 'region' => $highRisk ? 'Distal radius' : 'Knee', 'rom' => $highRisk ? 'Limited wrist flexion/extension' : 'Full ROM; pain on twist', 'neurovascular' => $highRisk ? 'Sensory deficit' : 'Intact', 'swelling' => $highRisk ? 'Dinner-fork deformity' : 'Mild effusion', 'exam_findings' => $highRisk ? 'Closed injury; soft compartments' : 'McMurray negative; stable ligaments', 'fracture_classification' => $highRisk ? 'Colles-type distal radius' : 'Soft tissue strain', ], $ownerRef, $ownerRef, 'exam', ); if ($highRisk) { $clinical->upsert( $organization, $visit, 'orthopedics', 'ortho_imaging', [ 'modality' => 'X-ray', 'region' => 'Right wrist', 'findings' => 'Displaced distal radius fracture; ulnar styloid intact', 'impression' => 'Extra-articular distal radius fracture', 'notes' => 'AP and lateral views', ], $ownerRef, $ownerRef, 'imaging', ); $clinical->upsert( $organization, $visit, 'orthopedics', 'ortho_plan', [ 'diagnosis' => 'Displaced distal radius fracture', 'plan' => 'Closed reduction and cast; elevate; analgesia', 'weight_bearing' => 'N/A — upper limb', 'procedure_planned' => true, 'follow_up' => '1 week with cast check / x-ray', 'advice' => 'Elevation; watch for numbness or colour change', ], $ownerRef, $ownerRef, 'plan', ); } if (! $visit->specialty_stage) { $visit->update(['specialty_stage' => $highRisk ? 'plan' : 'exam']); } $index++; } } private function seedEntClinicalDemo(Organization $organization, string $ownerRef): void { $branchIds = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->pluck('id'); if ($branchIds->isEmpty()) { return; } $departmentIds = Department::owned($ownerRef) ->whereIn('branch_id', $branchIds) ->where('type', 'ent') ->where('is_active', true) ->pluck('id'); if ($departmentIds->isEmpty()) { return; } $appointments = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->whereNotNull('visit_id') ->with('visit') ->orderBy('branch_id') ->orderBy('id') ->get(); if ($appointments->isEmpty()) { return; } $clinical = app(SpecialtyClinicalRecordService::class); $index = 0; foreach ($appointments as $appointment) { $visit = $appointment->visit; if (! $visit) { continue; } $highRisk = $index === 0; $clinical->upsert( $organization, $visit, 'ent', 'ent_history', [ 'history' => $highRisk ? 'Progressive sore throat and muffled voice; difficulty swallowing' : 'Recurrent ear discharge for 2 months', 'ear_symptoms' => $highRisk ? 'None' : 'Left otorrhoea; mild hearing loss', 'nose_symptoms' => 'None', 'throat_symptoms' => $highRisk ? 'Severe odynophagia; drooling' : 'None', 'past_ent' => $highRisk ? 'Prior tonsillitis' : 'Recurrent otitis media', 'medications' => $highRisk ? 'None recent' : 'Topical drops PRN', 'allergies' => 'NKDA', ], $ownerRef, $ownerRef, 'history', ); $clinical->upsert( $organization, $visit, 'ent', 'ent_exam', [ 'chief_complaint' => $highRisk ? 'Sore throat / possible peritonsillar abscess' : 'Chronic ear discharge', 'ear_findings' => $highRisk ? 'Normal TMs' : 'Left canal moist; TM perforated', 'nose_findings' => 'Normal', 'throat_findings' => $highRisk ? 'Trismus; unilateral tonsillar bulge; uvula deviation' : 'Oropharynx normal', 'hearing' => $highRisk ? 'Grossly normal' : 'Reduced left whispered voice', 'neck' => $highRisk ? 'Tender cervical nodes' : 'No lymphadenopathy', 'airway_concern' => $highRisk, 'exam_summary' => $highRisk ? 'Findings consistent with peritonsillar abscess — airway vigilance' : 'Chronic otitis media with perforation', ], $ownerRef, $ownerRef, 'exam', ); if ($highRisk) { $clinical->upsert( $organization, $visit, 'ent', 'ent_plan', [ 'diagnosis' => 'Suspected peritonsillar abscess', 'plan' => 'IV antibiotics; needle aspiration / I&D; airway monitoring', 'medications' => 'IV antibiotics per protocol; analgesia', 'procedure_planned' => true, 'follow_up' => 'Inpatient review post-drainage', 'advice' => 'NPO until airway safe; urgent escalation if stridor', ], $ownerRef, $ownerRef, 'plan', ); } if (! $visit->specialty_stage) { $visit->update(['specialty_stage' => $highRisk ? 'plan' : 'exam']); } $index++; } } private function seedOncologyClinicalDemo(Organization $organization, string $ownerRef): void { $branchIds = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->pluck('id'); if ($branchIds->isEmpty()) { return; } $departmentIds = Department::owned($ownerRef) ->whereIn('branch_id', $branchIds) ->where('type', 'oncology') ->where('is_active', true) ->pluck('id'); if ($departmentIds->isEmpty()) { return; } $appointments = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->whereNotNull('visit_id') ->with('visit') ->orderBy('branch_id') ->orderBy('id') ->get(); if ($appointments->isEmpty()) { return; } $clinical = app(SpecialtyClinicalRecordService::class); $index = 0; foreach ($appointments as $appointment) { $visit = $appointment->visit; if (! $visit) { continue; } $highRisk = $index === 0; $clinical->upsert($organization, $visit, 'oncology', 'onc_history', [ 'history' => $highRisk ? 'New diagnosis work-up; progressive fatigue and weight loss' : 'On adjuvant regimen; here for cycle review', 'cancer_history' => $highRisk ? 'Breast mass biopsied last month' : 'Stage II breast Ca — surgery 4 months ago', 'medications' => $highRisk ? 'Analgesia PRN' : 'Current chemo cycle medications', 'allergies' => 'NKDA', 'comorbidities' => $highRisk ? 'Hypertension' : 'None significant', 'social' => 'Family support available', ], $ownerRef, $ownerRef, 'history'); $clinical->upsert($organization, $visit, 'oncology', 'onc_staging', [ 'diagnosis' => $highRisk ? 'Suspected metastatic breast carcinoma' : 'Breast carcinoma — adjuvant', 'stage' => $highRisk ? 'cT4 N2 M1' : 'pT2 N1 M0', 'histology' => 'Invasive ductal carcinoma', 'performance_status' => $highRisk ? '3' : '1', 'exam_findings' => $highRisk ? 'Cachexia; axillary nodes; hepatic tenderness' : 'Well; surgical scar healed', 'sites' => $highRisk ? 'Breast, axilla, liver' : 'None residual known', ], $ownerRef, $ownerRef, 'staging'); if ($highRisk) { $clinical->upsert($organization, $visit, 'oncology', 'onc_plan', [ 'diagnosis' => 'Metastatic breast carcinoma', 'plan' => 'Staging imaging; MDT; supportive care; consider palliative systemic therapy', 'intent' => 'Palliative', 'medications' => 'Analgesia; antiemetics PRN', 'treatment_planned' => true, 'follow_up' => 'Chemo day-care after MDT', 'advice' => 'Return if fever or uncontrolled pain', ], $ownerRef, $ownerRef, 'plan'); } if (! $visit->specialty_stage) { $visit->update(['specialty_stage' => $highRisk ? 'plan' : 'staging']); } $index++; } } private function seedRenalClinicalDemo(Organization $organization, string $ownerRef): void { $branchIds = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->pluck('id'); if ($branchIds->isEmpty()) { return; } $departmentIds = Department::owned($ownerRef) ->whereIn('branch_id', $branchIds) ->where('type', 'renal') ->where('is_active', true) ->pluck('id'); if ($departmentIds->isEmpty()) { return; } $appointments = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->whereNotNull('visit_id') ->with('visit') ->orderBy('branch_id') ->orderBy('id') ->get(); if ($appointments->isEmpty()) { return; } $clinical = app(SpecialtyClinicalRecordService::class); $index = 0; foreach ($appointments as $appointment) { $visit = $appointment->visit; if (! $visit) { continue; } $highRisk = $index === 0; $clinical->upsert($organization, $visit, 'renal', 'renal_history', [ 'history' => $highRisk ? 'Missed last dialysis; progressive dyspnoea and oedema' : 'Routine thrice-weekly haemodialysis review', 'ckd_stage' => $highRisk ? 'CKD 5D — 2 years on HD' : 'CKD 5D — 8 months on HD', 'access_history' => 'Left AV fistula', 'medications' => 'EPO; phosphate binders; antihypertensives', 'allergies' => 'NKDA', 'comorbidities' => $highRisk ? 'Diabetes; heart failure' : 'Hypertension', ], $ownerRef, $ownerRef, 'history'); $clinical->upsert($organization, $visit, 'renal', 'renal_exam', [ 'chief_complaint' => $highRisk ? 'Fluid overload / missed dialysis' : 'Routine dialysis session', 'bp' => $highRisk ? '168/98' : '132/78', 'weight_kg' => $highRisk ? 78.5 : 65.2, 'fluid_status' => $highRisk ? 'Overload' : 'Euvolemic', 'access' => 'Left AVF thrills present', 'exam_findings' => $highRisk ? 'Bilateral basal crackles; sacral oedema' : 'Chest clear; no oedema', 'uremic_symptoms' => $highRisk ? 'Nausea; pruritus' : 'None', ], $ownerRef, $ownerRef, 'exam'); if ($highRisk) { $clinical->upsert($organization, $visit, 'renal', 'renal_session', [ 'session_type' => 'Hemodialysis', 'pre_weight_kg' => 78.5, 'uf_goal_l' => 3.5, 'access' => 'Left AVF', 'bp_pre' => '168/98', 'labs_summary' => 'K 5.8; Cr elevated; Hb 9.1', 'complications' => 'Hypotension mid-session anticipated', 'notes' => 'Urgent HD for fluid overload after missed session', ], $ownerRef, $ownerRef, 'dialysis'); } if (! $visit->specialty_stage) { $visit->update(['specialty_stage' => $highRisk ? 'dialysis' : 'exam']); } $index++; } } private function seedSurgeryClinicalDemo(Organization $organization, string $ownerRef): void { $branchIds = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->pluck('id'); if ($branchIds->isEmpty()) { return; } $departmentIds = Department::owned($ownerRef) ->whereIn('branch_id', $branchIds) ->where('type', 'surgery') ->where('is_active', true) ->pluck('id'); if ($departmentIds->isEmpty()) { return; } $appointments = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->whereNotNull('visit_id') ->with('visit') ->orderBy('branch_id') ->orderBy('id') ->get(); if ($appointments->isEmpty()) { return; } $clinical = app(SpecialtyClinicalRecordService::class); $index = 0; foreach ($appointments as $appointment) { $visit = $appointment->visit; if (! $visit) { continue; } $highRisk = $index === 0; $clinical->upsert($organization, $visit, 'surgery', 'surg_history', [ 'history' => $highRisk ? 'Right inguinal hernia with intermittent pain; electing repair' : 'Post-appendicectomy wound check', 'past_surgical' => $highRisk ? 'None' : 'Laparoscopic appendicectomy 10 days ago', 'medications' => $highRisk ? 'None' : 'Analgesia; antibiotics completed', 'allergies' => 'NKDA', 'comorbidities' => $highRisk ? 'Well-controlled diabetes' : 'None', 'anaesthesia_history' => 'No prior issues', ], $ownerRef, $ownerRef, 'history'); $clinical->upsert($organization, $visit, 'surgery', 'surg_exam', [ 'chief_complaint' => $highRisk ? 'Inguinal hernia — pre-op' : 'Wound review', 'exam_findings' => $highRisk ? 'Right reducible inguinal hernia; no strangulation' : 'Wound clean; mild erythema; no discharge', 'asa_class' => $highRisk ? 'II' : 'I', 'fitness' => 'Fit for day-case anaesthesia', 'site_marking' => $highRisk, ], $ownerRef, $ownerRef, 'exam'); if ($highRisk) { $clinical->upsert($organization, $visit, 'surgery', 'surg_plan', [ 'diagnosis' => 'Right inguinal hernia', 'proposed_procedure' => 'Open mesh hernia repair', 'plan' => 'Day-case repair; consent discussed including mesh risks', 'consent_obtained' => true, 'procedure_planned' => true, 'follow_up' => 'Wound check 7–10 days', 'advice' => 'NPO from midnight; bring meds list', ], $ownerRef, $ownerRef, 'plan'); } if (! $visit->specialty_stage) { $visit->update(['specialty_stage' => $highRisk ? 'plan' : 'exam']); } $index++; } } private function seedVaccinationClinicalDemo(Organization $organization, string $ownerRef): void { $branchIds = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->pluck('id'); if ($branchIds->isEmpty()) { return; } $departmentIds = Department::owned($ownerRef) ->whereIn('branch_id', $branchIds) ->where('type', 'vaccination') ->where('is_active', true) ->pluck('id'); if ($departmentIds->isEmpty()) { return; } $appointments = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->whereNotNull('visit_id') ->with('visit') ->orderBy('branch_id') ->orderBy('id') ->get(); if ($appointments->isEmpty()) { return; } $clinical = app(SpecialtyClinicalRecordService::class); $index = 0; foreach ($appointments as $appointment) { $visit = $appointment->visit; if (! $visit) { continue; } $alertCase = $index === 0; $clinical->upsert($organization, $visit, 'vaccination', 'vax_eligibility', [ 'vaccine' => $alertCase ? 'Yellow fever' : 'Tetanus toxoid', 'schedule' => $alertCase ? 'Travel' : 'Routine EPI', 'prior_doses' => $alertCase ? 'None documented' : 'Primary series complete', 'indications' => $alertCase ? 'Travel certificate required' : 'Antenatal TT booster', 'contraindications' => true, 'allergy_history' => 'NKDA', 'fever_today' => $alertCase, 'pregnancy' => ! $alertCase, 'cleared' => ! $alertCase, ], $ownerRef, $ownerRef, 'eligibility'); $clinical->upsert($organization, $visit, 'vaccination', 'vax_consent', [ 'information_given' => true, 'consent_obtained' => ! $alertCase, 'consent_type' => 'Verbal', 'consented_by' => 'Patient', 'risks_discussed' => true, 'notes' => $alertCase ? 'Defer pending fever resolution' : 'Consent for TT booster', ], $ownerRef, $ownerRef, $alertCase ? 'eligibility' : 'consent'); if (! $alertCase) { $clinical->upsert($organization, $visit, 'vaccination', 'vax_admin', [ 'vaccine' => 'Tetanus toxoid', 'dose' => '0.5 mL', 'site' => 'Left arm', 'batch' => 'TT-DEMO-'.($index + 1), 'expiry' => now()->addYear()->toDateString(), 'route' => 'IM', 'administered_by' => 'Demo nurse', 'adverse_events' => 'None immediate', 'next_due' => now()->addMonths(6)->toDateString(), 'outcome' => 'Completed — observe', ], $ownerRef, $ownerRef, 'administer'); } if (! $visit->specialty_stage) { $visit->update(['specialty_stage' => $alertCase ? 'eligibility' : 'administer']); } $index++; } } private function seedPathologyClinicalDemo(Organization $organization, string $ownerRef): void { $branchIds = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->pluck('id'); if ($branchIds->isEmpty()) { return; } $departmentIds = Department::owned($ownerRef) ->whereIn('branch_id', $branchIds) ->where('type', 'pathology') ->where('is_active', true) ->pluck('id'); if ($departmentIds->isEmpty()) { return; } $appointments = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->whereNotNull('visit_id') ->with('visit') ->orderBy('branch_id') ->orderBy('id') ->get(); if ($appointments->isEmpty()) { return; } $clinical = app(SpecialtyClinicalRecordService::class); $index = 0; foreach ($appointments as $appointment) { $visit = $appointment->visit; if (! $visit) { continue; } $urgent = $index === 0; $clinical->upsert($organization, $visit, 'pathology', 'path_request', [ 'specimen_type' => $urgent ? 'Breast core biopsy' : 'Cervical cytology', 'site' => $urgent ? 'Left breast' : 'Cervix', 'clinical_history' => $urgent ? 'Palpable mass; BIRADS 4; rule out malignancy' : 'Routine Pap smear; previous NILM', 'urgency' => $urgent ? 'Urgent' : 'Routine', 'tests_requested' => $urgent ? 'Histo + IHC if indicated' : 'Pap smear', 'requesting_clinician' => 'Demo clinician', ], $ownerRef, $ownerRef, 'request'); $clinical->upsert($organization, $visit, 'pathology', 'path_specimen', [ 'received_at' => now()->subHours(2)->format('Y-m-d H:i'), 'container_label' => 'PATH-DEMO-'.($index + 1), 'adequacy' => $urgent ? 'Adequate' : 'Adequate', 'fixative' => $urgent ? '10% formalin' : 'Liquid-based cytology', 'notes' => 'Received sealed and labelled', ], $ownerRef, $ownerRef, 'specimen'); if ($urgent) { $clinical->upsert($organization, $visit, 'pathology', 'path_processing', [ 'gross' => 'Two cores, 1.2 cm aggregate', 'blocks' => 'A1–A2', 'stains' => 'H&E', 'status' => 'Ready for reporting', 'notes' => 'Priority processing', ], $ownerRef, $ownerRef, 'processing'); } if (! $visit->specialty_stage) { $visit->update(['specialty_stage' => $urgent ? 'processing' : 'specimen']); } $index++; } } private function seedInfusionClinicalDemo(Organization $organization, string $ownerRef): void { $branchIds = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->pluck('id'); if ($branchIds->isEmpty()) { return; } $departmentIds = Department::owned($ownerRef) ->whereIn('branch_id', $branchIds) ->where('type', 'infusion') ->where('is_active', true) ->pluck('id'); if ($departmentIds->isEmpty()) { return; } $appointments = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->whereNotNull('visit_id') ->with('visit') ->orderBy('branch_id') ->orderBy('id') ->get(); if ($appointments->isEmpty()) { return; } $clinical = app(SpecialtyClinicalRecordService::class); $index = 0; foreach ($appointments as $appointment) { $visit = $appointment->visit; if (! $visit) { continue; } $notFit = $index === 0; $clinical->upsert($organization, $visit, 'infusion', 'infusion_assessment', [ 'indication' => $notFit ? 'IV iron — symptomatic anaemia' : 'IV antibiotics — cellulitis', 'history' => $notFit ? 'Hb 7.8; prior infusion reaction reported' : 'Day 3 of outpatient IV ceftriaxone', 'allergies' => $notFit ? 'Possible iron sucrose reaction' : 'NKDA', 'baseline_vitals' => $notFit ? 'BP 88/54; HR 112' : 'BP 118/74; HR 78', 'access_plan' => 'Peripheral IV', 'fit_for_infusion' => ! $notFit, ], $ownerRef, $ownerRef, 'assessment'); if (! $notFit) { $clinical->upsert($organization, $visit, 'infusion', 'infusion_protocol', [ 'medication' => 'Ceftriaxone', 'dose' => '2 g', 'diluent' => '100 mL NS', 'rate' => '30 minutes', 'duration' => '30 min', 'premeds' => 'None', 'ordered_by' => 'Demo clinician', ], $ownerRef, $ownerRef, 'protocol'); $clinical->upsert($organization, $visit, 'infusion', 'infusion_session', [ 'medication' => 'Ceftriaxone', 'dose' => '2 g', 'rate' => '30 min', 'access' => 'Left forearm PIV', 'start_time' => now()->subMinutes(20)->format('H:i'), 'end_time' => '', 'vitals' => 'Stable mid-infusion', 'reactions' => 'None', 'notes' => 'Infusing without incident', 'outcome' => 'In progress', ], $ownerRef, $ownerRef, 'session'); } if (! $visit->specialty_stage) { $visit->update(['specialty_stage' => $notFit ? 'assessment' : 'session']); } $index++; } } private function seedDermatologyClinicalDemo(Organization $organization, string $ownerRef): void { $branchIds = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->pluck('id'); if ($branchIds->isEmpty()) { return; } $departmentIds = Department::owned($ownerRef) ->whereIn('branch_id', $branchIds) ->where('type', 'dermatology') ->where('is_active', true) ->pluck('id'); if ($departmentIds->isEmpty()) { return; } $appointments = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->whereNotNull('visit_id') ->with('visit') ->orderBy('branch_id') ->orderBy('id') ->get(); if ($appointments->isEmpty()) { return; } $clinical = app(SpecialtyClinicalRecordService::class); $index = 0; foreach ($appointments as $appointment) { $visit = $appointment->visit; if (! $visit) { continue; } $urgent = $index === 0; $clinical->upsert( $organization, $visit, 'dermatology', 'derm_history', [ 'history' => $urgent ? 'Changing pigmented lesion on forearm noticed by partner' : 'Itchy rash on elbows for 6 weeks', 'past_skin' => $urgent ? 'Prior atypical naevus excised 2018' : 'Childhood eczema', 'medications' => $urgent ? 'None' : 'Emollients PRN', 'allergies' => 'NKDA', 'triggers' => $urgent ? 'Sun exposure' : 'Dry weather; detergents', 'family_history' => $urgent ? 'Mother melanoma' : 'None relevant', ], $ownerRef, $ownerRef, 'history', ); $clinical->upsert( $organization, $visit, 'dermatology', 'skin_exam', [ 'chief_complaint' => $urgent ? 'Changing mole' : 'Elbow rash', 'urgency' => $urgent ? 'Suspected malignancy' : 'Routine', 'duration' => $urgent ? '3 months' : '6 weeks', 'distribution' => $urgent ? 'Left forearm' : 'Bilateral elbows', 'morphology' => $urgent ? 'Asymmetric pigmented plaque with irregular border' : 'Erythematous plaques with silvery scale', 'itch_pain' => $urgent ? 'None' : 'Moderate itch', 'dermoscopy' => $urgent ? 'Atypical pigment network' : 'Regular scale pattern', 'differential' => $urgent ? 'Melanoma vs atypical naevus' : 'Psoriasis vs eczema', ], $ownerRef, $ownerRef, 'exam', ); if ($urgent) { $clinical->upsert( $organization, $visit, 'dermatology', 'derm_plan', [ 'diagnosis' => 'Suspicious pigmented lesion — rule out melanoma', 'plan' => 'Excision biopsy; sun protection counselling', 'medications' => 'None pending histology', 'procedure_planned' => true, 'follow_up' => 'Histology results in 10–14 days', 'advice' => 'Avoid sunburn; photograph lesion site', ], $ownerRef, $ownerRef, 'plan', ); } if (! $visit->specialty_stage) { $visit->update(['specialty_stage' => $urgent ? 'plan' : 'exam']); } $index++; } } private function seedPodiatryClinicalDemo(Organization $organization, string $ownerRef): void { $branchIds = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->pluck('id'); if ($branchIds->isEmpty()) { return; } $departmentIds = Department::owned($ownerRef) ->whereIn('branch_id', $branchIds) ->where('type', 'podiatry') ->where('is_active', true) ->pluck('id'); if ($departmentIds->isEmpty()) { return; } $appointments = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->whereNotNull('visit_id') ->with('visit') ->orderBy('branch_id') ->orderBy('id') ->get(); if ($appointments->isEmpty()) { return; } $clinical = app(SpecialtyClinicalRecordService::class); $index = 0; foreach ($appointments as $appointment) { $visit = $appointment->visit; if (! $visit) { continue; } $highRisk = $index === 0; $clinical->upsert( $organization, $visit, 'podiatry', 'pod_history', [ 'history' => $highRisk ? 'Non-healing plantar ulcer for 3 weeks' : 'Ingrown toenail with intermittent pain', 'diabetes_history' => $highRisk ? 'Type 2 DM 12 years; peripheral neuropathy' : 'No diabetes', 'medications' => $highRisk ? 'Metformin, insulin' : 'None', 'allergies' => 'NKDA', 'footwear' => $highRisk ? 'Ill-fitting sandals' : 'Closed shoes at work', 'prior_ulcers' => $highRisk ? 'Prior healed ulcer right foot 2022' : 'None', ], $ownerRef, $ownerRef, 'history', ); $clinical->upsert( $organization, $visit, 'podiatry', 'foot_exam', [ 'chief_complaint' => $highRisk ? 'Plantar ulcer' : 'Ingrown toenail', 'side' => $highRisk ? 'Right' : 'Left', 'diabetes' => $highRisk ? 'Active ulcer' : 'N/A', 'pulses' => $highRisk ? 'Dorsalis pedis weak' : 'Present bilaterally', 'sensation' => $highRisk ? 'Reduced monofilament' : 'Intact', 'skin_nails' => $highRisk ? 'Callus surrounding ulcer' : 'Medial nail fold inflamed', 'ulcer' => $highRisk ? '2 cm plantar ulcer, clean base' : '', 'offloading' => $highRisk ? 'Felt pad interim' : 'Wide toe box advised', ], $ownerRef, $ownerRef, 'exam', ); if ($highRisk) { $clinical->upsert( $organization, $visit, 'podiatry', 'pod_plan', [ 'diagnosis' => 'Diabetic foot ulcer — high risk', 'plan' => 'Debridement; offloading; wound care; vascular review if needed', 'offloading' => 'Total contact cast or removable boot', 'medications' => 'Dressings; analgesia as needed', 'procedure_planned' => true, 'follow_up' => '1 week wound clinic', 'advice' => 'Daily foot check; avoid barefoot walking', ], $ownerRef, $ownerRef, 'plan', ); } if (! $visit->specialty_stage) { $visit->update(['specialty_stage' => $highRisk ? 'plan' : 'exam']); } $index++; } } private function seedFertilityClinicalDemo(Organization $organization, string $ownerRef): void { $branchIds = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->pluck('id'); if ($branchIds->isEmpty()) { return; } $departmentIds = Department::owned($ownerRef) ->whereIn('branch_id', $branchIds) ->where('type', 'fertility') ->where('is_active', true) ->pluck('id'); if ($departmentIds->isEmpty()) { return; } $appointments = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->whereNotNull('visit_id') ->with('visit') ->orderBy('branch_id') ->orderBy('id') ->get(); if ($appointments->isEmpty()) { return; } $clinical = app(SpecialtyClinicalRecordService::class); $index = 0; foreach ($appointments as $appointment) { $visit = $appointment->visit; if (! $visit) { continue; } $priority = $index === 0; $clinical->upsert( $organization, $visit, 'fertility', 'fert_history', [ 'history' => $priority ? 'Trying to conceive 28 months; age 38; prior miscarriage' : 'Trying to conceive 14 months; regular cycles', 'trying_months' => $priority ? 28 : 14, 'obstetric_history' => $priority ? 'G1P0; miscarriage at 8 weeks 2024' : 'G0P0; cycles 28–30 days', 'partner_notes' => $priority ? 'Partner semen analysis pending' : 'Partner well; no known issues', 'medications' => 'Folic acid', 'allergies' => 'NKDA', ], $ownerRef, $ownerRef, 'history', ); $clinical->upsert( $organization, $visit, 'fertility', 'fert_exam', [ 'chief_complaint' => $priority ? 'Primary infertility — time-sensitive' : 'Fertility workup', 'priority' => $priority ? 'Time-sensitive' : 'Routine', 'partner_present' => true, 'cycle_day' => $priority ? 3 : 10, 'bmi' => $priority ? 27 : 23, 'exam_findings' => 'Pelvic exam unremarkable; uterus anteverted', 'notes' => $priority ? 'Discussed AMH and early referral pathway' : 'Baseline counselling given', ], $ownerRef, $ownerRef, 'exam', ); if ($priority) { $clinical->upsert( $organization, $visit, 'fertility', 'fert_plan', [ 'diagnosis' => 'Primary infertility — age-related priority', 'plan' => 'Complete AMH/hormones; semen analysis; pelvic scan; consider IUI counselling', 'medications' => 'Continue folic acid; ovulation induction if indicated', 'cycle_planned' => true, 'follow_up' => 'Cycle day 3 labs; review with results', 'advice' => 'Timed intercourse education; lifestyle counselling', ], $ownerRef, $ownerRef, 'plan', ); } if (! $visit->specialty_stage) { $visit->update(['specialty_stage' => $priority ? 'plan' : 'exam']); } $index++; } } private function seedChildWelfareClinicalDemo(Organization $organization, string $ownerRef): void { $branchIds = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->pluck('id'); if ($branchIds->isEmpty()) { return; } $departmentIds = Department::owned($ownerRef) ->whereIn('branch_id', $branchIds) ->where('type', 'child_welfare') ->where('is_active', true) ->pluck('id'); if ($departmentIds->isEmpty()) { return; } $appointments = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->whereNotNull('visit_id') ->with('visit') ->orderBy('branch_id') ->orderBy('id') ->get(); if ($appointments->isEmpty()) { return; } $clinical = app(SpecialtyClinicalRecordService::class); $index = 0; foreach ($appointments as $appointment) { $visit = $appointment->visit; if (! $visit) { continue; } $concern = $index === 0; $clinical->upsert( $organization, $visit, 'child_welfare', 'cwc_intake', [ 'visit_reason' => $concern ? 'Growth monitoring' : 'Routine well-child', 'caregiver' => $concern ? 'Mother — Ama Mensah' : 'Father — Kojo Boateng', 'history' => $concern ? 'Weight faltering over 2 months; reduced appetite; breast milk + porridge' : 'Well since last CWC; no illness; immunizations on track', 'birth_history' => $concern ? 'Term SVD; birth weight 2.8 kg' : 'Term SVD; birth weight 3.2 kg', 'immunization_status' => $concern ? 'Catch-up needed' : 'Up to date', 'feeding' => $concern ? 'Breastfeeding + thin porridge twice daily' : 'Breastfeeding; complementary feeds started', 'social_context' => $concern ? 'Single caregiver; food insecurity reported' : 'Stable household', 'safeguarding_notes' => $concern ? 'Nutrition concern; monitor home situation — clinical record access only' : '', 'allergies' => 'NKDA', ], $ownerRef, $ownerRef, 'intake', ); $clinical->upsert( $organization, $visit, 'child_welfare', 'cwc_assessment', [ 'age_months' => $concern ? 18 : 9, 'weight_kg' => $concern ? 8.1 : 8.6, 'height_cm' => $concern ? 76 : 70, 'head_circumference_cm' => $concern ? 45 : 44, 'muac_cm' => $concern ? 11.2 : 13.5, 'growth_status' => $concern ? 'Underweight' : 'Normal', 'nutrition_risk' => $concern ? 'High' : 'Low', 'development' => $concern ? 'Walks with support; 3–4 words' : 'Sits well; babbling', 'exam_findings' => $concern ? 'Thin; dry skin; no oedema; alert' : 'Well nourished; soft fontanelle; clear chest', 'safeguarding_flag' => $concern ? 'Concern' : 'None', 'assessment_summary' => $concern ? 'Underweight with high nutrition risk — counselling and close follow-up' : 'Healthy well-child visit; continue routine CWC schedule', ], $ownerRef, $ownerRef, 'assessment', ); if ($concern) { $clinical->upsert( $organization, $visit, 'child_welfare', 'cwc_plan', [ 'goals' => 'Restore weight gain; improve dietary diversity; complete catch-up immunizations', 'interventions' => 'Nutrition counselling; RUTF if eligible; growth recheck', 'counseling' => 'Feeding frequency; hygiene; danger signs', 'referrals' => 'Consider nutrition clinic if no weight gain in 2 weeks', 'medications' => 'Vitamin A if due; deworming per schedule', 'follow_up' => '2 weeks CWC', 'advice' => 'Return sooner if fever, diarrhoea, or poor feeding', ], $ownerRef, $ownerRef, 'plan', ); } if (! $visit->specialty_stage) { $visit->update(['specialty_stage' => $concern ? 'plan' : 'assessment']); } $index++; } } private function seedAmbulanceClinicalDemo(Organization $organization, string $ownerRef): void { $branchIds = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->pluck('id'); if ($branchIds->isEmpty()) { return; } $departmentIds = Department::owned($ownerRef) ->whereIn('branch_id', $branchIds) ->where('type', 'ambulance') ->where('is_active', true) ->pluck('id'); if ($departmentIds->isEmpty()) { return; } $appointments = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->whereNotNull('visit_id') ->with('visit') ->orderBy('branch_id') ->orderBy('id') ->get(); if ($appointments->isEmpty()) { return; } $clinical = app(SpecialtyClinicalRecordService::class); $index = 0; foreach ($appointments as $appointment) { $visit = $appointment->visit; if (! $visit) { continue; } $critical = $index === 0; $clinical->upsert( $organization, $visit, 'ambulance', 'amb_dispatch', [ 'call_nature' => $critical ? 'Medical emergency' : 'Transfer', 'priority' => $critical ? 'Immediate' : 'Routine', 'location' => $critical ? 'Kaneshie Market Road' : 'Ridge Hospital ward 3', 'caller_info' => $critical ? 'Bystander — 024XXXXXXX' : 'Ward nurse', 'crew' => $critical ? 'Paramedic + EMT' : 'EMT crew', 'vehicle' => $critical ? 'AMB-01' : 'AMB-02', 'dispatch_notes' => $critical ? 'Chest pain, diaphoretic; request ALS response' : 'Stable inter-facility transfer for imaging', 'eta_minutes' => $critical ? 8 : 20, ], $ownerRef, $ownerRef, 'dispatch', ); $clinical->upsert( $organization, $visit, 'ambulance', 'amb_scene', [ 'chief_complaint' => $critical ? 'Chest pain' : 'Inter-facility transfer', 'acuity' => $critical ? 'Critical' : 'Stable', 'mechanism' => $critical ? 'Sudden onset at rest' : 'Scheduled transfer', 'airway_ok' => true, 'breathing_ok' => $critical ? false : true, 'circulation_ok' => $critical ? false : true, 'gcs' => 15, 'vitals_summary' => $critical ? 'BP 88/54, HR 118, SpO2 91% on air, RR 28' : 'BP 124/78, HR 82, SpO2 98%, RR 16', 'scene_findings' => $critical ? 'Pale, diaphoretic, ongoing central chest pain radiating to left arm' : 'Patient comfortable on stretcher; IV in situ', 'hazards' => $critical ? 'Busy roadside' : 'None', ], $ownerRef, $ownerRef, 'on_scene', ); if ($critical) { $clinical->upsert( $organization, $visit, 'ambulance', 'amb_en_route', [ 'destination' => 'Korle Bu Teaching Hospital ED', 'interventions' => 'Oxygen via NRB; aspirin; IV access; cardiac monitoring', 'medications' => 'Aspirin 300 mg PO', 'response_to_treatment' => 'Pain partially eased; SpO2 improved to 95%', 'monitoring' => 'Continuous ECG; vitals q5min', 'deterioration' => false, 'eta_facility' => '12 minutes', 'notes' => 'Pre-alerted ED resus', ], $ownerRef, $ownerRef, 'transport', ); } if (! $visit->specialty_stage) { $visit->update(['specialty_stage' => $critical ? 'transport' : 'on_scene']); } $index++; } } private function seedDentistryClinicalDemo(Organization $organization, string $ownerRef): void { // Specialty departments are provisioned without organization_id; scope via branches. $branchIds = Branch::owned($ownerRef) ->where('organization_id', $organization->id) ->pluck('id'); if ($branchIds->isEmpty()) { return; } $departmentIds = Department::owned($ownerRef) ->whereIn('branch_id', $branchIds) ->where('type', 'dental') ->where('is_active', true) ->pluck('id'); if ($departmentIds->isEmpty()) { return; } $appointment = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN, Appointment::STATUS_IN_CONSULTATION]) ->whereNotNull('visit_id') ->with(['patient', 'visit']) ->latest('id') ->first(); if (! $appointment?->patient || ! $appointment->visit) { return; } $charts = app(\App\Services\Care\Dentistry\DentalChartService::class); $plans = app(\App\Services\Care\Dentistry\DentalTreatmentPlanService::class); $procedures = app(\App\Services\Care\Dentistry\DentalProcedureService::class); $chart = $charts->chartForPatient($organization, $appointment->patient, $ownerRef); $charts->updateTooth($chart, '16', [ 'status' => 'present', 'surfaces' => ['O'], 'conditions' => ['caries'], 'notes' => 'Demo occlusal caries', ], $ownerRef, $ownerRef, $appointment->visit); $plan = $plans->ensurePlan($organization, $appointment->patient, $ownerRef, $appointment->visit, $ownerRef); if ($plan->items()->count() === 0) { $plans->addItem($plan, $organization, [ 'procedure_code' => 'den.fill', 'tooth_code' => '16', 'surfaces' => ['O'], 'priority' => 10, 'notes' => 'Demo filling', ], $ownerRef, $ownerRef); $plans->accept($plan->fresh('items'), $ownerRef, $ownerRef); } $completedPatient = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('department_id', $departmentIds) ->where('status', Appointment::STATUS_COMPLETED) ->whereNotNull('visit_id') ->with('visit') ->latest('id') ->first(); if ($completedPatient?->visit && ! \App\Models\DentalProcedure::owned($ownerRef)->where('visit_id', $completedPatient->visit_id)->exists()) { try { $procedures->complete($organization, $completedPatient->visit, [ 'procedure_code' => 'den.fill', 'tooth_code' => '26', 'surfaces' => ['O'], 'anesthesia' => 'Local', 'bill' => true, 'notes' => 'Demo completed fill', ], $ownerRef, $ownerRef); } catch (\Throwable) { // Billing may be constrained in partial seeds — chart/plan still valuable. } } $perio = app(\App\Services\Care\Dentistry\DentalPerioService::class); if (! \App\Models\DentalPerioExam::owned($ownerRef)->where('patient_id', $appointment->patient_id)->exists()) { $perio->saveExam($organization, $appointment->patient, $ownerRef, [ ['tooth_code' => '16', 'surface' => 'B', 'probing_mm' => 3, 'bleeding' => true, 'plaque' => false], ['tooth_code' => '16', 'surface' => 'L', 'probing_mm' => 2, 'bleeding' => false, 'plaque' => true], ], $appointment->visit, 'Demo perio chart', $ownerRef); } $lab = app(\App\Services\Care\Dentistry\DentalLabCaseService::class); if (! \App\Models\DentalLabCase::owned($ownerRef)->where('patient_id', $appointment->patient_id)->exists()) { $lab->create($organization, $appointment->patient, $ownerRef, [ 'case_type' => 'crown', 'lab_name' => 'Demo Dental Lab', 'tooth_codes' => ['16'], 'status' => 'ordered', 'due_at' => now()->addDays(14)->toDateString(), 'notes' => 'Demo crown case', ], $appointment->visit, $ownerRef); } $recalls = app(\App\Services\Care\Dentistry\DentalRecallService::class); if (! \App\Models\DentalRecall::owned($ownerRef)->where('patient_id', $appointment->patient_id)->exists()) { $recalls->schedule($organization, $appointment->patient, $ownerRef, [ 'due_on' => now()->addMonths(6)->toDateString(), 'reason' => '6-month check', 'notes' => 'Demo recall', ], $ownerRef); } if (! $appointment->visit->specialty_stage) { $appointment->visit->update(['specialty_stage' => 'chair']); } } /** * @param list $branches * @param array> $departments * @param array $volumes * @return list */ private function seedPractitioners( Organization $organization, array $branches, array $departments, string $ownerRef, array $volumes, ): array { $practitioners = []; for ($i = 0; $i < $volumes['practitioners']; $i++) { $branch = $branches[$i % count($branches)]; $branchDepts = $departments[$branch->id] ?? []; $department = $branchDepts[$i % max(1, count($branchDepts))] ?? null; $key = sprintf('demo-prac-%s-%02d', $ownerRef, $i + 1); $practitioners[] = Practitioner::withTrashed()->updateOrCreate( [ 'organization_id' => $organization->id, 'user_ref' => $key, ], [ 'owner_ref' => $ownerRef, 'branch_id' => $branch->id, 'department_id' => $department?->id, 'member_id' => null, 'name' => 'Dr. '.self::FIRST_NAMES[$i % count(self::FIRST_NAMES)].' '.self::LAST_NAMES[$i % count(self::LAST_NAMES)], 'specialty' => self::SPECIALTIES[$i % count(self::SPECIALTIES)], 'room' => 'Consultation Room '.($i + 1), 'is_active' => true, 'deleted_at' => null, ], ); $practitioners[array_key_last($practitioners)]->syncAssignedBranches([(int) $branch->id]); } return $practitioners; } /** * @param list $branches * @param array $volumes * @return list */ private function seedPatients( Organization $organization, array $branches, string $ownerRef, array $volumes, ): array { $worldPeople = DemoWorld::people(); $patients = []; for ($i = 0; $i < $volumes['patients']; $i++) { $branch = $branches[$i % count($branches)]; $number = sprintf('DEMO-%s-%05d', Str::upper(Str::substr(md5($ownerRef), 0, 4)), $i + 1); $person = $worldPeople[$i] ?? null; if ($person === null) { $base = $worldPeople[$i % count($worldPeople)]; $seq = $i + 1; $person = [ 'first_name' => $base['first_name'], 'last_name' => $base['last_name'], 'gender' => $base['gender'], 'dob' => $base['dob'], 'phone' => '+23324'.str_pad((string) (1000000 + $seq), 7, '0', STR_PAD_LEFT), 'email' => sprintf('patient.%d@demo.ladill.com', $seq), 'national_id' => sprintf('GHA-729%06d', $seq), 'city' => $base['city'], 'address' => $base['address'], ]; } $patients[] = Patient::withTrashed()->updateOrCreate( [ 'organization_id' => $organization->id, 'patient_number' => $number, ], [ 'uuid' => $this->demoUuid("patient|{$ownerRef}|{$i}"), 'owner_ref' => $ownerRef, 'branch_id' => $branch->id, 'first_name' => $person['first_name'], 'last_name' => $person['last_name'], 'gender' => $person['gender'], 'date_of_birth' => $person['dob'], 'phone' => $person['phone'], 'email' => $person['email'], 'national_id' => $person['national_id'], 'city' => $person['city'], 'region' => 'Greater Accra', 'address' => $person['address'], 'deleted_at' => null, ], ); } return $patients; } /** * @param list $branches * @param array> $departments * @param list $practitioners * @param list $patients * @param array $volumes */ private function seedAppointmentsAndClinical( Organization $organization, array $branches, array $departments, array $practitioners, array $patients, string $ownerRef, array $volumes, ): int { $statuses = [ Appointment::STATUS_SCHEDULED, Appointment::STATUS_CHECKED_IN, Appointment::STATUS_WAITING, Appointment::STATUS_COMPLETED, Appointment::STATUS_COMPLETED, Appointment::STATUS_CANCELLED, ]; $reasons = ['Follow-up', 'New complaint', 'Review labs', 'Chronic care']; $symptoms = ['Fever', 'Cough', 'Headache', 'Fatigue']; $count = 0; for ($i = 0; $i < $volumes['appointments']; $i++) { $patient = $patients[$i % count($patients)]; $practitioner = $practitioners[$i % count($practitioners)]; $branch = collect($branches)->first( fn (Branch $b) => (int) $b->id === (int) $practitioner->branch_id, ) ?? $branches[$i % count($branches)]; $branchDepts = $departments[(int) $practitioner->branch_id] ?? ($departments[$branch->id] ?? []); // Align department to the practitioner desk so Queue tickets stay consultation-context. $department = collect($branchDepts)->first( fn (Department $dept) => (int) $dept->id === (int) ($practitioner->department_id ?? 0), ) ?? ($branchDepts[$i % max(1, count($branchDepts))] ?? null); $status = $statuses[$i % count($statuses)]; $scheduledAt = now()->subDays(14)->addHours($i * 3); $visit = null; if (in_array($status, [Appointment::STATUS_CHECKED_IN, Appointment::STATUS_WAITING, Appointment::STATUS_COMPLETED], true)) { $visit = Visit::withTrashed()->updateOrCreate( ['uuid' => $this->demoUuid("visit|{$ownerRef}|{$i}")], [ 'owner_ref' => $ownerRef, 'organization_id' => $organization->id, 'branch_id' => $branch->id, 'patient_id' => $patient->id, 'status' => $status === Appointment::STATUS_COMPLETED ? Visit::STATUS_COMPLETED : Visit::STATUS_IN_PROGRESS, 'checked_in_at' => $scheduledAt->copy()->addMinutes(5), 'completed_at' => $status === Appointment::STATUS_COMPLETED ? $scheduledAt->copy()->addHour() : null, 'checked_in_by' => $ownerRef, 'deleted_at' => null, ], ); } Appointment::withTrashed()->updateOrCreate( ['uuid' => $this->demoUuid("appt|{$ownerRef}|{$i}")], [ 'owner_ref' => $ownerRef, 'organization_id' => $organization->id, 'branch_id' => $branch->id, 'patient_id' => $patient->id, 'practitioner_id' => $practitioner->id, 'department_id' => $department?->id, 'visit_id' => $visit?->id, 'type' => $i % 5 === 0 ? Appointment::TYPE_WALK_IN : Appointment::TYPE_SCHEDULED, 'status' => $status, 'scheduled_at' => $scheduledAt, 'checked_in_at' => $visit?->checked_in_at, 'waiting_at' => in_array($status, [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN], true) ? $scheduledAt->copy()->addMinutes(5) : null, 'queue_position' => in_array($status, [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN], true) ? ($i + 1) : null, 'completed_at' => $status === Appointment::STATUS_COMPLETED ? $scheduledAt->copy()->addHour() : null, 'cancelled_at' => $status === Appointment::STATUS_CANCELLED ? $scheduledAt->copy()->subHour() : null, 'reason' => $reasons[$i % count($reasons)], 'created_by' => $ownerRef, 'deleted_at' => null, ], ); if ($visit && $status === Appointment::STATUS_COMPLETED) { $consultation = Consultation::withTrashed()->updateOrCreate( ['uuid' => $this->demoUuid("consult|{$ownerRef}|{$i}")], [ 'owner_ref' => $ownerRef, 'visit_id' => $visit->id, 'appointment_id' => null, 'practitioner_id' => $practitioner->id, 'patient_id' => $patient->id, 'status' => Consultation::STATUS_COMPLETED, 'symptoms' => $symptoms[$i % count($symptoms)], 'clinical_notes' => 'Demo consultation notes.', 'started_at' => $scheduledAt->copy()->addMinutes(15), 'completed_at' => $scheduledAt->copy()->addHour(), 'completed_by' => $ownerRef, 'deleted_at' => null, ], ); VitalSign::query()->updateOrCreate( [ 'consultation_id' => $consultation->id, 'recorded_at' => $scheduledAt->copy()->addMinutes(10), ], [ 'owner_ref' => $ownerRef, 'bp_systolic' => 110 + ($i % 30), 'bp_diastolic' => 70 + ($i % 15), 'pulse' => 60 + ($i % 40), 'temperature' => 36.5 + (($i % 10) / 10), 'weight_kg' => 55 + ($i % 40), 'height_cm' => 150 + ($i % 40), 'spo2' => 95 + ($i % 5), 'respiratory_rate' => 14 + ($i % 6), 'recorded_by' => $ownerRef, ], ); } $count++; } return $count; } /** * @param list $branches * @param list $practitioners * @param list $patients * @param array $volumes */ /** * Demo hardware inventory: browser wedge scanner + agent thermometer per branch (Pro+). * * @param list $branches */ private function seedDevices(Organization $organization, array $branches, string $ownerRef): void { $deviceService = app(DeviceService::class); foreach ($branches as $i => $branch) { Device::withTrashed()->updateOrCreate( [ 'organization_id' => $organization->id, 'branch_id' => $branch->id, 'name' => 'Demo barcode scanner', ], [ 'owner_ref' => $ownerRef, 'type' => 'barcode_scanner', 'connection_mode' => Device::MODE_BROWSER_WEDGE, 'status' => Device::STATUS_OFFLINE, 'device_token_hash' => null, 'metadata' => ['demo' => true], 'deleted_at' => null, ], ); $thermo = Device::withTrashed()->updateOrCreate( [ 'organization_id' => $organization->id, 'branch_id' => $branch->id, 'name' => 'Demo thermometer', ], [ 'owner_ref' => $ownerRef, 'type' => 'thermometer', 'connection_mode' => Device::MODE_AGENT, 'status' => Device::STATUS_OFFLINE, 'metadata' => ['demo' => true, 'note' => 'Token issued on first seed only if missing'], 'deleted_at' => null, ], ); if (! $thermo->device_token_hash) { // Deterministic demo token so sales can reuse docs examples after reseed. $plain = 'demo-care-device-'.$organization->id.'-'.$branch->id.'-thermo'; $thermo->update(['device_token_hash' => $deviceService->hashToken($plain)]); } } } /** * @return list */ private function seedInvestigationCatalog(Organization $organization, string $ownerRef): array { $types = []; foreach ([ ['name' => 'Fasting Blood Glucose', 'code' => 'FBG', 'category' => 'blood', 'unit' => 'mmol/L', 'low' => 3.9, 'high' => 6.1, 'price' => 2500], ['name' => 'Full Blood Count', 'code' => 'FBC', 'category' => 'blood', 'unit' => null, 'low' => null, 'high' => null, 'price' => 4500], ['name' => 'Malaria Parasite', 'code' => 'MP', 'category' => 'blood', 'unit' => null, 'low' => null, 'high' => null, 'price' => 1500], ['name' => 'Urinalysis', 'code' => 'UA', 'category' => 'urine', 'unit' => null, 'low' => null, 'high' => null, 'price' => 2000], ['name' => 'HbA1c', 'code' => 'HBA1C', 'category' => 'blood', 'unit' => '%', 'low' => null, 'high' => 5.7, 'price' => 6000], ['name' => 'Lipid Profile', 'code' => 'LIPID', 'category' => 'blood', 'unit' => null, 'low' => null, 'high' => null, 'price' => 5500], ['name' => 'Liver Function Test', 'code' => 'LFT', 'category' => 'blood', 'unit' => null, 'low' => null, 'high' => null, 'price' => 5000], ['name' => 'Kidney Function Test', 'code' => 'KFT', 'category' => 'blood', 'unit' => null, 'low' => null, 'high' => null, 'price' => 5000], ['name' => 'Blood Group & Rh', 'code' => 'BGRH', 'category' => 'blood', 'unit' => null, 'low' => null, 'high' => null, 'price' => 1800], ['name' => 'Pregnancy Test (Urine)', 'code' => 'HCG', 'category' => 'urine', 'unit' => null, 'low' => null, 'high' => null, 'price' => 1200], ['name' => 'Stool Routine Examination', 'code' => 'SRE', 'category' => 'stool', 'unit' => null, 'low' => null, 'high' => null, 'price' => 2200], ['name' => 'Chest X-Ray', 'code' => 'CXR', 'category' => 'xray', 'unit' => null, 'low' => null, 'high' => null, 'price' => 8000], ] as $spec) { $types[] = InvestigationType::withTrashed()->updateOrCreate( [ 'organization_id' => $organization->id, 'code' => $spec['code'], ], [ 'owner_ref' => $ownerRef, 'name' => $spec['name'], 'category' => $spec['category'], 'unit' => $spec['unit'], 'reference_low' => $spec['low'], 'reference_high' => $spec['high'], 'price_minor' => $spec['price'], 'is_active' => true, 'deleted_at' => null, ], ); } return $types; } /** * @param list $branches * @param list $practitioners * @param list $patients * @param list $types */ private function seedPaidModules( Organization $organization, array $branches, array $practitioners, array $patients, string $ownerRef, array $volumes, array $types = [], ): void { if ($types === []) { $types = $this->seedInvestigationCatalog($organization, $ownerRef); } $drugs = []; foreach ([ ['name' => 'Amoxicillin 500mg', 'unit' => 'capsule', 'price' => 1500], ['name' => 'Paracetamol 500mg', 'unit' => 'tablet', 'price' => 50], ['name' => 'Metformin 500mg', 'unit' => 'tablet', 'price' => 80], ['name' => 'Amlodipine 5mg', 'unit' => 'tablet', 'price' => 120], ] as $i => $spec) { $drug = Drug::withTrashed()->updateOrCreate( [ 'organization_id' => $organization->id, 'sku' => 'DEMO-DRUG-'.($i + 1), ], [ 'owner_ref' => $ownerRef, 'branch_id' => $branches[0]->id, 'name' => $spec['name'], 'generic_name' => $spec['name'], 'unit' => $spec['unit'], 'unit_price_minor' => $spec['price'], 'reorder_level' => 20, 'is_active' => true, 'deleted_at' => null, ], ); DrugBatch::query()->updateOrCreate( [ 'drug_id' => $drug->id, 'batch_number' => 'DEMO-BATCH-'.($i + 1), ], [ 'owner_ref' => $ownerRef, 'expiry_date' => now()->addYear()->toDateString(), 'quantity_on_hand' => 200, 'cost_minor' => (int) ($spec['price'] * 0.6), 'received_at' => now()->subDays(10), ], ); $drugs[] = $drug; } if ($patients === [] || $practitioners === [] || $types === [] || $branches === []) { return; } for ($i = 0; $i < $volumes['lab_requests']; $i++) { $patient = $patients[$i % count($patients)]; $branch = $branches[$i % count($branches)]; $practitioner = $practitioners[$i % count($practitioners)]; $type = $types[$i % count($types)]; $visit = Visit::withTrashed()->updateOrCreate( ['uuid' => $this->demoUuid("lab-visit|{$ownerRef}|{$i}")], [ 'owner_ref' => $ownerRef, 'organization_id' => $organization->id, 'branch_id' => $branch->id, 'patient_id' => $patient->id, 'status' => Visit::STATUS_IN_PROGRESS, 'checked_in_at' => now()->subDays($i % 10), 'checked_in_by' => $ownerRef, 'deleted_at' => null, ], ); $consultation = Consultation::withTrashed()->updateOrCreate( ['uuid' => $this->demoUuid("lab-consult|{$ownerRef}|{$i}")], [ 'owner_ref' => $ownerRef, 'visit_id' => $visit->id, 'practitioner_id' => $practitioner->id, 'patient_id' => $patient->id, 'status' => Consultation::STATUS_DRAFT, 'started_at' => now()->subDays($i % 10), 'deleted_at' => null, ], ); InvestigationRequest::withTrashed()->updateOrCreate( ['uuid' => $this->demoUuid("lab-req|{$ownerRef}|{$i}")], [ 'owner_ref' => $ownerRef, 'organization_id' => $organization->id, 'branch_id' => $branch->id, 'visit_id' => $visit->id, 'consultation_id' => $consultation->id, 'patient_id' => $patient->id, 'investigation_type_id' => $type->id, 'practitioner_id' => $practitioner->id, 'status' => InvestigationRequest::STATUS_PENDING, 'priority' => 'routine', 'clinical_notes' => 'Demo lab request', 'requested_by' => $ownerRef, 'deleted_at' => null, ], ); } for ($i = 0; $i < $volumes['prescriptions']; $i++) { $patient = $patients[$i % count($patients)]; $branch = $branches[$i % count($branches)]; $practitioner = $practitioners[$i % count($practitioners)]; $drug = $drugs[$i % count($drugs)]; $visit = Visit::withTrashed()->updateOrCreate( ['uuid' => $this->demoUuid("rx-visit|{$ownerRef}|{$i}")], [ 'owner_ref' => $ownerRef, 'organization_id' => $organization->id, 'branch_id' => $branch->id, 'patient_id' => $patient->id, 'status' => Visit::STATUS_IN_PROGRESS, 'checked_in_at' => now()->subDays($i % 7), 'checked_in_by' => $ownerRef, 'deleted_at' => null, ], ); $consultation = Consultation::withTrashed()->updateOrCreate( ['uuid' => $this->demoUuid("rx-consult|{$ownerRef}|{$i}")], [ 'owner_ref' => $ownerRef, 'visit_id' => $visit->id, 'practitioner_id' => $practitioner->id, 'patient_id' => $patient->id, 'status' => Consultation::STATUS_DRAFT, 'started_at' => now()->subDays($i % 7), 'deleted_at' => null, ], ); $prescription = Prescription::withTrashed()->updateOrCreate( ['uuid' => $this->demoUuid("rx|{$ownerRef}|{$i}")], [ 'owner_ref' => $ownerRef, 'organization_id' => $organization->id, 'visit_id' => $visit->id, 'consultation_id' => $consultation->id, 'patient_id' => $patient->id, 'practitioner_id' => $practitioner->id, 'status' => Prescription::STATUS_ACTIVE, 'prescribed_by' => $ownerRef, 'deleted_at' => null, ], ); $prescription->items()->updateOrCreate( [ 'prescription_id' => $prescription->id, 'name' => $drug->name, ], [ 'owner_ref' => $ownerRef, 'drug_id' => $drug->id, 'dosage' => '1 '.$drug->unit, 'frequency' => 'BD', 'duration' => '5 days', 'quantity' => 10, 'sort_order' => 0, ], ); } $branchCount = max(1, count($branches)); for ($i = 0; $i < $volumes['bills']; $i++) { $patient = $patients[$i % count($patients)]; $branch = $branches[$i % $branchCount]; $fee = 5000 + (($i % 5) * 1000); // Payment mix is per-branch (not i%3 with branchCount=3), otherwise HQ // only ever gets paid invoices and the Ridge cashier has nothing to collect. $slot = intdiv($i, $branchCount) % 3; if ($slot === 0) { $status = Bill::STATUS_PAID; $amountPaid = $fee; $balance = 0; } elseif ($slot === 1) { $status = Bill::STATUS_PARTIAL; $amountPaid = (int) floor($fee / 2); $balance = $fee - $amountPaid; } else { $status = Bill::STATUS_OPEN; $amountPaid = 0; $balance = $fee; } $visit = Visit::withTrashed()->updateOrCreate( ['uuid' => $this->demoUuid("bill-visit|{$ownerRef}|{$i}")], [ 'owner_ref' => $ownerRef, 'organization_id' => $organization->id, 'branch_id' => $branch->id, 'patient_id' => $patient->id, 'status' => Visit::STATUS_COMPLETED, 'checked_in_at' => now()->subDays($i % 12), 'completed_at' => now()->subDays($i % 12)->addHour(), 'checked_in_by' => $ownerRef, 'deleted_at' => null, ], ); $bill = Bill::withTrashed()->updateOrCreate( ['uuid' => $this->demoUuid("bill|{$ownerRef}|{$i}")], [ 'owner_ref' => $ownerRef, 'organization_id' => $organization->id, 'branch_id' => $branch->id, 'visit_id' => $visit->id, 'patient_id' => $patient->id, 'invoice_number' => sprintf('DEMO-INV-%s-%04d', Str::upper(Str::substr(md5($ownerRef), 0, 4)), $i + 1), 'status' => $status, 'subtotal_minor' => $fee, 'discount_minor' => 0, 'tax_minor' => 0, 'total_minor' => $fee, 'amount_paid_minor' => $amountPaid, 'balance_minor' => $balance, 'created_by' => $ownerRef, 'finalized_at' => now()->subDays($i % 12), 'deleted_at' => null, ], ); BillLineItem::query()->updateOrCreate( [ 'bill_id' => $bill->id, 'description' => 'Consultation fee', ], [ 'owner_ref' => $ownerRef, 'type' => 'consultation', 'quantity' => 1, 'unit_price_minor' => $fee, 'total_minor' => $fee, ], ); $paymentUuid = $this->demoUuid("pay|{$ownerRef}|{$i}"); if ($amountPaid > 0) { Payment::query()->updateOrCreate( ['uuid' => $paymentUuid], [ 'owner_ref' => $ownerRef, 'bill_id' => $bill->id, 'amount_minor' => $amountPaid, 'method' => Payment::METHOD_CASH, 'status' => Payment::STATUS_PAID, 'reference' => 'DEMO-PAY-'.($i + 1), 'paid_at' => now()->subDays($i % 12), 'recorded_by' => $ownerRef, ], ); } else { Payment::query()->where('uuid', $paymentUuid)->delete(); } } } /** * @param list $branches * @param list $practitioners * @param list $patients */ private function seedAssessments( Organization $organization, array $branches, array $practitioners, array $patients, string $ownerRef, int $count, ): void { $template = AssessmentTemplate::withTrashed()->updateOrCreate( [ 'code' => self::DEMO_TEMPLATE_CODE, 'version' => 1, ], [ 'organization_id' => $organization->id, 'name' => 'Demo Triage Screen', 'category' => AssessmentTemplate::CATEGORY_SCREENING, 'description' => 'Lightweight demo assessment for enterprise analytics.', 'is_current' => true, 'is_active' => true, 'scoring_strategy' => 'sum_items', 'deleted_at' => null, ], ); $question = AssessmentQuestion::query()->updateOrCreate( [ 'template_id' => $template->id, 'code' => 'severity', ], [ 'section' => 'Triage', 'label' => 'Severity (0-10)', 'answer_type' => AssessmentQuestion::TYPE_SCALE, 'is_required' => true, 'sort_order' => 1, 'score_key' => 'severity', 'options' => ['min' => 0, 'max' => 10], ], ); for ($i = 0; $i < $count; $i++) { $patient = $patients[$i % count($patients)]; $branch = $branches[$i % count($branches)]; $practitioner = $practitioners[$i % count($practitioners)]; $scoreValue = (float) ($i % 11); $assessment = Assessment::withTrashed()->updateOrCreate( ['uuid' => $this->demoUuid("assess|{$ownerRef}|{$i}")], [ 'owner_ref' => $ownerRef, 'organization_id' => $organization->id, 'branch_id' => $branch->id, 'patient_id' => $patient->id, 'template_id' => $template->id, 'practitioner_id' => $practitioner->id, 'status' => Assessment::STATUS_COMPLETED, 'assessed_at' => now()->subDays($i % 30), 'completed_at' => now()->subDays($i % 30), 'completed_by' => $ownerRef, 'started_by' => $ownerRef, 'notes' => 'Demo assessment', 'deleted_at' => null, ], ); AssessmentAnswer::query()->updateOrCreate( [ 'assessment_id' => $assessment->id, 'question_id' => $question->id, ], [ 'owner_ref' => $ownerRef, 'value_number' => $scoreValue, ], ); AssessmentScore::query()->updateOrCreate( ['assessment_id' => $assessment->id], [ 'owner_ref' => $ownerRef, 'template_code' => self::DEMO_TEMPLATE_CODE, 'total_score' => $scoreValue, 'max_score' => 10, 'subscores' => ['severity' => $scoreValue], 'severity_label' => $scoreValue >= 7 ? 'high' : ($scoreValue >= 4 ? 'moderate' : 'low'), 'computed_at' => now()->subDays($i % 30), ], ); } } private function demoUuid(string $key): string { $hex = md5('ladill-care-demo|'.$key); return sprintf( '%s-%s-%s-%s-%s', substr($hex, 0, 8), substr($hex, 8, 4), substr($hex, 12, 4), substr($hex, 16, 4), substr($hex, 20, 12), ); } }