Cashiers collect via Billing only; strip POS from demo staff grants and StaffUx/launcher so the suite hub no longer offers POS for that role. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -286,6 +286,9 @@ class SsoLoginController extends Controller
|
||||
{
|
||||
try {
|
||||
$access = $identity->appAccess($user->ownerRef(), $intended);
|
||||
$member = app(\App\Services\Care\OrganizationResolver::class)->memberFor($user);
|
||||
$access = app(\App\Services\Care\CarePermissions::class)
|
||||
->filterStaffAppAccess($member, $access);
|
||||
\App\Support\StaffUx::remember($access);
|
||||
|
||||
return $access['url'] !== '' ? $access['url'] : $intended;
|
||||
|
||||
@@ -74,16 +74,43 @@ class AppServiceProvider extends ServiceProvider
|
||||
|
||||
View::composer(['partials.launcher', 'partials.topbar-desktop-widgets', 'components.app-layout'], function () {
|
||||
$user = auth()->user();
|
||||
if (! $user || session()->has(\App\Support\StaffUx::SESSION_KEY)) {
|
||||
if (! $user) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$access = app(\App\Services\Identity\IdentityTeamClient::class)
|
||||
->appAccess($user->ownerRef());
|
||||
\App\Support\StaffUx::remember($access);
|
||||
} catch (\Throwable) {
|
||||
// Leave fail-open defaults until Identity is available.
|
||||
$member = app(\App\Services\Care\OrganizationResolver::class)->memberFor($user);
|
||||
$permissions = app(\App\Services\Care\CarePermissions::class);
|
||||
|
||||
if (! session()->has(\App\Support\StaffUx::SESSION_KEY)) {
|
||||
try {
|
||||
$access = app(\App\Services\Identity\IdentityTeamClient::class)
|
||||
->appAccess($user->ownerRef());
|
||||
\App\Support\StaffUx::remember(
|
||||
$permissions->filterStaffAppAccess($member, $access)
|
||||
);
|
||||
} catch (\Throwable) {
|
||||
// Leave fail-open defaults until Identity is available.
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Stale Identity grants may still list POS — strip for Care cashiers.
|
||||
$cached = session(\App\Support\StaffUx::SESSION_KEY);
|
||||
if (! is_array($cached) || ! empty($cached['full_access'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$filtered = $permissions->filterStaffAppAccess($member, [
|
||||
'full_access' => false,
|
||||
'apps' => array_values(array_map('strval', (array) ($cached['apps'] ?? []))),
|
||||
'show_hub' => (bool) ($cached['show_hub'] ?? false),
|
||||
'show_billing' => (bool) ($cached['show_billing'] ?? false),
|
||||
]);
|
||||
|
||||
if ($filtered['apps'] !== array_values(array_map('strval', (array) ($cached['apps'] ?? [])))
|
||||
|| (bool) ($filtered['show_hub'] ?? false) !== (bool) ($cached['show_hub'] ?? false)) {
|
||||
\App\Support\StaffUx::remember($filtered);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ class CarePermissions
|
||||
'pharmacy.view', 'pharmacy.manage',
|
||||
'service_queues.console',
|
||||
],
|
||||
// Cashiers run the Billing desk (bills / payments). They do not get
|
||||
// Ladill POS app access — see filterAllowedAppSlugs() / DemoWorld.
|
||||
'cashier' => [
|
||||
'dashboard.view', 'patients.view', 'bills.view', 'bills.manage', 'payments.manage',
|
||||
'service_queues.console',
|
||||
@@ -120,4 +122,47 @@ class CarePermissions
|
||||
{
|
||||
return $member !== null && $member->role === 'doctor';
|
||||
}
|
||||
|
||||
/**
|
||||
* Care cashiers collect via Billing, not Ladill POS. Strip POS from
|
||||
* Identity app grants so the suite launcher hides it.
|
||||
*
|
||||
* @param list<string>|null $slugs Null = unrestricted owner access.
|
||||
* @return list<string>|null
|
||||
*/
|
||||
public function filterAllowedAppSlugs(?Member $member, ?array $slugs): ?array
|
||||
{
|
||||
if ($slugs === null || $member?->role !== 'cashier') {
|
||||
return $slugs;
|
||||
}
|
||||
|
||||
return array_values(array_filter(
|
||||
array_map('strval', $slugs),
|
||||
fn (string $slug) => $slug !== 'pos',
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{full_access?: bool, apps?: list<string>, show_hub?: bool, show_billing?: bool} $access
|
||||
* @return array{full_access?: bool, apps?: list<string>, show_hub?: bool, show_billing?: bool}
|
||||
*/
|
||||
public function filterStaffAppAccess(?Member $member, array $access): array
|
||||
{
|
||||
if ($member?->role !== 'cashier') {
|
||||
return $access;
|
||||
}
|
||||
|
||||
$apps = $this->filterAllowedAppSlugs($member, array_values(array_map(
|
||||
'strval',
|
||||
(array) ($access['apps'] ?? []),
|
||||
))) ?? [];
|
||||
|
||||
$access['apps'] = $apps;
|
||||
|
||||
if (empty($access['full_access']) && count($apps) <= 1) {
|
||||
$access['show_hub'] = false;
|
||||
}
|
||||
|
||||
return $access;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,9 +286,18 @@ final class DemoWorld
|
||||
'key' => 'cashier',
|
||||
'email' => 'demo-pro-cashier@ladill.com',
|
||||
'name' => 'Kojo Cashier (Pro)',
|
||||
'apps' => ['care', 'pos'],
|
||||
// Care cashiers collect via Billing — not Ladill POS.
|
||||
'apps' => ['care'],
|
||||
'roles' => [
|
||||
'care' => 'cashier',
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'pos_cashier',
|
||||
'email' => 'demo-pro-pos-cashier@ladill.com',
|
||||
'name' => 'Kofi POS Cashier (Pro)',
|
||||
'apps' => ['pos'],
|
||||
'roles' => [
|
||||
'pos' => 'cashier',
|
||||
],
|
||||
],
|
||||
@@ -365,9 +374,18 @@ final class DemoWorld
|
||||
'key' => 'cashier',
|
||||
'email' => 'demo-enterprise-cashier@ladill.com',
|
||||
'name' => 'Kojo Cashier',
|
||||
'apps' => ['care', 'pos'],
|
||||
// Care cashiers collect via Billing — not Ladill POS.
|
||||
'apps' => ['care'],
|
||||
'roles' => [
|
||||
'care' => 'cashier',
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'pos_cashier',
|
||||
'email' => 'demo-enterprise-pos-cashier@ladill.com',
|
||||
'name' => 'Kofi POS Cashier',
|
||||
'apps' => ['pos'],
|
||||
'roles' => [
|
||||
'pos' => 'cashier',
|
||||
],
|
||||
],
|
||||
|
||||
@@ -193,6 +193,68 @@ class CareWebTest extends TestCase
|
||||
->assertDontSee('Patients in queue');
|
||||
}
|
||||
|
||||
public function test_cashier_pos_app_access_is_stripped_from_staff_ux(): void
|
||||
{
|
||||
Member::where('user_ref', $this->user->public_id)->update(['role' => 'cashier']);
|
||||
|
||||
$member = Member::where('user_ref', $this->user->public_id)->firstOrFail();
|
||||
$permissions = app(\App\Services\Care\CarePermissions::class);
|
||||
|
||||
$this->assertSame(
|
||||
['care'],
|
||||
$permissions->filterAllowedAppSlugs($member, ['care', 'pos']),
|
||||
);
|
||||
|
||||
$filtered = $permissions->filterStaffAppAccess($member, [
|
||||
'full_access' => false,
|
||||
'apps' => ['care', 'pos'],
|
||||
'show_hub' => true,
|
||||
'show_billing' => false,
|
||||
]);
|
||||
|
||||
$this->assertSame(['care'], $filtered['apps']);
|
||||
$this->assertFalse($filtered['show_hub']);
|
||||
|
||||
foreach (['pro', 'enterprise'] as $tier) {
|
||||
$cashier = collect(\App\Support\DemoWorld::staff($tier))->firstWhere('key', 'cashier');
|
||||
$this->assertNotNull($cashier);
|
||||
$this->assertSame(['care'], $cashier['apps']);
|
||||
$this->assertArrayNotHasKey('pos', $cashier['roles']);
|
||||
$this->assertSame('cashier', $cashier['roles']['care'] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_cashier_dashboard_hides_pos_launcher_and_keeps_billing(): void
|
||||
{
|
||||
$this->organization->update([
|
||||
'settings' => array_merge($this->organization->settings ?? [], [
|
||||
'plan' => 'pro',
|
||||
'plan_expires_at' => now()->addMonth()->toIso8601String(),
|
||||
]),
|
||||
]);
|
||||
|
||||
Member::where('user_ref', $this->user->public_id)->update([
|
||||
'role' => 'cashier',
|
||||
'branch_id' => Branch::firstOrFail()->id,
|
||||
]);
|
||||
|
||||
// Simulate a stale Identity grant that still includes POS.
|
||||
\App\Support\StaffUx::remember([
|
||||
'full_access' => false,
|
||||
'apps' => ['care', 'pos'],
|
||||
'show_hub' => true,
|
||||
'show_billing' => false,
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->get(route('care.dashboard'))
|
||||
->assertOk()
|
||||
->assertSee('Billing')
|
||||
->assertSee(route('care.bills.index'), false)
|
||||
->assertDontSee('https://pos.', false)
|
||||
->assertDontSee('>POS<', false);
|
||||
}
|
||||
|
||||
public function test_onboarding_creates_organization(): void
|
||||
{
|
||||
Organization::query()->delete();
|
||||
|
||||
Reference in New Issue
Block a user