createUser(['display_name' => 'Order Item Rule Customer']); foreach ($customerAttributes as $attribute) { api_fixtures()->addCustomerAttribute((int)$customer['id'], (string)$attribute); } $department = api_fixtures()->createDepartment(); $order = api_fixtures()->createOrder([ 'customer_id' => $customer['customer_number'], 'department_id' => $department['id'], 'reference' => 'RULE-CHECK', ]); $session = api_fixtures()->createUserSession([], ['group_id' => 1]); return [ 'customer' => $customer, 'department' => $department, 'order' => $order, 'session' => $session, ]; } function post_order_item(array $order, array $product, array $headers, array $overrides = []): \Tests\Support\Api\ApiResponse { return api_client()->post('/order/items', array_merge([ 'order_id' => $order['id'], 'product_id' => $product['id'], 'quantity' => 1, ], $overrides), $headers); } function custom_pricing_only_price_override(int $userId, int $productId, int $percentage): void { $statement = api_test_runtime()->db()->prepare( 'INSERT INTO `price_overrides` (`user_id`, `is_category`, `product_or_category_id`, `percentage`) VALUES (?, 0, ?, ?)' ); $productIdText = (string)$productId; $statement->bind_param('isi', $userId, $productIdText, $percentage); $statement->execute(); $statement->close(); api_fixtures()->cleanupDeleteWhere('price_overrides', [ 'user_id' => $userId, 'is_category' => 0, 'product_or_category_id' => $productIdText, ]); } function custom_pricing_only_department_price(int $departmentId, int $productId, int $price): void { $statement = api_test_runtime()->db()->prepare( 'INSERT INTO `product_department_prices` (`department_id`, `product_id`, `price`) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE `price` = VALUES(`price`)' ); $statement->bind_param('iii', $departmentId, $productId, $price); $statement->execute(); $statement->close(); api_fixtures()->cleanupDeleteWhere('product_department_prices', [ 'department_id' => $departmentId, 'product_id' => $productId, ]); } it('requires notes when adding the extraordinary chemistry product to an order', function (): void { api_test_covers('POST /order/items', 'validation'); $customer = api_fixtures()->createUser(['display_name' => 'Order Item Customer']); $department = api_fixtures()->createDepartment(); $order = api_fixtures()->createOrder([ 'customer_id' => $customer['customer_number'], 'department_id' => $department['id'], 'reference' => 'NOTE-REQUIRED', ]); $product = api_fixtures()->createProduct([ 'name' => \objects\products_o::EXTRAORDINARY_CHEMISTRY_PRODUCT_NAME, 'price' => 299, 'requires_note' => 0, ]); $session = api_fixtures()->createUserSession([], ['group_id' => 1]); api_client() ->post('/order/items', [ 'order_id' => $order['id'], 'product_id' => $product['id'], 'quantity' => 1, 'notes' => ' ', ], $session['headers']) ->assertStatus(400) ->assertEnvelope() ->assertSuccess(false) ->assertMessage('Notes is required for this product'); $response = api_client()->post('/order/items', [ 'order_id' => $order['id'], 'product_id' => $product['id'], 'quantity' => 1, 'notes' => 'Graffiti removal on left side', ], $session['headers']); $response ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect($response->data()['notes'] ?? null)->toBe('Graffiti removal on left side'); }); it('uses a product fixed price instead of the best discount when adding an order item', function (): void { api_test_covers('POST /order/items', 'pricing'); api_test_covers('GET /products', 'pricing'); $customer = api_fixtures()->createUser(['display_name' => 'Fixed Price Customer']); $department = api_fixtures()->createDepartment(); $cashier = api_fixtures()->createUser(['display_name' => 'Fixed Price Cashier']); $category = api_fixtures()->createCategory(['name' => 'Fixed Price Category']); $product = api_fixtures()->createProduct([ 'name' => 'Fixed Price Product', 'price' => 1000, 'category' => $category['id'], 'apply_category_discount' => 1, ]); api_fixtures()->createPriceOverride([ 'user_id' => $customer['id'], 'is_category' => 1, 'product_or_category_id' => (string)$category['id'], 'percentage' => 80, ]); api_fixtures()->createPriceOverride([ 'user_id' => $customer['id'], 'is_category' => 0, 'product_or_category_id' => (string)$product['id'], 'percentage' => 10, 'fixed_price' => 350, ]); $order = api_fixtures()->createOrder([ 'customer_id' => $customer['customer_number'], 'department_id' => $department['id'], 'cashier_id' => $cashier['id'], 'reference' => 'FIXED-PRICE', ]); $session = api_fixtures()->createUserSession(['add_order_items', 'list_products']); $productResponse = api_client()->get( '/products?final_price=true&id=' . $product['id'] . '&customer_id=' . $customer['customer_number'], $session['headers'] ); $productResponse ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect($productResponse->data()['price'] ?? null)->toBe(350); $response = api_client()->post('/order/items', [ 'order_id' => $order['id'], 'product_id' => $product['id'], 'quantity' => 1, ], $session['headers']); $response ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect($response->data()['price'] ?? null)->toBe(350); }); 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_product_rule_service::BLOCK_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'); $customer = api_fixtures()->createUser(['display_name' => 'Order Item Edit Customer']); $department = api_fixtures()->createDepartment(); $cashier = api_fixtures()->createUser(['display_name' => 'Order Item Cashier']); $order = api_fixtures()->createOrder([ 'customer_id' => $customer['customer_number'], 'department_id' => $department['id'], 'cashier_id' => $cashier['id'], 'reference' => 'NOTE-EDIT', ]); $product = api_fixtures()->createProduct([ 'id' => 902702, 'name' => \objects\products_o::EXTRAORDINARY_CHEMISTRY_PRODUCT_NAME, 'price' => 199, 'requires_note' => 1, ]); $orderItem = api_fixtures()->createOrderItem([ 'order_id' => $order['id'], 'product_id' => $product['id'], 'cashier_id' => $cashier['id'], 'price' => 199, 'quantity' => 1, 'notes' => 'Initial note', ]); $session = api_fixtures()->createUserSession(['edit_order_items', 'list_order_items']); api_client() ->put('/order/items', [ 'id' => $orderItem['id'], 'price' => 199, 'quantity' => 1, 'reference' => '', 'notes' => '', ], $session['headers']) ->assertStatus(400) ->assertEnvelope() ->assertSuccess(false) ->assertMessage('Notes is required for this product'); }); it('returns the extraordinary chemistry product with requires_note enabled', function (): void { api_test_covers('GET /products', 'happy'); $product = api_fixtures()->createProduct([ 'name' => \objects\products_o::EXTRAORDINARY_CHEMISTRY_PRODUCT_NAME, 'price' => 299, 'requires_note' => 0, ]); $session = api_fixtures()->createUserSession([], ['group_id' => 1]); $response = api_client()->get('/products?id=' . $product['id'], $session['headers']); $response ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect($response->data()['requires_note'] ?? null)->toBeTrue(); }); it('blocks addon products added as standalone additional order items for customers restricted from additional services', function (): void { api_test_covers('POST /order/items', 'customer-rule-validation'); $fixture = create_order_item_rule_fixture(['restrictAdditionalServices']); $primaryProduct = api_fixtures()->createProduct([ 'name' => 'Primary truck wash', 'price' => 200, ]); $addonProduct = api_fixtures()->createProduct([ 'name' => 'Drying add-on', 'category' => 4, 'price' => 50, ]); post_order_item($fixture['order'], $primaryProduct, $fixture['session']['headers']) ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); post_order_item($fixture['order'], $addonProduct, $fixture['session']['headers'], [ 'notes' => 'Addon customer rule check', ]) ->assertStatus(400) ->assertEnvelope() ->assertSuccess(false) ->assertMessage(\classes\customer_product_rule_service::BLOCK_MESSAGE); }); it('allows standalone additional order items when the customer is not restricted from additional services', function (): void { api_test_covers('POST /order/items', 'customer-rule-validation'); $fixture = create_order_item_rule_fixture(); $primaryProduct = api_fixtures()->createProduct([ 'name' => 'Primary unrestricted truck wash', 'price' => 200, ]); $addonProduct = api_fixtures()->createProduct([ 'name' => 'Unrestricted add-on', 'category' => 4, 'price' => 50, ]); post_order_item($fixture['order'], $primaryProduct, $fixture['session']['headers']) ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); post_order_item($fixture['order'], $addonProduct, $fixture['session']['headers']) ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); }); it('blocks related addon order items for customers restricted from additional services', function (): void { api_test_covers('POST /order/items', 'customer-rule-validation'); $fixture = create_order_item_rule_fixture(['restrictAdditionalServices']); $cashier = api_fixtures()->createUser(['display_name' => 'Order Item Rule Cashier']); $primaryProduct = api_fixtures()->createProduct([ 'name' => 'Primary related truck wash', 'price' => 200, ]); $addonProduct = api_fixtures()->createProduct([ 'name' => 'Related extra brush', 'price' => 35, ]); $primaryItem = api_fixtures()->createOrderItem([ 'order_id' => $fixture['order']['id'], 'product_id' => $primaryProduct['id'], 'cashier_id' => $cashier['id'], 'price' => 200, ]); post_order_item($fixture['order'], $addonProduct, $fixture['session']['headers'], [ 'related_item_id' => $primaryItem['id'], ]) ->assertStatus(400) ->assertEnvelope() ->assertSuccess(false) ->assertMessage(\classes\customer_product_rule_service::BLOCK_MESSAGE); }); it('blocks named restricted service products for the selected customer', function (string $attribute, array $productAttributes): void { api_test_covers('POST /order/items', 'customer-rule-validation'); $fixture = create_order_item_rule_fixture([$attribute]); $product = api_fixtures()->createProduct($productAttributes); post_order_item($fixture['order'], $product, $fixture['session']['headers']) ->assertStatus(400) ->assertEnvelope() ->assertSuccess(false) ->assertMessage(\classes\customer_product_rule_service::BLOCK_MESSAGE); })->with([ 'spot free' => ['restrictSpotFree', ['name' => 'Spot Free rinse', 'price' => 80]], 'interior cleaning' => ['restrictInteriorCleaning', ['name' => 'Indvendig vask', 'price' => 125]], 'tank cleaning' => ['restrictTankCleaning', ['name' => 'Tankrens', 'category' => 5, 'price' => 300]], ]); it('only allows tank cleaning products when the customer has the only tank cleaning rule', function (): void { api_test_covers('POST /order/items', 'customer-rule-validation'); $fixture = create_order_item_rule_fixture(['onlyTankCleaning']); $nonTankProduct = api_fixtures()->createProduct([ 'name' => 'Exterior truck wash', 'price' => 180, ]); $tankProduct = api_fixtures()->createProduct([ 'name' => 'Tank cleaning', 'category' => 5, 'price' => 300, ]); post_order_item($fixture['order'], $nonTankProduct, $fixture['session']['headers']) ->assertStatus(400) ->assertEnvelope() ->assertSuccess(false) ->assertMessage(\classes\customer_product_rule_service::BLOCK_MESSAGE); post_order_item($fixture['order'], $tankProduct, $fixture['session']['headers']) ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); }); it('uses the sentinel for missing custom-only department prices without discounts or cross-department prices', function (): void { api_test_covers('GET /products', 'happy'); api_test_covers('POST /order/items', 'happy'); $department = api_fixtures()->createDepartment([ 'name' => 'Custom Pricing Products', 'custom_pricing_only' => 1, ]); $otherDepartment = api_fixtures()->createDepartment(['name' => 'Custom Pricing Other']); $category = api_fixtures()->createCategory(['name' => 'Custom Pricing Products Category']); $product = api_fixtures()->createProduct([ 'name' => 'Custom Pricing Missing Product', 'category' => $category['id'], 'price' => 12345, ]); api_fixtures()->linkDepartmentCategory((int)$department['id'], (int)$category['id']); api_fixtures()->linkDepartmentCategory((int)$otherDepartment['id'], (int)$category['id']); custom_pricing_only_department_price((int)$otherDepartment['id'], (int)$product['id'], 3333); $customer = api_fixtures()->createUser(['display_name' => 'Custom Pricing Customer']); api_fixtures()->cacheEconomicCustomerDiscountPercentage((int)$customer['id'], 0); custom_pricing_only_price_override((int)$customer['id'], (int)$product['id'], 50); $session = api_fixtures()->createUserSession([ 'list_products', 'add_order_items', 'department_access_' . (int)$department['id'], ]); $productResponse = api_client()->get( '/products?final_price=true&id=' . (int)$product['id'] . '&department_id=' . (int)$department['id'] . '&customer_id=' . (int)$customer['customer_number'], $session['headers'] ); $productResponse ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect($productResponse->body)->not->toContain('12345'); expect($productResponse->body)->not->toContain('3333'); expect($productResponse->data()['price'] ?? null)->toBe(\objects\products_o::CUSTOM_PRICING_MISSING_PRICE); api_client()->get( '/products?final_price=true&id=' . (int)$product['id'] . '&department_id=' . (int)$otherDepartment['id'], $session['headers'] ) ->assertStatus(403) ->assertEnvelope() ->assertSuccess(false) ->assertMissingPermissions(['department_access_' . (int)$otherDepartment['id']]); $order = api_fixtures()->createOrder([ 'customer_id' => $customer['customer_number'], 'department_id' => $department['id'], 'reference' => 'CUSTOM-ONLY-ORDER', ]); $orderItem = api_client()->post('/order/items', [ 'order_id' => $order['id'], 'product_id' => $product['id'], 'quantity' => 1, ], $session['headers']); $orderItem ->assertStatus(200) ->assertEnvelope() ->assertSuccess(); expect((int)($orderItem->data()['price'] ?? 0))->toBe(\objects\products_o::CUSTOM_PRICING_MISSING_PRICE); });