From 1da02e24863b6590603383bc80651bc7ff8c31b4 Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Mon, 6 Jul 2026 17:13:04 +0200 Subject: [PATCH] Fix limited backoffice price save reset --- .../classes/limited_backoffice_service.php | 36 +++++----- .../tests/Api/LimitedBackofficeApiTest.php | 65 +++++++++++++++++++ 2 files changed, 86 insertions(+), 15 deletions(-) diff --git a/services/nginx/app/classes/limited_backoffice_service.php b/services/nginx/app/classes/limited_backoffice_service.php index 0f0f28a1..06f36aeb 100644 --- a/services/nginx/app/classes/limited_backoffice_service.php +++ b/services/nginx/app/classes/limited_backoffice_service.php @@ -474,26 +474,26 @@ class limited_backoffice_service $mysqli->begin_transaction(); try { - $priceUpdateAssignments = ['`price` = VALUES(`price`)']; - if ($this->tableHasColumn('product_department_prices', 'updated_at')) { - $priceUpdateAssignments[] = '`updated_at` = CURRENT_TIMESTAMP'; - } - - $statement = $mysqli->prepare( - 'INSERT INTO `product_department_prices` (`department_id`, `product_id`, `price`) - VALUES (?, ?, ?) - ON DUPLICATE KEY UPDATE ' . implode(', ', $priceUpdateAssignments) + $deleteStatement = $mysqli->prepare( + 'DELETE FROM `product_department_prices` WHERE `department_id` = ? AND `product_id` = ?' ); - if ($statement === false) { + $insertStatement = $mysqli->prepare( + 'INSERT INTO `product_department_prices` (`department_id`, `product_id`, `price`) VALUES (?, ?, ?)' + ); + if ($deleteStatement === false || $insertStatement === false) { throw new \RuntimeException('Unable to prepare department price update.'); } foreach ($normalizedPrices as $productId => $price) { - $statement->bind_param('iii', $departmentId, $productId, $price); - $statement->execute(); + $deleteStatement->bind_param('ii', $departmentId, $productId); + $deleteStatement->execute(); + + $insertStatement->bind_param('iii', $departmentId, $productId, $price); + $insertStatement->execute(); } - $statement->close(); + $deleteStatement->close(); + $insertStatement->close(); $mysqli->commit(); } catch (\Throwable $throwable) { $mysqli->rollback(); @@ -868,8 +868,14 @@ class limited_backoffice_service INNER JOIN `categories` c ON c.`id` = dc.`category_id` INNER JOIN `products` p ON p.`category` = dc.`category_id` LEFT JOIN `product_department_prices` pdp - ON pdp.`department_id` = dc.`department_id` - AND pdp.`product_id` = p.`id` + ON pdp.`id` = ( + SELECT pdp_latest.`id` + FROM `product_department_prices` pdp_latest + WHERE pdp_latest.`department_id` = dc.`department_id` + AND pdp_latest.`product_id` = p.`id` + ORDER BY pdp_latest.`id` DESC + LIMIT 1 + ) WHERE ' . implode(' AND ', $where) . ' ORDER BY c.`name` ASC, c.`id` ASC, p.`order_priority` ASC, p.`name` ASC, p.`id` ASC' ); diff --git a/services/nginx/app/tests/Api/LimitedBackofficeApiTest.php b/services/nginx/app/tests/Api/LimitedBackofficeApiTest.php index 8cc78d63..b3bbc994 100644 --- a/services/nginx/app/tests/Api/LimitedBackofficeApiTest.php +++ b/services/nginx/app/tests/Api/LimitedBackofficeApiTest.php @@ -75,6 +75,14 @@ function limited_backoffice_price_value(int $departmentId, int $productId): ?int return $row === null ? null : (int)$row['price']; } +function limited_backoffice_price_rows(int $departmentId, int $productId): array +{ + return api_test_runtime()->db()->query( + 'SELECT `id`, `price` FROM `product_department_prices` WHERE `department_id` = ' . $departmentId . + ' AND `product_id` = ' . $productId . ' ORDER BY `id` ASC' + )->fetch_all(MYSQLI_ASSOC); +} + function limited_backoffice_cleanup_created_employee(int $employeeId): void { $row = api_test_runtime()->queryOne( @@ -200,6 +208,63 @@ it('lists and updates explicit prices only for assigned departments', function ( expect(limited_backoffice_price_value((int)$department['id'], (int)$product['id']))->toBe(2222); expect(limited_backoffice_price_value((int)$otherDepartment['id'], (int)$product['id']))->toBe(4321); + expect($updated->data()['categories'][0]['products'][0]['price'] ?? null)->toBe(2222); +}); + +it('returns saved prices and collapses legacy duplicate department price rows', function (): void { + api_test_covers('PUT /limited-backoffice/departments/{departmentId}/prices', 'legacy duplicates'); + + $department = api_fixtures()->createDepartment(['name' => 'Limited Legacy Duplicate Prices']); + $category = api_fixtures()->createCategory(['name' => 'Limited Legacy Duplicate Category']); + $product = api_fixtures()->createProduct([ + 'name' => 'Legacy Duplicate Price', + 'category' => $category['id'], + ]); + api_fixtures()->linkDepartmentCategory((int)$department['id'], (int)$category['id']); + + $db = api_test_runtime()->db(); + $index = $db->query("SHOW INDEX FROM `product_department_prices` WHERE `Key_name` = 'uniq_product_department_prices_lookup'"); + if ($index === false) { + throw new RuntimeException('Unable to inspect product_department_prices lookup index.'); + } + $hadIndex = (int)$index->num_rows > 0; + if ($hadIndex) { + $db->query('ALTER TABLE `product_department_prices` DROP INDEX `uniq_product_department_prices_lookup`'); + } + + try { + limited_backoffice_price_insert((int)$department['id'], (int)$product['id'], 111); + limited_backoffice_price_insert((int)$department['id'], (int)$product['id'], 222); + expect(limited_backoffice_price_rows((int)$department['id'], (int)$product['id']))->toHaveCount(2); + + $session = limited_backoffice_manager_session([(int)$department['id']]); + $updated = api_client()->put('/limited-backoffice/departments/' . (int)$department['id'] . '/prices', [ + 'prices' => [ + ['product_id' => (int)$product['id'], 'price' => 333], + ], + ], $session['headers']); + + $updated + ->assertStatus(200) + ->assertEnvelope() + ->assertSuccess(); + + $rows = limited_backoffice_price_rows((int)$department['id'], (int)$product['id']); + expect($rows)->toHaveCount(1); + expect((int)$rows[0]['price'])->toBe(333); + expect($updated->data()['categories'][0]['products'][0]['price'] ?? null)->toBe(333); + } finally { + $db->query( + 'DELETE FROM `product_department_prices` WHERE `department_id` = ' . (int)$department['id'] . + ' AND `product_id` = ' . (int)$product['id'] + ); + if ($hadIndex) { + $db->query( + 'ALTER TABLE `product_department_prices` + ADD UNIQUE KEY `uniq_product_department_prices_lookup` (`department_id`, `product_id`)' + ); + } + } }); it('updates department prices when the price table has no updated_at column', function (): void {