Enable all specialty modules with demo traffic for Pro/Enterprise demos.
Deploy Ladill Care / deploy (push) Successful in 1m9s

Free and real onboarding stay opt-in; Pro/Enterprise demos activate every catalog specialty and seed waiting appointments so module pages are populated.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-17 21:55:30 +00:00
co-authored by Cursor
parent c461430441
commit db6d0617e9
2 changed files with 235 additions and 14 deletions
+188 -14
View File
@@ -110,6 +110,7 @@ class DemoTenantSeeder
$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);
@@ -122,6 +123,15 @@ class DemoTenantSeeder
$ownerRef,
$volumes,
);
if (in_array($plan, ['pro', 'enterprise'], true)) {
$appointmentCount += $this->seedSpecialtyDemoData(
$organization->fresh(),
$branches,
$practitioners,
$patients,
$ownerRef,
);
}
if ($volumes['paid_modules']) {
$this->seedPaidModules(
@@ -173,13 +183,17 @@ class DemoTenantSeeder
return;
}
$contexts = [
CareQueueContexts::CONSULTATION,
CareQueueContexts::PHARMACY,
CareQueueContexts::LABORATORY,
CareQueueContexts::BILLING,
CareQueueContexts::IMAGING,
...array_keys(config('care.specialty_modules', [])),
];
foreach ($branches as $branch) {
foreach ([
CareQueueContexts::CONSULTATION,
CareQueueContexts::PHARMACY,
CareQueueContexts::LABORATORY,
CareQueueContexts::BILLING,
] as $context) {
foreach ($contexts as $context) {
$bridge->syncMissingTickets($organization, $context, (int) $branch->id, 100);
}
}
@@ -395,10 +409,11 @@ class DemoTenantSeeder
'plan_expires_at' => now()->addYear()->toIso8601String(),
'billed_branches' => max(1, $billedBranches),
'demo' => true,
// Pro/Enterprise demos: Queue on natural role pages + dentistry specialty module.
// 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)
? ['dentistry' => true]
? $this->allSpecialtyFlagsEnabled()
: [],
// Demo seed uses the legacy clinical path. Explicitly clear leftover
// onboarding rollout so a failed race that created a real workflow
@@ -543,14 +558,30 @@ class DemoTenantSeeder
return $byBranch;
}
/**
* @return array<string, bool>
*/
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
{
try {
// Activates dentistry (depts + stubs). When queue_integration_enabled and
// QUEUE_API_* are configured, SpecialtyModuleService creates real Queue counters.
app(SpecialtyModuleService::class)->activate($organization->fresh(), $ownerRef, 'dentistry');
} catch (\Throwable) {
// Demo seed should not fail if Queue API is unreachable; settings flag remains.
$service = app(SpecialtyModuleService::class);
foreach (array_keys($service->catalog()) as $key) {
try {
// Activates depts + stubs. When queue_integration_enabled and QUEUE_API_*
// are configured, SpecialtyModuleService creates real Queue counters.
$service->activate($organization->fresh(), $ownerRef, $key);
} catch (\Throwable) {
// Demo seed should not fail if Queue API is unreachable; settings flag remains.
}
}
if (data_get($organization->fresh()->settings, 'queue_integration_enabled')) {
@@ -562,6 +593,149 @@ class DemoTenantSeeder
}
}
/**
* @param list<Branch> $branches
* @return array<int, list<Department>> 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<Branch> $branches
* @param list<Practitioner> $practitioners
* @param list<Patient> $patients
*/
private function seedSpecialtyDemoData(
Organization $organization,
array $branches,
array $practitioners,
array $patients,
string $ownerRef,
): int {
if ($branches === [] || $patients === [] || $practitioners === []) {
return 0;
}
$reasons = [
'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'],
];
$count = 0;
$catalog = config('care.specialty_modules', []);
foreach ($catalog as $key => $definition) {
$type = (string) ($definition['department_type'] ?? '');
if ($type === '') {
continue;
}
$moduleReasons = $reasons[$key] ?? ['Specialty visit'];
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;
}
$practitioner = Practitioner::withTrashed()->updateOrCreate(
[
'organization_id' => $organization->id,
'user_ref' => sprintf('demo-specialty-%s-%s', $key, $branch->id),
],
[
'owner_ref' => $ownerRef,
'branch_id' => $branch->id,
'department_id' => $department->id,
'member_id' => null,
'name' => ($definition['label'] ?? ucfirst($key)).' Clinic',
'specialty' => (string) ($definition['label'] ?? $key),
'room' => ($definition['label'] ?? 'Specialty').' Bay',
'is_active' => true,
'deleted_at' => null,
],
);
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,
],
);
}
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,
'completed_at' => $status === Appointment::STATUS_COMPLETED
? $scheduledAt->copy()->addHour()
: null,
'cancelled_at' => null,
'queue_position' => $slot + 1,
'reason' => $moduleReasons[$slot % count($moduleReasons)],
'created_by' => $ownerRef,
'deleted_at' => null,
],
);
$count++;
}
}
}
return $count;
}
/**
* @param list<Branch> $branches
* @param array<int, list<Department>> $departments