2153 lines
66 KiB
JavaScript
2153 lines
66 KiB
JavaScript
import { mockApi, primeMockSession } 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 WASH_CERTIFICATE_PRODUCT_ID = 41;
|
|
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 getByActionKey(scope, actionKey) {
|
|
return scope.locator(`[data-action-key="${actionKey}"]`);
|
|
}
|
|
|
|
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 normalizeRegistrationValue(value) {
|
|
if (value === null || value === undefined) {
|
|
return "";
|
|
}
|
|
|
|
return String(value)
|
|
.trim()
|
|
.toUpperCase()
|
|
.replace(/[^A-Z0-9]/g, "");
|
|
}
|
|
|
|
function normalizeSafetySealValue(value) {
|
|
if (value === null || value === undefined) {
|
|
return "";
|
|
}
|
|
|
|
return String(value).trim();
|
|
}
|
|
|
|
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: "",
|
|
safety_seal: "",
|
|
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,
|
|
stripeInvoiceSend: 0,
|
|
stripeInvoiceDelete: 0,
|
|
departmentsGet: 0,
|
|
departmentCategoriesGet: 0,
|
|
bookingsGet: 0,
|
|
orderBookingsGet: 0,
|
|
bookingComplete: 0,
|
|
bookingSetOrderId: 0,
|
|
vehiclesSearchGet: 0,
|
|
vehiclesGet: 0,
|
|
unknownVehiclesGet: 0,
|
|
vehicleCustomerSuggestionsGet: 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,
|
|
cvrSearchGet: 0,
|
|
customerRegistrationPost: 0,
|
|
lprPost: 0,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function createRequestLog(overrides = {}) {
|
|
return {
|
|
orderCreates: [],
|
|
orderUpdates: [],
|
|
orderDeletes: [],
|
|
orderItemCreates: [],
|
|
orderItemUpdates: [],
|
|
orderItemDeletes: [],
|
|
bookingCompletions: [],
|
|
bookingOrderAssignments: [],
|
|
attachmentUploads: [],
|
|
attachmentDeletes: [],
|
|
stripeInvoiceSends: [],
|
|
stripeInvoiceDeletes: [],
|
|
cvrSearches: [],
|
|
customerRegistrations: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function isWashCertificateProduct(product) {
|
|
const productId = Number(product?.id ?? product?.product_id ?? product?.product?.id ?? 0);
|
|
if (productId === WASH_CERTIFICATE_PRODUCT_ID) {
|
|
return true;
|
|
}
|
|
|
|
return /vaskecertifikat|wash certificate|safety seal/i.test(String(product?.name ?? product?.product?.name ?? ""));
|
|
}
|
|
|
|
function orderContainsWashCertificate(fixture, orderId) {
|
|
return (fixture.orderItemsByOrderId[orderId] || []).some((item) => isWashCertificateProduct(item?.product || item));
|
|
}
|
|
|
|
function ensureWashCertificateAttachment(fixture, orderId) {
|
|
if (!Array.isArray(fixture.attachmentsByOrderId[orderId])) {
|
|
fixture.attachmentsByOrderId[orderId] = [];
|
|
}
|
|
|
|
const existingAttachment =
|
|
(fixture.attachmentsByOrderId[orderId] || []).find((attachment) => {
|
|
return String(attachment?.content?.other || "").toUpperCase() === "WASH_CERTIFICATE";
|
|
}) || null;
|
|
|
|
if (existingAttachment) {
|
|
return existingAttachment;
|
|
}
|
|
|
|
const attachmentId = fixture.nextAttachmentId++;
|
|
const attachment = {
|
|
id: attachmentId,
|
|
object_type: "orders",
|
|
object_id: orderId,
|
|
content: {
|
|
image: null,
|
|
document: `wash_certificate_${orderId}.pdf`,
|
|
relation: null,
|
|
other: "WASH_CERTIFICATE",
|
|
src: null,
|
|
},
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
deleted_at: null,
|
|
};
|
|
fixture.attachmentsByOrderId[orderId].push(attachment);
|
|
return attachment;
|
|
}
|
|
|
|
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 },
|
|
],
|
|
});
|
|
const defaultCvrSearchResponses = {
|
|
41004355: {
|
|
data: {
|
|
vat: 41004355,
|
|
status: "Normal",
|
|
name: "Truckwash ApS",
|
|
address: "Letland Alle 2",
|
|
zipcode: 2630,
|
|
city: "Taastrup",
|
|
phone: "21754690",
|
|
email: "mikkel@truckwash.dk",
|
|
},
|
|
},
|
|
43423010: {
|
|
data: {
|
|
vat: 43423010,
|
|
status: "Normal",
|
|
name: "Wash Group ApS",
|
|
address: "Nordhavn 4",
|
|
zipcode: 2100,
|
|
city: "Kobenhavn O",
|
|
phone: "42331128",
|
|
email: "billing@wash-group.test",
|
|
},
|
|
},
|
|
};
|
|
|
|
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]: [],
|
|
},
|
|
stripeModuleOrdersByOrderId: {
|
|
[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,
|
|
},
|
|
cvrSearchResponses: defaultCvrSearchResponses,
|
|
vehicleCustomerSuggestionsByReg: {},
|
|
customerRegistrationResponse: {
|
|
status: 200,
|
|
data: {
|
|
success: true,
|
|
},
|
|
},
|
|
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.vehicleCustomerSuggestionsByReg = mergeObjectMaps({}, fixture.vehicleCustomerSuggestionsByReg);
|
|
fixture.ordersById = mergeObjectMaps({}, fixture.ordersById);
|
|
fixture.orderItemsByOrderId = mergeObjectMaps({}, fixture.orderItemsByOrderId);
|
|
fixture.attachmentsByOrderId = mergeObjectMaps({}, fixture.attachmentsByOrderId);
|
|
fixture.stripeModuleOrdersByOrderId = mergeObjectMaps({}, fixture.stripeModuleOrdersByOrderId);
|
|
fixture.paymentIntentsByOrderId = mergeObjectMaps({}, fixture.paymentIntentsByOrderId);
|
|
fixture.bookingsById = mergeObjectMaps({}, fixture.bookingsById);
|
|
fixture.cvrSearchResponses = mergeObjectMaps({}, fixture.cvrSearchResponses);
|
|
fixture.customerRegistrationResponse = clone(
|
|
fixture.customerRegistrationResponse || { status: 200, data: { success: true } }
|
|
);
|
|
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),
|
|
vehicleCustomerSuggestionsByReg: mergeObjectMaps(
|
|
base.vehicleCustomerSuggestionsByReg,
|
|
overrides.vehicleCustomerSuggestionsByReg
|
|
),
|
|
ordersById: mergeObjectMaps(base.ordersById, overrides.ordersById),
|
|
orderItemsByOrderId: mergeObjectMaps(base.orderItemsByOrderId, overrides.orderItemsByOrderId),
|
|
attachmentsByOrderId: mergeObjectMaps(base.attachmentsByOrderId, overrides.attachmentsByOrderId),
|
|
stripeModuleOrdersByOrderId: mergeObjectMaps(
|
|
base.stripeModuleOrdersByOrderId,
|
|
overrides.stripeModuleOrdersByOrderId
|
|
),
|
|
paymentIntentsByOrderId: mergeObjectMaps(base.paymentIntentsByOrderId, overrides.paymentIntentsByOrderId),
|
|
bookingsById: mergeObjectMaps(base.bookingsById, overrides.bookingsById),
|
|
cvrSearchResponses: mergeObjectMaps(base.cvrSearchResponses, overrides.cvrSearchResponses),
|
|
customerRegistrationResponse: clone(overrides.customerRegistrationResponse ?? base.customerRegistrationResponse),
|
|
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,
|
|
booking_matches: Object.prototype.hasOwnProperty.call(options, "bookingMatches")
|
|
? clone(options.bookingMatches)
|
|
: [],
|
|
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,
|
|
safetySeal: options.safetySeal ?? "",
|
|
washId: options.washId ?? null,
|
|
bookingId,
|
|
bookingSelectionSkippedPlate: options.bookingSelectionSkippedPlate ?? "",
|
|
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 primeMockSession(page, { token });
|
|
}
|
|
|
|
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),
|
|
},
|
|
};
|
|
}
|
|
|
|
function getStripeInvoiceAmount(fixture, orderId) {
|
|
return (fixture.orderItemsByOrderId[orderId] || []).reduce((sum, item) => {
|
|
return sum + Number(item?.price || 0) * Number(item?.quantity || 1);
|
|
}, 0);
|
|
}
|
|
|
|
function isStripeInvoiceTerminalStatus(status) {
|
|
return ["paid", "void", "uncollectible", "deleted"].includes(String(status || ""));
|
|
}
|
|
|
|
export function createStripeModuleOrder(
|
|
orderId,
|
|
{
|
|
customerId = CARD_CUSTOMER_ID,
|
|
invoiceId = `in_${orderId}`,
|
|
status = "open",
|
|
paid = false,
|
|
amountDue = 0,
|
|
amountPaid = 0,
|
|
url = null,
|
|
} = {}
|
|
) {
|
|
return {
|
|
id: orderId,
|
|
invoice_id: invoiceId,
|
|
customer_id: customerId,
|
|
url: url || `https://stripe.example.test/invoices/${invoiceId}`,
|
|
created_at: new Date().toISOString(),
|
|
paid,
|
|
status,
|
|
amount_due: amountDue,
|
|
amount_paid: amountPaid,
|
|
};
|
|
}
|
|
|
|
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" || value === "is 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 toOrderBookingListEntry(booking, stripDetails = false) {
|
|
if (!stripDetails || !booking || typeof booking !== "object") {
|
|
return booking;
|
|
}
|
|
|
|
const summaryBooking = { ...booking };
|
|
delete summaryBooking.items;
|
|
delete summaryBooking.parsed_services;
|
|
return summaryBooking;
|
|
}
|
|
|
|
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("/modules/stripe/invoice") && method === "POST") {
|
|
recordCounter(fixture, "stripeInvoiceSend");
|
|
recordLog(fixture, "stripeInvoiceSends", body);
|
|
const orderId = toPositiveInteger(body.order_id);
|
|
const existingInvoice = fixture.stripeModuleOrdersByOrderId[orderId] || {};
|
|
if (existingInvoice?.invoice_id && !isStripeInvoiceTerminalStatus(existingInvoice.status)) {
|
|
await route.fulfill(
|
|
json(
|
|
{
|
|
success: false,
|
|
data: {
|
|
message: "A Stripe payment link is already active for this order.",
|
|
code: "stripe_invoice_exists",
|
|
stripeModuleOrders: clone(existingInvoice),
|
|
},
|
|
},
|
|
409
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
const amountDue = getStripeInvoiceAmount(fixture, orderId);
|
|
const nextInvoice = createStripeModuleOrder(orderId, {
|
|
customerId: fixture.ordersById?.[orderId]?.customer_id ?? CARD_CUSTOMER_ID,
|
|
invoiceId: `in_${orderId}_${fixture.requestCounters.stripeInvoiceSend}`,
|
|
amountDue,
|
|
});
|
|
fixture.stripeModuleOrdersByOrderId[orderId] = nextInvoice;
|
|
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: {
|
|
id: nextInvoice.invoice_id,
|
|
customer: nextInvoice.customer_id,
|
|
hosted_invoice_url: nextInvoice.url,
|
|
paid: nextInvoice.paid,
|
|
status: nextInvoice.status,
|
|
amount_due: nextInvoice.amount_due,
|
|
amount_paid: nextInvoice.amount_paid,
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/modules/stripe/invoice") && method === "DELETE") {
|
|
recordCounter(fixture, "stripeInvoiceDelete");
|
|
recordLog(fixture, "stripeInvoiceDeletes", body);
|
|
const orderId = toPositiveInteger(body.order_id);
|
|
const existingInvoice = fixture.stripeModuleOrdersByOrderId[orderId] || {};
|
|
if (!existingInvoice?.invoice_id) {
|
|
fixture.stripeModuleOrdersByOrderId[orderId] = {};
|
|
await route.fulfill(json({ success: true, data: { stripeModuleOrders: [] } }));
|
|
return;
|
|
}
|
|
|
|
if (Boolean(existingInvoice.paid) || String(existingInvoice.status || "") === "paid") {
|
|
await route.fulfill(
|
|
json(
|
|
{
|
|
success: false,
|
|
data: {
|
|
message: "A paid Stripe payment link cannot be cancelled.",
|
|
code: "stripe_invoice_paid",
|
|
stripeModuleOrders: clone(existingInvoice),
|
|
},
|
|
},
|
|
409
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
fixture.stripeModuleOrdersByOrderId[orderId] = {};
|
|
await route.fulfill(json({ success: true, data: { stripeModuleOrders: [] } }));
|
|
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;
|
|
const delayMs = Number(fixture.departmentCategoriesDelayMs ?? 0);
|
|
if (delayMs > 0) {
|
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
}
|
|
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.map((booking) => toOrderBookingListEntry(booking, fixture.orderBookingListStripsDetails === true))
|
|
),
|
|
})
|
|
);
|
|
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("/department/vehicle/customer-suggestions") && method === "GET") {
|
|
recordCounter(fixture, "vehicleCustomerSuggestionsGet");
|
|
const reg = normalizeRegistrationValue(parsedUrl.searchParams.get("reg_1"));
|
|
const suggestions = fixture.vehicleCustomerSuggestionsByReg[reg] || [];
|
|
await route.fulfill(json({ success: true, data: clone(suggestions) }));
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/cvr/search") && method === "GET") {
|
|
recordCounter(fixture, "cvrSearchGet");
|
|
const query = String(parsedUrl.searchParams.get("query") || "").trim();
|
|
recordLog(fixture, "cvrSearches", { query });
|
|
|
|
const responseConfig = fixture.cvrSearchResponses?.[query] || null;
|
|
const delayMs = Number(responseConfig?.delayMs ?? 0);
|
|
const status = Number(responseConfig?.status ?? 200);
|
|
|
|
if (delayMs > 0) {
|
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
}
|
|
|
|
if (status >= 400) {
|
|
await route.fulfill(
|
|
json(
|
|
{
|
|
success: false,
|
|
data: {
|
|
message: responseConfig?.message || "Unable to load CVR details",
|
|
},
|
|
},
|
|
status
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: clone(responseConfig?.data ?? null),
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (pathname.endsWith("/auth/register/cvr") && method === "POST") {
|
|
recordCounter(fixture, "customerRegistrationPost");
|
|
recordLog(fixture, "customerRegistrations", body);
|
|
|
|
const responseConfig = fixture.customerRegistrationResponse || {};
|
|
const status = Number(responseConfig.status ?? 200);
|
|
|
|
if (status >= 400) {
|
|
await route.fulfill(
|
|
json(
|
|
{
|
|
success: false,
|
|
data: {
|
|
message: responseConfig.message || "Unable to register customer",
|
|
},
|
|
},
|
|
status
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
const customerNumber = toPositiveInteger(body.companyPhone);
|
|
if (customerNumber) {
|
|
fixture.customersByNumber[customerNumber] = createCustomer(customerNumber, {
|
|
name: body?.searchResult?.name || `Customer ${customerNumber}`,
|
|
address: body?.searchResult?.address || "Demo Street 1",
|
|
zip: String(body?.searchResult?.zipcode || "2630"),
|
|
city: body?.searchResult?.city || "Taastrup",
|
|
mobilePhone: String(body.contactPhone || body.companyPhone || ""),
|
|
email: body.contactEmail || body.invoiceEmail || "",
|
|
corporateIdentificationNumber: String(body.cvr || "").padStart(8, "0"),
|
|
economic_customer: customerNumber,
|
|
});
|
|
fixture.customerAttributesByNumber[customerNumber] = fixture.customerAttributesByNumber[customerNumber] || [];
|
|
fixture.customerNotesByNumber[customerNumber] = fixture.customerNotesByNumber[customerNumber] || [];
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: clone(responseConfig.data ?? { success: true }),
|
|
})
|
|
);
|
|
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);
|
|
const delayMs = Number(
|
|
(category ? fixture.productsDelayMsByCategory?.[category] : null) ?? fixture.productsDelayMs ?? 0
|
|
);
|
|
|
|
if (productId) {
|
|
if (delayMs > 0) {
|
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
}
|
|
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);
|
|
}
|
|
if (delayMs > 0) {
|
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
}
|
|
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 || "",
|
|
po: body.po || "",
|
|
safety_seal: normalizeSafetySealValue(body.safety_seal),
|
|
reg_1: body.reg_1 || "",
|
|
reg_2: body.reg_2 || "",
|
|
reg_3: body.reg_3 || "",
|
|
booking_id: toPositiveInteger(body.booking_id),
|
|
created_at: new Date().toISOString(),
|
|
});
|
|
fixture.orderItemsByOrderId[orderId] = [];
|
|
fixture.attachmentsByOrderId[orderId] = [];
|
|
fixture.stripeModuleOrdersByOrderId[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.field === "reg_1" || body.field === "reg_2" || body.field === "reg_3"
|
|
? normalizeRegistrationValue(body.value)
|
|
: body.field === "safety_seal"
|
|
? normalizeSafetySealValue(body.value)
|
|
: body.value;
|
|
} else {
|
|
Object.assign(order, body);
|
|
if (Object.prototype.hasOwnProperty.call(body, "reg_1")) {
|
|
order.reg_1 = normalizeRegistrationValue(body.reg_1);
|
|
}
|
|
if (Object.prototype.hasOwnProperty.call(body, "reg_2")) {
|
|
order.reg_2 = normalizeRegistrationValue(body.reg_2);
|
|
}
|
|
if (Object.prototype.hasOwnProperty.call(body, "reg_3")) {
|
|
order.reg_3 = normalizeRegistrationValue(body.reg_3);
|
|
}
|
|
if (Object.prototype.hasOwnProperty.call(body, "safety_seal")) {
|
|
order.safety_seal = normalizeSafetySealValue(body.safety_seal);
|
|
}
|
|
}
|
|
}
|
|
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.stripeModuleOrdersByOrderId[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),
|
|
includes: {
|
|
stripeModuleOrders: clone(fixture.stripeModuleOrdersByOrderId[orderId] || {}),
|
|
},
|
|
})
|
|
);
|
|
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();
|
|
if (orderContainsWashCertificate(fixture, orderId)) {
|
|
ensureWashCertificateAttachment(fixture, orderId);
|
|
}
|
|
}
|
|
}
|
|
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,
|
|
sessionData: {
|
|
customer_number: 555555,
|
|
display_name: "POS Mobile E2E",
|
|
...sessionData,
|
|
},
|
|
});
|
|
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,
|
|
});
|
|
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(() => {});
|
|
}
|