- Introduced `.prettierrc.json` to enforce consistent code formatting across the project. - Updated unit and e2e test files to address formatting issues, improve readability, and ensure alignment with the new Prettier configuration.
1766 lines
54 KiB
JavaScript
1766 lines
54 KiB
JavaScript
import { mockApi, seedAuthenticatedState } from "./network.js";
|
|
|
|
export const API_HOST =
|
|
/https?:\/\/(?:api\.truckwash\.io(?::\d+)?\/.*|localhost(?::\d+)?\/api\/.*|127\.0\.0\.1(?::\d+)?\/api\/.*)/i;
|
|
export const DEFAULT_DEPARTMENT_ID = 1;
|
|
export const DEFAULT_ORDER_ID = 9201;
|
|
export const DEFAULT_LAST_ORDER_ID = 9100;
|
|
export const DEFAULT_BOOKING_ID = 8101;
|
|
export const REGULAR_CUSTOMER_ID = 12345;
|
|
export const CARD_CUSTOMER_ID = 999;
|
|
export const MOBILE_PERMISSIONS = ["admin", "department_access_1"];
|
|
export const MOBILE_NEXT_STEP_COOLDOWN_MS = 2100;
|
|
|
|
const ATTACHMENT_DOWNLOAD_URL = "https://cdn.example.test/mobile-pos";
|
|
|
|
export function json(body, status = 200) {
|
|
return {
|
|
status,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(body),
|
|
};
|
|
}
|
|
|
|
function clone(value) {
|
|
if (value === undefined) {
|
|
return undefined;
|
|
}
|
|
return JSON.parse(JSON.stringify(value));
|
|
}
|
|
|
|
function toPositiveInteger(value) {
|
|
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
}
|
|
|
|
function createCustomer(customerNumber, overrides = {}) {
|
|
return {
|
|
id: customerNumber,
|
|
customerNumber,
|
|
name: `Customer ${customerNumber}`,
|
|
address: "Demo Street 1",
|
|
zip: "2630",
|
|
city: "Taastrup",
|
|
mobilePhone: "12345678",
|
|
email: `customer-${customerNumber}@example.com`,
|
|
corporateIdentificationNumber: String(customerNumber).padStart(8, "0"),
|
|
barred: false,
|
|
economic_customer: customerNumber,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createAddon(product, overrides = {}) {
|
|
return {
|
|
id: product.id,
|
|
name: product.name,
|
|
price: Number(product.price ?? 0),
|
|
product: {
|
|
...clone(product),
|
|
addons: [],
|
|
},
|
|
quantity: 0,
|
|
min: -1,
|
|
max: -1,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createOrderRecord(orderId, overrides = {}) {
|
|
return {
|
|
id: orderId,
|
|
customer_id: REGULAR_CUSTOMER_ID,
|
|
department_id: DEFAULT_DEPARTMENT_ID,
|
|
reference: "",
|
|
notes: "",
|
|
po: "",
|
|
reg_1: "AB12345",
|
|
reg_2: "",
|
|
reg_3: "",
|
|
invoice_collection_id: null,
|
|
booking_id: null,
|
|
completed_at: null,
|
|
created_at: "2026-01-01T10:00:00.000Z",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function buildOrderItem(product, overrides = {}, id = null) {
|
|
const orderId = toPositiveInteger(overrides.order_id) ?? DEFAULT_ORDER_ID;
|
|
return {
|
|
id,
|
|
order_id: orderId,
|
|
product_id: Number(overrides.product_id ?? product.id),
|
|
product: {
|
|
...clone(product),
|
|
addons: clone(product.addons || []),
|
|
},
|
|
quantity: Number(overrides.quantity ?? 1),
|
|
notes: String(overrides.notes ?? ""),
|
|
reference: String(overrides.reference ?? ""),
|
|
related_item_id: overrides.related_item_id ?? null,
|
|
price: Number(overrides.price ?? product.price ?? 0),
|
|
};
|
|
}
|
|
|
|
function createBookingRecord(id, overrides = {}) {
|
|
return {
|
|
id,
|
|
customer_number: REGULAR_CUSTOMER_ID,
|
|
department: DEFAULT_DEPARTMENT_ID,
|
|
reg_1: "BOOK123",
|
|
reg_2: "",
|
|
reg_3: "",
|
|
reference: `BOOK-${id}`,
|
|
notes: `Booking notes ${id}`,
|
|
po: `PO-${id}`,
|
|
order_id: null,
|
|
status: "pending",
|
|
items: [],
|
|
created_at: "2026-01-01T09:00:00.000Z",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function buildDefaultProducts() {
|
|
const products = [
|
|
{
|
|
id: 53,
|
|
name: "Tank truck wash",
|
|
description: "Primary mobile wash product",
|
|
price: 599,
|
|
subscription_allowed: true,
|
|
category: 4,
|
|
piktogram: "truck",
|
|
apply_category_discount: true,
|
|
requires_note: false,
|
|
is_wash: true,
|
|
display_in_booking_form: true,
|
|
order_priority: 1,
|
|
addons: [],
|
|
},
|
|
{
|
|
id: 63,
|
|
name: "Box trailer wash",
|
|
description: "Alternate mobile wash product",
|
|
price: 499,
|
|
subscription_allowed: true,
|
|
category: 4,
|
|
piktogram: "truck",
|
|
apply_category_discount: true,
|
|
requires_note: false,
|
|
is_wash: true,
|
|
display_in_booking_form: true,
|
|
order_priority: 2,
|
|
addons: [],
|
|
},
|
|
{
|
|
id: 71,
|
|
name: "Interior rinse",
|
|
description: "Mobile addon",
|
|
price: 99,
|
|
subscription_allowed: true,
|
|
category: 8,
|
|
piktogram: "spray",
|
|
apply_category_discount: false,
|
|
requires_note: false,
|
|
is_wash: false,
|
|
display_in_booking_form: true,
|
|
order_priority: 1,
|
|
addons: [],
|
|
},
|
|
{
|
|
id: 41,
|
|
name: "Safety seal certificate",
|
|
description: "Wash certificate",
|
|
price: 25,
|
|
subscription_allowed: true,
|
|
category: 8,
|
|
piktogram: "certificate",
|
|
apply_category_discount: false,
|
|
requires_note: false,
|
|
is_wash: false,
|
|
display_in_booking_form: true,
|
|
order_priority: 2,
|
|
addons: [],
|
|
},
|
|
{
|
|
id: 81,
|
|
name: "Tank inspection",
|
|
description: "Non-wash booking product",
|
|
price: 150,
|
|
subscription_allowed: true,
|
|
category: 8,
|
|
piktogram: "clipboard",
|
|
apply_category_discount: false,
|
|
requires_note: false,
|
|
is_wash: false,
|
|
display_in_booking_form: true,
|
|
order_priority: 3,
|
|
addons: [],
|
|
},
|
|
{
|
|
id: 91,
|
|
name: "Extra detergent",
|
|
description: "Additional service",
|
|
price: 75,
|
|
subscription_allowed: true,
|
|
category: 8,
|
|
piktogram: "sparkles",
|
|
apply_category_discount: false,
|
|
requires_note: false,
|
|
is_wash: false,
|
|
display_in_booking_form: true,
|
|
order_priority: 4,
|
|
addons: [],
|
|
},
|
|
];
|
|
|
|
const byId = Object.fromEntries(products.map((product) => [product.id, product]));
|
|
byId[53].addons = [createAddon(byId[71], { min: 0, max: 3 }), createAddon(byId[41], { min: 0, max: 1 })];
|
|
byId[63].addons = [createAddon(byId[71], { min: 0, max: 2 })];
|
|
return clone(products);
|
|
}
|
|
|
|
function createRequestCounters(overrides = {}) {
|
|
return {
|
|
readersGet: 0,
|
|
paymentIntentGet: 0,
|
|
paymentIntentCreate: 0,
|
|
paymentIntentDelete: 0,
|
|
paymentIntentCapture: 0,
|
|
departmentsGet: 0,
|
|
departmentCategoriesGet: 0,
|
|
bookingsGet: 0,
|
|
orderBookingsGet: 0,
|
|
bookingComplete: 0,
|
|
bookingSetOrderId: 0,
|
|
vehiclesSearchGet: 0,
|
|
vehiclesGet: 0,
|
|
unknownVehiclesGet: 0,
|
|
usersCustomerGet: 0,
|
|
customersGet: 0,
|
|
customerNotesGet: 0,
|
|
customerAttributesGet: 0,
|
|
discountsGet: 0,
|
|
productsGet: 0,
|
|
recommendedGet: 0,
|
|
ordersGet: 0,
|
|
orderGet: 0,
|
|
orderCreate: 0,
|
|
orderUpdate: 0,
|
|
orderDelete: 0,
|
|
orderItemsGet: 0,
|
|
orderItemsPost: 0,
|
|
orderItemsPut: 0,
|
|
orderItemsDelete: 0,
|
|
markAsCompleted: 0,
|
|
attachmentsGet: 0,
|
|
attachmentsDelete: 0,
|
|
attachmentUpload: 0,
|
|
attachmentDownload: 0,
|
|
lprPost: 0,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createRequestLog(overrides = {}) {
|
|
return {
|
|
orderCreates: [],
|
|
orderUpdates: [],
|
|
orderDeletes: [],
|
|
orderItemCreates: [],
|
|
orderItemUpdates: [],
|
|
orderItemDeletes: [],
|
|
bookingCompletions: [],
|
|
bookingOrderAssignments: [],
|
|
attachmentUploads: [],
|
|
attachmentDeletes: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createFailureBudget(overrides = {}) {
|
|
return {
|
|
paymentIntentGet: 0,
|
|
paymentIntentCreate: 0,
|
|
paymentIntentCapture: 0,
|
|
orderCreate: 0,
|
|
bookingComplete: 0,
|
|
attachmentUpload: 0,
|
|
orderDelete: 0,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function mergeObjectMaps(base, overrides = {}) {
|
|
const result = {};
|
|
Object.entries(base || {}).forEach(([key, value]) => {
|
|
result[key] = clone(value);
|
|
});
|
|
Object.entries(overrides || {}).forEach(([key, value]) => {
|
|
result[key] = clone(value);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
function buildDefaultFixture() {
|
|
const products = buildDefaultProducts();
|
|
const productById = Object.fromEntries(products.map((product) => [product.id, product]));
|
|
const regularCustomer = createCustomer(REGULAR_CUSTOMER_ID, {
|
|
name: "Pleno Logistics",
|
|
email: "pos-mobile@example.com",
|
|
});
|
|
const cardCustomer = createCustomer(CARD_CUSTOMER_ID, {
|
|
name: "Card Terminal Customer",
|
|
email: "card-customer@example.com",
|
|
corporateIdentificationNumber: "99999999",
|
|
});
|
|
|
|
const lastOrder = createOrderRecord(DEFAULT_LAST_ORDER_ID, {
|
|
customer_id: REGULAR_CUSTOMER_ID,
|
|
reference: "LAST-ORDER-REF",
|
|
reg_1: "AB12345",
|
|
created_at: "2026-01-01T08:30:00.000Z",
|
|
});
|
|
const cardOrder = createOrderRecord(DEFAULT_ORDER_ID, {
|
|
customer_id: CARD_CUSTOMER_ID,
|
|
reference: "CARD-REF-9201",
|
|
reg_1: "AB12345",
|
|
created_at: "2026-01-01T10:00:00.000Z",
|
|
});
|
|
const lastOrderPrimary = buildOrderItem(
|
|
productById[53],
|
|
{
|
|
order_id: DEFAULT_LAST_ORDER_ID,
|
|
quantity: 1,
|
|
},
|
|
9001
|
|
);
|
|
const lastOrderAddon = buildOrderItem(
|
|
productById[71],
|
|
{
|
|
order_id: DEFAULT_LAST_ORDER_ID,
|
|
quantity: 1,
|
|
related_item_id: 9001,
|
|
},
|
|
9002
|
|
);
|
|
const bookingPrimary = createBookingRecord(DEFAULT_BOOKING_ID, {
|
|
reg_1: "BOOK123",
|
|
reference: "BOOKING-REF-8101",
|
|
notes: "Booking notes from planner",
|
|
po: "PO-8101",
|
|
items: [
|
|
{ id: 53, name: productById[53].name, price: productById[53].price, quantity: 1 },
|
|
{ id: 71, name: productById[71].name, price: productById[71].price, quantity: 1 },
|
|
],
|
|
});
|
|
const nonWashBooking = createBookingRecord(8102, {
|
|
reg_1: "NOWASH1",
|
|
reference: "BOOKING-REF-8102",
|
|
notes: "Needs wash fallback",
|
|
po: "PO-8102",
|
|
items: [
|
|
{ id: 81, name: productById[81].name, price: productById[81].price, quantity: 1 },
|
|
{ id: 71, name: productById[71].name, price: productById[71].price, quantity: 1 },
|
|
],
|
|
});
|
|
const safetySealBooking = createBookingRecord(8103, {
|
|
reg_1: "SEAL123",
|
|
reference: "BOOKING-REF-8103",
|
|
notes: "Requires safety seal",
|
|
po: "PO-8103",
|
|
items: [
|
|
{ id: 53, name: productById[53].name, price: productById[53].price, quantity: 1 },
|
|
{ id: 41, name: productById[41].name, price: productById[41].price, quantity: 1 },
|
|
],
|
|
});
|
|
|
|
return {
|
|
departmentId: DEFAULT_DEPARTMENT_ID,
|
|
cardCustomerId: CARD_CUSTOMER_ID,
|
|
regularCustomerId: REGULAR_CUSTOMER_ID,
|
|
departments: [
|
|
{
|
|
id: DEFAULT_DEPARTMENT_ID,
|
|
name: "Taastrup",
|
|
},
|
|
],
|
|
failureBudget: createFailureBudget(),
|
|
requestCounters: createRequestCounters(),
|
|
requestLog: createRequestLog(),
|
|
customersByNumber: {
|
|
[REGULAR_CUSTOMER_ID]: regularCustomer,
|
|
[CARD_CUSTOMER_ID]: cardCustomer,
|
|
},
|
|
customerAttributesByNumber: {
|
|
[REGULAR_CUSTOMER_ID]: [],
|
|
[CARD_CUSTOMER_ID]: [
|
|
{
|
|
id: 1,
|
|
customer_number: CARD_CUSTOMER_ID,
|
|
attribute: "invoiceWithStripe",
|
|
},
|
|
],
|
|
},
|
|
customerNotesByNumber: {
|
|
[REGULAR_CUSTOMER_ID]: [
|
|
{
|
|
id: 1,
|
|
note: "Customer note for mobile POS",
|
|
},
|
|
],
|
|
},
|
|
employees: [
|
|
{
|
|
id: 7,
|
|
display_name: "Jeppe",
|
|
},
|
|
],
|
|
products,
|
|
departmentCategories: [
|
|
{
|
|
id: 11,
|
|
department_id: DEFAULT_DEPARTMENT_ID,
|
|
category: {
|
|
id: 4,
|
|
name: "Wash",
|
|
meta: {
|
|
products: [53, 63],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: 12,
|
|
department_id: DEFAULT_DEPARTMENT_ID,
|
|
category: {
|
|
id: 8,
|
|
name: "Extras",
|
|
meta: {
|
|
products: [71, 41, 81, 91],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
vehicles: [
|
|
{
|
|
id: 7001,
|
|
reg: "AB12345",
|
|
customer_id: REGULAR_CUSTOMER_ID,
|
|
customer_name: regularCustomer.name,
|
|
type: 53,
|
|
status: "verified",
|
|
barred: false,
|
|
wash_subscription: false,
|
|
addons: {
|
|
enabled: 1,
|
|
available: 2,
|
|
list: [71, 41],
|
|
},
|
|
reference: "REF-AB12345",
|
|
last_order_id: DEFAULT_LAST_ORDER_ID,
|
|
},
|
|
{
|
|
id: 7002,
|
|
reg: "BOOK123",
|
|
customer_id: REGULAR_CUSTOMER_ID,
|
|
customer_name: regularCustomer.name,
|
|
type: null,
|
|
status: "booked",
|
|
barred: false,
|
|
wash_subscription: false,
|
|
addons: {
|
|
enabled: 1,
|
|
available: 2,
|
|
list: [71],
|
|
},
|
|
reference: "BOOKING-REF-8101",
|
|
booking_id: DEFAULT_BOOKING_ID,
|
|
last_order_id: null,
|
|
},
|
|
{
|
|
id: 7003,
|
|
reg: "NOWASH1",
|
|
customer_id: REGULAR_CUSTOMER_ID,
|
|
customer_name: regularCustomer.name,
|
|
type: null,
|
|
status: "booked",
|
|
barred: false,
|
|
wash_subscription: false,
|
|
addons: {
|
|
enabled: 1,
|
|
available: 2,
|
|
list: [71],
|
|
},
|
|
reference: "BOOKING-REF-8102",
|
|
booking_id: 8102,
|
|
last_order_id: null,
|
|
},
|
|
{
|
|
id: 7004,
|
|
reg: "SEAL123",
|
|
customer_id: REGULAR_CUSTOMER_ID,
|
|
customer_name: regularCustomer.name,
|
|
type: null,
|
|
status: "booked",
|
|
barred: false,
|
|
wash_subscription: false,
|
|
addons: {
|
|
enabled: 1,
|
|
available: 2,
|
|
list: [41],
|
|
},
|
|
reference: "BOOKING-REF-8103",
|
|
booking_id: 8103,
|
|
last_order_id: null,
|
|
},
|
|
],
|
|
unknownVehicles: [
|
|
{
|
|
reg_1: "ZZ00000",
|
|
},
|
|
],
|
|
ordersById: {
|
|
[DEFAULT_LAST_ORDER_ID]: lastOrder,
|
|
[DEFAULT_ORDER_ID]: cardOrder,
|
|
},
|
|
orderItemsByOrderId: {
|
|
[DEFAULT_LAST_ORDER_ID]: [lastOrderPrimary, lastOrderAddon],
|
|
[DEFAULT_ORDER_ID]: [],
|
|
},
|
|
attachmentsByOrderId: {
|
|
[DEFAULT_LAST_ORDER_ID]: [
|
|
{
|
|
id: 301,
|
|
content: {
|
|
other: "last-order-note.pdf",
|
|
},
|
|
},
|
|
],
|
|
[DEFAULT_ORDER_ID]: [],
|
|
},
|
|
paymentIntentsByOrderId: {},
|
|
readers: [
|
|
{
|
|
id: "reader_online_1",
|
|
label: "Mobile Reader",
|
|
status: "online",
|
|
action: null,
|
|
},
|
|
],
|
|
bookingsById: {
|
|
[DEFAULT_BOOKING_ID]: bookingPrimary,
|
|
8102: nonWashBooking,
|
|
8103: safetySealBooking,
|
|
},
|
|
pendingBookings: null,
|
|
bookingStatusById: {
|
|
[DEFAULT_BOOKING_ID]: "pending",
|
|
8102: "pending",
|
|
8103: "pending",
|
|
},
|
|
deletedOrderIds: [],
|
|
markCompletedOrderIds: [],
|
|
nextOrderId: 9300,
|
|
nextOrderItemId: 9800,
|
|
nextAttachmentId: 400,
|
|
};
|
|
}
|
|
|
|
function normalizeFixture(fixture) {
|
|
fixture.products = Array.isArray(fixture.products) ? fixture.products : [];
|
|
fixture.vehicles = Array.isArray(fixture.vehicles) ? fixture.vehicles : [];
|
|
fixture.departments = Array.isArray(fixture.departments) ? fixture.departments : [];
|
|
fixture.departmentCategories = Array.isArray(fixture.departmentCategories) ? fixture.departmentCategories : [];
|
|
fixture.employees = Array.isArray(fixture.employees) ? fixture.employees : [];
|
|
fixture.requestCounters = createRequestCounters(fixture.requestCounters);
|
|
fixture.requestLog = createRequestLog(fixture.requestLog);
|
|
fixture.failureBudget = createFailureBudget(fixture.failureBudget);
|
|
fixture.customersByNumber = mergeObjectMaps({}, fixture.customersByNumber);
|
|
fixture.customerAttributesByNumber = mergeObjectMaps({}, fixture.customerAttributesByNumber);
|
|
fixture.customerNotesByNumber = mergeObjectMaps({}, fixture.customerNotesByNumber);
|
|
fixture.ordersById = mergeObjectMaps({}, fixture.ordersById);
|
|
fixture.orderItemsByOrderId = mergeObjectMaps({}, fixture.orderItemsByOrderId);
|
|
fixture.attachmentsByOrderId = mergeObjectMaps({}, fixture.attachmentsByOrderId);
|
|
fixture.paymentIntentsByOrderId = mergeObjectMaps({}, fixture.paymentIntentsByOrderId);
|
|
fixture.bookingsById = mergeObjectMaps({}, fixture.bookingsById);
|
|
fixture.deletedOrderIds = Array.isArray(fixture.deletedOrderIds) ? fixture.deletedOrderIds : [];
|
|
fixture.markCompletedOrderIds = Array.isArray(fixture.markCompletedOrderIds) ? fixture.markCompletedOrderIds : [];
|
|
|
|
if (!fixture.pendingBookings) {
|
|
fixture.pendingBookings = Object.values(fixture.bookingsById).filter((booking) => !booking.order_id);
|
|
} else {
|
|
fixture.pendingBookings = clone(fixture.pendingBookings);
|
|
}
|
|
|
|
if (!fixture.bookingStatusById) {
|
|
fixture.bookingStatusById = {};
|
|
}
|
|
Object.values(fixture.bookingsById).forEach((booking) => {
|
|
fixture.bookingStatusById[booking.id] = booking.status || fixture.bookingStatusById[booking.id] || "pending";
|
|
});
|
|
return fixture;
|
|
}
|
|
|
|
export function createMobilePosFixture(overrides = {}) {
|
|
const base = buildDefaultFixture();
|
|
const fixture = {
|
|
...base,
|
|
...clone(overrides),
|
|
departments: overrides.departments ? clone(overrides.departments) : clone(base.departments),
|
|
products: overrides.products ? clone(overrides.products) : clone(base.products),
|
|
departmentCategories: overrides.departmentCategories
|
|
? clone(overrides.departmentCategories)
|
|
: clone(base.departmentCategories),
|
|
employees: overrides.employees ? clone(overrides.employees) : clone(base.employees),
|
|
vehicles: overrides.vehicles ? clone(overrides.vehicles) : clone(base.vehicles),
|
|
unknownVehicles: overrides.unknownVehicles ? clone(overrides.unknownVehicles) : clone(base.unknownVehicles),
|
|
failureBudget: createFailureBudget(overrides.failureBudget),
|
|
requestCounters: createRequestCounters(overrides.requestCounters),
|
|
requestLog: createRequestLog(overrides.requestLog),
|
|
customersByNumber: mergeObjectMaps(base.customersByNumber, overrides.customersByNumber),
|
|
customerAttributesByNumber: mergeObjectMaps(base.customerAttributesByNumber, overrides.customerAttributesByNumber),
|
|
customerNotesByNumber: mergeObjectMaps(base.customerNotesByNumber, overrides.customerNotesByNumber),
|
|
ordersById: mergeObjectMaps(base.ordersById, overrides.ordersById),
|
|
orderItemsByOrderId: mergeObjectMaps(base.orderItemsByOrderId, overrides.orderItemsByOrderId),
|
|
attachmentsByOrderId: mergeObjectMaps(base.attachmentsByOrderId, overrides.attachmentsByOrderId),
|
|
paymentIntentsByOrderId: mergeObjectMaps(base.paymentIntentsByOrderId, overrides.paymentIntentsByOrderId),
|
|
bookingsById: mergeObjectMaps(base.bookingsById, overrides.bookingsById),
|
|
bookingStatusById: mergeObjectMaps(base.bookingStatusById, overrides.bookingStatusById),
|
|
deletedOrderIds: clone(overrides.deletedOrderIds ?? base.deletedOrderIds),
|
|
markCompletedOrderIds: clone(overrides.markCompletedOrderIds ?? base.markCompletedOrderIds),
|
|
};
|
|
return normalizeFixture(fixture);
|
|
}
|
|
|
|
function getProductById(fixture, productId) {
|
|
return fixture.products.find((product) => Number(product.id) === Number(productId)) || null;
|
|
}
|
|
|
|
function createStateProductSnapshot(fixture, productId) {
|
|
const product = getProductById(fixture, productId);
|
|
return product ? clone(product) : null;
|
|
}
|
|
|
|
export function createAttachmentFile(name, id = null) {
|
|
return {
|
|
id,
|
|
filename: name,
|
|
base64String: `data:text/plain;base64,${Buffer.from(name).toString("base64")}`,
|
|
};
|
|
}
|
|
|
|
export function buildMobilePosState(options = {}) {
|
|
const fixture = options.fixture || createMobilePosFixture();
|
|
const customerId = Object.prototype.hasOwnProperty.call(options, "customerId")
|
|
? options.customerId
|
|
: fixture.regularCustomerId ?? REGULAR_CUSTOMER_ID;
|
|
const primaryItemId = options.primaryItemId ?? 53;
|
|
const includePrimaryItem = options.includePrimaryItem ?? true;
|
|
const vehicleType = Object.prototype.hasOwnProperty.call(options, "vehicleType")
|
|
? options.vehicleType
|
|
: includePrimaryItem
|
|
? primaryItemId
|
|
: 53;
|
|
const bookingId = options.bookingId ?? null;
|
|
const reference = options.reference ?? (bookingId ? `BOOKING-${bookingId}` : "REF-9201");
|
|
const primaryItem = Object.prototype.hasOwnProperty.call(options, "primaryItem")
|
|
? clone(options.primaryItem)
|
|
: includePrimaryItem
|
|
? createStateProductSnapshot(fixture, primaryItemId)
|
|
: null;
|
|
|
|
return {
|
|
vehicles: {
|
|
vehicle_1: {
|
|
reg: options.reg ?? "AB12345",
|
|
reg_2: options.reg2 ?? "",
|
|
reg_3: options.reg3 ?? "",
|
|
customer_id: customerId,
|
|
type: vehicleType,
|
|
status: options.vehicleStatus ?? (bookingId ? "booked" : "verified"),
|
|
barred: false,
|
|
booking_id: bookingId,
|
|
addons: [],
|
|
reference,
|
|
last_order_id: Object.prototype.hasOwnProperty.call(options, "lastOrderId")
|
|
? options.lastOrderId
|
|
: DEFAULT_LAST_ORDER_ID,
|
|
},
|
|
vehicle_2: options.vehicle_2 ?? null,
|
|
vehicle_3: options.vehicle_3 ?? null,
|
|
activeVehicleIndex: options.activeVehicleIndex ?? 1,
|
|
},
|
|
views: {
|
|
manualInput: options.manualInput ?? false,
|
|
vehicleSelection: options.vehicleSelection ?? false,
|
|
additionalItemSelection: options.additionalItemSelection ?? false,
|
|
transactionHistoryView: options.transactionHistoryView ?? false,
|
|
},
|
|
transactionItems: {
|
|
primaryItem,
|
|
additionalItems: Object.prototype.hasOwnProperty.call(options, "additionalItems")
|
|
? clone(options.additionalItems)
|
|
: [],
|
|
},
|
|
categories: {
|
|
list: clone(options.categoriesList ?? []),
|
|
selected: clone(options.selectedCategory ?? null),
|
|
},
|
|
productList: {
|
|
list: clone(options.productList ?? []),
|
|
},
|
|
metadata: {
|
|
customerId,
|
|
notes: options.notes ?? "",
|
|
reference,
|
|
washId: options.washId ?? null,
|
|
bookingId,
|
|
laneId: options.laneId ?? null,
|
|
},
|
|
attachments: {
|
|
files: Object.prototype.hasOwnProperty.call(options, "attachmentFiles") ? clone(options.attachmentFiles) : [],
|
|
base64: Object.prototype.hasOwnProperty.call(options, "attachmentsBase64")
|
|
? clone(options.attachmentsBase64)
|
|
: [],
|
|
wash_certificate: options.washCertificate ?? false,
|
|
},
|
|
transactionHistory: clone(options.transactionHistory ?? []),
|
|
lastVehicleOrders: clone(
|
|
options.lastVehicleOrders ?? {
|
|
vehicle_1: null,
|
|
vehicle_2: null,
|
|
vehicle_3: null,
|
|
}
|
|
),
|
|
timestamp: options.timestamp ?? Date.now(),
|
|
};
|
|
}
|
|
|
|
export function seedMobilePosState(page, optionsOrSnapshot = {}) {
|
|
const snapshot = optionsOrSnapshot?.vehicles ? clone(optionsOrSnapshot) : buildMobilePosState(optionsOrSnapshot);
|
|
return page.addInitScript((payload) => {
|
|
window.localStorage.setItem("pos", JSON.stringify(payload));
|
|
}, snapshot);
|
|
}
|
|
|
|
export function seedStoredPosOrderId(page, orderId) {
|
|
return page.addInitScript((value) => {
|
|
if (value) {
|
|
window.localStorage.setItem("pos_order_id", String(value));
|
|
return;
|
|
}
|
|
window.localStorage.removeItem("pos_order_id");
|
|
}, orderId);
|
|
}
|
|
|
|
export function getStoredPosSnapshot(page) {
|
|
return page.evaluate(() => {
|
|
const storedValue = window.localStorage.getItem("pos");
|
|
return storedValue ? JSON.parse(storedValue) : null;
|
|
});
|
|
}
|
|
|
|
export function suppressVueDevtoolsOverlay(page) {
|
|
return page.addInitScript(() => {
|
|
const selectors = [
|
|
"#__vue-devtools-container__",
|
|
".vue-devtools__anchor-btn",
|
|
".vue-devtools__panel",
|
|
".vue-devtools__panel-content",
|
|
];
|
|
const hideDevtools = () => {
|
|
const root = document.documentElement;
|
|
if (root && !root.querySelector("style[data-test-hide-vue-devtools='1']")) {
|
|
const style = document.createElement("style");
|
|
style.setAttribute("data-test-hide-vue-devtools", "1");
|
|
style.textContent = `${selectors.join(
|
|
", "
|
|
)} { display: none !important; visibility: hidden !important; pointer-events: none !important; opacity: 0 !important; }`;
|
|
root.appendChild(style);
|
|
}
|
|
selectors.forEach((selector) => {
|
|
document.querySelectorAll(selector).forEach((element) => {
|
|
element.setAttribute("aria-hidden", "true");
|
|
element.style.setProperty("display", "none", "important");
|
|
element.style.setProperty("visibility", "hidden", "important");
|
|
element.style.setProperty("pointer-events", "none", "important");
|
|
element.style.setProperty("opacity", "0", "important");
|
|
});
|
|
});
|
|
};
|
|
hideDevtools();
|
|
document.addEventListener("DOMContentLoaded", hideDevtools);
|
|
new MutationObserver(hideDevtools).observe(document, { childList: true, subtree: true });
|
|
});
|
|
}
|
|
|
|
export function waitForMobileNextStepCooldown(page, timeout = MOBILE_NEXT_STEP_COOLDOWN_MS) {
|
|
return page.waitForTimeout(timeout);
|
|
}
|
|
|
|
export async function primeOperatorSession(page, { token, permissions = MOBILE_PERMISSIONS, sessionData = {} } = {}) {
|
|
await seedAuthenticatedState(page, token);
|
|
await page.goto("/login");
|
|
await page.evaluate(
|
|
async ({ sessionToken, sessionPermissions, data }) => {
|
|
const sessionModule = await import("/src/components/session/token/SessionUser.vue");
|
|
window.localStorage.setItem("token", sessionToken);
|
|
sessionModule.SessionUser.token.value = sessionToken;
|
|
sessionModule.SessionUser.authenticated.value = true;
|
|
sessionModule.SessionUser.permissions.value = sessionPermissions;
|
|
sessionModule.SessionUser.initiated.value = true;
|
|
sessionModule.SessionUser.user.customer_number.value = data.customer_number ?? 12345;
|
|
sessionModule.SessionUser.user.display_name.value = data.display_name ?? "POS Mobile E2E";
|
|
},
|
|
{
|
|
sessionToken: token,
|
|
sessionPermissions: permissions,
|
|
data: sessionData,
|
|
}
|
|
);
|
|
}
|
|
|
|
export function createPaymentIntent(
|
|
orderId,
|
|
{ status = "requires_capture", amount = 123400, readerId = "reader_online_1", taxPercentage = 25 } = {}
|
|
) {
|
|
const isSucceeded = status === "succeeded";
|
|
return {
|
|
id: `pi_${orderId}`,
|
|
amount,
|
|
amount_capturable: isSucceeded ? 0 : amount,
|
|
amount_received: isSucceeded ? amount : 0,
|
|
currency: "dkk",
|
|
status,
|
|
payment_method_types: ["card_present"],
|
|
created: Math.floor(Date.now() / 1000),
|
|
metadata: {
|
|
order_id: String(orderId),
|
|
reader_id: String(readerId),
|
|
reader: String(readerId),
|
|
tax_percentage: String(taxPercentage),
|
|
},
|
|
};
|
|
}
|
|
|
|
export function paymentIntentResponse(paymentIntent, extra = {}) {
|
|
return {
|
|
success: true,
|
|
data: {
|
|
payment_intent: paymentIntent || null,
|
|
has_payment_intent: !!paymentIntent,
|
|
...extra,
|
|
},
|
|
};
|
|
}
|
|
|
|
function extractRequestBody(request) {
|
|
try {
|
|
return request.postDataJSON?.() || {};
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function extractOrderId(request, parsedUrl) {
|
|
const queryId = toPositiveInteger(parsedUrl.searchParams.get("id"));
|
|
if (queryId) {
|
|
return queryId;
|
|
}
|
|
const body = extractRequestBody(request);
|
|
return toPositiveInteger(body.id ?? body.order_id);
|
|
}
|
|
|
|
function removeBookingFromPending(fixture, bookingId) {
|
|
fixture.pendingBookings = fixture.pendingBookings.filter((booking) => Number(booking.id) !== Number(bookingId));
|
|
}
|
|
|
|
function upsertPendingBooking(fixture, booking) {
|
|
const existingIndex = fixture.pendingBookings.findIndex((entry) => Number(entry.id) === Number(booking.id));
|
|
if (booking.order_id) {
|
|
if (existingIndex >= 0) {
|
|
fixture.pendingBookings.splice(existingIndex, 1);
|
|
}
|
|
return;
|
|
}
|
|
if (existingIndex >= 0) {
|
|
fixture.pendingBookings[existingIndex] = clone(booking);
|
|
return;
|
|
}
|
|
fixture.pendingBookings.push(clone(booking));
|
|
}
|
|
|
|
function filterBookings(bookings, filters) {
|
|
if (!filters) {
|
|
return bookings;
|
|
}
|
|
const entries = String(filters)
|
|
.split(",")
|
|
.map((entry) => entry.split(":"))
|
|
.filter(([key, value]) => key && value !== undefined);
|
|
|
|
return bookings.filter((booking) => {
|
|
return entries.every(([key, value]) => {
|
|
if (key === "department") {
|
|
return Number(booking.department) === Number(value);
|
|
}
|
|
if (key === "order_id") {
|
|
if (value === "null") {
|
|
return !booking.order_id;
|
|
}
|
|
return Number(booking.order_id) === Number(value);
|
|
}
|
|
if (key === "reg_1") {
|
|
return String(booking.reg_1 || "").toUpperCase() === String(value || "").toUpperCase();
|
|
}
|
|
if (key === "status") {
|
|
return String(booking.status || "").toLowerCase() === String(value || "").toLowerCase();
|
|
}
|
|
return true;
|
|
});
|
|
});
|
|
}
|
|
|
|
function recordCounter(fixture, key) {
|
|
fixture.requestCounters[key] = Number(fixture.requestCounters[key] || 0) + 1;
|
|
}
|
|
|
|
function recordLog(fixture, key, value) {
|
|
if (!Array.isArray(fixture.requestLog[key])) {
|
|
fixture.requestLog[key] = [];
|
|
}
|
|
fixture.requestLog[key].push(clone(value));
|
|
}
|
|
|
|
export async function mockMobilePosApi(page, fixture) {
|
|
await page.route(API_HOST, async (route) => {
|
|
const request = route.request();
|
|
const parsedUrl = new URL(request.url());
|
|
const pathname = parsedUrl.pathname;
|
|
const method = request.method();
|
|
const body = extractRequestBody(request);
|
|
|
|
if (pathname.endsWith("/modules/stripe/department/terminal/readers") && method === "GET") {
|
|
recordCounter(fixture, "readersGet");
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: {
|
|
data: clone(fixture.readers),
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/module/stripe/payment_intent") && method === "GET") {
|
|
recordCounter(fixture, "paymentIntentGet");
|
|
if (fixture.failureBudget.paymentIntentGet > 0) {
|
|
fixture.failureBudget.paymentIntentGet -= 1;
|
|
await route.fulfill(
|
|
json(
|
|
{
|
|
success: false,
|
|
data: {
|
|
message: "Unable to load payment intent state",
|
|
},
|
|
},
|
|
500
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
const orderId = extractOrderId(request, parsedUrl);
|
|
await route.fulfill(
|
|
json(
|
|
paymentIntentResponse(orderId ? fixture.paymentIntentsByOrderId[orderId] || null : null, {
|
|
message: "No active payment intent for this order.",
|
|
})
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/module/stripe/payment_intent") && method === "POST") {
|
|
recordCounter(fixture, "paymentIntentCreate");
|
|
if (fixture.failureBudget.paymentIntentCreate > 0) {
|
|
fixture.failureBudget.paymentIntentCreate -= 1;
|
|
await route.fulfill(
|
|
json(
|
|
{
|
|
success: false,
|
|
data: {
|
|
message: "Unable to create payment intent",
|
|
},
|
|
},
|
|
500
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
const orderId = toPositiveInteger(body.id);
|
|
const readerId = body.reader || "reader_online_1";
|
|
const existingIntent = fixture.paymentIntentsByOrderId[orderId] || null;
|
|
const nextIntent =
|
|
existingIntent ||
|
|
createPaymentIntent(orderId, {
|
|
status: "requires_capture",
|
|
readerId,
|
|
taxPercentage: Number(body.tax_percentage ?? 25),
|
|
});
|
|
nextIntent.metadata = {
|
|
...(nextIntent.metadata || {}),
|
|
order_id: String(orderId),
|
|
reader_id: String(readerId),
|
|
reader: String(readerId),
|
|
tax_percentage: String(body.tax_percentage ?? nextIntent.metadata?.tax_percentage ?? 25),
|
|
};
|
|
fixture.paymentIntentsByOrderId[orderId] = nextIntent;
|
|
await route.fulfill(
|
|
json(
|
|
paymentIntentResponse(nextIntent, {
|
|
reused: !!existingIntent,
|
|
})
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/module/stripe/payment_intent") && method === "DELETE") {
|
|
recordCounter(fixture, "paymentIntentDelete");
|
|
const orderId = toPositiveInteger(body.id) ?? extractOrderId(request, parsedUrl);
|
|
delete fixture.paymentIntentsByOrderId[orderId];
|
|
await route.fulfill(json(paymentIntentResponse(null, { cleared: true })));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/module/stripe/payment_intent/capture") && method === "POST") {
|
|
recordCounter(fixture, "paymentIntentCapture");
|
|
if (fixture.failureBudget.paymentIntentCapture > 0) {
|
|
fixture.failureBudget.paymentIntentCapture -= 1;
|
|
await route.fulfill(
|
|
json(
|
|
{
|
|
success: false,
|
|
data: {
|
|
message: "Unable to capture payment intent",
|
|
},
|
|
},
|
|
500
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
const orderId = toPositiveInteger(body.id);
|
|
const currentIntent = fixture.paymentIntentsByOrderId[orderId] || createPaymentIntent(orderId);
|
|
const succeededIntent = {
|
|
...currentIntent,
|
|
status: "succeeded",
|
|
amount_capturable: 0,
|
|
amount_received: Number(currentIntent.amount || 0),
|
|
};
|
|
fixture.paymentIntentsByOrderId[orderId] = succeededIntent;
|
|
await route.fulfill(json(paymentIntentResponse(succeededIntent)));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/departments") && method === "GET") {
|
|
recordCounter(fixture, "departmentsGet");
|
|
await route.fulfill(json({ success: true, data: clone(fixture.departments) }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/departments/categories") && method === "GET") {
|
|
recordCounter(fixture, "departmentCategoriesGet");
|
|
const departmentId = toPositiveInteger(parsedUrl.searchParams.get("id"));
|
|
const categories = departmentId
|
|
? fixture.departmentCategories.filter((entry) => Number(entry.department_id) === Number(departmentId))
|
|
: fixture.departmentCategories;
|
|
await route.fulfill(json({ success: true, data: clone(categories) }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/departments/order/recommended") && method === "GET") {
|
|
recordCounter(fixture, "recommendedGet");
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: {
|
|
reg_1: {
|
|
motorapi: [53],
|
|
order_history: { 2: [], 3: [], 4: [], 5: [] },
|
|
},
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/bookings") && method === "GET") {
|
|
recordCounter(fixture, "bookingsGet");
|
|
const bookingId = toPositiveInteger(parsedUrl.searchParams.get("id"));
|
|
if (bookingId) {
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: clone(fixture.bookingsById[bookingId] || null),
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
const bookings = filterBookings(Object.values(fixture.bookingsById), parsedUrl.searchParams.get("filters"));
|
|
await route.fulfill(json({ success: true, data: clone(bookings) }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/order-bookings/complete") && method === "POST") {
|
|
recordCounter(fixture, "bookingComplete");
|
|
recordLog(fixture, "bookingCompletions", body);
|
|
if (fixture.failureBudget.bookingComplete > 0) {
|
|
fixture.failureBudget.bookingComplete -= 1;
|
|
await route.fulfill(
|
|
json(
|
|
{
|
|
success: false,
|
|
data: {
|
|
message: "Unable to complete booking",
|
|
},
|
|
},
|
|
500
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
const bookingId = toPositiveInteger(body.id);
|
|
const booking = fixture.bookingsById[bookingId];
|
|
if (booking) {
|
|
booking.status = "completed";
|
|
booking.safety_seal = body.safety_seal ? String(body.safety_seal) : "";
|
|
fixture.bookingStatusById[bookingId] = "completed";
|
|
removeBookingFromPending(fixture, bookingId);
|
|
}
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: clone(booking || { id: bookingId, status: "completed" }),
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/order-bookings") && method === "GET") {
|
|
recordCounter(fixture, "orderBookingsGet");
|
|
const bookingId = toPositiveInteger(parsedUrl.searchParams.get("id"));
|
|
if (bookingId) {
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: clone(fixture.bookingsById[bookingId] || null),
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
const bookings = filterBookings(
|
|
fixture.pendingBookings ?? Object.values(fixture.bookingsById),
|
|
parsedUrl.searchParams.get("filters")
|
|
);
|
|
await route.fulfill(json({ success: true, data: clone(bookings) }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/order-bookings") && method === "PUT") {
|
|
recordCounter(fixture, "bookingSetOrderId");
|
|
recordLog(fixture, "bookingOrderAssignments", body);
|
|
const bookingId = toPositiveInteger(body.id);
|
|
const booking = fixture.bookingsById[bookingId];
|
|
if (booking) {
|
|
booking.order_id = body.order_id ?? body.value ?? null;
|
|
upsertPendingBooking(fixture, booking);
|
|
}
|
|
await route.fulfill(json({ success: true, data: clone(booking || null) }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/vehicles/search") && method === "GET") {
|
|
recordCounter(fixture, "vehiclesSearchGet");
|
|
const search = String(parsedUrl.searchParams.get("search") || "").toUpperCase();
|
|
const vehicles = fixture.vehicles.filter(
|
|
(vehicle) =>
|
|
!search ||
|
|
String(vehicle.reg || "")
|
|
.toUpperCase()
|
|
.includes(search)
|
|
);
|
|
await route.fulfill(json({ success: true, data: clone(vehicles) }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/vehicles") && method === "GET") {
|
|
recordCounter(fixture, "vehiclesGet");
|
|
const search = String(parsedUrl.searchParams.get("search") || "").toUpperCase();
|
|
const vehicles = fixture.vehicles.filter(
|
|
(vehicle) =>
|
|
!search ||
|
|
String(vehicle.reg || "")
|
|
.toUpperCase()
|
|
.includes(search)
|
|
);
|
|
await route.fulfill(json({ success: true, data: clone(vehicles) }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/department/vehicles/unknown-customer") && method === "GET") {
|
|
recordCounter(fixture, "unknownVehiclesGet");
|
|
const search = String(parsedUrl.searchParams.get("search") || "").toUpperCase();
|
|
const vehicles = fixture.unknownVehicles.filter(
|
|
(vehicle) =>
|
|
!search ||
|
|
String(vehicle.reg_1 || "")
|
|
.toUpperCase()
|
|
.includes(search)
|
|
);
|
|
await route.fulfill(json({ success: true, data: clone(vehicles) }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/users/customer") && method === "GET") {
|
|
recordCounter(fixture, "usersCustomerGet");
|
|
const customerNumber =
|
|
toPositiveInteger(parsedUrl.searchParams.get("customer_number")) ?? fixture.regularCustomerId;
|
|
const customer = fixture.customersByNumber[customerNumber] || null;
|
|
if (!customer) {
|
|
await route.fulfill(
|
|
json(
|
|
{
|
|
success: false,
|
|
data: {
|
|
message: "Customer not found",
|
|
},
|
|
},
|
|
404
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: {
|
|
customer_name: customer.name,
|
|
economic_customer: clone(customer),
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/customers") && method === "GET") {
|
|
recordCounter(fixture, "customersGet");
|
|
const search = String(parsedUrl.searchParams.get("search") || "").toLowerCase();
|
|
const customers = Object.values(fixture.customersByNumber).filter((customer) => {
|
|
if (!search) {
|
|
return true;
|
|
}
|
|
return (
|
|
String(customer.customerNumber).includes(search) ||
|
|
String(customer.name || "")
|
|
.toLowerCase()
|
|
.includes(search)
|
|
);
|
|
});
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: clone(customers),
|
|
meta: {
|
|
pagination: {
|
|
page: 1,
|
|
limit: 10,
|
|
total: customers.length,
|
|
},
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/customer/notes") && method === "GET") {
|
|
recordCounter(fixture, "customerNotesGet");
|
|
const customerNumber =
|
|
toPositiveInteger(parsedUrl.searchParams.get("customer_number")) ??
|
|
toPositiveInteger(parsedUrl.searchParams.get("id")) ??
|
|
fixture.regularCustomerId;
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: clone(fixture.customerNotesByNumber[customerNumber] || []),
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/public/employees") && method === "GET") {
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: clone(fixture.employees || []),
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/customer/attributes") && method === "GET") {
|
|
recordCounter(fixture, "customerAttributesGet");
|
|
const customerNumber =
|
|
toPositiveInteger(parsedUrl.searchParams.get("customer_number")) ??
|
|
toPositiveInteger(parsedUrl.searchParams.get("id")) ??
|
|
fixture.regularCustomerId;
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: clone(fixture.customerAttributesByNumber[customerNumber] || []),
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/superuser/user/discounts") && method === "GET") {
|
|
recordCounter(fixture, "discountsGet");
|
|
await route.fulfill(json({ success: true, data: [] }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/products") && method === "GET") {
|
|
recordCounter(fixture, "productsGet");
|
|
const productId = toPositiveInteger(parsedUrl.searchParams.get("id"));
|
|
const category = toPositiveInteger(parsedUrl.searchParams.get("category"));
|
|
const isWash = parsedUrl.searchParams.get("is_wash");
|
|
const limit = Number(parsedUrl.searchParams.get("limit") || 0);
|
|
|
|
if (productId) {
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: clone(getProductById(fixture, productId)),
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
let products = fixture.products.slice();
|
|
if (category) {
|
|
products = products.filter((product) => Number(product.category) === Number(category));
|
|
}
|
|
if (isWash !== null && isWash !== "") {
|
|
const expected = String(isWash) === "true";
|
|
products = products.filter((product) => Boolean(product.is_wash) === expected);
|
|
}
|
|
if (Number.isFinite(limit) && limit > 0) {
|
|
products = products.slice(0, limit);
|
|
}
|
|
await route.fulfill(json({ success: true, data: clone(products) }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders") && method === "GET") {
|
|
recordCounter(fixture, "ordersGet");
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: clone(Object.values(fixture.ordersById)),
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders") && method === "POST") {
|
|
recordCounter(fixture, "orderCreate");
|
|
recordLog(fixture, "orderCreates", body);
|
|
if (fixture.failureBudget.orderCreate > 0) {
|
|
fixture.failureBudget.orderCreate -= 1;
|
|
await route.fulfill(
|
|
json(
|
|
{
|
|
success: false,
|
|
data: {
|
|
message: "Unable to create order",
|
|
},
|
|
},
|
|
500
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
const orderId = fixture.nextOrderId++;
|
|
fixture.ordersById[orderId] = createOrderRecord(orderId, {
|
|
customer_id: Number(body.customer_id),
|
|
department_id: Number(body.department_id),
|
|
reference: body.reference || "",
|
|
notes: body.notes || "",
|
|
reg_1: body.reg_1 || "",
|
|
reg_2: body.reg_2 || "",
|
|
reg_3: body.reg_3 || "",
|
|
created_at: new Date().toISOString(),
|
|
});
|
|
fixture.orderItemsByOrderId[orderId] = [];
|
|
fixture.attachmentsByOrderId[orderId] = [];
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: {
|
|
id: orderId,
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if ((pathname.endsWith("/orders") || pathname.endsWith("/order")) && method === "PUT") {
|
|
recordCounter(fixture, "orderUpdate");
|
|
recordLog(fixture, "orderUpdates", body);
|
|
const orderId = toPositiveInteger(body.id);
|
|
const order = fixture.ordersById[orderId];
|
|
if (order) {
|
|
if (body.field) {
|
|
order[body.field] = body.value;
|
|
} else {
|
|
Object.assign(order, body);
|
|
}
|
|
}
|
|
await route.fulfill(json({ success: true, data: clone(order || null) }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders") && method === "DELETE") {
|
|
recordCounter(fixture, "orderDelete");
|
|
const orderId = toPositiveInteger(parsedUrl.searchParams.get("id")) ?? toPositiveInteger(body.id);
|
|
recordLog(fixture, "orderDeletes", { id: orderId });
|
|
if (fixture.failureBudget.orderDelete > 0) {
|
|
fixture.failureBudget.orderDelete -= 1;
|
|
await route.fulfill(
|
|
json(
|
|
{
|
|
success: false,
|
|
data: {
|
|
message: "Unable to delete order",
|
|
},
|
|
},
|
|
500
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
if (orderId) {
|
|
delete fixture.ordersById[orderId];
|
|
delete fixture.orderItemsByOrderId[orderId];
|
|
delete fixture.attachmentsByOrderId[orderId];
|
|
delete fixture.paymentIntentsByOrderId[orderId];
|
|
fixture.deletedOrderIds.push(orderId);
|
|
}
|
|
await route.fulfill(json({ success: true, data: true }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/order") && method === "GET") {
|
|
recordCounter(fixture, "orderGet");
|
|
const orderId = toPositiveInteger(parsedUrl.searchParams.get("id"));
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: clone(fixture.ordersById[orderId] || null),
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/order/items") && method === "GET") {
|
|
recordCounter(fixture, "orderItemsGet");
|
|
const orderId = toPositiveInteger(parsedUrl.searchParams.get("order_id"));
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: clone(fixture.orderItemsByOrderId[orderId] || []),
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/order/items") && method === "POST") {
|
|
recordCounter(fixture, "orderItemsPost");
|
|
recordLog(fixture, "orderItemCreates", body);
|
|
const orderId = toPositiveInteger(body.order_id);
|
|
const productId = toPositiveInteger(body.product_id);
|
|
const order = fixture.ordersById[orderId];
|
|
const product = getProductById(fixture, productId);
|
|
if (!order || !product) {
|
|
await route.fulfill(
|
|
json(
|
|
{
|
|
success: false,
|
|
data: {
|
|
message: "Order or product not found",
|
|
},
|
|
},
|
|
422
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
const orderItemId = fixture.nextOrderItemId++;
|
|
const item = buildOrderItem(
|
|
product,
|
|
{
|
|
...body,
|
|
order_id: orderId,
|
|
},
|
|
orderItemId
|
|
);
|
|
if (!Array.isArray(fixture.orderItemsByOrderId[orderId])) {
|
|
fixture.orderItemsByOrderId[orderId] = [];
|
|
}
|
|
fixture.orderItemsByOrderId[orderId].push(item);
|
|
await route.fulfill(json({ success: true, data: clone(item) }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/order/items") && method === "PUT") {
|
|
recordCounter(fixture, "orderItemsPut");
|
|
recordLog(fixture, "orderItemUpdates", body);
|
|
const orderItemId = toPositiveInteger(body.id);
|
|
Object.keys(fixture.orderItemsByOrderId).forEach((orderIdKey) => {
|
|
fixture.orderItemsByOrderId[orderIdKey] = (fixture.orderItemsByOrderId[orderIdKey] || []).map((item) => {
|
|
if (Number(item.id) !== orderItemId) {
|
|
return item;
|
|
}
|
|
return {
|
|
...item,
|
|
price: Number(body.price ?? item.price),
|
|
notes: body.notes ?? item.notes,
|
|
reference: body.reference ?? item.reference,
|
|
quantity: Number(body.quantity ?? item.quantity),
|
|
};
|
|
});
|
|
});
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: {
|
|
id: orderItemId,
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/order/items") && method === "DELETE") {
|
|
recordCounter(fixture, "orderItemsDelete");
|
|
const orderItemId = toPositiveInteger(parsedUrl.searchParams.get("id")) ?? toPositiveInteger(body.id);
|
|
recordLog(fixture, "orderItemDeletes", { id: orderItemId });
|
|
Object.keys(fixture.orderItemsByOrderId).forEach((orderIdKey) => {
|
|
fixture.orderItemsByOrderId[orderIdKey] = (fixture.orderItemsByOrderId[orderIdKey] || []).filter(
|
|
(item) => Number(item.id) !== orderItemId
|
|
);
|
|
});
|
|
await route.fulfill(json({ success: true, data: true }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/mark_as_completed") && method === "POST") {
|
|
recordCounter(fixture, "markAsCompleted");
|
|
const orderId = toPositiveInteger(body.id);
|
|
if (orderId) {
|
|
fixture.markCompletedOrderIds.push(orderId);
|
|
if (fixture.ordersById[orderId]) {
|
|
fixture.ordersById[orderId].completed_at = new Date().toISOString();
|
|
}
|
|
}
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: {
|
|
id: orderId,
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/attachments") && method === "GET") {
|
|
recordCounter(fixture, "attachmentsGet");
|
|
const orderId =
|
|
toPositiveInteger(parsedUrl.searchParams.get("id")) ??
|
|
toPositiveInteger(parsedUrl.searchParams.get("order_id"));
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: clone(fixture.attachmentsByOrderId[orderId] || []),
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/attachments") && method === "DELETE") {
|
|
recordCounter(fixture, "attachmentsDelete");
|
|
const orderId = toPositiveInteger(body.order_id) ?? toPositiveInteger(parsedUrl.searchParams.get("order_id"));
|
|
const attachmentId =
|
|
toPositiveInteger(body.attachment_id) ?? toPositiveInteger(parsedUrl.searchParams.get("attachment_id"));
|
|
recordLog(fixture, "attachmentDeletes", { order_id: orderId, attachment_id: attachmentId });
|
|
fixture.attachmentsByOrderId[orderId] = (fixture.attachmentsByOrderId[orderId] || []).filter(
|
|
(attachment) => Number(attachment.id) !== attachmentId
|
|
);
|
|
await route.fulfill(json({ success: true, data: true }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/attachments/upload") && method === "POST") {
|
|
recordCounter(fixture, "attachmentUpload");
|
|
recordLog(fixture, "attachmentUploads", body);
|
|
if (fixture.failureBudget.attachmentUpload > 0) {
|
|
fixture.failureBudget.attachmentUpload -= 1;
|
|
await route.fulfill(
|
|
json(
|
|
{
|
|
success: false,
|
|
data: {
|
|
message: "Unable to upload attachment",
|
|
},
|
|
},
|
|
500
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
const orderId = toPositiveInteger(body.order_id);
|
|
const attachment = {
|
|
id: fixture.nextAttachmentId++,
|
|
content: {
|
|
other: body.file_name || `upload-${fixture.nextAttachmentId}.png`,
|
|
},
|
|
};
|
|
if (!Array.isArray(fixture.attachmentsByOrderId[orderId])) {
|
|
fixture.attachmentsByOrderId[orderId] = [];
|
|
}
|
|
fixture.attachmentsByOrderId[orderId].push(attachment);
|
|
await route.fulfill(json({ success: true, data: clone(attachment) }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/orders/attachments/download") && method === "GET") {
|
|
recordCounter(fixture, "attachmentDownload");
|
|
const orderId = toPositiveInteger(parsedUrl.searchParams.get("order_id"));
|
|
const attachmentId = toPositiveInteger(parsedUrl.searchParams.get("attachment_id"));
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: {
|
|
download_link: `${ATTACHMENT_DOWNLOAD_URL}/${orderId}/${attachmentId}`,
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/vehicle/product-suggestions") && method === "GET") {
|
|
await route.fulfill(json({ success: true, data: [] }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/modules/scanner/lpr") && method === "POST") {
|
|
recordCounter(fixture, "lprPost");
|
|
await route.fulfill(
|
|
json({
|
|
success: false,
|
|
data: {
|
|
message: "License plate extraction failed",
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
await route.fallback();
|
|
});
|
|
}
|
|
|
|
export async function gotoMobilePos(
|
|
page,
|
|
{ step = 1, orderId = null, customerId = null, departmentId = DEFAULT_DEPARTMENT_ID } = {}
|
|
) {
|
|
const params = new URLSearchParams();
|
|
if (orderId !== null && orderId !== undefined) {
|
|
params.set("id", String(orderId));
|
|
}
|
|
if (customerId !== null && customerId !== undefined) {
|
|
params.set("customer_id", String(customerId));
|
|
}
|
|
if (step !== null && step !== undefined) {
|
|
params.set("step", String(step));
|
|
}
|
|
const query = params.toString();
|
|
await page.goto(`/admin/${departmentId}/modules/pos${query ? `?${query}` : ""}`);
|
|
}
|
|
|
|
export async function setupMobilePosPage(
|
|
page,
|
|
fixture,
|
|
{
|
|
token = "pos-mobile-token",
|
|
permissions = MOBILE_PERMISSIONS,
|
|
seedState = {},
|
|
route = {},
|
|
sessionData = {},
|
|
storedOrderId = null,
|
|
} = {}
|
|
) {
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions,
|
|
});
|
|
await mockMobilePosApi(page, fixture);
|
|
if (seedState !== false) {
|
|
await seedMobilePosState(page, {
|
|
fixture,
|
|
customerId: null,
|
|
includePrimaryItem: false,
|
|
...seedState,
|
|
});
|
|
}
|
|
if (storedOrderId) {
|
|
await seedStoredPosOrderId(page, storedOrderId);
|
|
}
|
|
await primeOperatorSession(page, {
|
|
token,
|
|
permissions,
|
|
sessionData: {
|
|
customer_number: 555555,
|
|
display_name: "POS Mobile E2E",
|
|
...sessionData,
|
|
},
|
|
});
|
|
await gotoMobilePos(page, {
|
|
departmentId: fixture.departmentId ?? DEFAULT_DEPARTMENT_ID,
|
|
...route,
|
|
});
|
|
await page
|
|
.evaluate(() => {
|
|
const selectors = [
|
|
"#__vue-devtools-container__",
|
|
".vue-devtools__anchor-btn",
|
|
".vue-devtools__panel",
|
|
".vue-devtools__panel-content",
|
|
];
|
|
selectors.forEach((selector) => {
|
|
document.querySelectorAll(selector).forEach((element) => {
|
|
element.setAttribute("aria-hidden", "true");
|
|
element.style.setProperty("display", "none", "important");
|
|
element.style.setProperty("visibility", "hidden", "important");
|
|
element.style.setProperty("pointer-events", "none", "important");
|
|
element.style.setProperty("opacity", "0", "important");
|
|
});
|
|
});
|
|
})
|
|
.catch(() => {});
|
|
}
|