diff --git a/services/nginx/app/classes/limited_backoffice_service.php b/services/nginx/app/classes/limited_backoffice_service.php index d8ecd4c4..8e786258 100644 --- a/services/nginx/app/classes/limited_backoffice_service.php +++ b/services/nginx/app/classes/limited_backoffice_service.php @@ -112,6 +112,124 @@ class limited_backoffice_service ], ]; + /** + * @var array + */ + private const ROLE_PERMISSION_CAPABILITIES = [ + 'user' => [ + 'group' => 'account', + 'capability' => 'sign_in', + ], + 'permissions_list_own' => [ + 'group' => 'account', + 'capability' => 'view_own_permissions', + ], + 'list_orders' => [ + 'group' => 'orders', + 'capability' => 'view_orders', + ], + 'add_order' => [ + 'group' => 'orders', + 'capability' => 'create_orders', + ], + 'edit_order' => [ + 'group' => 'orders', + 'capability' => 'edit_orders', + ], + 'delete_order' => [ + 'group' => 'orders', + 'capability' => 'delete_orders', + ], + 'list_order_items' => [ + 'group' => 'orders', + 'capability' => 'view_order_items', + ], + 'add_order_items' => [ + 'group' => 'orders', + 'capability' => 'create_order_items', + ], + 'edit_order_items' => [ + 'group' => 'orders', + 'capability' => 'update_order_lines', + ], + 'delete_order_items' => [ + 'group' => 'orders', + 'capability' => 'remove_order_lines', + ], + 'charge_order' => [ + 'group' => 'orders', + 'capability' => 'charge_orders', + ], + 'list_bookings' => [ + 'group' => 'bookings', + 'capability' => 'view_department_bookings', + ], + 'list_own_bookings' => [ + 'group' => 'bookings', + 'capability' => 'view_own_bookings', + ], + 'edit_bookings' => [ + 'group' => 'bookings', + 'capability' => 'update_bookings', + ], + 'add_booking' => [ + 'group' => 'bookings', + 'capability' => 'create_bookings', + ], + 'complete_bookings' => [ + 'group' => 'bookings', + 'capability' => 'mark_bookings_complete', + ], + 'resend_booking_confirmations' => [ + 'group' => 'bookings', + 'capability' => 'send_booking_confirmations', + ], + 'department_timebookings_entries_get' => [ + 'group' => 'time_bookings', + 'capability' => 'view_time_booking_entries', + ], + 'department_timebookings_entries_post' => [ + 'group' => 'time_bookings', + 'capability' => 'create_time_booking_entries', + ], + 'department_timebookings_entries_put' => [ + 'group' => 'time_bookings', + 'capability' => 'edit_time_booking_entries', + ], + 'statistics_orders_new' => [ + 'group' => 'reports', + 'capability' => 'view_order_statistics', + ], + 'statistics_bookings_new' => [ + 'group' => 'reports', + 'capability' => 'view_booking_statistics', + ], + self::PERMISSION_ACCESS => [ + 'group' => 'limited_backoffice', + 'capability' => 'open_limited_backoffice', + ], + self::PERMISSION_MANAGE_PRICES => [ + 'group' => 'limited_backoffice', + 'capability' => 'manage_department_prices', + ], + self::PERMISSION_MANAGE_EMPLOYEES => [ + 'group' => 'limited_backoffice', + 'capability' => 'manage_employee_access', + ], + ]; + + /** + * @var array + */ + private const ROLE_PERMISSION_GROUP_ORDER = [ + 'account', + 'orders', + 'bookings', + 'time_bookings', + 'reports', + 'limited_backoffice', + ]; + /** * @var array */ @@ -123,7 +241,7 @@ class limited_backoffice_service } /** - * @return array + * @return array}>}> */ public function rolePresets(): array { @@ -133,11 +251,45 @@ class limited_backoffice_service 'key' => $key, 'label' => $preset['label'], 'description' => $preset['description'], + 'permission_groups' => $this->rolePermissionGroups($preset['permissions']), ]; } return $roles; } + /** + * @param array $permissions + * @return array}> + */ + private function rolePermissionGroups(array $permissions): array + { + $groups = []; + foreach ($permissions as $permission) { + $capability = self::ROLE_PERMISSION_CAPABILITIES[$permission] ?? null; + if ($capability === null) { + throw new \RuntimeException('Missing limited backoffice role capability for permission: ' . $permission); + } + + $group = $capability['group']; + $groups[$group] ??= []; + $groups[$group][] = $capability['capability']; + } + + $payload = []; + foreach (self::ROLE_PERMISSION_GROUP_ORDER as $group) { + if (!isset($groups[$group])) { + continue; + } + + $payload[] = [ + 'key' => $group, + 'capabilities' => array_values(array_unique($groups[$group])), + ]; + } + + return $payload; + } + /** * @return array */ diff --git a/services/nginx/app/tests/Api/LimitedBackofficeApiTest.php b/services/nginx/app/tests/Api/LimitedBackofficeApiTest.php index 87a780e2..a685d338 100644 --- a/services/nginx/app/tests/Api/LimitedBackofficeApiTest.php +++ b/services/nginx/app/tests/Api/LimitedBackofficeApiTest.php @@ -285,7 +285,54 @@ it('creates updates lists and deactivates scoped employees without exposing raw ->assertSuccess(); expect(array_column($roles->data(), 'key'))->toBe(['viewer', 'cashier', 'booking_coordinator', 'operations_lead', 'department_admin']); + $rolesByKey = array_column($roles->data(), null, 'key'); + expect($rolesByKey['viewer']['permission_groups'] ?? null)->toBe([ + [ + 'key' => 'account', + 'capabilities' => ['sign_in', 'view_own_permissions'], + ], + ]); + $departmentAdminGroups = array_column($rolesByKey['department_admin']['permission_groups'] ?? [], 'capabilities', 'key'); + expect($departmentAdminGroups['limited_backoffice'] ?? null)->toBe([ + 'open_limited_backoffice', + 'manage_department_prices', + 'manage_employee_access', + ]); expect($roles->body)->not->toContain('department_access_'); + $rolePayload = $roles->data(); + $rolePayloadStrings = []; + array_walk_recursive($rolePayload, static function ($value) use (&$rolePayloadStrings): void { + if (is_string($value)) { + $rolePayloadStrings[] = $value; + } + }); + foreach ([ + 'list_orders', + 'add_order', + 'edit_order', + 'delete_order', + 'list_order_items', + 'add_order_items', + 'edit_order_items', + 'delete_order_items', + 'charge_order', + 'list_bookings', + 'list_own_bookings', + 'edit_bookings', + 'add_booking', + 'complete_bookings', + 'resend_booking_confirmations', + 'department_timebookings_entries_get', + 'department_timebookings_entries_post', + 'department_timebookings_entries_put', + 'statistics_orders_new', + 'statistics_bookings_new', + 'limited_backoffice_access', + 'limited_backoffice_prices_manage', + 'limited_backoffice_employees_manage', + ] as $rawPermission) { + expect($rolePayloadStrings)->not->toContain($rawPermission); + } $created = api_client()->post('/limited-backoffice/employees', [ 'display_name' => 'Limited Cashier',