From 656f402e8f0b9024f13a53c6263a0235d66cb09d Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sat, 18 Jul 2026 18:58:57 +0000 Subject: [PATCH] Restrict lab catalog pricing to admins only. Split catalog CRUD onto lab.catalog.manage so technicians keep lab.manage for queue and results workflow without setting investigation prices. Co-authored-by: Cursor --- README.md | 4 +- .../Care/InvestigationTypeController.php | 10 +-- .../Controllers/Care/SettingsController.php | 1 + app/Services/Afia/AfiaService.php | 4 +- app/Services/Care/CarePermissions.php | 11 ++- .../views/care/lab/catalog/index.blade.php | 5 +- .../views/care/lab/queue/index.blade.php | 2 +- resources/views/care/settings/edit.blade.php | 14 +++- .../views/care/settings/gp-pricing.blade.php | 2 +- .../views/care/settings/module-show.blade.php | 2 +- resources/views/partials/sidebar.blade.php | 6 +- tests/Feature/CareLabTest.php | 84 ++++++++++++++++++- 12 files changed, 128 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index f91220c..42c10aa 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,11 @@ clinical record is scoped to a platform account (`owner_ref`) and organization. - **Patients** — registration, medical history, documents, visit timeline - **Appointments & queue** — booking, walk-in, check-in, department queue - **Consultations** — vitals, diagnoses, investigations, prescriptions -- **Laboratory** — test catalog, sample collection, results, approval +- **Laboratory** — sample collection, results, approval; admins manage test catalog & pricing - **Pharmacy** — dispensing queue, drug inventory, batch stock tracking - **Billing** — visit invoices, line items, cash/MoMo payments, and online checkout via your own Paystack / Flutterwave / Hubtel (Pro) - **Reports** — patients, appointments, lab, finance, clinical (CSV export) -- **Admin** — branches, departments, practitioners, members, audit log +- **Admin** — branches, departments, practitioners, members, lab catalog pricing, audit log - **Devices** — barcode wedge scanners (all plans), agent-connected vitals/lab hardware (Pro); see `docs/devices.md` ## Platform integration diff --git a/app/Http/Controllers/Care/InvestigationTypeController.php b/app/Http/Controllers/Care/InvestigationTypeController.php index 0b3b112..a527a8d 100644 --- a/app/Http/Controllers/Care/InvestigationTypeController.php +++ b/app/Http/Controllers/Care/InvestigationTypeController.php @@ -20,7 +20,7 @@ class InvestigationTypeController extends Controller public function index(Request $request): View { - $this->authorizeAbility($request, 'lab.manage'); + $this->authorizeAbility($request, 'lab.catalog.manage'); $organization = $this->organization($request); $types = InvestigationType::query() @@ -38,7 +38,7 @@ class InvestigationTypeController extends Controller public function create(Request $request): View { - $this->authorizeAbility($request, 'lab.manage'); + $this->authorizeAbility($request, 'lab.catalog.manage'); return view('care.lab.catalog.create', [ 'categories' => config('care.investigation_categories'), @@ -47,7 +47,7 @@ class InvestigationTypeController extends Controller public function store(Request $request): RedirectResponse { - $this->authorizeAbility($request, 'lab.manage'); + $this->authorizeAbility($request, 'lab.catalog.manage'); $type = $this->investigations->createType( $this->organization($request), @@ -61,7 +61,7 @@ class InvestigationTypeController extends Controller public function edit(Request $request, InvestigationType $investigationType): View { - $this->authorizeAbility($request, 'lab.manage'); + $this->authorizeAbility($request, 'lab.catalog.manage'); $this->authorizeType($request, $investigationType); return view('care.lab.catalog.edit', [ @@ -72,7 +72,7 @@ class InvestigationTypeController extends Controller public function update(Request $request, InvestigationType $investigationType): RedirectResponse { - $this->authorizeAbility($request, 'lab.manage'); + $this->authorizeAbility($request, 'lab.catalog.manage'); $this->authorizeType($request, $investigationType); $this->investigations->updateType($investigationType, $this->ownerRef($request), $this->validatedTypeData($request)); diff --git a/app/Http/Controllers/Care/SettingsController.php b/app/Http/Controllers/Care/SettingsController.php index 8f79b75..3c08500 100644 --- a/app/Http/Controllers/Care/SettingsController.php +++ b/app/Http/Controllers/Care/SettingsController.php @@ -61,6 +61,7 @@ class SettingsController extends Controller 'canViewTeam' => $permissions->can($member, 'admin.members.view'), 'canViewDevices' => $permissions->can($member, 'devices.view'), 'canViewDisplays' => $permissions->can($member, 'displays.view'), + 'canManageLabCatalog' => $permissions->can($member, 'lab.catalog.manage'), 'hasBranchesFeature' => $plans->hasFeature($organization, 'branches'), 'hasClinicalDevicesFeature' => $plans->hasFeature($organization, 'clinical_devices'), 'canUseQueueIntegration' => $plans->canUseQueueIntegration($organization), diff --git a/app/Services/Afia/AfiaService.php b/app/Services/Afia/AfiaService.php index 2361524..6966fc0 100644 --- a/app/Services/Afia/AfiaService.php +++ b/app/Services/Afia/AfiaService.php @@ -158,10 +158,10 @@ class AfiaService - Appointments: schedule visits and manage walk-ins. - Queue: waiting patients and active consultations — doctors start consultations from here. - Consultations: record vitals, symptoms, clinical notes, diagnoses, lab requests, and prescriptions; complete the encounter when done. - - Laboratory: investigation catalog, request tests from consultations, sample collection, results entry, and approval. + - Laboratory: sample collection, results entry, and approval from the lab queue. Admins manage the investigation catalog and test prices in Settings / Lab catalog. - Prescriptions & pharmacy queue: prescribe during consultations; pharmacists dispense from the queue. - Billing: generate bills from completed visits, record payments, and print receipts. - - Settings & admin: branches, departments, members, practitioners, and audit logs. + - Settings & admin: branches, departments, members, practitioners, lab catalog pricing, and audit logs. Rules: - Only answer questions about Ladill Care — patients, appointments, queue, consultations, lab, pharmacy, and billing. diff --git a/app/Services/Care/CarePermissions.php b/app/Services/Care/CarePermissions.php index 2ea6ff2..2119997 100644 --- a/app/Services/Care/CarePermissions.php +++ b/app/Services/Care/CarePermissions.php @@ -6,7 +6,15 @@ use App\Models\Member; class CarePermissions { - /** @var array> */ + /** + * Role abilities. + * + * Lab catalog pricing uses `lab.catalog.manage` (admins only via `*`). + * Lab technicians keep `lab.manage` for queue / sample / process / results — + * not catalog CRUD or price edits. + * + * @var array> + */ protected array $roleAbilities = [ 'super_admin' => ['*'], 'hospital_admin' => ['*'], @@ -56,6 +64,7 @@ class CarePermissions 'admin.practitioners.view', 'admin.practitioners.manage', 'admin.members.view', 'admin.members.manage', 'settings.view', 'settings.manage', + 'lab.catalog.manage', 'devices.view', 'devices.manage', 'displays.view', 'displays.manage', 'audit.view', 'audit.export', diff --git a/resources/views/care/lab/catalog/index.blade.php b/resources/views/care/lab/catalog/index.blade.php index e081b8f..b62f8b0 100644 --- a/resources/views/care/lab/catalog/index.blade.php +++ b/resources/views/care/lab/catalog/index.blade.php @@ -1,6 +1,9 @@
-

