diff --git a/app/Http/Controllers/Care/SettingsController.php b/app/Http/Controllers/Care/SettingsController.php index c61c02e..5de75b2 100644 --- a/app/Http/Controllers/Care/SettingsController.php +++ b/app/Http/Controllers/Care/SettingsController.php @@ -79,7 +79,7 @@ class SettingsController extends Controller ]); } - public function update(Request $request, QueueClient $queue, CareQueueProvisioner $provisioner): RedirectResponse + public function update(Request $request, QueueClient $queue): RedirectResponse { $this->authorizeAbility($request, 'settings.manage'); $organization = $this->organization($request); @@ -136,6 +136,8 @@ class SettingsController extends Controller $wantsQueue = false; } $settings['queue_integration_enabled'] = $wantsQueue; + $queueWasEnabled = (bool) data_get($organization->settings, 'queue_integration_enabled'); + $queueJustEnabled = $wantsQueue && ! $queueWasEnabled; $settings[AppointmentPatientNotifier::SETTING_BOOKED_SMS] = $request->boolean('notify_appointment_booked_sms'); $settings[AppointmentPatientNotifier::SETTING_BOOKED_EMAIL] = $request->boolean('notify_appointment_booked_email'); @@ -184,27 +186,49 @@ class SettingsController extends Controller } } + // Queue department sync + waiting-ticket backfill can take minutes over HTTP. + // Never block the settings response on it — run after the redirect goes out. if ($wantsQueue && $queue->configured()) { - try { - $organization = $organization->fresh(); - $provisioner->provisionOrganization($organization); - // Backfill tickets for patients already waiting when Queue was just linked. - $bridge = app(CareQueueBridge::class); - $branches = Branch::owned($owner) - ->where('organization_id', $organization->id) - ->where('is_active', true) - ->pluck('id'); - foreach ($branches as $branchId) { - foreach (array_keys(CareQueueContexts::departments()) as $context) { - if ($context === CareQueueContexts::RECEPTION) { - continue; - } - $bridge->syncMissingTickets($organization, $context, (int) $branchId, 50); - } + $organizationId = (int) $organization->id; + $backfillTickets = $queueJustEnabled; + dispatch(function () use ($organizationId, $owner, $backfillTickets) { + $organization = Organization::query()->find($organizationId); + if (! $organization) { + return; } - } catch (\Throwable) { - // Best-effort; role pages will lazy-ensure on next use. - } + + try { + app(CareQueueProvisioner::class)->provisionOrganization($organization); + } catch (\Throwable) { + return; + } + + if (! $backfillTickets) { + return; + } + + try { + $bridge = app(CareQueueBridge::class); + $branches = Branch::owned($owner) + ->where('organization_id', $organization->id) + ->where('is_active', true) + ->pluck('id'); + foreach ($branches as $branchId) { + foreach (array_keys(CareQueueContexts::departments()) as $context) { + if ($context === CareQueueContexts::RECEPTION) { + continue; + } + try { + $bridge->syncMissingTickets($organization, $context, (int) $branchId, 25); + } catch (\Throwable) { + // Continue other contexts/branches. + } + } + } + } catch (\Throwable) { + // Best-effort; role pages will lazy-ensure on next use. + } + })->afterResponse(); } AuditLogger::record($owner, 'organization.updated', $organization->id, $owner, Organization::class, $organization->id); diff --git a/app/Services/Care/DemoTenantSeeder.php b/app/Services/Care/DemoTenantSeeder.php index a57fbe2..d363674 100644 --- a/app/Services/Care/DemoTenantSeeder.php +++ b/app/Services/Care/DemoTenantSeeder.php @@ -97,75 +97,75 @@ class DemoTenantSeeder $this->ensureOnboardedShell($user, $plan); if ($reset) { - DB::transaction(fn () => $this->resetTenant($ownerRef)); + $this->resetTenant($ownerRef); $this->ensureOnboardedShell($user, $plan); } - return DB::transaction(function () use ($user, $ownerRef, $plan, $volumes) { - $organization = $this->upsertOrganization($user, $ownerRef, $plan, $volumes['branches']); - $this->upsertOwnerMember($organization, $ownerRef); + // 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); - $this->seedStaffMembers($organization, $ownerRef, $plan, $branches); - $departments = $this->seedDepartments($branches, $ownerRef, $volumes); - if (in_array($plan, ['pro', 'enterprise'], true)) { - $this->seedSpecialtyModules($organization, $ownerRef); - $departments = $this->departmentsByBranch($branches, $ownerRef); - } - $practitioners = $this->seedPractitioners($organization, $branches, $departments, $ownerRef, $volumes); - $patients = $this->seedPatients($organization, $branches, $ownerRef, $volumes); - $appointmentCount = $this->seedAppointmentsAndClinical( + $branches = $this->seedBranches($organization, $ownerRef, $volumes); + $this->seedStaffMembers($organization, $ownerRef, $plan, $branches); + $departments = $this->seedDepartments($branches, $ownerRef, $volumes); + if (in_array($plan, ['pro', 'enterprise'], true)) { + $this->seedSpecialtyModules($organization, $ownerRef); + $departments = $this->departmentsByBranch($branches, $ownerRef); + } + $practitioners = $this->seedPractitioners($organization, $branches, $departments, $ownerRef, $volumes); + $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, + ); + } + + if ($volumes['paid_modules']) { + $this->seedPaidModules( $organization, $branches, - $departments, $practitioners, $patients, $ownerRef, $volumes, ); - if (in_array($plan, ['pro', 'enterprise'], true)) { - $appointmentCount += $this->seedSpecialtyDemoData( - $organization->fresh(), - $branches, - $practitioners, - $patients, - $ownerRef, - ); - } + $this->seedDevices($organization, $branches, $ownerRef); + } - if ($volumes['paid_modules']) { - $this->seedPaidModules( - $organization, - $branches, - $practitioners, - $patients, - $ownerRef, - $volumes, - ); - $this->seedDevices($organization, $branches, $ownerRef); - } + if ($volumes['assessments'] > 0) { + $this->seedAssessments( + $organization, + $branches, + $practitioners, + $patients, + $ownerRef, + $volumes['assessments'], + ); + } - if ($volumes['assessments'] > 0) { - $this->seedAssessments( - $organization, - $branches, - $practitioners, - $patients, - $ownerRef, - $volumes['assessments'], - ); - } + $this->syncQueueTicketsForDemo($organization->fresh(), $branches); - $this->syncQueueTicketsForDemo($organization->fresh(), $branches); - - return [ - 'organization' => $organization->fresh(), - 'branches' => count($branches), - 'patients' => count($patients), - 'appointments' => $appointmentCount, - 'plan' => $plan, - ]; - }); + return [ + 'organization' => $organization->fresh(), + 'branches' => count($branches), + 'patients' => count($patients), + 'appointments' => $appointmentCount, + 'plan' => $plan, + ]; } /** diff --git a/app/Services/Messaging/MessagingCredentialsService.php b/app/Services/Messaging/MessagingCredentialsService.php index 0685e92..d746794 100644 --- a/app/Services/Messaging/MessagingCredentialsService.php +++ b/app/Services/Messaging/MessagingCredentialsService.php @@ -14,10 +14,30 @@ class MessagingCredentialsService public function forOrganization(Organization $organization): MessagingCredential { - return MessagingCredential::query()->firstOrCreate( - ['organization_id' => $organization->id], - ['owner_ref' => $organization->owner_ref], - ); + $existing = MessagingCredential::query() + ->where('organization_id', $organization->id) + ->first(); + if ($existing) { + return $existing; + } + + try { + return MessagingCredential::query()->create([ + 'organization_id' => $organization->id, + 'owner_ref' => $organization->owner_ref, + ]); + } catch (\Throwable $e) { + // Concurrent create (or a long demo-seed transaction holding a lock) + // — prefer returning the row another request just wrote over 500ing. + $existing = MessagingCredential::query() + ->where('organization_id', $organization->id) + ->first(); + if ($existing) { + return $existing; + } + + throw $e; + } } /**