diff --git a/services/nginx/app/classes/customer_product_rule_service.php b/services/nginx/app/classes/customer_product_rule_service.php index e0ed9499..c54159ec 100644 --- a/services/nginx/app/classes/customer_product_rule_service.php +++ b/services/nginx/app/classes/customer_product_rule_service.php @@ -12,6 +12,7 @@ class customer_product_rule_service private const ADDON_CATEGORY_ID = 4; private const TANK_CLEANING_CATEGORY_ID = 5; + private const SPOT_FREE_PRODUCT_IDS = [23, 24]; /** * @return array{rule:string,message:string}|null @@ -52,7 +53,7 @@ class customer_product_rule_service } if ($customer->doesUserHaveAttribute('restrictSpotFree') - && $this->containsAny($searchableProduct, ['spot free', 'spotfree'])) { + && $this->isSpotFreeProduct((int)$product->id, $searchableProduct)) { return $this->violation('restrictSpotFree'); } @@ -101,6 +102,15 @@ class customer_product_rule_service return $this->containsAny($searchableProduct, ['tank cleaning', 'tankcleaning', 'tankrens', 'tank rens']); } + private function isSpotFreeProduct(int $productId, string $searchableProduct): bool + { + if (in_array($productId, self::SPOT_FREE_PRODUCT_IDS, true)) { + return true; + } + + return $this->containsAny($searchableProduct, ['spot free', 'spotfree', 'skylning med ro']); + } + private function searchableProductText(products_o $product, string $categoryName): string { return strtolower(trim((string)$product->name->value() . ' ' . $categoryName)); diff --git a/services/nginx/app/classes/invoice_period_flag_service.php b/services/nginx/app/classes/invoice_period_flag_service.php index 56730164..13a7c343 100644 --- a/services/nginx/app/classes/invoice_period_flag_service.php +++ b/services/nginx/app/classes/invoice_period_flag_service.php @@ -22,6 +22,7 @@ class invoice_period_flag_service private const ORDER_FIELDS = ['customer', 'reference', 'po', 'notes']; private const ORDER_ITEM_FIELDS = ['notes', 'quantity', 'reference', 'price']; private const WASH_CERTIFICATE_PRODUCT_ID = 41; + private const SPOT_FREE_PRODUCT_IDS = [23, 24]; private array $economicCustomerDiscountCache = []; private array $userDisplayNameCache = []; private array $orderItemsPreviewCache = []; @@ -939,7 +940,7 @@ class invoice_period_flag_service } $restrictedProducts = [ - 'restrictSpotFree' => ['customer_rule_restrict_spot_free', ['spot free', 'spotfree']], + 'restrictSpotFree' => ['customer_rule_restrict_spot_free', ['spot free', 'spotfree', 'skylning med ro']], 'restrictInteriorCleaning' => ['customer_rule_restrict_interior_cleaning', ['interior', 'indvendig']], 'exemptFromAdministrationFee' => ['customer_rule_exempt_from_administration_fees', ['administration fee', 'administrationsgebyr', 'administration']], ]; @@ -2097,6 +2098,11 @@ class invoice_period_flag_service private function rowMatchesProductTerms(array $row, array $terms): bool { + if (in_array((int)($row['product_id'] ?? 0), self::SPOT_FREE_PRODUCT_IDS, true) + && in_array('spotfree', array_map('strtolower', $terms), true)) { + return true; + } + $haystack = strtolower(trim( (string)($row['product_name'] ?? '') . ' ' . (string)($row['category_name'] ?? '') diff --git a/services/nginx/app/tests/Api/CollectedInvoiceBulkActionsApiTest.php b/services/nginx/app/tests/Api/CollectedInvoiceBulkActionsApiTest.php index 564130a0..b864899f 100644 --- a/services/nginx/app/tests/Api/CollectedInvoiceBulkActionsApiTest.php +++ b/services/nginx/app/tests/Api/CollectedInvoiceBulkActionsApiTest.php @@ -93,6 +93,84 @@ it('previews and applies customer rule cleanup only after exact typed confirmati ->and(bulk_action_order_item_deleted_at((int)$orderItem['id']))->not->toBeNull(); }); +it('previews and applies customer rule cleanup for both spotfree addon products', function (): void { + api_test_covers('POST /collected-invoices/bulk-actions/preview', 'customer-rule-cleanup-spotfree-addons'); + api_test_covers('POST /collected-invoices/bulk-actions/apply', 'customer-rule-cleanup-spotfree-addons'); + + $customer = api_fixtures()->createUser(['display_name' => 'Bulk Cleanup Spotfree Addons Customer']); + api_fixtures()->addCustomerAttribute((int)$customer['id'], 'restrictSpotFree'); + $department = api_fixtures()->createDepartment(); + $invoiceCollection = api_fixtures()->createInvoiceCollection([ + 'customer_number' => $customer['customer_number'], + ]); + $order = api_fixtures()->createOrder([ + 'customer_id' => $customer['customer_number'], + 'department_id' => $department['id'], + 'invoice_collection_id' => $invoiceCollection['id'], + ]); + $spotfreeVanProduct = api_fixtures()->createProduct([ + 'id' => 23, + 'name' => 'Skylning med RO - Varevogn', + 'category' => 4, + 'price' => 39, + ]); + $spotfreeTruckProduct = api_fixtures()->createProduct([ + 'id' => 24, + 'name' => 'Skylning med RO - Lastbil', + 'category' => 4, + 'price' => 39, + ]); + $vanOrderItem = api_fixtures()->createOrderItem([ + 'order_id' => $order['id'], + 'product_id' => $spotfreeVanProduct['id'], + 'cashier_id' => 1, + 'price' => 39, + ]); + $truckOrderItem = api_fixtures()->createOrderItem([ + 'order_id' => $order['id'], + 'product_id' => $spotfreeTruckProduct['id'], + 'cashier_id' => 1, + 'price' => 39, + ]); + $session = api_fixtures()->createUserSession(['reset_collected_invoice_economic']); + + $previewResponse = api_client()->post('/collected-invoices/bulk-actions/preview', [ + 'action' => 'remove_customer_rule_violations', + 'invoice_collection_ids' => [$invoiceCollection['id']], + 'locale' => 'en', + ], $session['headers']); + + $previewResponse + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + $preview = $previewResponse->data(); + $previewOrderItemIds = array_map('intval', array_column($preview['order_items'] ?? [], 'order_item_id')); + sort($previewOrderItemIds); + + expect($preview['summary']['changed_count'] ?? null)->toBe(2) + ->and($previewOrderItemIds)->toBe([(int)$vanOrderItem['id'], (int)$truckOrderItem['id']]) + ->and(bulk_action_order_item_deleted_at((int)$vanOrderItem['id']))->toBeNull() + ->and(bulk_action_order_item_deleted_at((int)$truckOrderItem['id']))->toBeNull(); + + $applyResponse = api_client()->post('/collected-invoices/bulk-actions/apply', [ + 'preview_id' => $preview['preview_id'], + 'action' => 'remove_customer_rule_violations', + 'invoice_collection_ids' => [$invoiceCollection['id']], + 'confirmation_text' => 'Confirm', + 'locale' => 'en', + ], $session['headers']); + + $applyResponse + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + expect(bulk_action_order_item_deleted_at((int)$vanOrderItem['id']))->not->toBeNull() + ->and(bulk_action_order_item_deleted_at((int)$truckOrderItem['id']))->not->toBeNull(); +}); + it('merges selected invoice collections into the explicit target after confirmation', function (): void { api_test_covers('POST /collected-invoices/bulk-actions/preview', 'merge'); api_test_covers('POST /collected-invoices/bulk-actions/apply', 'merge');