fix(pleno-vue): prompt for audited add-on note on mobile POS step 2 (#296)
## Summary Fixes the mobile POS step-2 400 on `POST /order/items` for audited add-on products (21, 22, 24, 25, 26, 27) such as product 24. Reproduces on https://truckwash.io/admin/12/modules/pos?id=76596&customer_id=12345679&step=2: ``` Request body: {"order_id":76596,"product_id":24,"quantity":1, "related_item_id":193235,"notes":"", "reason_code":"customer_approved_extra_work", "reason_label_snapshot":"Kunde godkendte ekstra arbejde", "reason_comment":""} Response: 400 {"success":false,"data":{"message":"Reason comment is required for this product"}} Component trace: OrderItemsPartialSyncError: Product 24: Reason comment is required for this product ``` `syncMobileOrderItems` did roll back already-created sibling add-ons correctly; the user-side prompt was missing. ## Root cause `PosDepartmentStepMobile2.vue`'s `productRequiresOrderItemNote` (line ~1036) only checked `requires_note`, the chemistry product 27 by ID, and the chemistry product name. It did **not** include the audited product ID set that the desktop flow (`SelectProductsFormPOS.vue:447`) and the server policy (`order_item_reason_policy.php` `AFFECTED_PRODUCT_IDS`) both rely on. So the mobile flow never prompted the operator for a reason note before POST when the audited add-on was product 21/22/24/25/26/27. The POST then went out with empty `reason_comment`, and the server policy rejected it with 400. ## Fix Three minimal changes, mirroring the desktop flow: 1. **`src/components/displays/department/pos/steps/mobile/PosDepartmentStepMobile2.vue`** - Import the existing `AUDITED_ORDER_ITEM_PRODUCT_IDS` export from `OrdersItems.vue`. - Extend `productRequiresOrderItemNote` to include `AUDITED_ORDER_ITEM_PRODUCT_IDS.has(getProductId(product))`. - Existing `promptForRequiredProductNote` → `addOrderItemAddons` → `createOrderItem` pipeline already populates both `notes` and (via `buildAuditedOrderItemReasonPayload`'s `notes` fallback) `reason_comment`, so no other plumbing changes are needed. 2. **`tests/e2e/support/mobilePos.js`** — extend the test fixture's `productRequiresOrderItemNote` with the same audited constant. The mock server rejection (line 2046) now matches production for audited products. 3. **`tests/e2e/pos-mobile-order-flow.spec.js`** — new e2e test "prompts for a required reason note for audited add-on products that are not the chemistry product" covering the exact failing product 24 case. Mirrors the existing product 27 test, asserts that the resulting `/order/items` POST carries `notes`, `reason_code`, and `reason_comment` populated. ## Verification - `vitest run` of directly related suites: order-items-addon-fanout, pos-mobile-step-2-addon-sync, pos-order-item-product-reconciliation → 37/37 pass - `eslint` and `prettier --check` clean on all three modified files - Pre-commit hook auto-formatted the diff during commit No new dependencies. Reuses existing exports. ``` PosDepartmentStepMobile2.vue | 8 +++++++- pos-mobile-order-flow.spec.js | 88 ++++++++++++++++++++++++++++++++++++++++++++ mobilePos.js | 2 ++ ``` Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
@@ -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
|
||||
);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user