Implement Care RBAC role→permission→app matrix.
Deploy Ladill Care / deploy (push) Successful in 57s

Replace broad doctor/nurse specialty access with granular roles and primary
apps, permission inheritance for lab/BB managers, and cannot-rules for
discharge, lab approve, and cashier vs billing officer.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-20 00:24:09 +00:00
co-authored by Cursor
parent 55a1288d30
commit ac870bcf33
21 changed files with 913 additions and 239 deletions
@@ -143,7 +143,7 @@ class InvestigationController extends Controller
public function approve(Request $request, InvestigationRequest $investigation): JsonResponse
{
$this->authorizeAbility($request, 'lab.manage');
$this->authorizeAbility($request, 'pathology.result.approve');
$this->authorizeInvestigation($request, $investigation);
$updated = $this->investigations->approve($investigation, $this->ownerRef($request), $this->ownerRef($request));
+14 -3
View File
@@ -87,9 +87,14 @@ class BillController extends Controller
public function callNext(Request $request): RedirectResponse
{
$this->authorizeAbility($request, 'bills.manage');
$organization = $this->organization($request);
$permissions = app(CarePermissions::class);
$member = $this->member($request);
abort_unless(
$permissions->can($member, 'payments.manage')
|| $permissions->can($member, 'bills.manage'),
403,
);
$organization = $this->organization($request);
$branchId = $this->branchContext->resolve($request, $organization, $member);
abort_unless($branchId > 0, 422);
abort_unless($this->queueBridge->isEnabled($organization), 404);
@@ -108,7 +113,13 @@ class BillController extends Controller
public function serve(Request $request, Bill $bill): RedirectResponse
{
$this->authorizeAbility($request, 'bills.manage');
$permissions = app(CarePermissions::class);
$member = $this->member($request);
abort_unless(
$permissions->can($member, 'payments.manage')
|| $permissions->can($member, 'bills.manage'),
403,
);
$this->authorizeBill($request, $bill);
$organization = $this->organization($request);
abort_unless($this->queueBridge->isEnabled($organization), 404);
@@ -128,7 +128,7 @@ class EmergencyWorkspaceController extends Controller
SpecialtyVisitStageService $stages,
EmergencyWorkflowService $workflow,
): RedirectResponse {
$this->authorizeAbility($request, 'consultations.manage');
$this->authorizeAbility($request, 'emergency.discharge');
$this->assertEmergencyManage($request, $modules);
$this->assertVisit($request, $visit);
@@ -228,7 +228,7 @@ class InvestigationController extends Controller
public function approve(Request $request, InvestigationRequest $investigation): RedirectResponse
{
$this->authorizeAbility($request, 'lab.manage');
$this->authorizeAbility($request, 'pathology.result.approve');
$this->authorizeInvestigation($request, $investigation);
$this->investigations->approve($investigation, $this->ownerRef($request), $this->ownerRef($request));
+13 -3
View File
@@ -8,6 +8,7 @@ use App\Models\Branch;
use App\Models\Member;
use App\Models\Practitioner;
use App\Services\Care\AuditLogger;
use App\Services\Care\CarePermissions;
use App\Services\Identity\IdentityTeamClient;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -31,7 +32,7 @@ class MemberController extends Controller
->get();
$adminRoles = ['super_admin', 'hospital_admin'];
$clinicalRoles = ['doctor', 'nurse', 'lab_technician', 'lab_manager', 'pharmacist', 'blood_bank_manager'];
$clinicalRoles = app(CarePermissions::class)->clinicalPractitionerRoles();
$heroStats = [
'total' => $members->count(),
@@ -71,6 +72,7 @@ class MemberController extends Controller
'organization' => $organization,
'branches' => $branches,
'roles' => config('care.roles'),
'practitionerRoles' => app(CarePermissions::class)->clinicalPractitionerRoles(),
'mailboxOptions' => $mailboxOptions,
'specialties' => Practitioner::specialtyOptions(),
]);
@@ -82,11 +84,16 @@ class MemberController extends Controller
$organization = $this->organization($request);
$owner = $this->ownerRef($request);
$permissions = app(CarePermissions::class);
$needsPractitionerDesk = $permissions->usesPractitionerBranchScope(
new Member(['role' => $request->input('role')])
);
$validated = $request->validate([
'email' => ['required', 'email', 'max:255'],
'role' => ['required', 'string', 'in:'.implode(',', array_keys(config('care.roles')))],
'branch_id' => [
Rule::requiredIf(fn () => $request->input('role') === 'doctor'),
Rule::requiredIf(fn () => $needsPractitionerDesk),
'nullable',
'integer',
'exists:care_branches,id',
@@ -138,7 +145,10 @@ class MemberController extends Controller
AuditLogger::record($owner, 'member.invited', $organization->id, $owner, Member::class, $member->id);
$createPractitioner = $request->boolean('create_practitioner', $validated['role'] === 'doctor');
$createPractitioner = $request->boolean(
'create_practitioner',
app(CarePermissions::class)->usesPractitionerBranchScope(new Member(['role' => $validated['role']])),
);
if ($createPractitioner) {
$name = trim((string) ($validated['practitioner_name'] ?? '')) ?: Str::headline(Str::before($email, '@'));
$practitioner = Practitioner::query()->firstOrCreate(
@@ -210,7 +210,7 @@ class PractitionerController extends Controller
{
return Member::owned($owner)
->where('organization_id', $organizationId)
->whereIn('role', ['doctor', 'nurse', 'lab_technician', 'lab_manager', 'pharmacist', 'blood_bank_manager', 'hospital_admin', 'super_admin'])
->whereIn('role', app(\App\Services\Care\CarePermissions::class)->clinicalPractitionerRoles())
->orderBy('user_ref')
->get();
}