fix(pleno-vue): prompt for audited add-on note on mobile step 2

The mobile flow's productRequiresOrderItemNote only checked
requires_note + chemistry product 27, so audited add-ons (21, 22,
24, 25, 26, 27) silently posted without reason_comment. The server
policy then rejected POST /order/items with 400 \u201cReason comment is
required for this product\u201d and syncMobileOrderItems rolled the
partial add-on batch back via OrderItemsPartialSyncError.

Match the desktop flow: include AUDITED_ORDER_ITEM_PRODUCT_IDS so
promptForRequiredProductNote runs before POST and the existing
buildAuditedOrderItemReasonPayload falls back to notes for
reason_comment.

Updates the e2e fixture mirror in mobilePos.js so the mock server
rejects empty notes for audited products, matching production.
Adds a product-24 e2e test alongside the product-27 test.

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
openhands
2026-08-13 08:00:04 +00:00
parent 253d72f7fb
commit d269513025
3 changed files with 97 additions and 1 deletions
@@ -48,7 +48,12 @@ import {
registerPosStepSaveBarrier,
saveOrderMetadataField,
} from "@/components/shop/POSDepartmentProcess.vue";
import { createOrderItem, getOrderItems, removeOrderItem } from "@/components/shop/OrdersItems.vue";
import {
AUDITED_ORDER_ITEM_PRODUCT_IDS,
createOrderItem,
getOrderItems,
removeOrderItem,
} from "@/components/shop/OrdersItems.vue";
import { syncMobileOrderItems } from "@/components/displays/department/pos/steps/mobile/utils/syncMobileOrderItems.js";
import { PosProduct } from "@/components/displays/department/pos/steps/mobile/objects/PosProduct.vue";
import PosDepartmentStepMobileButtonClearAll from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobileButtonClearAll.vue";
@@ -1035,6 +1040,7 @@ const productRequiresOrderItemNote = (product: any) => {
return (
isEnabledFlag(product?.requires_note ?? product?.product?.requires_note) ||
AUDITED_ORDER_ITEM_PRODUCT_IDS.has(getProductId(product)) ||
getProductId(product) === EXTRAORDINARY_CHEMISTRY_PRODUCT_ID ||
getProductName(product) === EXTRAORDINARY_CHEMISTRY_PRODUCT_NAME
);
+88
View File
@@ -4213,6 +4213,94 @@ test.describe("POS mobile order flow", () => {
await waitForStepReset(page);
});
test("prompts for a required reason note for audited add-on products that are not the chemistry product", async ({
page,
}) => {
const orderId = 9416;
const baseFixture = createMobilePosFixture();
const auditedProduct = {
id: 24,
name: "Højtryk - ekstra tid",
description: "Audited addon that requires a reason comment",
price: 95,
subscription_allowed: true,
category: 8,
piktogram: "24",
apply_category_discount: false,
requires_note: false,
is_wash: false,
display_in_booking_form: true,
order_priority: 6,
addons: [],
};
const primaryProduct = {
...fixtureProduct(53),
addons: [
...fixtureProduct(53).addons,
{
id: auditedProduct.id,
name: auditedProduct.name,
price: auditedProduct.price,
product: { ...auditedProduct },
quantity: 1,
min: 0,
max: -1,
},
],
};
const fixture = createMobilePosFixture({
products: baseFixture.products
.map((product) => {
if (Number(product.id) !== 53) {
return product;
}
return primaryProduct;
})
.concat(auditedProduct),
ordersById: {
[orderId]: buildRegularOrder(orderId),
},
orderItemsByOrderId: {
[orderId]: [],
},
});
await setupMobilePosPage(page, fixture, {
token: "mobile-product-24-note-token",
seedState: {
customerId: REGULAR_CUSTOMER_ID,
reg: "AB12345",
reference: "PRODUCT-24-NOTE",
primaryItem: primaryProduct,
vehicleType: 53,
lastOrderId: null,
},
route: {
step: 2,
orderId,
customerId: REGULAR_CUSTOMER_ID,
},
});
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("pos-mobile-addon-24-value")).toHaveText("1", { timeout: 10_000 });
await page.getByTestId("pos-mobile-next-step").click();
await expect(page.locator('[data-testid="pos-mobile-popup"][data-popup-id="add_product_note"]')).toBeVisible({
timeout: 10_000,
});
await page.getByTestId("pos-mobile-product-note-input").fill("Højtryk bagpå venstre side");
await page.getByTestId("pos-mobile-product-note-confirm").click();
await expect.poll(() => fixture.requestCounters.orderItemsPost, { timeout: 10_000 }).toBe(2);
await expect.poll(() => fixture.requestCounters.markAsCompleted, { timeout: 10_000 }).toBe(1);
const product24Create = fixture.requestLog.orderItemCreates.find((entry) => Number(entry.product_id) === 24);
expect(product24Create?.notes).toBe("Højtryk bagpå venstre side");
expect(product24Create?.reason_code).toBe("customer_approved_extra_work");
expect(product24Create?.reason_comment).toBe("Højtryk bagpå venstre side");
await waitForStepReset(page);
});
test("booking hydration applies booking items, reference, notes, and po", async ({ page }) => {
const orderId = 9405;
const fixture = createMobilePosFixture({
+2
View File
@@ -11,6 +11,7 @@ export const CARD_CUSTOMER_ID = 999;
export const WASH_CERTIFICATE_PRODUCT_ID = 41;
const EXTRAORDINARY_CHEMISTRY_PRODUCT_ID = 27;
const EXTRAORDINARY_CHEMISTRY_PRODUCT_NAME = "Ekstraordinær pr. 10 min inkl. kemi";
const AUDITED_ORDER_ITEM_PRODUCT_IDS = new Set([21, 22, 24, 25, 26, 27]);
export const MOBILE_PERMISSIONS = ["admin", "department_access_1"];
export const MOBILE_NEXT_STEP_COOLDOWN_MS = 2100;
@@ -342,6 +343,7 @@ function productRequiresOrderItemNote(product) {
const productId = Number(product?.product?.id ?? product?.product_id ?? product?.id ?? 0);
return (
isEnabledFlag(product?.requires_note ?? product?.product?.requires_note) ||
AUDITED_ORDER_ITEM_PRODUCT_IDS.has(productId) ||
productId === EXTRAORDINARY_CHEMISTRY_PRODUCT_ID ||
String(product?.product?.name ?? product?.name ?? "").trim() === EXTRAORDINARY_CHEMISTRY_PRODUCT_NAME
);