Files
pleno-vue/tests/unit/pos-department-process.spec.js
T
b1e0c61df0 Collect audit reason for extra time sales (#240)
## Summary
- Add a shared POS audit helper for approved 10-minute extra sale
reason/comment payloads.
- Prompt for audit metadata in desktop add/copy, desktop item edit,
booking hydration, and mobile completion rebuild flows.
- Include preview evidence files under
`docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/`.

## Verification
- `npm ci --legacy-peer-deps`
- `npm run lint`
- `npm run build`

## Visual change previews
### View: POS extra sale audit
**Description:** POS order item add/edit flows now require an approved
reason for “10 min ekstra”, with a comment field available and required
for the `other` reason.

#### Mobile (390x844)
**Before:**
![Mobile
before](../blob/69fa62f6e38e5c6a28d6aa00c95f749df33b3e0a/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-mobile-before.png?raw=true)

**After:**
![Mobile
after](../blob/69fa62f6e38e5c6a28d6aa00c95f749df33b3e0a/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-mobile-after.png?raw=true)

#### Tablet (768x1024)
**Before:**
![Tablet
before](../blob/69fa62f6e38e5c6a28d6aa00c95f749df33b3e0a/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-tablet-before.png?raw=true)

**After:**
![Tablet
after](../blob/69fa62f6e38e5c6a28d6aa00c95f749df33b3e0a/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-tablet-after.png?raw=true)

#### Desktop (1440x900)
**Before:**
![Desktop
before](../blob/69fa62f6e38e5c6a28d6aa00c95f749df33b3e0a/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-desktop-before.png?raw=true)

**After:**
![Desktop
after](../blob/69fa62f6e38e5c6a28d6aa00c95f749df33b3e0a/docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-desktop-after.png?raw=true)

## Notes
- Automatic merge remains disabled per Workboard contract.

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-08-13 17:49:27 +02:00

1028 lines
31 KiB
JavaScript

// @vitest-environment jsdom
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
getOrderItems: vi.fn(),
createOrderItem: vi.fn(),
removeOrderItem: vi.fn(),
}));
vi.mock("axios", () => ({
default: {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
},
}));
vi.mock("@/components/numberplatescanners/Scans.vue", () => ({
getScansDepartmentPagination: vi.fn(),
}));
vi.mock("vue-router", () => ({
useRouter: () => ({
currentRoute: {
value: {
params: {
departmentId: "1",
},
},
},
}),
}));
vi.mock("@/components/request/HandleGlobalError.vue", () => ({
parseError: vi.fn(),
clearErrors: vi.fn(),
}));
vi.mock("@/components/shop/CustomerNotes.vue", () => ({
getNotes: vi.fn(),
createNote: vi.fn(),
deleteNote: vi.fn(),
}));
vi.mock("@/components/shop/OrdersItems.vue", () => ({
createOrderItem: mocks.createOrderItem,
getOrderItems: mocks.getOrderItems,
removeOrderItem: mocks.removeOrderItem,
}));
vi.mock("@/components/shop/CustomerAttributes.vue", () => ({
getAttributes: vi.fn(),
}));
vi.mock("@/components/session/token/SessionUser.vue", () => ({
SessionUser: {
request: vi.fn(),
hasPermission: vi.fn(() => true),
functions: {
redirectTo: {
department: vi.fn(),
},
getDepartmentIdFromUrl: vi.fn(),
},
objects: {
global: {
language: {
possible_duplicates: "Possible duplicates",
},
},
orders: {
meta: {
endpoint: "/orders",
},
functions: {
mark_as_completed: vi.fn(),
fetchAttachments: vi.fn(),
removeAttachment: vi.fn(),
uploadAttachment: vi.fn(),
downloadAttachment: vi.fn(),
get_department_id: vi.fn(),
},
get: {
single: vi.fn(),
},
set: {
booking_id: vi.fn(),
department_id: vi.fn(),
notes: vi.fn(),
po: vi.fn(),
reference: vi.fn(),
reg_1: vi.fn(),
reg_2: vi.fn(),
reg_3: vi.fn(),
safety_seal: vi.fn(),
},
},
order_bookings: {
meta: {
endpoint: "/order_bookings",
},
get: {
single: vi.fn(),
},
set: {
order_id: vi.fn(),
},
},
products: {
get: {
all: vi.fn(),
single: vi.fn(),
},
},
vehicles: {
meta: {
endpoint: "/vehicles",
},
},
},
},
}));
vi.mock("@/components/session/authenticatedRequest.vue", () => ({
authenticatedRequest: vi.fn(),
}));
vi.mock("sweetalert2", () => ({
default: {
fire: vi.fn(() => Promise.resolve({ isConfirmed: false })),
showValidationMessage: vi.fn(),
isLoading: vi.fn(() => false),
},
}));
import axios from "axios";
import { getAttributes } from "@/components/shop/CustomerAttributes.vue";
import { getNotes } from "@/components/shop/CustomerNotes.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
import {
clearActivePosOrderContext,
clearCustomerSelection,
clearPendingOrderItems,
clearStoredPosOrderId,
createOrder,
completed_at,
customer_data,
customer_attributes,
customer_attributes_customer_number,
customer_attributes_status,
customer_id,
customer_name,
department_id,
ensureVehiclePlateBookingsLoaded,
getAddonRestriction,
getVehiclePlateBookings,
getProductRestriction,
invoiceCollectionId,
loadCustomerAttributes,
loadOrderItems,
nextStep,
nextStepDelay,
order_id,
order_items,
hydrateSelectedOrderBookingForDesktop,
order_notes,
order_po,
order_safety_seal,
reference,
reg_1,
reg_2,
reg_3,
pendingBookings,
previousStep,
registerPosStepSaveBarrier,
retryCustomerAttributes,
restoreStoredPosOrderId,
saveOrderMetadataField,
searchAndSelectCustomer,
setOrderId,
showFakeCreateOrderItem,
scan_data,
scans,
selectedOrderBookingId,
step,
} from "@/components/shop/POSDepartmentProcess.vue";
describe("POSDepartmentProcess order metadata persistence", () => {
beforeEach(() => {
window.history.replaceState({}, "", "/admin/12/modules/pos?step=2");
clearActivePosOrderContext({ clearStep: true });
localStorage.setItem("token", "test-token");
nextStepDelay.value = 0;
order_id.value = 54518;
customer_id.value = 999;
customer_name.value = "Card Terminal Customer";
customer_data.value = {};
department_id.value = 12;
step.value = 2;
Object.values(SessionUser.objects.orders.set).forEach((setter) => setter.mockReset?.());
});
it("applies saved metadata responses to shared POS state", async () => {
SessionUser.objects.orders.set.reference.mockResolvedValueOnce({
data: {
data: {
id: 54518,
customer_id: 999,
department_id: 12,
reference: "STEP-2-REF",
notes: "Visible note",
po: "PO-STEP-2",
safety_seal: "SEAL-1",
reg_1: "aa 12345",
reg_2: "",
reg_3: "",
invoice_collection_id: 200,
completed_at: null,
},
},
});
await expect(saveOrderMetadataField("reference", "STEP-2-REF", 54518)).resolves.toBe("STEP-2-REF");
expect(SessionUser.objects.orders.set.reference).toHaveBeenCalledWith(54518, "STEP-2-REF");
expect(reference.value).toBe("STEP-2-REF");
expect(order_notes.value).toBe("Visible note");
expect(order_po.value).toBe("PO-STEP-2");
expect(order_safety_seal.value).toBe("SEAL-1");
expect(reg_1.value).toBe("AA12345");
});
it("preserves registration input formatting for the save request and normalizes shared state", async () => {
SessionUser.objects.orders.set.reg_1.mockResolvedValueOnce({
data: {
data: {
id: 54518,
reg_1: "CD1234",
},
},
});
await expect(saveOrderMetadataField("reg_1", "cd-12 34", 54518)).resolves.toBe("CD1234");
expect(SessionUser.objects.orders.set.reg_1).toHaveBeenCalledWith(54518, "CD-12 34");
expect(reg_1.value).toBe("CD1234");
});
it("waits for registered field saves before moving from step 2 to step 3", async () => {
let resolveBarrier;
const barrier = vi.fn(
() =>
new Promise((resolve) => {
resolveBarrier = () => {
reference.value = "BARRIER-SAVED";
resolve();
};
})
);
const unregister = registerPosStepSaveBarrier(barrier);
try {
const moveToNextStep = nextStep({ isMobile: false, orderCreation: false });
await Promise.resolve();
expect(barrier).toHaveBeenCalledTimes(1);
expect(window.location.search).toBe("?step=2");
resolveBarrier();
await moveToNextStep;
expect(reference.value).toBe("BARRIER-SAVED");
expect(window.location.search).toContain("step=3");
expect(window.location.search).toContain("id=54518");
expect(window.location.search).toContain("customer_id=999");
} finally {
unregister();
nextStepDelay.value = 0;
}
});
it("waits for registered field saves before moving backward", async () => {
step.value = 3;
window.history.replaceState({}, "", "/admin/12/modules/pos?id=54518&customer_id=999&step=3");
const barrier = vi.fn(async () => {
order_po.value = "PO-BACK-SAVED";
});
const unregister = registerPosStepSaveBarrier(barrier);
try {
await previousStep();
expect(barrier).toHaveBeenCalledTimes(1);
expect(order_po.value).toBe("PO-BACK-SAVED");
expect(step.value).toBe(2);
expect(window.location.search).toContain("step=2");
} finally {
unregister();
nextStepDelay.value = 0;
}
});
});
describe("POSDepartmentProcess.ensureVehiclePlateBookingsLoaded", () => {
beforeEach(() => {
department_id.value = 531;
SessionUser.request.mockReset();
});
it("uses exact-plate cache even when forced again", async () => {
SessionUser.request.mockImplementation(async (_endpoint, _method, params = {}) => {
const filters = String(params.filters || "");
if (filters.includes("reg_1:EC21234")) {
return {
data: {
data: [
{
id: 9910,
department: 531,
reg_1: "EC21234",
reg_2: "",
order_id: null,
datetime: "2026-03-20T08:00:00.000Z",
},
],
},
};
}
return { data: { data: [] } };
});
await expect(ensureVehiclePlateBookingsLoaded("EC21234", { force: true })).resolves.toEqual([
expect.objectContaining({ id: 9910 }),
]);
await expect(ensureVehiclePlateBookingsLoaded("EC21234", { force: true })).resolves.toEqual([
expect.objectContaining({ id: 9910 }),
]);
expect(getVehiclePlateBookings("EC21234")).toEqual([expect.objectContaining({ id: 9910 })]);
expect(SessionUser.request).toHaveBeenCalledTimes(2);
expect(SessionUser.request).toHaveBeenNthCalledWith(
1,
"/order_bookings",
"GET",
expect.objectContaining({ filters: "department:531,reg_1:EC21234,order_id:is null", limit: 250 })
);
expect(SessionUser.request).toHaveBeenNthCalledWith(
2,
"/order_bookings",
"GET",
expect.objectContaining({ filters: "department:531,reg_2:EC21234,order_id:is null", limit: 250 })
);
});
});
describe("POSDepartmentProcess.loadOrderItems", () => {
beforeEach(() => {
order_id.value = null;
order_items.value = [];
mocks.getOrderItems.mockReset();
});
it("skips loading when order id is missing or invalid", async () => {
order_id.value = null;
await expect(loadOrderItems()).resolves.toEqual([]);
order_id.value = 0;
await expect(loadOrderItems()).resolves.toEqual([]);
order_id.value = "abc";
await expect(loadOrderItems()).resolves.toEqual([]);
expect(mocks.getOrderItems).not.toHaveBeenCalled();
expect(order_items.value).toEqual([]);
});
it("loads order items for a valid order id", async () => {
order_id.value = "9201";
mocks.getOrderItems.mockResolvedValue({
data: {
data: [{ id: 1, price: 10, quantity: 1 }],
},
});
await expect(loadOrderItems()).resolves.toEqual([{ id: 1, price: 10, quantity: 1 }]);
expect(mocks.getOrderItems).toHaveBeenCalledWith(9201);
expect(order_items.value).toEqual([{ id: 1, price: 10, quantity: 1 }]);
});
it("does not create pending order rows without product details", () => {
expect(showFakeCreateOrderItem(63, 1, 0)).toBeNull();
expect(showFakeCreateOrderItem({ id: 63 }, 1, 0)).toBeNull();
expect(order_items.value).toEqual([]);
});
it("clears pending rows before replacing them with reloaded order items", async () => {
order_id.value = "9201";
const pendingProduct = { id: 63, name: "Indvendig vask Forvogn", price: 399 };
const realOrderItem = {
id: 9801,
product_id: 63,
product: pendingProduct,
quantity: 1,
price: 399,
};
mocks.getOrderItems.mockResolvedValue({
data: {
data: [realOrderItem],
},
});
const pendingItem = showFakeCreateOrderItem(pendingProduct, 1, 399);
expect(pendingItem).toEqual(expect.objectContaining({ _pending: true, product: pendingProduct }));
expect(order_items.value).toHaveLength(1);
clearPendingOrderItems();
expect(order_items.value).toEqual([]);
await expect(loadOrderItems()).resolves.toEqual([realOrderItem]);
expect(order_items.value).toEqual([realOrderItem]);
});
it("falls back to empty list when response payload is malformed", async () => {
order_id.value = 9201;
mocks.getOrderItems.mockResolvedValue({
data: {
data: null,
},
});
await expect(loadOrderItems()).resolves.toEqual([]);
expect(mocks.getOrderItems).toHaveBeenCalledWith(9201);
expect(order_items.value).toEqual([]);
});
});
describe("POSDepartmentProcess.setOrderId", () => {
beforeEach(() => {
localStorage.clear();
order_id.value = null;
order_items.value = [];
mocks.getOrderItems.mockReset();
});
it("can skip loading order items when the caller already has them", () => {
setOrderId(9201, { loadItems: false });
expect(order_id.value).toBe(9201);
expect(mocks.getOrderItems).not.toHaveBeenCalled();
});
});
describe("POSDepartmentProcess.searchAndSelectCustomer", () => {
beforeEach(() => {
clearCustomerSelection();
authenticatedRequest.mockReset();
getNotes.mockReset();
getNotes.mockResolvedValue({ data: { data: [] } });
getAttributes.mockReset();
getAttributes.mockResolvedValue({ data: { success: true, data: [] } });
});
const mockCustomerResponse = (customerNumber = 12345679, name = "Pleno Logistics") => ({
data: {
data: {
customer_number: customerNumber,
customer_name: name,
economic_customer: {
customerNumber,
name,
barred: false,
},
},
},
});
it("returns the already selected customer without a duplicate lookup", async () => {
authenticatedRequest.mockResolvedValueOnce(mockCustomerResponse());
const selectedCustomer = await searchAndSelectCustomer(12345679);
const cachedCustomer = await searchAndSelectCustomer(12345679);
expect(authenticatedRequest).toHaveBeenCalledTimes(1);
expect(authenticatedRequest).toHaveBeenCalledWith("/users/customer?customer_number=12345679", "GET");
expect(cachedCustomer).toBe(customer_data.value);
expect(cachedCustomer).toEqual(selectedCustomer);
expect(customer_id.value).toBe(12345679);
expect(customer_name.value).toBe("Pleno Logistics");
});
it("shares concurrent lookups for the same customer number", async () => {
let resolveRequest;
authenticatedRequest.mockReturnValueOnce(
new Promise((resolve) => {
resolveRequest = resolve;
})
);
const firstLookup = searchAndSelectCustomer(12345679);
const secondLookup = searchAndSelectCustomer(12345679);
resolveRequest(mockCustomerResponse());
const [firstCustomer, secondCustomer] = await Promise.all([firstLookup, secondLookup]);
expect(authenticatedRequest).toHaveBeenCalledTimes(1);
expect(firstCustomer).toEqual(secondCustomer);
expect(customer_id.value).toBe(12345679);
});
it("performs a new lookup when forceRefresh is requested", async () => {
authenticatedRequest
.mockResolvedValueOnce(mockCustomerResponse(12345679, "Pleno Logistics"))
.mockResolvedValueOnce(mockCustomerResponse(12345679, "Pleno Logistics Updated"));
await searchAndSelectCustomer(12345679);
const refreshedCustomer = await searchAndSelectCustomer(12345679, { forceRefresh: true });
expect(authenticatedRequest).toHaveBeenCalledTimes(2);
expect(refreshedCustomer.name).toBe("Pleno Logistics Updated");
expect(customer_name.value).toBe("Pleno Logistics Updated");
});
it("fails closed while customer restrictions load and resolves exact product IDs when ready", async () => {
let resolveAttributes;
getAttributes.mockReturnValueOnce(
new Promise((resolve) => {
resolveAttributes = resolve;
})
);
authenticatedRequest.mockResolvedValueOnce(mockCustomerResponse());
await searchAndSelectCustomer(12345679);
expect(customer_attributes_status.value).toBe("loading");
expect(getProductRestriction({ id: 53 })).toMatchObject({
restricted: true,
messageKey: "pos.restrictions.loading",
});
resolveAttributes({
data: {
success: true,
data: [
{
id: 1,
attribute: "onlyTankCleaning",
product_restriction: {
version: 1,
disabled_product_ids: [53],
collections: [],
},
},
],
},
});
await vi.waitFor(() => expect(customer_attributes_status.value).toBe("ready"));
expect(getProductRestriction({ id: "53" }).restricted).toBe(true);
expect(getProductRestriction({ id: 63 }).restricted).toBe(false);
});
it("fails closed when an active product-impact rule lacks its structured restriction payload", async () => {
getAttributes.mockResolvedValueOnce({
data: {
success: true,
data: [{ id: 1, attribute: "restrictSpotFree" }],
},
});
authenticatedRequest.mockResolvedValueOnce(mockCustomerResponse());
await searchAndSelectCustomer(12345679);
await vi.waitFor(() => expect(customer_attributes_status.value).toBe("error"));
expect(customer_attributes.value).toEqual([]);
expect(getProductRestriction({ id: 23 })).toMatchObject({
restricted: true,
messageKey: "pos.restrictions.load_failed",
});
});
it("ignores a late customer-attribute response after the selected customer changes", async () => {
let resolveFirstAttributes;
getAttributes
.mockReturnValueOnce(
new Promise((resolve) => {
resolveFirstAttributes = resolve;
})
)
.mockResolvedValueOnce({ data: { success: true, data: [] } });
authenticatedRequest
.mockResolvedValueOnce(mockCustomerResponse(12345679, "First customer"))
.mockResolvedValueOnce(mockCustomerResponse(12345680, "Second customer"));
await searchAndSelectCustomer(12345679);
await searchAndSelectCustomer(12345680);
await vi.waitFor(() => expect(customer_attributes_status.value).toBe("ready"));
resolveFirstAttributes({
data: {
success: true,
data: [
{
id: 2,
attribute: "onlyTankCleaning",
product_restriction: { version: 1, disabled_product_ids: [53], collections: [] },
},
],
},
});
await Promise.resolve();
expect(customer_id.value).toBe(12345680);
expect(customer_attributes.value).toEqual([]);
expect(getProductRestriction({ id: 53 }).restricted).toBe(false);
});
it("loads restrictions for an explicit customer when the POS-global customer is blank", async () => {
let resolveAttributes;
getAttributes.mockReturnValueOnce(
new Promise((resolve) => {
resolveAttributes = resolve;
})
);
customer_id.value = "";
const loadRequest = loadCustomerAttributes(12345679);
expect(getAttributes).toHaveBeenCalledWith(12345679);
expect(customer_attributes_status.value).toBe("loading");
expect(customer_attributes_customer_number.value).toBeNull();
expect(getProductRestriction({ id: 53 }, { customerNumber: 12345679 })).toMatchObject({
restricted: true,
messageKey: "pos.restrictions.loading",
});
resolveAttributes({
data: {
success: true,
data: [
{
id: 1,
attribute: "restrictSpotFree",
product_restriction: { version: 1, disabled_product_ids: [53], collections: [] },
},
],
},
});
await loadRequest;
expect(customer_id.value).toBe("");
expect(customer_attributes_status.value).toBe("ready");
expect(customer_attributes_customer_number.value).toBe(12345679);
expect(getProductRestriction({ id: 53 }, { customerNumber: 12345679 }).restricted).toBe(true);
expect(getAddonRestriction({ option_id: 53 }, { customerNumber: 12345679 }).restricted).toBe(true);
});
it("retries an explicit target and then reuses the last requested target", async () => {
getAttributes
.mockRejectedValueOnce(new Error("attributes unavailable"))
.mockResolvedValueOnce({ data: { success: true, data: [] } })
.mockResolvedValueOnce({ data: { success: true, data: [] } });
customer_id.value = "";
await loadCustomerAttributes(12345679);
expect(customer_attributes_status.value).toBe("error");
await retryCustomerAttributes(12345680);
expect(customer_attributes_status.value).toBe("ready");
expect(customer_attributes_customer_number.value).toBe(12345680);
await retryCustomerAttributes();
expect(getAttributes.mock.calls.map(([customerNumber]) => customerNumber)).toEqual([12345679, 12345680, 12345680]);
expect(customer_attributes_customer_number.value).toBe(12345680);
});
it("ignores stale explicit-target responses without consulting the POS-global customer", async () => {
let resolveFirstAttributes;
getAttributes
.mockReturnValueOnce(
new Promise((resolve) => {
resolveFirstAttributes = resolve;
})
)
.mockResolvedValueOnce({
data: {
success: true,
data: [
{
id: 2,
attribute: "restrictSpotFree",
product_restriction: { version: 1, disabled_product_ids: [63], collections: [] },
},
],
},
});
customer_id.value = "";
const firstRequest = loadCustomerAttributes(12345679);
await loadCustomerAttributes(12345680);
resolveFirstAttributes({
data: {
success: true,
data: [
{
id: 1,
attribute: "restrictSpotFree",
product_restriction: { version: 1, disabled_product_ids: [53], collections: [] },
},
],
},
});
await firstRequest;
expect(customer_id.value).toBe("");
expect(customer_attributes_customer_number.value).toBe(12345680);
expect(getProductRestriction({ id: 63 }, { customerNumber: 12345680 }).restricted).toBe(true);
expect(getProductRestriction({ id: 53 }, { customerNumber: 12345680 }).restricted).toBe(false);
expect(getProductRestriction({ id: 63 }, { customerNumber: 12345679 })).toMatchObject({
restricted: true,
messageKey: "pos.restrictions.loading",
});
});
});
describe("POSDepartmentProcess.hydrateSelectedOrderBookingForDesktop", () => {
beforeEach(() => {
order_id.value = 51207;
customer_id.value = 12345679;
customer_name.value = "";
department_id.value = 2;
reference.value = "";
order_notes.value = "";
order_po.value = "";
reg_1.value = "";
reg_2.value = "";
order_items.value = [];
selectedOrderBookingId.value = 7001;
pendingBookings.value = [];
mocks.getOrderItems.mockReset();
mocks.createOrderItem.mockReset();
SessionUser.objects.products.get.single.mockReset();
SessionUser.objects.products.get.all.mockReset();
SessionUser.objects.orders.set.reference.mockReset();
SessionUser.objects.orders.set.reg_1.mockReset();
SessionUser.objects.orders.set.reg_2.mockReset();
SessionUser.objects.orders.set.notes.mockReset();
SessionUser.objects.orders.set.po.mockReset();
SessionUser.objects.order_bookings.get.single.mockReset();
});
it("hydrates booking items without forwarding booking-controlled prices", async () => {
pendingBookings.value = [
{
id: 7001,
customer_number: 12345679,
reference: "BOOK-7001",
reg_1: "AB12345",
reg_2: "CD12345",
notes: "Booking notes",
po: "PO-7001",
items: [
{ id: 10, price: 1, quantity: 1 },
{ id: 20, price: 2, quantity: 3, notes: "Addon note" },
],
},
];
mocks.getOrderItems.mockResolvedValueOnce({ data: { data: [] } }).mockResolvedValueOnce({ data: { data: [] } });
SessionUser.objects.products.get.single.mockResolvedValue({ id: 10, is_wash: true, price: 500 });
mocks.createOrderItem.mockResolvedValueOnce({ data: { data: { id: 9001 } } }).mockResolvedValueOnce({});
await expect(hydrateSelectedOrderBookingForDesktop()).resolves.toBe(true);
expect(mocks.createOrderItem).toHaveBeenNthCalledWith(1, 51207, 10, 1, null, null, 500, null, {
extra_sale_comment: null,
extra_sale_reason_code: null,
});
expect(mocks.createOrderItem).toHaveBeenNthCalledWith(2, 51207, 20, 3, 9001, "Addon note", 500, null, {
extra_sale_comment: null,
extra_sale_reason_code: null,
});
expect(SessionUser.objects.products.get.single).toHaveBeenCalledWith(10, {
department_id: 2,
customer_id: 12345679,
category_id: null,
final_price: true,
});
});
});
describe("POSDepartmentProcess.clearActivePosOrderContext", () => {
it("clears active order, customer, metadata and stored mobile order id", () => {
localStorage.setItem("pos_order_id", "51211");
step.value = 2;
order_id.value = 51211;
customer_id.value = 12345679;
customer_name.value = "Existing Customer";
order_items.value = [{ id: 1 }];
reference.value = "STALE-REF";
order_notes.value = "STALE-NOTES";
order_po.value = "STALE-PO";
order_safety_seal.value = "STALE-SEAL";
reg_1.value = "AB12345";
reg_2.value = "CD12345";
reg_3.value = "EF12345";
invoiceCollectionId.value = 7001;
completed_at.value = "2026-01-01T10:00:00.000Z";
scans.value = [{ id: 1 }];
scan_data.value = [{ id: 2 }];
expect(clearActivePosOrderContext({ clearStep: true })).toBe(true);
expect(step.value).toBe(1);
expect(order_id.value).toBeNull();
expect(customer_id.value).toBe("");
expect(customer_name.value).toBe("");
expect(order_items.value).toEqual([]);
expect(reference.value).toBe("");
expect(order_notes.value).toBe("");
expect(order_po.value).toBe("");
expect(order_safety_seal.value).toBe("");
expect(reg_1.value).toBe("");
expect(reg_2.value).toBe("");
expect(reg_3.value).toBe("");
expect(invoiceCollectionId.value).toBeNull();
expect(completed_at.value).toBeNull();
expect(scans.value).toEqual([]);
expect(scan_data.value).toEqual([]);
expect(localStorage.getItem("pos_order_id")).toBeNull();
});
});
describe("POSDepartmentProcess.restoreStoredPosOrderId", () => {
beforeEach(() => {
clearStoredPosOrderId();
order_id.value = null;
department_id.value = "";
SessionUser.objects.orders.get.single.mockReset();
SessionUser.objects.orders.functions.get_department_id.mockReset();
SessionUser.objects.orders.set.department_id.mockReset();
});
it("clears invalid stored order ids before validation", async () => {
localStorage.setItem("pos_order_id", "invalid");
await expect(restoreStoredPosOrderId()).resolves.toBeNull();
expect(localStorage.getItem("pos_order_id")).toBeNull();
expect(SessionUser.objects.orders.get.single).not.toHaveBeenCalled();
});
it("restores a valid stored order id when order context matches", async () => {
localStorage.setItem("pos_order_id", "51211");
SessionUser.objects.orders.get.single.mockResolvedValue({
id: 51211,
customer_id: 12345679,
department_id: 2,
completed_at: null,
});
await expect(
restoreStoredPosOrderId({
validateOrder: true,
customerId: 12345679,
departmentId: 2,
})
).resolves.toBe(51211);
expect(order_id.value).toBe(51211);
expect(localStorage.getItem("pos_order_id")).toBe("51211");
expect(SessionUser.objects.orders.get.single).toHaveBeenCalledWith(51211);
});
it("clears stored order id when customer or department does not match", async () => {
localStorage.setItem("pos_order_id", "51211");
order_id.value = 51211;
SessionUser.objects.orders.get.single.mockResolvedValue({
id: 51211,
customer_id: 999,
department_id: 88,
completed_at: null,
});
await expect(
restoreStoredPosOrderId({
validateOrder: true,
customerId: 12345679,
departmentId: 2,
})
).resolves.toBeNull();
expect(order_id.value).toBeNull();
expect(localStorage.getItem("pos_order_id")).toBeNull();
});
it("clears stored order id when order lookup fails", async () => {
localStorage.setItem("pos_order_id", "51211");
order_id.value = 51211;
SessionUser.objects.orders.get.single.mockRejectedValue(new Error("Order not found"));
await expect(
restoreStoredPosOrderId({
validateOrder: true,
})
).resolves.toBeNull();
expect(order_id.value).toBeNull();
expect(localStorage.getItem("pos_order_id")).toBeNull();
});
it("rejects a stored current order from a different department instead of moving it", async () => {
localStorage.setItem("pos_order_id", "51211");
department_id.value = 2;
SessionUser.objects.orders.get.single.mockResolvedValue({
id: 51211,
customer_id: 12345679,
department_id: 88,
completed_at: null,
});
SessionUser.objects.orders.functions.get_department_id.mockResolvedValue(88);
await expect(
restoreStoredPosOrderId({
validateOrder: true,
customerId: 12345679,
departmentId: 2,
syncDepartment: true,
})
).resolves.toBeNull();
expect(SessionUser.objects.orders.set.department_id).not.toHaveBeenCalled();
expect(order_id.value).toBeNull();
expect(localStorage.getItem("pos_order_id")).toBeNull();
});
});
describe("POSDepartmentProcess.createOrder department sync", () => {
beforeEach(() => {
window.history.replaceState({}, "", "/admin/1/modules/pos");
order_id.value = null;
department_id.value = "";
axios.post.mockReset();
SessionUser.functions.getDepartmentIdFromUrl.mockReset();
SessionUser.functions.getDepartmentIdFromUrl.mockReturnValue(undefined);
SessionUser.objects.orders.functions.get_department_id.mockReset();
SessionUser.objects.orders.set.department_id.mockReset();
localStorage.setItem("token", "test-token");
});
it("rejects an existing current order from a different department instead of updating it", async () => {
order_id.value = 9201;
department_id.value = 7;
SessionUser.objects.orders.functions.get_department_id.mockResolvedValue(3);
await expect(createOrder({ isMobile: true })).resolves.toBe(false);
expect(SessionUser.objects.orders.set.department_id).not.toHaveBeenCalled();
expect(axios.post).not.toHaveBeenCalled();
});
it("reuses an existing current order when it already belongs to the selected department", async () => {
order_id.value = 9201;
department_id.value = 7;
SessionUser.objects.orders.functions.get_department_id.mockResolvedValue(7);
await expect(createOrder({ isMobile: true })).resolves.toBe(true);
expect(SessionUser.objects.orders.set.department_id).not.toHaveBeenCalled();
expect(axios.post).not.toHaveBeenCalled();
});
it("creates a new order in the selected department", async () => {
department_id.value = 7;
axios.post.mockResolvedValue({
data: {
success: true,
data: {
id: 9202,
},
},
});
await expect(createOrder({ isMobile: true })).resolves.toBe(true);
expect(axios.post).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
department_id: 7,
is_handheld: true,
}),
expect.any(Object)
);
expect(order_id.value).toBe(9202);
expect(department_id.value).toBe(7);
});
it("falls back to the route department when the selected department ref is not numeric", async () => {
department_id.value = "Demo";
axios.post.mockResolvedValue({
data: {
success: true,
data: {
id: 9203,
},
},
});
await expect(createOrder({ isMobile: false })).resolves.toBe(true);
expect(axios.post).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
department_id: 1,
is_handheld: false,
}),
expect.any(Object)
);
expect(order_id.value).toBe(9203);
expect(department_id.value).toBe(1);
});
});