Files
pleno-vue/tests/e2e/pos-customer-rules.spec.js
T
Jeppe B a0c11e4bb7 Fix POS release-blocking customer rules and completion (#208)
Resolve direct-order product reconciliation context, preserve valid selections when restricted products are activated, remove the redundant certificate request, and add focused regression coverage.
2026-07-21 15:42:50 +02:00

608 lines
21 KiB
JavaScript

import { test, expect } from "@playwright/test";
import { createPosFixture, mockApi, primeMockSession } from "./support/network.js";
const POS_PERMISSIONS = [
"admin",
"department_access_12",
"delete_order",
"edit_order_items",
"get_user",
"list_customer_attributes",
"get_custom_prices_other",
];
const POS_STEP_TIMEOUT = 20_000;
function json(body, status = 200) {
return {
status,
contentType: "application/json",
body: JSON.stringify(body),
};
}
async function primeOperatorSession(page, token = "pos-customer-rules-token") {
await primeMockSession(page, { token, bootPath: "/login" });
}
async function openPosAndSelectCustomer(page, customer) {
await page.goto("/admin/12/modules/pos?step=1");
await expect(page.getByTestId("pos-step-1")).toBeVisible({ timeout: POS_STEP_TIMEOUT });
await page.locator("#reg_1").fill("AB12345");
await page.locator("#pos_select_customer_input").fill(String(customer.customerNumber));
await expect(page.locator(".customer-drop-down-select").first()).toBeVisible();
await page.locator(".customer-drop-down-select").first().click();
await expect(page.locator(".field.has-addons input[disabled]").last()).toHaveValue(customer.name);
await expect(page.locator(".pos-selected-customer__title").filter({ hasText: customer.name }).first()).toBeVisible();
}
async function expectSameRenderedWidth(left, right, tolerance = 1) {
await expect(left).toBeVisible();
await expect(right).toBeVisible();
await expect
.poll(async () => {
const [leftBox, rightBox] = await Promise.all([left.boundingBox(), right.boundingBox()]);
if (!leftBox || !rightBox) {
return Number.POSITIVE_INFINITY;
}
return Math.abs(leftBox.width - rightBox.width);
})
.toBeLessThanOrEqual(tolerance);
}
async function expectControlsContained(container, controls, tolerance = 1) {
await expect(container).toBeVisible();
await Promise.all(controls.map((control) => expect(control).toBeVisible()));
await expect
.poll(async () => {
const [containerBox, ...controlBoxes] = await Promise.all([
container.boundingBox(),
...controls.map((control) => control.boundingBox()),
]);
if (!containerBox || controlBoxes.some((box) => !box)) {
return Number.POSITIVE_INFINITY;
}
const containerRight = containerBox.x + containerBox.width;
return Math.max(
...controlBoxes.flatMap((box) => [containerBox.x - box.x, box.x + box.width - containerRight]),
0
);
})
.toBeLessThanOrEqual(tolerance);
}
function buildTodayTimestamp(time = "10:00:00.000Z") {
return `${new Date().toISOString().split("T")[0]}T${time}`;
}
async function commitDesktopReg1ByBlur(page, value = "AB12345") {
const reg1Input = page.locator("#reg_1");
await reg1Input.fill(value);
await reg1Input.evaluate((element) => {
element.blur();
});
}
function getSelectedCustomerPhoneValue(page) {
return page
.locator(".pos-selected-customer__row")
.filter({ has: page.locator(".pos-selected-customer__label", { hasText: "Telefon" }) })
.first()
.locator(".pos-selected-customer__value");
}
test("rules tab reloads customer attributes when the panel becomes visible", async ({ page }, testInfo) => {
test.skip(!testInfo.project.name.includes("desktop"), "Desktop only");
const fixture = createPosFixture({
customerAttributesByNumber: {
12345679: [
{ id: 1, customer_number: 12345679, attribute: "invoiceAllOrdersIndividually" },
{ id: 2, customer_number: 12345679, attribute: "invoiceWithStripe" },
],
},
});
const customer = fixture.customersByNumber[12345679];
await mockApi(page, {
authenticated: true,
permissions: POS_PERMISSIONS,
edgeGateways: false,
pos: fixture,
});
await primeOperatorSession(page);
await page.goto("/admin/12/modules/pos?step=1");
await expect(page.getByTestId("pos-step-1")).toBeVisible({ timeout: POS_STEP_TIMEOUT });
await page.locator("#reg_1").fill("AB12345");
await page.locator("#pos_select_customer_input").fill(String(customer.customerNumber));
await expect(page.locator(".customer-drop-down-select").first()).toBeVisible();
await page.locator(".customer-drop-down-select").first().click();
await expect(page.locator(".field.has-addons input[disabled]").last()).toHaveValue(customer.name);
await expect(page.locator(".pos-selected-customer__title").filter({ hasText: customer.name }).first()).toBeVisible();
const rulesTab = page.locator('[data-testid="pos-customer-tab-rules"]:visible').first();
await expect(rulesTab).toBeVisible();
const customerAttributesResponse = page.waitForResponse((response) => {
return (
response.request().method() === "GET" &&
response.url().includes(`/customer/attributes?customer_number=${customer.customerNumber}`)
);
});
await rulesTab.click();
await customerAttributesResponse;
await expect(page.locator("#invoiceAllOrdersIndividually:visible")).toBeChecked();
await expect(page.locator("#invoiceWithStripe:visible")).toBeChecked();
});
test("duplicate warning moves below customer rules when rules tab is selected", async ({ page }, testInfo) => {
test.skip(!testInfo.project.name.includes("desktop"), "Desktop only");
const fixture = createPosFixture({
ordersById: {
54520: {
id: 54520,
customer_id: 12345679,
department_id: 12,
reference: "DUP-RULES-TAB",
po: "",
safety_seal: "",
notes: "",
reg_1: "AB12345",
reg_2: "",
reg_3: "",
invoice_collection_id: null,
booking_id: null,
completed_at: null,
closed_at: null,
created_at: buildTodayTimestamp("09:30:00.000Z"),
include_in_invoice: null,
},
},
});
const customer = fixture.customersByNumber[12345679];
await mockApi(page, {
authenticated: true,
permissions: POS_PERMISSIONS,
edgeGateways: false,
pos: fixture,
});
await primeOperatorSession(page, "pos-customer-rules-duplicate-placement-token");
await openPosAndSelectCustomer(page, customer);
await commitDesktopReg1ByBlur(page);
const actionRail = page.getByTestId("pos-step-1-action-rail");
const actionRailWarning = actionRail.getByTestId("pos-desktop-duplicate-warning-inline");
await expect(actionRailWarning).toBeVisible({ timeout: 10_000 });
await page.locator('[data-testid="pos-customer-tab-rules"]:visible').first().click();
const rulesFooter = page.getByTestId("pos-customer-rules-footer");
const rulesFooterWarning = rulesFooter.getByTestId("pos-desktop-duplicate-warning-inline");
const rulesFooterNext = rulesFooter.getByTestId("pos-next-step");
await expect(rulesFooterWarning).toBeVisible({ timeout: 10_000 });
await expect(rulesFooterNext).toBeVisible({ timeout: 10_000 });
await expect(actionRail).toHaveCount(0);
const lastRuleRow = page.getByTestId("pos-customer-rule-row").last();
const lastRuleBox = await lastRuleRow.boundingBox();
const warningBox = await rulesFooterWarning.boundingBox();
const footerNextBox = await rulesFooterNext.boundingBox();
expect(lastRuleBox).not.toBeNull();
expect(warningBox).not.toBeNull();
expect(footerNextBox).not.toBeNull();
expect(warningBox.y).toBeGreaterThanOrEqual(lastRuleBox.y + lastRuleBox.height);
expect(footerNextBox.y).toBeGreaterThanOrEqual(warningBox.y + warningBox.height);
await page.locator('[data-testid="pos-customer-tab-details"]:visible').first().click();
await expect(actionRailWarning).toBeVisible({ timeout: 10_000 });
await expect(actionRail.getByTestId("pos-next-step")).toBeVisible({ timeout: 10_000 });
await expect(page.getByTestId("pos-customer-rules-footer")).toHaveCount(0);
});
test("customer details renders top-level phone object when economic customer payload is invalid", async ({
page,
}, testInfo) => {
test.skip(!testInfo.project.name.includes("desktop"), "Desktop only");
const fixture = createPosFixture();
const customer = fixture.customersByNumber[12345679];
let usersCustomerInterceptCount = 0;
await mockApi(page, {
authenticated: true,
permissions: POS_PERMISSIONS,
edgeGateways: false,
pos: fixture,
});
await page.route(/\/users\/customer(?:\?.*)?$/, async (route) => {
if (route.request().method() !== "GET") {
await route.fallback();
return;
}
usersCustomerInterceptCount += 1;
await route.fulfill(
json({
success: true,
data: {
customer_name: customer.name,
phone: {
country_code: 45,
number: 42331128,
},
economic_customer: [],
},
})
);
});
await primeOperatorSession(page);
await page.goto(`/admin/12/modules/pos?customer_id=${customer.customerNumber}&step=1`);
await expect(page.getByTestId("pos-step-1")).toBeVisible({ timeout: POS_STEP_TIMEOUT });
await expect(page.locator(".pos-selected-customer__title").filter({ hasText: customer.name }).first()).toBeVisible();
await expect.poll(() => usersCustomerInterceptCount).toBeGreaterThan(0);
const phoneValue = getSelectedCustomerPhoneValue(page);
await expect(phoneValue).toHaveText("42331128");
await expect(phoneValue).not.toContainText("{");
await expect(phoneValue).not.toContainText("country_code");
await expect(phoneValue).not.toContainText("number");
});
test("customer details renders economic customer mobilePhone object as number only", async ({ page }, testInfo) => {
test.skip(!testInfo.project.name.includes("desktop"), "Desktop only");
const baseFixture = createPosFixture();
const baseCustomer = baseFixture.customersByNumber[12345679];
const fixture = createPosFixture({
customersByNumber: {
12345679: {
...baseCustomer,
mobilePhone: {
country_code: 45,
number: 42331128,
},
},
},
});
const customer = fixture.customersByNumber[12345679];
await mockApi(page, {
authenticated: true,
permissions: POS_PERMISSIONS,
edgeGateways: false,
pos: fixture,
});
await primeOperatorSession(page);
await openPosAndSelectCustomer(page, customer);
const phoneValue = getSelectedCustomerPhoneValue(page);
await expect(phoneValue).toHaveText("42331128");
await expect(phoneValue).not.toContainText("{");
await expect(phoneValue).not.toContainText("country_code");
await expect(phoneValue).not.toContainText("number");
});
test("only tankcleaning customers can only add tankcleaning products", async ({ page }, testInfo) => {
test.skip(!testInfo.project.name.includes("desktop"), "Desktop only");
const baseFixture = createPosFixture();
const tankCleaningProduct = {
id: 66,
name: "Tank cleaning 4 spulehoveder",
description: "Tankcleaning service",
price: 799,
subscription_allowed: true,
category: 5,
piktogram: "truck",
apply_category_discount: true,
requires_note: false,
is_wash: false,
display_in_booking_form: true,
order_priority: 1,
addons: [],
};
const fixture = createPosFixture({
customerAttributesByNumber: {
12345679: [
{
id: 1,
customer_number: 12345679,
attribute: "onlyTankCleaning",
product_restriction: {
version: 1,
disabled_product_ids: [53],
collections: [{ id: 501, name: "Legacy migration", product_ids: [53] }],
},
},
],
},
products: [...baseFixture.products, tankCleaningProduct],
departmentCategories: [
...baseFixture.departmentCategories,
{ id: 13, department_id: 12, category: { id: 5, name: "Tankcleaning", meta: { products: [66] } } },
],
});
await mockApi(page, {
authenticated: true,
permissions: POS_PERMISSIONS,
edgeGateways: false,
pos: fixture,
});
await primeOperatorSession(page, "pos-only-tankcleaning-token");
const orderItemPosts = [];
page.on("request", (request) => {
if (request.method() === "POST" && new URL(request.url()).pathname.endsWith("/order/items")) {
orderItemPosts.push(request.postDataJSON());
}
});
const customer = fixture.customersByNumber[12345679];
const customerAttributesResponse = page.waitForResponse((response) => {
return (
response.request().method() === "GET" &&
response.url().includes(`/customer/attributes?customer_number=${customer.customerNumber}`)
);
});
await openPosAndSelectCustomer(page, customer);
await customerAttributesResponse;
await page.getByTestId("pos-step-1").getByTestId("pos-next-step").click();
await expect(page.getByTestId("pos-step-2")).toBeVisible({ timeout: POS_STEP_TIMEOUT });
const restrictedProductCard = page.getByTestId("pos-product-card-53").first();
await restrictedProductCard.click();
await expect(restrictedProductCard).not.toHaveClass(/is-selected/);
await expect(page.locator(".swal2-popup:visible")).toContainText("Produktet er ikke tilladt for denne kunde");
await page.locator(".swal2-confirm:visible").click();
await expect(page.getByTestId("pos-add-to-cart-53")).toHaveCount(0);
expect(orderItemPosts).toEqual([]);
await page.locator(".tabs li").filter({ hasText: "Tankcleaning" }).click();
await expect(page.getByTestId("pos-product-card-66").first()).toBeVisible();
await page.getByTestId("pos-product-card-66").first().click();
await expect(page.getByTestId("pos-add-to-cart-66").first()).toBeEnabled();
const addRequest = page.waitForRequest((request) => {
return request.method() === "POST" && request.url().includes("/order/items");
});
await page.getByTestId("pos-add-to-cart-66").first().click();
const request = await addRequest;
expect(request.postDataJSON().product_id).toBe(66);
expect(orderItemPosts).toHaveLength(1);
});
test("exact customer-rule configuration disables desktop add-on controls by option_id without placeholder rows", async ({
page,
}, testInfo) => {
test.skip(!testInfo.project.name.includes("desktop"), "Desktop only");
const baseFixture = createPosFixture();
const primaryProduct = {
...baseFixture.products.find((product) => Number(product.id) === 53),
};
const addonProduct = {
...baseFixture.products.find((product) => Number(product.id) === 63),
addons: [],
};
const allowedLookalikeProduct = {
...addonProduct,
id: 64,
name: "Allowed additional-service lookalike",
};
primaryProduct.addons = [
{
id: 9001,
option_id: addonProduct.id,
name: addonProduct.name,
price: addonProduct.price,
product: addonProduct,
min: 0,
max: 1,
},
{
id: 9002,
option_id: allowedLookalikeProduct.id,
name: allowedLookalikeProduct.name,
price: allowedLookalikeProduct.price,
product: allowedLookalikeProduct,
min: 0,
max: 1,
},
];
const fixture = createPosFixture({
customerAttributesByNumber: {
12345679: [
{
id: 1,
customer_number: 12345679,
attribute: "restrictAdditionalServices",
product_restriction: {
version: 1,
disabled_product_ids: [addonProduct.id],
collections: [{ id: 502, name: "Blocked add-ons", product_ids: [addonProduct.id] }],
},
},
],
},
products: [
primaryProduct,
addonProduct,
allowedLookalikeProduct,
...baseFixture.products.filter((product) => ![53, 63, 64].includes(Number(product.id))),
],
});
const customer = fixture.customersByNumber[12345679];
const orderItemPosts = [];
await mockApi(page, {
authenticated: true,
permissions: POS_PERMISSIONS,
edgeGateways: false,
pos: fixture,
});
page.on("request", (request) => {
if (request.method() === "POST" && request.url().includes("/order/items")) {
orderItemPosts.push(request.postDataJSON());
}
});
await primeOperatorSession(page, "pos-restricted-desktop-addons-token");
const customerAttributesResponse = page.waitForResponse(
(response) =>
response.request().method() === "GET" &&
response.url().includes(`/customer/attributes?customer_number=${customer.customerNumber}`)
);
await openPosAndSelectCustomer(page, customer);
await customerAttributesResponse;
await page.getByTestId("pos-step-1").getByTestId("pos-next-step").click();
await expect(page.getByTestId("pos-step-2")).toBeVisible({ timeout: POS_STEP_TIMEOUT });
await page.getByTestId("pos-product-card-53").first().click();
const restrictedAddonRow = page.getByTestId("pos-addon-restriction-63");
const restrictedIncreaseButton = page.getByTestId("pos-addon-63-increase");
const restrictedAddonButton = page.getByTestId("pos-addon-63-name");
const restrictedDecreaseButton = page.getByTestId("pos-addon-63-decrease");
const allowedAddonButton = page.getByTestId("pos-addon-64-name");
await expect(restrictedAddonRow).toBeVisible();
await expect(restrictedIncreaseButton).toBeDisabled();
await expect(restrictedAddonButton).toBeDisabled();
await expect(restrictedDecreaseButton).toBeDisabled();
await expect(allowedAddonButton).toBeEnabled();
await expectSameRenderedWidth(restrictedAddonButton, allowedAddonButton);
await expectControlsContained(restrictedAddonRow, [
restrictedIncreaseButton,
restrictedAddonButton,
restrictedDecreaseButton,
]);
await page.getByTestId("pos-addon-restriction-tooltip-63").hover();
await expect(page.locator(".tooltip-content:visible").last()).toContainText(
"Tilvalg er ikke tilladt for denne kunde"
);
for (const testId of ["pos-addon-63-increase", "pos-addon-63-name", "pos-addon-63-decrease"]) {
await page.getByTestId(testId).evaluate((button) => button.click());
}
expect(orderItemPosts).toEqual([]);
await allowedAddonButton.click();
await page.getByTestId("pos-add-to-cart-53").first().click();
await expect.poll(() => orderItemPosts.length, { timeout: 10_000 }).toBe(2);
expect(orderItemPosts.map((body) => Number(body.product_id))).toEqual([53, 64]);
expect(Number(orderItemPosts[1].related_item_id)).toBeGreaterThan(0);
const cartPanel = page.getByTestId("pos-step-2").getByTestId("pos-order-panel-cart");
await expect(cartPanel).not.toContainText("Ingen data");
await expect(cartPanel).not.toContainText("0 DKK");
});
test("desktop remains fail-closed when an active product rule has a malformed restriction payload", async ({
page,
}, testInfo) => {
test.skip(!testInfo.project.name.includes("desktop"), "Desktop only");
const fixture = createPosFixture({
customerAttributesByNumber: {
12345679: [{ id: 1, customer_number: 12345679, attribute: "restrictSpotFree" }],
},
});
const customer = fixture.customersByNumber[12345679];
const orderItemPosts = [];
await mockApi(page, {
authenticated: true,
permissions: POS_PERMISSIONS,
edgeGateways: false,
pos: fixture,
});
page.on("request", (request) => {
if (request.method() === "POST" && request.url().includes("/order/items")) {
orderItemPosts.push(request.postDataJSON());
}
});
await primeOperatorSession(page, "pos-malformed-customer-restrictions-token");
await openPosAndSelectCustomer(page, customer);
await page.getByTestId("pos-step-1").getByTestId("pos-next-step").click();
await expect(page.getByTestId("pos-step-2")).toBeVisible({ timeout: POS_STEP_TIMEOUT });
await expect(page.getByTestId("pos-customer-restrictions-load-error")).toBeVisible();
const blockedProductCard = page.getByTestId("pos-product-card-53").first();
await blockedProductCard.click();
await expect(blockedProductCard).not.toHaveClass(/is-selected/);
await expect(page.locator(".swal2-popup:visible")).toContainText(/Kunderegel|Customer rule/i);
await page.locator(".swal2-confirm:visible").click();
await expect(page.locator('[data-testid^="pos-add-to-cart-"]:visible')).toHaveCount(0);
expect(orderItemPosts).toEqual([]);
});
test("desktop clears pending basket row when customer-rule create is rejected", async ({ page }, testInfo) => {
test.skip(!testInfo.project.name.includes("desktop"), "Desktop only");
const fixture = createPosFixture();
const customer = fixture.customersByNumber[12345679];
let rejectedCreateCount = 0;
await mockApi(page, {
authenticated: true,
permissions: POS_PERMISSIONS,
edgeGateways: false,
pos: fixture,
});
await page.route(/\/order\/items(?:\?.*)?$/, async (route) => {
const request = route.request();
if (request.method() !== "POST") {
await route.fallback();
return;
}
rejectedCreateCount += 1;
await route.fulfill(
json(
{
success: false,
data: {
code: "CUSTOMER_RULE_PRODUCT_RESTRICTED",
message: "This product is not allowed for the selected customer",
},
},
400
)
);
});
await primeOperatorSession(page, "pos-customer-rule-reject-token");
await openPosAndSelectCustomer(page, customer);
await page.getByTestId("pos-step-1").getByTestId("pos-next-step").click();
await expect(page.getByTestId("pos-step-2")).toBeVisible({ timeout: POS_STEP_TIMEOUT });
await page.getByTestId("pos-product-card-53").first().click();
await page.getByTestId("pos-add-to-cart-53").first().click();
await expect.poll(() => rejectedCreateCount, { timeout: 10_000 }).toBe(1);
await expect(page.getByText("Produktet blev afvist af kundereglerne og er ikke tilføjet")).toBeVisible({
timeout: 10_000,
});
const cartPanel = page.getByTestId("pos-step-2").getByTestId("pos-order-panel-cart");
await expect(cartPanel.getByTestId("pos-order-empty-state")).toBeVisible();
await expect(cartPanel.locator('[data-testid^="pos-order-item-name-"]')).toHaveCount(0);
});