From 8a749cffa376f895ce782e95dfdbc2183cb9eb44 Mon Sep 17 00:00:00 2001 From: Jeppe B <2jepp9350@gmail.com> Date: Mon, 1 Jun 2026 23:58:03 +0200 Subject: [PATCH] Fix system search department scoping for generic entities --- .../app/classes/system_search_service.php | 27 +++++++++++++++++-- .../nginx/app/routes/systemSearchRoute.php | 22 +++++++++++++++ .../SystemSearchServiceIntentFlowTest.php | 21 +++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/services/nginx/app/classes/system_search_service.php b/services/nginx/app/classes/system_search_service.php index 4581681f..461bc6f9 100644 --- a/services/nginx/app/classes/system_search_service.php +++ b/services/nginx/app/classes/system_search_service.php @@ -28,6 +28,7 @@ class system_search_service $allowedTypes = $this->normalizeTypes((array)($options['allowed_types'] ?? [])); $ownOnlyTypes = $this->normalizeTypes((array)($options['own_only_types'] ?? [])); $ownCustomerNumber = isset($options['own_customer_number']) ? (int)$options['own_customer_number'] : null; + $allowedDepartmentIds = array_values(array_unique(array_map('intval', (array)($options['allowed_department_ids'] ?? [])))); $permissionsCatalogAll = (array)($options['permissions_catalog_all'] ?? []); $permissionsCatalogOwn = (array)($options['permissions_catalog_own'] ?? []); $moduleConfigVisibility = (array)($options['module_config_visibility'] ?? []); @@ -81,6 +82,7 @@ class system_search_service 'offset' => $offset, 'own' => $ownCustomerNumber, 'own_only' => $ownOnlyTypes, + 'dept' => $allowedDepartmentIds, 'assoc' => $includeAssociations, 'dbg' => $debugIntent, 'ctx' => $this->permissionContextFingerprint($permissionsCatalogAll, $permissionsCatalogOwn, $moduleConfigVisibility), @@ -103,7 +105,8 @@ class system_search_service $ownCustomerNumber, $permissionsCatalogAll, $permissionsCatalogOwn, - $moduleConfigVisibility + $moduleConfigVisibility, + $allowedDepartmentIds ); $intentMeta = [ @@ -147,7 +150,8 @@ class system_search_service $ownCustomerNumber, $permissionsCatalogAll, $permissionsCatalogOwn, - $moduleConfigVisibility + $moduleConfigVisibility, + $allowedDepartmentIds ); } else { $intentMeta['status'] = 'fallback'; @@ -177,6 +181,7 @@ class system_search_service $permissionsCatalogAll, $permissionsCatalogOwn, $moduleConfigVisibility, + $allowedDepartmentIds, [$customerNumber] ); foreach ($associated as &$item) { @@ -237,6 +242,7 @@ class system_search_service * @param array $permissionsCatalogAll * @param array $permissionsCatalogOwn * @param array $moduleConfigVisibility + * @param array $allowedDepartmentIds * @param array $forcedCustomerNumbers * @return array> */ @@ -249,6 +255,7 @@ class system_search_service array $permissionsCatalogAll, array $permissionsCatalogOwn, array $moduleConfigVisibility, + array $allowedDepartmentIds = [], array $forcedCustomerNumbers = [] ): array { $results = []; @@ -264,6 +271,7 @@ class system_search_service $permissionsCatalogAll, $permissionsCatalogOwn, $moduleConfigVisibility, + $allowedDepartmentIds, $forcedCustomerNumbers ); $results = $this->mergeResults($results, $rows); @@ -276,6 +284,7 @@ class system_search_service * @param array $permissionsCatalogAll * @param array $permissionsCatalogOwn * @param array $moduleConfigVisibility + * @param array $allowedDepartmentIds * @param array $forcedCustomerNumbers * @return array> */ @@ -288,6 +297,7 @@ class system_search_service array $permissionsCatalogAll, array $permissionsCatalogOwn, array $moduleConfigVisibility, + array $allowedDepartmentIds, array $forcedCustomerNumbers ): array { if ($this->isGenericEntityType($entityType)) { @@ -297,6 +307,7 @@ class system_search_service $entityBoost, $ownOnly, $ownCustomerNumber, + $allowedDepartmentIds, $forcedCustomerNumbers ); } @@ -784,6 +795,7 @@ class system_search_service /** * @param array $terms + * @param array $allowedDepartmentIds * @param array $forcedCustomerNumbers * @return array> */ @@ -793,6 +805,7 @@ class system_search_service int $entityBoost, bool $ownOnly, ?int $ownCustomerNumber, + array $allowedDepartmentIds = [], array $forcedCustomerNumbers = [] ): array { if (empty($terms)) { @@ -891,6 +904,8 @@ class system_search_service $terms, $customerNumbers, $customerField, + $allowedDepartmentIds, + $departmentField, $fixedConditions ); @@ -1067,6 +1082,8 @@ class system_search_service * @param array $terms * @param array $customerNumbers * @param string|null $customerField + * @param array $departmentIds + * @param string|null $departmentField * @param array $fixedConditions * @return array> */ @@ -1077,6 +1094,8 @@ class system_search_service array $terms, array $customerNumbers = [], ?string $customerField = null, + array $departmentIds = [], + ?string $departmentField = null, array $fixedConditions = [] ): array { global $db; @@ -1109,6 +1128,10 @@ class system_search_service $wheres[] = "`$customerField` IN (" . implode(',', array_map('intval', $customerNumbers)) . ")"; } + if (!empty($departmentIds) && $departmentField !== null && in_array($departmentField, $fields, true)) { + $wheres[] = "`$departmentField` IN (" . implode(',', array_map('intval', $departmentIds)) . ")"; + } + $termClauses = []; foreach ($terms as $term) { $escaped = $db->escape_string($term); diff --git a/services/nginx/app/routes/systemSearchRoute.php b/services/nginx/app/routes/systemSearchRoute.php index 359de126..fe08ec17 100644 --- a/services/nginx/app/routes/systemSearchRoute.php +++ b/services/nginx/app/routes/systemSearchRoute.php @@ -113,6 +113,7 @@ class systemSearchRoute 'allowed_types' => $allowedTypes, 'own_only_types' => $ownOnlyTypes, 'own_customer_number' => $this->resolveEffectiveCustomerNumber(), + 'allowed_department_ids' => $this->resolveAllowedDepartmentIds($user), 'permissions_catalog_all' => $permissionsCatalogAll, 'permissions_catalog_own' => $permissionsCatalogOwn, 'module_config_visibility' => $this->buildModuleConfigVisibility(), @@ -503,6 +504,27 @@ class systemSearchRoute return $flat; } + /** + * @param mixed $user + * @return array + */ + private function resolveAllowedDepartmentIds(mixed $user): array + { + if ($user === false) { + return []; + } + + try { + $departments = $user->getGroup()->getDepartments(); + if (!is_array($departments)) { + return []; + } + return array_values(array_unique(array_map('intval', $departments))); + } catch (Throwable) { + return []; + } + } + private function parseTypeList(mixed $value): array { $result = []; diff --git a/services/nginx/app/tests/Unit/Search/SystemSearchServiceIntentFlowTest.php b/services/nginx/app/tests/Unit/Search/SystemSearchServiceIntentFlowTest.php index 7b7b37d0..bb6666a2 100644 --- a/services/nginx/app/tests/Unit/Search/SystemSearchServiceIntentFlowTest.php +++ b/services/nginx/app/tests/Unit/Search/SystemSearchServiceIntentFlowTest.php @@ -127,6 +127,7 @@ if (!class_exists('TestableSystemSearchService')) { array $permissionsCatalogAll, array $permissionsCatalogOwn, array $moduleConfigVisibility, + array $allowedDepartmentIds = [], array $forcedCustomerNumbers = [] ): array { $this->lexicalCalls[] = [ @@ -135,6 +136,7 @@ if (!class_exists('TestableSystemSearchService')) { 'entityBoost' => $entityBoost, 'ownOnlyTypes' => $ownOnlyTypes, 'ownCustomerNumber' => $ownCustomerNumber, + 'allowedDepartmentIds' => $allowedDepartmentIds, 'forcedCustomerNumbers' => $forcedCustomerNumbers, ]; if (empty($this->queuedLexicalResults)) { @@ -348,3 +350,22 @@ it('caps AI-driven expanded terms to prevent query amplification', function (): } expect($maxLen)->toBeLessThanOrEqual(64); }); + + +it('passes allowed department ids into lexical execution context', function (): void { + $parser = new FakeSystemSearchIntentParser(); + + $service = new TestableSystemSearchService($parser, [[ + ['entity_type' => 'orders', 'entity_id' => '1', 'title' => 'Order #1', 'score' => 70], + ]]); + + $service->search([ + 'query' => 'order', + 'allowed_types' => ['orders'], + 'include_associations' => false, + 'allowed_department_ids' => [3, '7', 3], + ]); + + expect(count($service->lexicalCalls))->toBe(1); + expect($service->lexicalCalls[0]['allowedDepartmentIds'])->toBe([3, 7]); +});