Expand specialty modules; pin Emergency and Blood Bank on Pro.
Deploy Ladill Care / deploy (push) Successful in 1m37s

Add Cardiology through Child Welfare to the specialty catalog, keep Emergency and Blood Bank always enabled on Pro/Enterprise sidebars, and generate matching demo specialty doctors.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-18 09:27:05 +00:00
co-authored by Cursor
parent f87f216fb4
commit e9a76fec80
9 changed files with 524 additions and 140 deletions
@@ -21,6 +21,11 @@ class SettingsModulesController extends Controller
$member = $this->member($request);
$canManage = app(\App\Services\Care\CarePermissions::class)->can($member, 'settings.manage');
if ($modules->canManage($organization)) {
$modules->ensureDefaultModulesProvisioned($organization, $this->ownerRef($request));
$organization->refresh();
}
return view('care.settings.modules', [
'organization' => $organization,
'canManage' => $canManage,
@@ -46,9 +51,13 @@ class SettingsModulesController extends Controller
$desired = [];
foreach ($keys as $key) {
$desired[$key] = $request->boolean("modules.{$key}");
if ($modules->isDefaultOnPaidPlans($key)) {
$desired[$key] = true;
}
}
$result = $modules->sync($organization, $this->ownerRef($request), $desired);
$modules->ensureDefaultModulesProvisioned($organization, $this->ownerRef($request));
$result = $modules->sync($organization->fresh(), $this->ownerRef($request), $desired);
if ($result['errors'] !== []) {
return back()->with('error', 'Some modules could not be updated: '.implode(' ', $result['errors']));
+17
View File
@@ -802,11 +802,28 @@ class DemoTenantSeeder
}
$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', 'Immunization CWC', 'Well-baby visit'],
];
$count = 0;
@@ -41,15 +41,69 @@ class SpecialtyModuleService
return $catalog[$key] ?? null;
}
public function isDefaultOnPaidPlans(string $key): bool
{
return (bool) ($this->definition($key)['default_on_paid_plans'] ?? false);
}
public function isEnabled(Organization $organization, string $key): bool
{
if (! $this->definition($key)) {
return false;
}
if ($this->isDefaultOnPaidPlans($key) && $this->plans->hasPaidPlan($organization)) {
return true;
}
return (bool) data_get($organization->settings, "specialty_modules.{$key}", false);
}
/**
* @return list<string>
*/
public function defaultKeysForPaidPlans(): array
{
$keys = [];
foreach (array_keys($this->catalog()) as $key) {
if ($this->isDefaultOnPaidPlans($key)) {
$keys[] = $key;
}
}
return $keys;
}
/**
* Provision Emergency / Blood Bank (and any other default_on_paid_plans modules).
*/
public function ensureDefaultModulesProvisioned(Organization $organization, string $ownerRef): void
{
if (! $this->plans->hasPaidPlan($organization)) {
return;
}
foreach ($this->defaultKeysForPaidPlans() as $key) {
$provisioned = data_get(
$organization->settings,
"specialty_module_provisioning.{$key}.active",
);
if ($provisioned) {
continue;
}
try {
$this->activate($organization->fresh(), $ownerRef, $key);
$organization->refresh();
} catch (\Throwable $e) {
Log::warning('specialty_module.default_provision_failed', [
'key' => $key,
'message' => $e->getMessage(),
]);
}
}
}
/**
* @return list<string>
*/
@@ -116,6 +170,11 @@ class SpecialtyModuleService
}
if ($member && $member->role === 'doctor') {
// Default Pro surfaces (Emergency, Blood Bank) are available to all doctors.
if ($this->isDefaultOnPaidPlans($key)) {
return true;
}
return $this->doctorAssignedToModule($organization, $member, $key);
}
@@ -187,9 +246,23 @@ class SpecialtyModuleService
foreach ($this->catalog() as $key => $definition) {
$want = (bool) ($desired[$key] ?? false);
if ($this->isDefaultOnPaidPlans($key) && $this->plans->hasPaidPlan($organization)) {
$want = true;
}
$have = $this->isEnabled($organization, $key);
if ($want === $have) {
// Still ensure defaults are provisioned even when already "enabled".
if ($want && $this->isDefaultOnPaidPlans($key)
&& ! data_get($organization->settings, "specialty_module_provisioning.{$key}.active")) {
try {
$this->activate($organization, $ownerRef, $key);
$organization->refresh();
} catch (\Throwable $e) {
$errors[] = ($definition['label'] ?? $key).': '.$e->getMessage();
}
}
continue;
}
@@ -274,6 +347,10 @@ class SpecialtyModuleService
throw new \InvalidArgumentException("Unknown specialty module [{$key}].");
}
if ($this->isDefaultOnPaidPlans($key) && $this->plans->hasPaidPlan($organization)) {
throw new \RuntimeException(($this->definition($key)['label'] ?? $key).' stays enabled on Pro and Enterprise.');
}
$settings = $organization->settings ?? [];
$modules = is_array($settings['specialty_modules'] ?? null) ? $settings['specialty_modules'] : [];
$modules[$key] = false;
+61 -73
View File
@@ -186,6 +186,36 @@ final class DemoWorld
* account_role?: string
* }>>
*/
/**
* Care specialty module doctors (Pro / Enterprise). Emails: demo-{tier}-{key}@ladill.com
*
* @var array<string, string> module key => display name
*/
public const SPECIALTY_DOCTORS = [
'emergency' => 'Dr. Kojo Emergency',
'blood_bank' => 'Dr. Ama Blood Bank',
'dentistry' => 'Dr. Ama Dental',
'ophthalmology' => 'Dr. Kofi Eye',
'physiotherapy' => 'Dr. Efua Physio',
'maternity' => 'Dr. Abena Maternity',
'radiology' => 'Dr. Yaw Radiology',
'cardiology' => 'Dr. Kwesi Cardiology',
'psychiatry' => 'Dr. Adwoa Psychiatry',
'pediatrics' => 'Dr. Fiifi Pediatrics',
'orthopedics' => 'Dr. Kojo Orthopedics',
'ent' => 'Dr. Esi ENT',
'oncology' => 'Dr. Mansa Oncology',
'renal' => 'Dr. Nana Renal',
'surgery' => 'Dr. Kwadwo Surgery',
'vaccination' => 'Dr. Afia Vaccination',
'pathology' => 'Dr. Yaw Pathology',
'infusion' => 'Dr. Akosua Infusion',
'dermatology' => 'Dr. Ama Dermatology',
'podiatry' => 'Dr. Kofi Podiatry',
'fertility' => 'Dr. Abena Fertility',
'child_welfare' => 'Dr. Efua Child Welfare',
];
public const STAFF = [
'free' => [
[
@@ -272,41 +302,6 @@ final class DemoWorld
'meet' => 'member',
],
],
[
'key' => 'dentistry',
'email' => 'demo-pro-dentistry@ladill.com',
'name' => 'Dr. Ama Dental (Pro)',
'apps' => ['care'],
'roles' => ['care' => 'doctor'],
],
[
'key' => 'ophthalmology',
'email' => 'demo-pro-ophthalmology@ladill.com',
'name' => 'Dr. Kofi Eye (Pro)',
'apps' => ['care'],
'roles' => ['care' => 'doctor'],
],
[
'key' => 'physiotherapy',
'email' => 'demo-pro-physiotherapy@ladill.com',
'name' => 'Dr. Efua Physio (Pro)',
'apps' => ['care'],
'roles' => ['care' => 'doctor'],
],
[
'key' => 'maternity',
'email' => 'demo-pro-maternity@ladill.com',
'name' => 'Dr. Abena Maternity (Pro)',
'apps' => ['care'],
'roles' => ['care' => 'doctor'],
],
[
'key' => 'radiology',
'email' => 'demo-pro-radiology@ladill.com',
'name' => 'Dr. Yaw Radiology (Pro)',
'apps' => ['care'],
'roles' => ['care' => 'doctor'],
],
],
'enterprise' => [
[
@@ -381,41 +376,6 @@ final class DemoWorld
'events' => 'member',
],
],
[
'key' => 'dentistry',
'email' => 'demo-enterprise-dentistry@ladill.com',
'name' => 'Dr. Ama Dental',
'apps' => ['care'],
'roles' => ['care' => 'doctor'],
],
[
'key' => 'ophthalmology',
'email' => 'demo-enterprise-ophthalmology@ladill.com',
'name' => 'Dr. Kofi Eye',
'apps' => ['care'],
'roles' => ['care' => 'doctor'],
],
[
'key' => 'physiotherapy',
'email' => 'demo-enterprise-physiotherapy@ladill.com',
'name' => 'Dr. Efua Physio',
'apps' => ['care'],
'roles' => ['care' => 'doctor'],
],
[
'key' => 'maternity',
'email' => 'demo-enterprise-maternity@ladill.com',
'name' => 'Dr. Abena Maternity',
'apps' => ['care'],
'roles' => ['care' => 'doctor'],
],
[
'key' => 'radiology',
'email' => 'demo-enterprise-radiology@ladill.com',
'name' => 'Dr. Yaw Radiology',
'apps' => ['care'],
'roles' => ['care' => 'doctor'],
],
],
];
@@ -541,7 +501,35 @@ final class DemoWorld
*/
public static function staff(string $tier): array
{
return self::STAFF[self::normalizeTier($tier)] ?? [];
$tier = self::normalizeTier($tier);
$roster = self::STAFF[$tier] ?? [];
if (! in_array($tier, ['pro', 'enterprise'], true)) {
return $roster;
}
foreach (self::SPECIALTY_DOCTORS as $key => $name) {
$email = sprintf('demo-%s-%s@ladill.com', $tier, $key);
$already = false;
foreach ($roster as $row) {
if (strtolower((string) ($row['email'] ?? '')) === $email) {
$already = true;
break;
}
}
if ($already) {
continue;
}
$suffix = $tier === 'pro' ? ' (Pro)' : '';
$roster[] = [
'key' => $key,
'email' => $email,
'name' => $name.$suffix,
'apps' => ['care'],
'roles' => ['care' => 'doctor'],
];
}
return $roster;
}
/**
@@ -554,8 +542,8 @@ final class DemoWorld
return null;
}
foreach (self::STAFF as $roster) {
foreach ($roster as $member) {
foreach (['free', 'pro', 'enterprise'] as $tier) {
foreach (self::staff($tier) as $member) {
if (strtolower($member['email']) === $email) {
return $member;
}