Resolve direct-order product reconciliation context, preserve valid selections when restricted products are activated, remove the redundant certificate request, and add focused regression coverage.
167 lines
4.9 KiB
JavaScript
167 lines
4.9 KiB
JavaScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const { canonicalProductLookup } = vi.hoisted(() => ({
|
|
canonicalProductLookup: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@/components/numberplatescanners/Scans.vue", () => ({
|
|
getScansDepartmentPagination: vi.fn(),
|
|
}));
|
|
vi.mock("@/components/request/HandleGlobalError.vue", () => ({
|
|
clearErrors: vi.fn(),
|
|
parseError: vi.fn(),
|
|
}));
|
|
vi.mock("@/components/shop/CustomerNotes.vue", () => ({
|
|
createNote: vi.fn(),
|
|
deleteNote: vi.fn(),
|
|
getNotes: vi.fn(),
|
|
}));
|
|
vi.mock("@/components/shop/OrdersItems.vue", () => ({
|
|
createOrderItem: vi.fn(),
|
|
getOrderItems: vi.fn(),
|
|
removeOrderItem: vi.fn(),
|
|
}));
|
|
vi.mock("@/components/shop/CustomerAttributes.vue", () => ({
|
|
getAttributes: vi.fn(),
|
|
}));
|
|
vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
|
SessionUser: {
|
|
functions: {},
|
|
objects: {
|
|
products: {
|
|
get: {
|
|
single: canonicalProductLookup,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}));
|
|
vi.mock("@/components/session/authenticatedRequest.vue", () => ({
|
|
authenticatedRequest: vi.fn(),
|
|
}));
|
|
vi.mock("@/components/displays/department/pos/utils/washCertificate.js", () => ({
|
|
doesOrderContainWashCertificateProduct: vi.fn(() => false),
|
|
}));
|
|
vi.mock("@/features/customer/customerProductRules.js", () => ({
|
|
getCustomerProductRestriction: vi.fn(),
|
|
getProductCategoryRestrictionForCustomer: vi.fn(),
|
|
hasValidCustomerProductRestrictionContract: vi.fn(() => true),
|
|
isProductCategoryRestrictedForCustomer: vi.fn(() => false),
|
|
}));
|
|
vi.mock("sweetalert2", () => ({
|
|
default: { fire: vi.fn() },
|
|
}));
|
|
|
|
import { customer_id, department_id, reconcileOrderItemProducts } from "@/components/shop/POSDepartmentProcess.vue";
|
|
|
|
const inconsistentItem = (overrides = {}) => ({
|
|
id: 9101,
|
|
order_id: 54518,
|
|
product_id: 63,
|
|
product: {
|
|
id: 53,
|
|
name: "Stale embedded product",
|
|
embedded_only: "preserve me",
|
|
},
|
|
quantity: 2,
|
|
price: 777,
|
|
notes: "Order-item note",
|
|
...overrides,
|
|
});
|
|
|
|
describe("reconcileOrderItemProducts", () => {
|
|
beforeEach(() => {
|
|
canonicalProductLookup.mockReset();
|
|
canonicalProductLookup.mockResolvedValue({
|
|
id: 63,
|
|
name: "Canonical product",
|
|
price: 399,
|
|
canonical_only: true,
|
|
});
|
|
department_id.value = 12;
|
|
customer_id.value = 12345679;
|
|
});
|
|
|
|
it("uses explicit order context instead of stale global POS context", async () => {
|
|
await reconcileOrderItemProducts([inconsistentItem()], {
|
|
departmentId: 44,
|
|
customerId: 87654321,
|
|
});
|
|
|
|
expect(canonicalProductLookup).toHaveBeenCalledOnce();
|
|
expect(canonicalProductLookup).toHaveBeenCalledWith(63, {
|
|
department_id: 44,
|
|
customer_id: 87654321,
|
|
category_id: null,
|
|
final_price: true,
|
|
});
|
|
});
|
|
|
|
it("merges canonical product data without losing order-item or embedded product fields", async () => {
|
|
const [result] = await reconcileOrderItemProducts([inconsistentItem()]);
|
|
|
|
expect(result).toMatchObject({
|
|
id: 9101,
|
|
order_id: 54518,
|
|
product_id: 63,
|
|
quantity: 2,
|
|
price: 777,
|
|
notes: "Order-item note",
|
|
product: {
|
|
id: 63,
|
|
name: "Canonical product",
|
|
price: 399,
|
|
embedded_only: "preserve me",
|
|
canonical_only: true,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("fetches a missing canonical product only once for duplicate product IDs", async () => {
|
|
const results = await reconcileOrderItemProducts([
|
|
inconsistentItem({ id: 9101 }),
|
|
inconsistentItem({ id: 9102, product: { id: 54, name: "Another stale product" } }),
|
|
]);
|
|
|
|
expect(canonicalProductLookup).toHaveBeenCalledOnce();
|
|
expect(results.map((item) => item.product.id)).toEqual([63, 63]);
|
|
expect(results.map((item) => item.product.name)).toEqual(["Canonical product", "Canonical product"]);
|
|
});
|
|
|
|
it("does not reuse a mismatched embedded product as canonical data for another item", async () => {
|
|
const [result] = await reconcileOrderItemProducts([
|
|
inconsistentItem({ id: 9101 }),
|
|
inconsistentItem({
|
|
id: 9102,
|
|
product_id: 64,
|
|
product: { id: 63, name: "Stale product from another mismatched item" },
|
|
}),
|
|
]);
|
|
|
|
expect(canonicalProductLookup).toHaveBeenCalledWith(63, expect.any(Object));
|
|
expect(result.product.name).toBe("Canonical product");
|
|
});
|
|
|
|
it("does not fall back to stale global context when explicit order context is empty", async () => {
|
|
await reconcileOrderItemProducts([inconsistentItem()], {
|
|
departmentId: null,
|
|
customerId: null,
|
|
});
|
|
|
|
expect(canonicalProductLookup).toHaveBeenCalledWith(63, {
|
|
department_id: null,
|
|
customer_id: null,
|
|
category_id: null,
|
|
final_price: true,
|
|
});
|
|
});
|
|
|
|
it.each([undefined, null, {}, "not-an-array"])(
|
|
"normalizes malformed item input %# to an empty list",
|
|
async (items) => {
|
|
await expect(reconcileOrderItemProducts(items)).resolves.toEqual([]);
|
|
expect(canonicalProductLookup).not.toHaveBeenCalled();
|
|
}
|
|
);
|
|
});
|