Replace Maternity with Women's Health (OB/GYN) and configurable service lines.
Deploy Ladill Care / deploy (push) Successful in 26s

Keeps Fertility as a separate specialty, adds OB/GYN and fertility staff roles, and migrates live org settings from maternity → womens_health.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-20 09:48:19 +00:00
co-authored by Cursor
parent 938f351ede
commit 56a663a777
33 changed files with 708 additions and 189 deletions
@@ -45,13 +45,160 @@ class SpecialtyModuleService
return config('care.specialty_modules', []);
}
/**
* Legacy maternity module key womens_health.
*/
public function normalizeKey(string $key): string
{
return $key === 'maternity' ? 'womens_health' : $key;
}
public function definition(string $key): ?array
{
$key = $this->normalizeKey($key);
$catalog = $this->catalog();
return $catalog[$key] ?? null;
}
/**
* Migrate org settings from maternity womens_health (idempotent).
*/
public function migrateMaternityToWomensHealth(Organization $organization): Organization
{
$settings = is_array($organization->settings) ? $organization->settings : [];
$modules = is_array($settings['specialty_modules'] ?? null) ? $settings['specialty_modules'] : [];
$provisioning = is_array($settings['specialty_module_provisioning'] ?? null)
? $settings['specialty_module_provisioning']
: [];
$changed = false;
if (! empty($modules['maternity']) && empty($modules['womens_health'])) {
$modules['womens_health'] = true;
$modules['maternity'] = false;
$changed = true;
}
if (isset($provisioning['maternity']) && ! isset($provisioning['womens_health'])) {
$provisioning['womens_health'] = $provisioning['maternity'];
unset($provisioning['maternity']);
$changed = true;
} elseif (isset($provisioning['maternity']) && isset($provisioning['womens_health'])) {
unset($provisioning['maternity']);
$changed = true;
}
if (! $changed) {
return $organization;
}
$settings['specialty_modules'] = $modules;
$settings['specialty_module_provisioning'] = $provisioning;
$organization->update(['settings' => $settings]);
// Point legacy maternity departments at the new type when still labeled maternity.
Department::query()
->where('organization_id', $organization->id)
->where('type', 'maternity')
->update(['type' => 'womens_health']);
return $organization->fresh();
}
/**
* @return array<string, array{label: string, default_on: bool}>
*/
public function serviceLines(string $moduleKey): array
{
$definition = $this->definition($moduleKey);
$lines = $definition['service_lines'] ?? [];
if (! is_array($lines)) {
return [];
}
$out = [];
foreach ($lines as $key => $line) {
if (! is_string($key) || ! is_array($line)) {
continue;
}
$out[$key] = [
'label' => (string) ($line['label'] ?? $key),
'default_on' => (bool) ($line['default_on'] ?? true),
];
}
return $out;
}
/**
* Enabled service lines for an org module (defaults when unset).
*
* @return array<string, bool>
*/
public function enabledServiceLines(Organization $organization, string $moduleKey): array
{
$moduleKey = $this->normalizeKey($moduleKey);
$catalog = $this->serviceLines($moduleKey);
if ($catalog === []) {
return [];
}
$stored = data_get(
$organization->settings,
"specialty_module_provisioning.{$moduleKey}.service_lines",
[],
);
$stored = is_array($stored) ? $stored : [];
$out = [];
foreach ($catalog as $key => $line) {
$out[$key] = array_key_exists($key, $stored)
? (bool) $stored[$key]
: $line['default_on'];
}
return $out;
}
/**
* @param array<string, bool> $desired
*/
public function syncServiceLines(Organization $organization, string $moduleKey, array $desired): void
{
$moduleKey = $this->normalizeKey($moduleKey);
$catalog = $this->serviceLines($moduleKey);
abort_unless($catalog !== [], 404);
$settings = is_array($organization->settings) ? $organization->settings : [];
$provisioning = is_array($settings['specialty_module_provisioning'] ?? null)
? $settings['specialty_module_provisioning']
: [];
$record = is_array($provisioning[$moduleKey] ?? null) ? $provisioning[$moduleKey] : [];
$lines = [];
foreach (array_keys($catalog) as $key) {
$lines[$key] = (bool) ($desired[$key] ?? false);
}
// Keep at least one line on so the module remains usable.
if (! in_array(true, $lines, true)) {
$first = array_key_first($catalog);
if ($first !== null) {
$lines[$first] = true;
}
}
$record['service_lines'] = $lines;
$provisioning[$moduleKey] = $record;
$settings['specialty_module_provisioning'] = $provisioning;
$organization->update(['settings' => $settings]);
}
public function isServiceLineEnabled(Organization $organization, string $moduleKey, string $lineKey): bool
{
$enabled = $this->enabledServiceLines($organization, $moduleKey);
return (bool) ($enabled[$lineKey] ?? false);
}
/**
* Heroicon-style icon identifier from the specialty catalog (e.g. bolt, heart).
*/
@@ -86,10 +233,18 @@ class SpecialtyModuleService
public function isEnabled(Organization $organization, string $key): bool
{
$key = $this->normalizeKey($key);
if (! $this->definition($key)) {
return false;
}
// One-time maternity → womens_health settings migration.
if ($key === 'womens_health'
&& data_get($organization->settings, 'specialty_modules.maternity')) {
$organization = $this->migrateMaternityToWomensHealth($organization);
}
if ($this->isDefaultOnPaidPlans($key) && $this->plans->hasPaidPlan($organization)) {
return true;
}
@@ -245,6 +400,7 @@ class SpecialtyModuleService
*/
public function memberAccessLevel(Organization $organization, ?Member $member, string $key): string
{
$key = $this->normalizeKey($key);
$cacheKey = $organization->id.'|'.($member?->id ?? 'guest').'|'.$key;
if (isset($this->memberAccessLevelCache[$cacheKey])) {
return $this->memberAccessLevelCache[$cacheKey];
@@ -294,6 +450,7 @@ class SpecialtyModuleService
*/
public function memberCanManage(Organization $organization, ?Member $member, string $key): bool
{
$key = $this->normalizeKey($key);
if (! $this->isEnabled($organization, $key) || ! $member) {
return false;
}
@@ -358,6 +515,7 @@ class SpecialtyModuleService
*/
public function memberCanView(Organization $organization, ?Member $member, string $key): bool
{
$key = $this->normalizeKey($key);
if ($this->memberCanManage($organization, $member, $key)
|| $this->memberCanRefer($organization, $member, $key)) {
return true;
@@ -413,6 +571,7 @@ class SpecialtyModuleService
*/
public function memberCanRefer(Organization $organization, ?Member $member, string $key): bool
{
$key = $this->normalizeKey($key);
if ($this->memberCanManage($organization, $member, $key)) {
return true;
}