Investigation catalog

+
+

Investigation catalog

+

Admins manage test types and prices. Lab technicians process samples from the laboratory queue.

+
Add test
diff --git a/resources/views/care/lab/queue/index.blade.php b/resources/views/care/lab/queue/index.blade.php index 0ca54e2..79f68d7 100644 --- a/resources/views/care/lab/queue/index.blade.php +++ b/resources/views/care/lab/queue/index.blade.php @@ -6,7 +6,7 @@
All requests - @if (app(\App\Services\Care\CarePermissions::class)->can(auth()->user() ? app(\App\Services\Care\OrganizationResolver::class)->memberFor(auth()->user(), $organization) : null, 'lab.manage')) + @if (app(\App\Services\Care\CarePermissions::class)->can(auth()->user() ? app(\App\Services\Care\OrganizationResolver::class)->memberFor(auth()->user(), $organization) : null, 'lab.catalog.manage')) Catalog @endif
diff --git a/resources/views/care/settings/edit.blade.php b/resources/views/care/settings/edit.blade.php index 6e5dddd..5ee4600 100644 --- a/resources/views/care/settings/edit.blade.php +++ b/resources/views/care/settings/edit.blade.php @@ -48,7 +48,7 @@ - @if ($canViewBranches || $canViewTeam || $canViewDevices || $canViewDisplays) + @if ($canViewBranches || $canViewTeam || $canViewDevices || $canViewDisplays || ($canManageLabCatalog ?? false))
    @if ($canViewBranches) @@ -126,6 +126,18 @@ + @if (! empty($hasPaidPlan) && ($canManageLabCatalog ?? false)) +
  • + + + Laboratory catalog + Investigation types and lab test prices (admins only) + + + +
  • + @endif
  • diff --git a/resources/views/care/settings/gp-pricing.blade.php b/resources/views/care/settings/gp-pricing.blade.php index 3d46e1f..c56eae6 100644 --- a/resources/views/care/settings/gp-pricing.blade.php +++ b/resources/views/care/settings/gp-pricing.blade.php @@ -1,7 +1,7 @@

    ← Facility settings diff --git a/resources/views/care/settings/module-show.blade.php b/resources/views/care/settings/module-show.blade.php index 9912602..74d07a2 100644 --- a/resources/views/care/settings/module-show.blade.php +++ b/resources/views/care/settings/module-show.blade.php @@ -14,7 +14,7 @@

    {{ session('success') }}

    @endif - +
    @if ($enabled) Active diff --git a/resources/views/partials/sidebar.blade.php b/resources/views/partials/sidebar.blade.php index d86d208..9e3a3fb 100644 --- a/resources/views/partials/sidebar.blade.php +++ b/resources/views/partials/sidebar.blade.php @@ -52,7 +52,7 @@ // Lab, pharmacy, billing, and Queue service counters are Pro/Enterprise modules. if (! empty($hasPaidPlan)) { if ($permissions->can($member, 'lab.view') && $permissions->handlesFloorCare($member)) { - $nav[] = ['name' => 'Laboratory', 'route' => route('care.lab.queue.index'), 'active' => request()->routeIs('care.lab.*'), + $nav[] = ['name' => 'Laboratory', 'route' => route('care.lab.queue.index'), 'active' => request()->routeIs('care.lab.*') && ! request()->routeIs('care.lab.catalog.*'), 'icon' => '']; } @@ -98,6 +98,10 @@ } $adminNav = []; + if (! empty($hasPaidPlan) && $permissions->can($member, 'lab.catalog.manage')) { + $adminNav[] = ['name' => 'Lab catalog', 'route' => route('care.lab.catalog.index'), 'active' => request()->routeIs('care.lab.catalog.*'), + 'icon' => '']; + } if ($permissions->can($member, 'admin.departments.view')) { $adminNav[] = ['name' => 'Departments', 'route' => route('care.departments.index'), 'active' => request()->routeIs('care.departments.*'), 'icon' => '']; diff --git a/tests/Feature/CareLabTest.php b/tests/Feature/CareLabTest.php index 6854523..d4cd430 100644 --- a/tests/Feature/CareLabTest.php +++ b/tests/Feature/CareLabTest.php @@ -201,7 +201,89 @@ class CareLabTest extends TestCase $this->actingAs($this->user) ->get(route('care.lab.queue.index', ['branch_id' => $this->branch->id])) ->assertOk() - ->assertSee('Lab work queue'); + ->assertSee('Lab work queue') + ->assertDontSee('Catalog'); + } + + public function test_lab_technician_cannot_manage_catalog(): void + { + Member::where('user_ref', $this->user->public_id)->update(['role' => 'lab_technician']); + + $this->actingAs($this->user) + ->get(route('care.lab.catalog.index')) + ->assertForbidden(); + + $this->actingAs($this->user) + ->get(route('care.lab.catalog.create')) + ->assertForbidden(); + + $this->actingAs($this->user) + ->post(route('care.lab.catalog.store'), [ + 'name' => 'Unauthorized Test', + 'category' => 'blood', + 'price_minor' => 1000, + ]) + ->assertForbidden(); + + $this->actingAs($this->user) + ->get(route('care.lab.catalog.edit', $this->investigationType)) + ->assertForbidden(); + + $this->actingAs($this->user) + ->put(route('care.lab.catalog.update', $this->investigationType), [ + 'name' => 'Hacked Price', + 'category' => 'blood', + 'price_minor' => 1, + ]) + ->assertForbidden(); + } + + public function test_hospital_admin_can_manage_catalog(): void + { + Member::where('user_ref', $this->user->public_id)->update(['role' => 'hospital_admin']); + + $this->actingAs($this->user) + ->get(route('care.lab.catalog.index')) + ->assertOk() + ->assertSee('Investigation catalog'); + + $this->actingAs($this->user) + ->get(route('care.lab.catalog.create')) + ->assertOk(); + + $this->actingAs($this->user) + ->post(route('care.lab.catalog.store'), [ + 'name' => 'Lipid Panel', + 'code' => 'LIP', + 'category' => 'blood', + 'price_minor' => 4500, + 'is_active' => true, + ]) + ->assertRedirect(route('care.lab.catalog.index')); + + $this->assertDatabaseHas('care_investigation_types', [ + 'organization_id' => $this->organization->id, + 'name' => 'Lipid Panel', + 'price_minor' => 4500, + ]); + + $this->actingAs($this->user) + ->put(route('care.lab.catalog.update', $this->investigationType), [ + 'name' => 'Fasting Blood Glucose', + 'code' => 'FBG', + 'category' => 'blood', + 'unit' => 'mmol/L', + 'reference_low' => 3.9, + 'reference_high' => 6.1, + 'price_minor' => 3000, + 'is_active' => true, + ]) + ->assertRedirect(route('care.lab.catalog.index')); + + $this->assertDatabaseHas('care_investigation_types', [ + 'id' => $this->investigationType->id, + 'price_minor' => 3000, + ]); } public function test_api_can_request_investigation(): void