Adds `reason_code`, `reason_label_snapshot`, `reason_comment` columns to `order_items` and integrates the `order_item_reason_policy` class into the POST and PUT /order/items routes. Validation order on audited products (consistent across POST and PUT): 1. If `reason_code` is present, validate reason first — emits the most specific error (invalid code, deprecated code, missing reason_comment). 2. If notes are provided but empty/whitespace, return "Notes is required for this product" (the legacy message). 3. Otherwise run reason validation — covers the missing-reason_code case. PHP api suite went from 284/290 to 290/290 (was 6 OrderItemsApiTest failures, now 0). Wired `addItemToOrder`, `updateOrderItem`, and `getItemAsArray` to persist and return the new columns.
771 lines
29 KiB
PHP
771 lines
29 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
usesApiSuite();
|
|
|
|
function create_order_item_rule_fixture(array $customerAttributes = []): array
|
|
{
|
|
$customer = api_fixtures()->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 configure_customer_rule_product(string $attribute, int $productId): void
|
|
{
|
|
new \classes\customer_rule_product_restriction_service();
|
|
$db = api_test_runtime()->db();
|
|
$safeAttribute = $db->real_escape_string($attribute);
|
|
$result = $db->query(
|
|
"SELECT id FROM customer_rule_product_collections
|
|
WHERE attribute = '{$safeAttribute}' ORDER BY sort_order, id LIMIT 1"
|
|
);
|
|
$collectionId = $result && $result->num_rows > 0 ? (int)$result->fetch_assoc()['id'] : 0;
|
|
if ($collectionId < 1) {
|
|
$db->query(
|
|
"INSERT INTO customer_rule_product_collections (attribute, name, sort_order)
|
|
VALUES ('{$safeAttribute}', 'API exact restriction', 0)"
|
|
);
|
|
$collectionId = (int)$db->insert_id;
|
|
}
|
|
$db->query(
|
|
"INSERT IGNORE INTO customer_rule_product_collection_products (collection_id, product_id)
|
|
VALUES ({$collectionId}, {$productId})"
|
|
);
|
|
api_fixtures()->cleanupDeleteWhere('customer_rule_product_collection_products', [
|
|
'collection_id' => $collectionId,
|
|
'product_id' => $productId,
|
|
]);
|
|
}
|
|
|
|
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([
|
|
'id' => \classes\order_item_reason_policy::EXTRAORDINARY_CHEMISTRY_PRODUCT_ID,
|
|
'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',
|
|
'reason_code' => 'customer_approved_extra_work',
|
|
'reason_comment' => 'Graffiti removal on left side',
|
|
], $session['headers']);
|
|
|
|
$response
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
expect($response->data()['notes'] ?? null)->toBe('Graffiti removal on left side');
|
|
expect($response->data()['reason_code'] ?? null)->toBe('customer_approved_extra_work');
|
|
expect($response->data()['reason_label_snapshot'] ?? null)->toBe('Kunde godkendte ekstra arbejde');
|
|
expect($response->data()['reason_comment'] ?? null)->toBe('Graffiti removal on left side');
|
|
});
|
|
|
|
it('requires valid approved reason data for audited add-on order items', function (array $payload, string $message): void {
|
|
api_test_covers('POST /order/items', 'validation');
|
|
|
|
$customer = api_fixtures()->createUser(['display_name' => 'Reason Required Customer']);
|
|
$department = api_fixtures()->createDepartment();
|
|
$order = api_fixtures()->createOrder([
|
|
'customer_id' => $customer['customer_number'],
|
|
'department_id' => $department['id'],
|
|
'reference' => 'REASON-REQUIRED',
|
|
]);
|
|
$product = api_fixtures()->createProduct([
|
|
'id' => \classes\order_item_reason_policy::EXTRAORDINARY_CHEMISTRY_PRODUCT_ID,
|
|
'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', array_merge([
|
|
'order_id' => $order['id'],
|
|
'product_id' => $product['id'],
|
|
'quantity' => 1,
|
|
'notes' => 'Extra wash work',
|
|
], $payload), $session['headers'])
|
|
->assertStatus(400)
|
|
->assertEnvelope()
|
|
->assertSuccess(false)
|
|
->assertMessage($message);
|
|
})->with([
|
|
'missing code' => [[], 'Reason code is required for this product'],
|
|
'invalid code' => [['reason_code' => 'not_approved', 'reason_comment' => 'Extra wash work'], 'Reason code is invalid for this product'],
|
|
'deprecated code' => [['reason_code' => 'legacy_note_only', 'reason_comment' => 'Extra wash work'], 'Reason code is deprecated for this product'],
|
|
'missing comment' => [['reason_code' => 'customer_approved_extra_work', 'reason_comment' => ' ', 'notes' => ''], 'Reason comment is required for this product'],
|
|
]);
|
|
|
|
it('requires reason data when editing audited add-on order items', function (): void {
|
|
api_test_covers('PUT /order/items', 'validation');
|
|
|
|
$customer = api_fixtures()->createUser(['display_name' => 'Reason Edit Customer']);
|
|
$department = api_fixtures()->createDepartment();
|
|
$product = api_fixtures()->createProduct([
|
|
'id' => \classes\order_item_reason_policy::EXTRAORDINARY_CHEMISTRY_PRODUCT_ID,
|
|
'name' => \objects\products_o::EXTRAORDINARY_CHEMISTRY_PRODUCT_NAME,
|
|
'price' => 299,
|
|
'requires_note' => 0,
|
|
]);
|
|
$order = api_fixtures()->createOrder([
|
|
'customer_id' => $customer['customer_number'],
|
|
'department_id' => $department['id'],
|
|
'reference' => 'REASON-EDIT',
|
|
]);
|
|
$session = api_fixtures()->createUserSession([], ['group_id' => 1]);
|
|
$created = api_client()->post('/order/items', [
|
|
'order_id' => $order['id'],
|
|
'product_id' => $product['id'],
|
|
'quantity' => 1,
|
|
'notes' => 'Initial reason',
|
|
'reason_code' => 'customer_approved_extra_work',
|
|
'reason_comment' => 'Initial reason',
|
|
], $session['headers'])->data();
|
|
|
|
api_client()->put('/order/items', [
|
|
'id' => $created['id'],
|
|
'price' => 299,
|
|
'notes' => 'Updated text only',
|
|
'reference' => '',
|
|
'quantity' => 1,
|
|
], $session['headers'])
|
|
->assertStatus(400)
|
|
->assertEnvelope()
|
|
->assertSuccess(false)
|
|
->assertMessage('Reason code is required for this product');
|
|
|
|
api_client()->put('/order/items', [
|
|
'id' => $created['id'],
|
|
'price' => 299,
|
|
'notes' => 'Updated text with reason',
|
|
'reference' => '',
|
|
'quantity' => 1,
|
|
'reason_code' => 'quality_rework',
|
|
'reason_comment' => 'Updated text with reason',
|
|
], $session['headers'])
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
});
|
|
|
|
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', 'department_access_' . $department['id']]);
|
|
|
|
$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]);
|
|
configure_customer_rule_product('onlyTankCleaning', (int)$washProduct['id']);
|
|
|
|
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', 'department_access_' . $department['id']]);
|
|
|
|
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 standalone category 8 products 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' => 'Extra detergent',
|
|
'category' => 8,
|
|
'price' => 50,
|
|
]);
|
|
configure_customer_rule_product('restrictAdditionalServices', (int)$addonProduct['id']);
|
|
|
|
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 category 8 products 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 additional service',
|
|
'category' => 8,
|
|
'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 configured 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',
|
|
'category' => 8,
|
|
'price' => 35,
|
|
]);
|
|
$primaryItem = api_fixtures()->createOrderItem([
|
|
'order_id' => $fixture['order']['id'],
|
|
'product_id' => $primaryProduct['id'],
|
|
'cashier_id' => $cashier['id'],
|
|
'price' => 200,
|
|
]);
|
|
configure_customer_rule_product('restrictAdditionalServices', (int)$addonProduct['id']);
|
|
|
|
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('still blocks related add-ons covered by a specific customer product rule', function (): void {
|
|
api_test_covers('POST /order/items', 'customer-rule-validation');
|
|
|
|
$fixture = create_order_item_rule_fixture(['restrictInteriorCleaning']);
|
|
$cashier = api_fixtures()->createUser(['display_name' => 'Specific Related Rule Cashier']);
|
|
$primaryProduct = api_fixtures()->createProduct(['name' => 'Primary truck wash', 'price' => 200]);
|
|
$interiorProduct = api_fixtures()->createProduct(['name' => 'Indvendig vask Forvogn', 'category' => 4, 'price' => 35]);
|
|
$primaryItem = api_fixtures()->createOrderItem([
|
|
'order_id' => $fixture['order']['id'],
|
|
'product_id' => $primaryProduct['id'],
|
|
'cashier_id' => $cashier['id'],
|
|
'price' => 200,
|
|
]);
|
|
configure_customer_rule_product('restrictInteriorCleaning', (int)$interiorProduct['id']);
|
|
|
|
post_order_item($fixture['order'], $interiorProduct, $fixture['session']['headers'], [
|
|
'related_item_id' => $primaryItem['id'],
|
|
])
|
|
->assertStatus(400)
|
|
->assertEnvelope()
|
|
->assertSuccess(false)
|
|
->assertMessage(\classes\customer_product_rule_service::BLOCK_MESSAGE);
|
|
});
|
|
|
|
it('does not infer additional-service restrictions from category 4, product names, or existing order items', function (): void {
|
|
api_test_covers('POST /order/items', 'customer-rule-validation');
|
|
|
|
$fixture = create_order_item_rule_fixture(['restrictAdditionalServices']);
|
|
$primaryProduct = api_fixtures()->createProduct([
|
|
'name' => 'Primary wash',
|
|
'category' => 4,
|
|
'price' => 200,
|
|
]);
|
|
$namedAddonProduct = api_fixtures()->createProduct([
|
|
'name' => 'Trailer add-on',
|
|
'category' => 4,
|
|
'price' => 35,
|
|
]);
|
|
|
|
post_order_item($fixture['order'], $primaryProduct, $fixture['session']['headers'])
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
|
|
post_order_item($fixture['order'], $namedAddonProduct, $fixture['session']['headers'])
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
});
|
|
|
|
it('does not infer additional-service restrictions from the legacy Tillægsydelser category after migration', function (): void {
|
|
api_test_covers('POST /order/items', 'customer-rule-validation');
|
|
|
|
$fixture = create_order_item_rule_fixture(['restrictAdditionalServices']);
|
|
$category = api_fixtures()->createCategory(['name' => 'Tillægsydelser']);
|
|
$product = api_fixtures()->createProduct([
|
|
'name' => 'Legacy additional service',
|
|
'category' => $category['id'],
|
|
'price' => 35,
|
|
]);
|
|
|
|
post_order_item($fixture['order'], $product, $fixture['session']['headers'])
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
});
|
|
|
|
it('validates that related order items exist, are active, and belong to the target order', function (): void {
|
|
api_test_covers('POST /order/items', 'related-item-validation');
|
|
|
|
$fixture = create_order_item_rule_fixture();
|
|
$cashier = api_fixtures()->createUser(['display_name' => 'Related Item Cashier']);
|
|
$product = api_fixtures()->createProduct(['name' => 'Related item validation product', 'price' => 35]);
|
|
$otherOrder = api_fixtures()->createOrder([
|
|
'customer_id' => $fixture['customer']['customer_number'],
|
|
'department_id' => $fixture['department']['id'],
|
|
'cashier_id' => $cashier['id'],
|
|
]);
|
|
$validParent = api_fixtures()->createOrderItem([
|
|
'order_id' => $fixture['order']['id'],
|
|
'product_id' => $product['id'],
|
|
'cashier_id' => $cashier['id'],
|
|
'price' => 35,
|
|
]);
|
|
$otherOrderParent = api_fixtures()->createOrderItem([
|
|
'order_id' => $otherOrder['id'],
|
|
'product_id' => $product['id'],
|
|
'cashier_id' => $cashier['id'],
|
|
'price' => 35,
|
|
]);
|
|
$deletedParent = api_fixtures()->createOrderItem([
|
|
'order_id' => $fixture['order']['id'],
|
|
'product_id' => $product['id'],
|
|
'cashier_id' => $cashier['id'],
|
|
'price' => 35,
|
|
'deleted_at' => '2026-07-15 12:00:00',
|
|
]);
|
|
|
|
foreach ([$deletedParent['id'], 999999999] as $missingParentId) {
|
|
post_order_item($fixture['order'], $product, $fixture['session']['headers'], [
|
|
'related_item_id' => $missingParentId,
|
|
])
|
|
->assertStatus(400)
|
|
->assertEnvelope()
|
|
->assertSuccess(false)
|
|
->assertMessage('Related order item not found');
|
|
}
|
|
|
|
post_order_item($fixture['order'], $product, $fixture['session']['headers'], [
|
|
'related_item_id' => $otherOrderParent['id'],
|
|
])
|
|
->assertStatus(400)
|
|
->assertEnvelope()
|
|
->assertSuccess(false)
|
|
->assertMessage('Related order item must belong to the same order');
|
|
|
|
$response = post_order_item($fixture['order'], $product, $fixture['session']['headers'], [
|
|
'related_item_id' => $validParent['id'],
|
|
]);
|
|
$response
|
|
->assertStatus(200)
|
|
->assertEnvelope()
|
|
->assertSuccess();
|
|
expect((int)($response->data()['related_item_id'] ?? 0))->toBe((int)$validParent['id']);
|
|
});
|
|
|
|
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);
|
|
configure_customer_rule_product($attribute, (int)$product['id']);
|
|
|
|
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,
|
|
]);
|
|
configure_customer_rule_product('onlyTankCleaning', (int)$nonTankProduct['id']);
|
|
|
|
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);
|
|
});
|