Fix Care settings 500s and slow saves under Queue/demo load.
Deploy Ladill Care / deploy (push) Successful in 52s

Defer Queue provision and ticket backfill after the settings response, harden messaging credential creation against lock races, and stop wrapping demo seed in a long-held transaction.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-17 22:57:25 +00:00
co-authored by Cursor
parent 88b5a4e3ab
commit 640b62010a
3 changed files with 123 additions and 79 deletions
@@ -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);
+55 -55
View File
@@ -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,
];
}
/**
@@ -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;
}
}
/**