diff --git a/services/nginx/app/classes/customer_order_product_policy.php b/services/nginx/app/classes/customer_order_product_policy.php new file mode 100644 index 00000000..cca75373 --- /dev/null +++ b/services/nginx/app/classes/customer_order_product_policy.php @@ -0,0 +1,98 @@ +query($sql); + if (!$result || $result->num_rows < 1) { + return null; + } + + $row = $result->fetch_assoc(); + return is_array($row) ? $row : null; + } + + private static function rowMatchesProductTerms(array $row, array $terms): bool + { + $haystack = strtolower(trim( + (string)($row['product_name'] ?? $row['name'] ?? '') . ' ' . + (string)($row['category_name'] ?? '') + )); + + foreach ($terms as $term) { + if ($term !== '' && str_contains($haystack, strtolower($term))) { + return true; + } + } + + return false; + } +} diff --git a/services/nginx/app/classes/invoice_period_flag_service.php b/services/nginx/app/classes/invoice_period_flag_service.php index bb8e0a56..09778baf 100644 --- a/services/nginx/app/classes/invoice_period_flag_service.php +++ b/services/nginx/app/classes/invoice_period_flag_service.php @@ -2036,8 +2036,7 @@ class invoice_period_flag_service private function rowIsTankCleaningProduct(array $row): bool { - return (int)($row['product_category'] ?? 0) === 5 - || $this->rowMatchesProductTerms($row, ['tank cleaning', 'tankcleaning', 'tankrens']); + return customer_order_product_policy::isTankCleaningProductRow($row); } private function isIncludedOrderItem(array $row): bool diff --git a/services/nginx/app/objects/order_items_o.php b/services/nginx/app/objects/order_items_o.php index 38a43603..248a4603 100644 --- a/services/nginx/app/objects/order_items_o.php +++ b/services/nginx/app/objects/order_items_o.php @@ -3,6 +3,7 @@ namespace objects; use classes\db; +use classes\customer_order_product_policy; use classes\object_property; use Exception; use traits\db_object_t; @@ -93,6 +94,7 @@ class order_items_o extends db { global $db, $response; try { + customer_order_product_policy::assertOrderAllowsProduct($order_id, $product_id); // Avoid SQL injection $reference = $db->escape_string($reference); $notes = $db->escape_string($notes); @@ -167,6 +169,7 @@ class order_items_o extends db try { // Get the order $order = (new orders_o())->getOrderById($order_id); + customer_order_product_policy::assertOrderAllowsProduct($order_id, $product_id); // Get the product price $price = (new products_o())->getProductById($product_id)->getDepartmentPrice((int)$order->department_id->value()); @@ -354,4 +357,4 @@ class order_items_o extends db { return (new products_o())->select((int)$this->product_id->value()); } -} \ No newline at end of file +} diff --git a/services/nginx/app/tests/Api/OrderItemsApiTest.php b/services/nginx/app/tests/Api/OrderItemsApiTest.php index 577c80f6..5e21cacc 100644 --- a/services/nginx/app/tests/Api/OrderItemsApiTest.php +++ b/services/nginx/app/tests/Api/OrderItemsApiTest.php @@ -15,7 +15,6 @@ it('requires notes when adding the extraordinary chemistry product to an order', 'reference' => 'NOTE-REQUIRED', ]); $product = api_fixtures()->createProduct([ - 'id' => 902701, 'name' => \objects\products_o::EXTRAORDINARY_CHEMISTRY_PRODUCT_NAME, 'price' => 299, 'requires_note' => 0, @@ -49,6 +48,85 @@ it('requires notes when adding the extraordinary chemistry product to an order', expect($response->data()['notes'] ?? null)->toBe('Graffiti removal on left side'); }); +it('only allows tankcleaning products for only tankcleaning customers', function (): void { + api_test_covers('POST /order/items', 'customer_rules'); + + $customer = api_fixtures()->createUser(['display_name' => 'Only Tankcleaning Customer']); + api_fixtures()->addCustomerAttribute((int)$customer['id'], 'onlyTankCleaning'); + $department = api_fixtures()->createDepartment(); + $order = api_fixtures()->createOrder([ + 'customer_id' => $customer['customer_number'], + 'department_id' => $department['id'], + 'reference' => 'ONLY-TANK', + ]); + $washProduct = api_fixtures()->createProduct([ + 'name' => 'Forvogn', + 'price' => 649, + 'category' => 4, + ]); + $tankCleaningProduct = api_fixtures()->createProduct([ + 'name' => 'Saebe/kemi, 1-4 spulehoveder', + 'price' => 299, + 'category' => 5, + ]); + $session = api_fixtures()->createUserSession([], ['group_id' => 1]); + + api_client() + ->post('/order/items', [ + 'order_id' => $order['id'], + 'product_id' => $washProduct['id'], + 'quantity' => 1, + ], $session['headers']) + ->assertStatus(400) + ->assertEnvelope() + ->assertSuccess(false) + ->assertMessage(\classes\customer_order_product_policy::ONLY_TANKCLEANING_MESSAGE); + + $response = api_client()->post('/order/items', [ + 'order_id' => $order['id'], + 'product_id' => $tankCleaningProduct['id'], + 'quantity' => 1, + ], $session['headers']); + + $response + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect((int)($response->data()['product_id'] ?? 0))->toBe((int)$tankCleaningProduct['id']); +}); + +it('allows non-tankcleaning products for customers without the only tankcleaning attribute', function (): void { + api_test_covers('POST /order/items', 'customer_rules'); + + $customer = api_fixtures()->createUser(['display_name' => 'Regular Order Item Customer']); + $department = api_fixtures()->createDepartment(); + $order = api_fixtures()->createOrder([ + 'customer_id' => $customer['customer_number'], + 'department_id' => $department['id'], + 'reference' => 'REGULAR-WASH', + ]); + $washProduct = api_fixtures()->createProduct([ + 'name' => 'Forvogn', + 'price' => 649, + 'category' => 4, + ]); + $session = api_fixtures()->createUserSession([], ['group_id' => 1]); + + $response = api_client()->post('/order/items', [ + 'order_id' => $order['id'], + 'product_id' => $washProduct['id'], + 'quantity' => 1, + ], $session['headers']); + + $response + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect((int)($response->data()['product_id'] ?? 0))->toBe((int)$washProduct['id']); +}); + it('does not allow clearing notes for order items whose product requires notes', function (): void { api_test_covers('PUT /order/items', 'validation'); @@ -62,7 +140,6 @@ it('does not allow clearing notes for order items whose product requires notes', 'reference' => 'NOTE-EDIT', ]); $product = api_fixtures()->createProduct([ - 'id' => 902702, 'name' => 'API Note Required Product', 'price' => 199, 'requires_note' => 1, @@ -75,7 +152,7 @@ it('does not allow clearing notes for order items whose product requires notes', 'quantity' => 1, 'notes' => 'Initial note', ]); - $session = api_fixtures()->createUserSession(['edit_order_items']); + $session = api_fixtures()->createUserSession(['edit_order_items', 'list_order_items']); api_client() ->put('/order/items', [ @@ -95,7 +172,6 @@ it('returns the extraordinary chemistry product with requires_note enabled', fun api_test_covers('GET /products', 'happy'); $product = api_fixtures()->createProduct([ - 'id' => 902703, 'name' => \objects\products_o::EXTRAORDINARY_CHEMISTRY_PRODUCT_NAME, 'price' => 299, 'requires_note' => 0, diff --git a/services/nginx/app/tests/Unit/Orders/CustomerOrderProductPolicyTest.php b/services/nginx/app/tests/Unit/Orders/CustomerOrderProductPolicyTest.php new file mode 100644 index 00000000..8da99e2b --- /dev/null +++ b/services/nginx/app/tests/Unit/Orders/CustomerOrderProductPolicyTest.php @@ -0,0 +1,40 @@ + 5, + 'product_name' => 'Saebe/kemi, 1-4 spulehoveder', + 'category_name' => 'Other', + ]))->toBeTrue() + ->and(customer_order_product_policy::isTankCleaningProductRow([ + 'product_category' => 3, + 'product_name' => 'Tank cleaning 4 spulehoveder', + 'category_name' => 'Other', + ]))->toBeTrue() + ->and(customer_order_product_policy::isTankCleaningProductRow([ + 'product_category' => 3, + 'product_name' => 'Saebe/kemi, 1-4 spulehoveder', + 'category_name' => 'Tankrens', + ]))->toBeTrue(); +}); + +it('detects only tankcleaning violations only for attributed customers and non-tank products', function (): void { + $washProduct = [ + 'product_category' => 4, + 'product_name' => 'Forvogn', + 'category_name' => 'Udvendig', + ]; + $tankCleaningProduct = [ + 'product_category' => 5, + 'product_name' => 'Tank cleaning 4 spulehoveder', + 'category_name' => 'Tank cleaning', + ]; + + expect(customer_order_product_policy::onlyTankCleaningViolation(true, $washProduct))->toBeTrue() + ->and(customer_order_product_policy::onlyTankCleaningViolation(true, $tankCleaningProduct))->toBeFalse() + ->and(customer_order_product_policy::onlyTankCleaningViolation(false, $washProduct))->toBeFalse(); +});