diff --git a/app/Http/Controllers/Api/Concerns/ScopesApiToAccount.php b/app/Http/Controllers/Api/Concerns/ScopesApiToAccount.php index 2e14860..22364f6 100644 --- a/app/Http/Controllers/Api/Concerns/ScopesApiToAccount.php +++ b/app/Http/Controllers/Api/Concerns/ScopesApiToAccount.php @@ -11,7 +11,16 @@ use Illuminate\Http\Request; trait ScopesApiToAccount { + /** + * Tenant data owner — always the organization owner_ref. + * Staff API tokens must scope to the employer tenant, not the staff public_id. + */ protected function ownerRef(Request $request): string + { + return (string) $this->organization($request)->owner_ref; + } + + protected function actorRef(Request $request): string { return (string) $request->user()->public_id; } diff --git a/app/Http/Controllers/Care/Concerns/ScopesToAccount.php b/app/Http/Controllers/Care/Concerns/ScopesToAccount.php index 7c09a02..15a2d49 100644 --- a/app/Http/Controllers/Care/Concerns/ScopesToAccount.php +++ b/app/Http/Controllers/Care/Concerns/ScopesToAccount.php @@ -12,7 +12,17 @@ use Illuminate\Http\Request; trait ScopesToAccount { + /** + * Tenant data owner — always the organization owner_ref. + * Staff members must read/write the employer's records, not their own public_id. + */ protected function ownerRef(Request $request): string + { + return (string) $this->organization($request)->owner_ref; + } + + /** Acting user (audit / created_by), distinct from the tenant owner. */ + protected function actorRef(Request $request): string { return (string) $request->user()->public_id; } diff --git a/app/Http/Controllers/Care/MemberController.php b/app/Http/Controllers/Care/MemberController.php index 8af1770..7503fd9 100644 --- a/app/Http/Controllers/Care/MemberController.php +++ b/app/Http/Controllers/Care/MemberController.php @@ -149,13 +149,13 @@ class MemberController extends Controller $this->authorizeAbility($request, 'admin.members.manage'); $this->authorizeOwner($request, $member); - abort_if($member->user_ref === $this->ownerRef($request), 422, 'You cannot remove yourself.'); + abort_if($member->user_ref === $this->actorRef($request), 422, 'You cannot remove yourself.'); $memberId = $member->id; $organizationId = $member->organization_id; $member->delete(); - AuditLogger::record($this->ownerRef($request), 'member.deleted', $organizationId, $this->ownerRef($request), Member::class, $memberId); + AuditLogger::record($this->ownerRef($request), 'member.deleted', $organizationId, $this->actorRef($request), Member::class, $memberId); return redirect()->route('care.members.index')->with('success', 'Member removed.'); } diff --git a/app/Services/Care/BranchContext.php b/app/Services/Care/BranchContext.php index b351238..bd56c32 100644 --- a/app/Services/Care/BranchContext.php +++ b/app/Services/Care/BranchContext.php @@ -37,7 +37,7 @@ class BranchContext { $scope = $this->organizations->branchScope($member); - return Branch::owned((string) $request->user()->public_id) + return Branch::owned((string) $organization->owner_ref) ->where('organization_id', $organization->id) ->where('is_active', true) ->when($scope, fn ($q) => $q->where('id', $scope)) diff --git a/tests/Feature/CareStaffTenantScopeTest.php b/tests/Feature/CareStaffTenantScopeTest.php new file mode 100644 index 0000000..12c033c --- /dev/null +++ b/tests/Feature/CareStaffTenantScopeTest.php @@ -0,0 +1,109 @@ +withoutMiddleware(EnsurePlatformSession::class); + + $owner = User::create([ + 'public_id' => 'demo-pro-owner', + 'name' => 'Demo Pro Owner', + 'email' => 'demo-pro@ladill.com', + ]); + + $doctor = User::create([ + 'public_id' => 'demo-pro-doctor-pid', + 'name' => 'Demo Pro Doctor', + 'email' => 'demo-pro-doctor@ladill.com', + ]); + + $organization = Organization::create([ + 'owner_ref' => $owner->public_id, + 'name' => 'Demo Pro Clinic', + 'slug' => 'demo-pro-clinic', + 'timezone' => 'UTC', + 'settings' => [ + 'onboarded' => true, + 'plan' => 'pro', + 'plan_expires_at' => now()->addMonth()->toIso8601String(), + ], + ]); + + Member::create([ + 'owner_ref' => $owner->public_id, + 'organization_id' => $organization->id, + 'user_ref' => $owner->public_id, + 'role' => 'hospital_admin', + 'branch_id' => null, + ]); + + $branch = Branch::create([ + 'owner_ref' => $owner->public_id, + 'organization_id' => $organization->id, + 'name' => 'East Legon Care', + 'is_active' => true, + ]); + + Member::create([ + 'owner_ref' => $owner->public_id, + 'organization_id' => $organization->id, + 'user_ref' => $doctor->public_id, + 'role' => 'doctor', + 'branch_id' => $branch->id, + ]); + + $patient = Patient::create([ + 'uuid' => (string) \Illuminate\Support\Str::uuid(), + 'owner_ref' => $owner->public_id, + 'organization_id' => $organization->id, + 'branch_id' => $branch->id, + 'patient_number' => 'DEMO-8883-00001', + 'first_name' => 'Ama', + 'last_name' => 'Mensah', + ]); + + Appointment::create([ + 'uuid' => (string) \Illuminate\Support\Str::uuid(), + 'owner_ref' => $owner->public_id, + 'organization_id' => $organization->id, + 'branch_id' => $branch->id, + 'patient_id' => $patient->id, + 'type' => Appointment::TYPE_WALK_IN, + 'status' => Appointment::STATUS_WAITING, + 'scheduled_at' => now(), + 'waiting_at' => now(), + 'queue_position' => 1, + 'reason' => 'Toothache', + 'created_by' => $owner->public_id, + ]); + + $this->actingAs($doctor) + ->get(route('care.queue.index')) + ->assertOk() + ->assertSee('East Legon Care') + ->assertSee('Ama Mensah') + ->assertSee('Toothache') + ->assertDontSee('No patients waiting.'); + + $this->actingAs($doctor) + ->get(route('care.patients.index')) + ->assertOk() + ->assertSee('Ama Mensah') + ->assertSee('DEMO-8883-00001'); + } +}