Files
pleno-vue/tests/e2e/support/network.js
T
Jeppe Bundgaard 8f3ec2cf09 Add booking selection popup and refactor POS booking flow logic:
- Introduced `PosDepartmentStepMobilePopupSelectOrderBooking.vue` for mobile booking selection.
- Refactored vehicle and booking handling in `SelectVehicleFormPOS.vue` with better plate matching, booking persistence, and customer association logic.
- Updated `PosVehicle` object type to include booking matches.
- Enhanced POS transaction history and booking views with improved readability, functionality, and `data-testid` attributes.
- Adjusted `PosPopup.vue` components to support the new booking popup.
2026-04-13 17:27:37 +02:00

3195 lines
97 KiB
JavaScript

const API_HOST =
/https?:\/\/(?:api\.truckwash\.io(?::\d+)?\/.*|localhost(?::\d+)?\/api\/.*|127\.0\.0\.1(?::\d+)?\/api\/.*)/i;
const TINY_PNG = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAukB9pY9ZxQAAAAASUVORK5CYII=",
"base64"
);
const TINY_PDF = Buffer.from(
"%PDF-1.1\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n2 0 obj<</Type/Pages/Count 1/Kids[3 0 R]>>endobj\n3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 200 200]>>endobj\ntrailer<</Root 1 0 R>>\n%%EOF",
"utf8"
);
function json(body, status = 200) {
return {
status,
contentType: "application/json",
body: JSON.stringify(body),
};
}
function binary(body, contentType = "image/png", status = 200) {
return {
status,
contentType,
body,
};
}
function getAttachmentPreviewContentType(attachment = null) {
const filename = String(
attachment?.content?.image || attachment?.content?.document || attachment?.content?.other || ""
).toLowerCase();
if (filename.endsWith(".pdf")) {
return {
body: TINY_PDF,
contentType: "application/pdf",
};
}
return {
body: TINY_PNG,
contentType: "image/png",
};
}
function mergeFixture(base, overrides = {}) {
return {
...base,
...overrides,
previewByKey: {
...(base.previewByKey || {}),
...(overrides.previewByKey || {}),
},
summaryBySessionId: {
...(base.summaryBySessionId || {}),
...(overrides.summaryBySessionId || {}),
},
summaryByKey: {
...(base.summaryByKey || {}),
...(overrides.summaryByKey || {}),
},
answerResponseByKey: {
...(base.answerResponseByKey || {}),
...(overrides.answerResponseByKey || {}),
},
attachmentsByTaskId: {
...(base.attachmentsByTaskId || {}),
...(overrides.attachmentsByTaskId || {}),
},
attachmentDownloadByKey: {
...(base.attachmentDownloadByKey || {}),
...(overrides.attachmentDownloadByKey || {}),
},
};
}
function toSqlDateTime(value = new Date()) {
return new Date(value).toISOString().slice(0, 19).replace("T", " ");
}
function normalizeCreatedAtValue(value) {
if (value === null || value === undefined || value === "") {
return value;
}
const normalized = String(value).trim();
const dateTimeMatch = normalized.match(/^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2})(?::(\d{2}))?$/);
if (dateTimeMatch) {
return `${dateTimeMatch[1]} ${dateTimeMatch[2]}:${dateTimeMatch[3] || "00"}`;
}
return normalized;
}
function normalizeRegistrationValue(value) {
if (value === null || value === undefined) {
return "";
}
return String(value)
.trim()
.toUpperCase()
.replace(/[^A-Z0-9]/g, "");
}
function normalizeIncludeInInvoiceValue(value) {
if (value === null || value === undefined || value === "" || value === "use_department" || value === "null") {
return null;
}
if (value === true || value === 1 || value === "1" || value === "true" || value === "include") {
return true;
}
if (value === false || value === 0 || value === "0" || value === "false" || value === "exclude") {
return false;
}
return null;
}
function resolveDepartmentIncludeInInvoice(posFixture, departmentId) {
const department = (posFixture.departments || []).find((entry) => Number(entry.id) === Number(departmentId));
if (!department) {
return true;
}
if (typeof department.exclude_from_invoicing === "boolean") {
return !department.exclude_from_invoicing;
}
if (typeof department.include_in_invoice === "boolean") {
return department.include_in_invoice;
}
return true;
}
function withEffectiveOrderState(posFixture, order) {
if (!order) {
return order;
}
const includeInInvoice = normalizeIncludeInInvoiceValue(order.include_in_invoice);
const orderId = Number(order.id || 0);
const attachments = Array.isArray(order.attachments)
? order.attachments
: Array.isArray(posFixture.attachmentsByOrderId?.[orderId])
? posFixture.attachmentsByOrderId[orderId]
: [];
return {
...order,
attachments,
include_in_invoice: includeInInvoice,
include_in_invoice_effective:
includeInInvoice === null ? resolveDepartmentIncludeInInvoice(posFixture, order.department_id) : includeInInvoice,
};
}
function parseFilterExpressions(filters) {
return String(filters || "")
.split(",")
.map((entry) => entry.trim())
.filter(Boolean)
.reduce((result, entry) => {
const separatorIndex = entry.indexOf(":");
if (separatorIndex === -1) {
return result;
}
const key = entry.slice(0, separatorIndex);
const value = entry.slice(separatorIndex + 1);
if (key) {
result[key] = value;
}
return result;
}, {});
}
function filterPosOrders(posFixture, filters) {
const filterMap = parseFilterExpressions(filters);
return Object.values(posFixture.ordersById || {}).filter((order) => {
if (filterMap.id && Number(order.id) !== Number(filterMap.id)) {
return false;
}
if (filterMap.customer_id && Number(order.customer_id) !== Number(filterMap.customer_id)) {
return false;
}
if (filterMap.department_id && Number(order.department_id) !== Number(filterMap.department_id)) {
return false;
}
if (filterMap.reg_1 && String(order.reg_1 || "").toUpperCase() !== String(filterMap.reg_1).toUpperCase()) {
return false;
}
const createdAtDate = String(order.created_at || "").slice(0, 10);
if (filterMap["created_at-date_from"] && (!createdAtDate || createdAtDate < filterMap["created_at-date_from"])) {
return false;
}
if (filterMap["created_at-date_to"] && (!createdAtDate || createdAtDate > filterMap["created_at-date_to"])) {
return false;
}
return true;
});
}
function filterNumberPlateScans(posFixture, { filters = "", search = "", order = "created_at:desc" } = {}) {
const filterMap = parseFilterExpressions(filters);
const searchValue = String(search || "")
.trim()
.toUpperCase();
const [orderBy = "created_at", orderDirection = "desc"] = String(order || "created_at:desc").split(":");
const filteredRows = (posFixture.numberPlateScans || []).filter((scan) => {
if (filterMap.department_id && Number(scan.department_id) !== Number(filterMap.department_id)) {
return false;
}
if (
searchValue &&
!String(scan.plate || "")
.toUpperCase()
.includes(searchValue)
) {
return false;
}
return true;
});
return filteredRows.sort((left, right) => {
const leftValue = left?.[orderBy];
const rightValue = right?.[orderBy];
const leftComparable = typeof leftValue === "string" ? leftValue.toUpperCase() : leftValue;
const rightComparable = typeof rightValue === "string" ? rightValue.toUpperCase() : rightValue;
if (leftComparable === rightComparable) {
return 0;
}
if (leftComparable > rightComparable) {
return orderDirection === "desc" ? -1 : 1;
}
return orderDirection === "desc" ? 1 : -1;
});
}
function paginateRows(rows, page = 1, limit = 10) {
const currentPage = Math.max(1, Number.parseInt(page, 10) || 1);
const perPage = Math.max(1, Number.parseInt(limit, 10) || 10);
const startIndex = (currentPage - 1) * perPage;
const pagedRows = rows.slice(startIndex, startIndex + perPage);
return {
rows: pagedRows,
meta: {
pagination: {
current_page: currentPage,
per_page: perPage,
total: rows.length,
},
},
};
}
function createSelfServeFixture(overrides = {}) {
const departments = [
{
id: 1,
name: "Copenhagen",
address: "Alpha 1",
latitude: 55.6761,
longitude: 12.5683,
self_serve_enabled: true,
lanes: [
{
id: 7,
department: 1,
name: "7",
status: "AVAILABLE",
machine_available: true,
products: [2, 3],
relay_in_id: "IN-7",
relay_out_id: "OUT-7",
relay_machine_id: "M-7",
relay_machine_program_picker_id: "MPP-7",
relay_machine_cleaner_id: "MC-7",
dynamic_image_id: 77,
machine_type_id: null,
},
{
id: 8,
department: 1,
name: "8",
status: "FAULT",
machine_available: false,
products: [2, 3],
relay_in_id: "IN-8",
relay_out_id: "OUT-8",
relay_machine_id: "M-8",
relay_machine_program_picker_id: "MPP-8",
relay_machine_cleaner_id: "MC-8",
dynamic_image_id: 78,
machine_type_id: null,
},
],
},
{
id: 2,
name: "Odense",
address: "Beta 2",
latitude: 55.4038,
longitude: 10.4024,
self_serve_enabled: true,
lanes: [
{
id: 9,
department: 2,
name: "9",
status: "AVAILABLE",
machine_available: true,
products: [2, 4],
relay_in_id: "IN-9",
relay_out_id: "OUT-9",
relay_machine_id: "M-9",
relay_machine_program_picker_id: "MPP-9",
relay_machine_cleaner_id: "MC-9",
dynamic_image_id: 79,
machine_type_id: null,
},
],
},
{
id: 3,
name: "Aarhus",
address: "Gamma 3",
latitude: 56.1629,
longitude: 10.2039,
self_serve_enabled: true,
lanes: [
{
id: 10,
department: 3,
name: "10",
status: "AVAILABLE",
machine_available: false,
products: [3, 4],
relay_in_id: "IN-10",
relay_out_id: "OUT-10",
relay_machine_id: "M-10",
relay_machine_program_picker_id: "MPP-10",
relay_machine_cleaner_id: "MC-10",
dynamic_image_id: 80,
machine_type_id: null,
},
],
},
];
const products = [
{
id: 2,
name: "Truck",
price: 100,
description: "Large truck",
piktogram: "truck",
is_wash: true,
subscription_allowed: true,
category: 4,
},
{
id: 3,
name: "Van",
price: 80,
description: "Medium van",
piktogram: "van",
is_wash: true,
subscription_allowed: true,
category: 4,
},
{
id: 4,
name: "Car",
price: 60,
description: "Small car",
piktogram: "car",
is_wash: true,
subscription_allowed: true,
category: 4,
},
];
const baseFixture = {
departments,
departmentLanes: departments.flatMap((department) => department.lanes),
products,
customerVehicles: [
{ id: 1, reg: "AB12345", type: 2 },
{ id: 2, reg: "CD67890", type: 3 },
],
previewByKey: {
"7:AB12345": {
allowed: true,
machine_available: true,
lane: { id: 7, name: "7" },
session: { id: 501, status: "IN_PROGRESS", allowed: true },
questions: [
{
id: 11,
question: "Is the tarp removed?",
description: "Required before machine wash.",
answer: null,
order_priority: 1,
},
],
conditions: [],
rules: [],
tasks: [
{
id: 9001,
task: "Prepare the truck",
description: "Complete the pre-wash checks.",
order_priority: 1,
services: ["MACHINE"],
buttons: [1],
},
],
allowed_services: ["MACHINE"],
},
"7:ZZ00000": {
allowed: true,
machine_available: true,
lane: { id: 7, name: "7" },
session: { id: 601, status: "IN_PROGRESS", allowed: true },
questions: [{ id: 21, question: "Ready for direct wash?", answer: null, order_priority: 1 }],
conditions: [],
rules: [],
tasks: [],
allowed_services: ["MACHINE"],
},
},
summaryBySessionId: {
501: {
session: { id: 501, status: "IN_PROGRESS", allowed: true },
lane: { id: 7, name: "7" },
questions: [
{
id: 11,
question: "Is the tarp removed?",
description: "Required before machine wash.",
answer: null,
order_priority: 1,
},
],
conditions: [],
rules: [],
tasks: [
{
id: 9001,
task: "Prepare the truck",
description: "Complete the pre-wash checks.",
order_priority: 1,
services: ["MACHINE"],
buttons: [1],
},
],
events: [{ id: 1, type: "STARTED", created_at: "2026-01-01T10:00:00.000Z" }],
},
601: {
session: { id: 601, status: "IN_PROGRESS", allowed: true },
lane: { id: 7, name: "7" },
questions: [{ id: 21, question: "Ready for direct wash?", answer: null, order_priority: 1 }],
conditions: [],
rules: [],
tasks: [],
events: [{ id: 2, type: "STARTED", created_at: "2026-01-01T11:00:00.000Z" }],
},
},
summaryByKey: {},
answerResponseByKey: {
"7:AB12345:11:true": {
session: { id: 501, status: "IN_PROGRESS", allowed: true },
lane: { id: 7, name: "7" },
questions: [
{
id: 11,
question: "Is the tarp removed?",
description: "Required before machine wash.",
answer: true,
order_priority: 1,
},
],
conditions: [],
rules: [],
tasks: [
{
id: 9001,
task: "Prepare the truck",
description: "Complete the pre-wash checks.",
order_priority: 1,
services: ["MACHINE"],
buttons: [1],
},
],
events: [{ id: 3, type: "QUESTION_ANSWERED", created_at: "2026-01-01T10:01:00.000Z" }],
},
"7:ZZ00000:21:true": {
session: { id: 601, status: "IN_PROGRESS", allowed: true },
lane: { id: 7, name: "7" },
questions: [{ id: 21, question: "Ready for direct wash?", answer: true, order_priority: 1 }],
conditions: [],
rules: [],
tasks: [],
events: [{ id: 4, type: "QUESTION_ANSWERED", created_at: "2026-01-01T11:01:00.000Z" }],
},
},
attachmentsByTaskId: {
9001: [
{ id: 301, content: { other: "prep.pdf" } },
{ id: 302, content: { other: "prep.jpg" } },
],
},
attachmentDownloadByKey: {
"9001:301": { download_link: "https://cdn.example.test/prep.pdf" },
"9001:302": { download_link: "https://cdn.example.test/prep.jpg" },
},
laneAllowedServices: ["MACHINE"],
commandResponse: { success: true },
relayResponse: { success: true },
dynamicImage: TINY_PNG,
};
const fixture = mergeFixture(baseFixture, overrides);
fixture.departmentLanes = fixture.departments.flatMap((department) => department.lanes || []);
fixture.laneById = fixture.departmentLanes.reduce((accumulator, lane) => {
accumulator[String(lane.id)] = lane;
return accumulator;
}, {});
return fixture;
}
function createEdgeGatewayFixture(options = {}) {
const primaryBrokerConnected = options.brokerConnected ?? true;
return {
pendingDiscoveryByGatewayId: {},
pendingUpdateByGatewayId: {},
shellSessionsById: {},
gateways: [
{
id: 701,
department_id: 1,
label: "CPH Edge 01",
hostname: "cph-edge-01",
status: "ONLINE",
transport_mode: "gateway",
department_transport_mode: "gateway",
release_channel: "stable",
installed_version: "1.2.0",
target_version: "1.2.1",
last_heartbeat_at: "2026-04-08 08:15:00",
last_seen_ip: "10.1.0.14",
discovery_status: "READY",
metadata: {
broker_connected: primaryBrokerConnected,
system_metrics: {
latency_ms: 184,
cpu_usage_pct: 27,
memory_usage_pct: 61,
memory_used_bytes: 2621440000,
memory_total_bytes: 4294967296,
disk_usage_pct: 58,
disk_used_bytes: 249108103168,
disk_total_bytes: 429496729600,
disk_mount: "/",
},
},
inventory: [
{
id: 1,
device_id: "shelly-plus-01",
local_ip: "10.1.0.31",
model: "Shelly Plus 2PM",
channel_count: 2,
online: true,
capabilities: { generation: 2 },
},
{
id: 2,
device_id: "shelly-mini-offline",
local_ip: "10.1.0.34",
model: "Shelly Mini 1",
channel_count: 1,
online: false,
capabilities: { generation: 3 },
},
],
bindings: [
{
id: 1,
relay_id: "M-7",
device_id: "shelly-plus-01",
local_ip: "10.1.0.31",
channel: 0,
binding_source: "MANUAL",
},
{
id: 2,
relay_id: "M-7-LEGACY",
device_id: "shelly-missing-legacy",
local_ip: "10.1.0.99",
channel: 1,
binding_source: "MANUAL",
},
],
recent_updates: [{ id: 11, target_version: "1.2.1", status: "COMPLETED" }],
recent_shell_sessions: [],
recent_commands: [],
audit_logs: [{ id: 501, created_at: "2026-04-08 08:16:00", action: "GATEWAY_CLAIMED", actor_type: "USER" }],
},
{
id: 702,
department_id: 2,
label: "ODE Edge 01",
hostname: "ode-edge-01",
status: "OFFLINE",
transport_mode: "gateway",
department_transport_mode: "cloud",
release_channel: "stable",
installed_version: "1.1.0",
target_version: "1.1.0",
last_heartbeat_at: "2026-04-07 21:04:00",
last_seen_ip: "10.2.0.14",
discovery_status: "STALE",
metadata: {
broker_connected: false,
system_metrics: {
latency_ms: 412,
cpu_usage_pct: 9,
memory_usage_pct: 42,
memory_used_bytes: 1073741824,
memory_total_bytes: 2147483648,
disk_usage_pct: 76,
disk_used_bytes: 163208757248,
disk_total_bytes: 214748364800,
disk_mount: "/",
},
},
inventory: [],
bindings: [],
recent_updates: [],
recent_shell_sessions: [],
recent_commands: [],
audit_logs: [{ id: 502, created_at: "2026-04-07 21:05:00", action: "HEARTBEAT_TIMEOUT", actor_type: "SYSTEM" }],
},
],
};
}
function buildEdgeGatewayShellSocketConfig(overrides = {}) {
return {
mode: "success",
prompt: "root@pi:~# ",
directoryListing: "agent.mjs",
...overrides,
};
}
function settleEdgeGatewayWork(edgeGatewayFixture, gatewayId) {
const gateway = edgeGatewayFixture.gateways.find((entry) => entry.id === gatewayId) || null;
if (!gateway) {
return null;
}
const pendingDiscovery = edgeGatewayFixture.pendingDiscoveryByGatewayId[gatewayId];
if (pendingDiscovery) {
pendingDiscovery.fetchCount = (pendingDiscovery.fetchCount || 0) + 1;
if (pendingDiscovery.fetchCount >= 2) {
gateway.discovery_status = "READY";
if (!gateway.inventory.some((device) => device.device_id === pendingDiscovery.device.device_id)) {
gateway.inventory = [...gateway.inventory, pendingDiscovery.device];
}
gateway.recent_commands = (gateway.recent_commands || []).map((job) =>
Number(job.id) === Number(pendingDiscovery.jobId) ? { ...job, status: "COMPLETED" } : job
);
delete edgeGatewayFixture.pendingDiscoveryByGatewayId[gatewayId];
}
}
const pendingUpdate = edgeGatewayFixture.pendingUpdateByGatewayId[gatewayId];
if (pendingUpdate) {
pendingUpdate.fetchCount = (pendingUpdate.fetchCount || 0) + 1;
if (pendingUpdate.fetchCount >= 2) {
gateway.recent_updates = (gateway.recent_updates || []).map((job) =>
Number(job.id) === Number(pendingUpdate.jobId) ? { ...job, status: "COMPLETED" } : job
);
delete edgeGatewayFixture.pendingUpdateByGatewayId[gatewayId];
}
}
return gateway;
}
function createShellSessionFixture(edgeGatewayFixture, gatewayId, session, shellConfig) {
const state = {
gatewayId,
session,
prompt: shellConfig.prompt,
directoryListing: shellConfig.directoryListing,
inputBuffer: "",
events: [],
nextEventId: 1,
};
const pushEvent = (event_type, payload = {}) => {
state.events.push({
id: state.nextEventId,
session_id: session.id,
event_type,
payload,
created_at: `2026-04-09 12:${String(40 + state.nextEventId).padStart(2, "0")}:00`,
});
state.nextEventId += 1;
};
if (shellConfig.mode === "fail-before-open") {
state.session = {
...state.session,
closed_at: "2026-04-09 12:45:30",
metadata: {
closed_reason: "open_failed",
},
};
} else {
pushEvent("OPENED", {});
pushEvent("OUTPUT", { data: shellConfig.prompt });
}
edgeGatewayFixture.shellSessionsById[session.id] = state;
return state;
}
export function createPosFixture(overrides = {}) {
const defaultCustomer = {
id: 1,
customerNumber: 12345679,
name: "(TEST) Pleno Vognmandsforretning",
address: "Demo Street 1",
zip: "2630",
city: "Taastrup",
mobilePhone: "12345678",
email: "2jepp9350@gmail.com",
corporateIdentificationNumber: "12345678",
economic_customer: 12345679,
barred: false,
};
const cardCustomer = {
id: 2,
customerNumber: 999,
name: "Card Terminal Customer",
address: "Terminal Street 9",
zip: "2630",
city: "Taastrup",
mobilePhone: "87654321",
email: "card@example.com",
corporateIdentificationNumber: "99999999",
economic_customer: 999,
barred: false,
};
const products = [
{
id: 53,
name: "Forvogn",
description: "Primary wash product",
price: 649,
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: "Indvendig vask Forvogn",
description: "Add-on wash service",
price: 399,
subscription_allowed: true,
category: 4,
piktogram: "truck",
apply_category_discount: true,
requires_note: false,
is_wash: false,
display_in_booking_form: true,
order_priority: 2,
addons: [],
},
{
id: 64,
name: "Vaskecertifikat - Safety Seal",
description: "Safety seal",
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: 3,
addons: [],
},
{
id: 65,
name: "Ekstraordinær pr. 10 min inkl. kemi",
description: "Additional time",
price: 299,
subscription_allowed: true,
category: 8,
piktogram: "timer",
apply_category_discount: false,
requires_note: false,
is_wash: false,
display_in_booking_form: true,
order_priority: 4,
addons: [],
},
];
const baseFixture = {
departments: [
{ id: 1, name: "Taastrup", exclude_from_invoicing: true },
{ id: 12, name: "Demo", exclude_from_invoicing: false },
],
customersByNumber: {
[defaultCustomer.customerNumber]: defaultCustomer,
[cardCustomer.customerNumber]: cardCustomer,
},
collectedInvoices: [
{
id: 101,
customer_number: defaultCustomer.customerNumber,
customer_name: defaultCustomer.name,
total_net_amount: 100,
created_at: "2026-04-10",
closed_at: "2026-04-10",
},
{
id: 200,
customer_number: cardCustomer.customerNumber,
customer_name: cardCustomer.name,
total_net_amount: 250,
created_at: "2026-04-30",
closed_at: "2026-04-30",
},
{
id: 201,
customer_number: cardCustomer.customerNumber,
customer_name: cardCustomer.name,
total_net_amount: 325,
created_at: "2026-05-01",
closed_at: "2026-05-01",
},
...[102, 103, 104, 105, 106].map((invoiceId, index) => ({
id: invoiceId,
customer_number: 1002 + index,
customer_name: `Customer ${invoiceId}`,
total_net_amount: 110 + index * 10,
})),
],
nextCollectedInvoiceId: 300,
customerAttributesByNumber: {
[defaultCustomer.customerNumber]: [
{ id: 1, customer_number: defaultCustomer.customerNumber, attribute: "invoiceAllOrdersIndividually" },
],
[cardCustomer.customerNumber]: [
{ id: 2, customer_number: cardCustomer.customerNumber, attribute: "invoiceWithStripe" },
],
},
customerNotesByNumber: {},
products,
departmentCategories: [
{ id: 11, department_id: 12, category: { id: 4, name: "Udvendig", meta: { products: [53, 63] } } },
{ id: 12, department_id: 12, category: { id: 8, name: "Tillæg", meta: { products: [64, 65] } } },
],
vehicles: [
{
id: 7001,
reg: "EC21235",
customer_id: defaultCustomer.customerNumber,
customer_name: defaultCustomer.name,
type: 53,
status: "verified",
barred: false,
wash_subscription: false,
addons: { enabled: 0, available: 0, list: [] },
reference: "EC21233 - Test Ref. / Intern nummer",
last_order_id: 54518,
},
],
unknownVehicles: [],
orderBookings: [],
bookingOrderAssignments: [],
completedBookingIds: [],
nextOrderBookingId: 9001,
numberPlateScanners: [
{ id: 1, name: "North scanner" },
{ id: 2, name: "South scanner" },
],
numberPlateScans: [
{
id: 801,
department_id: 12,
plate: "AB12345",
plate_scanner_id: 1,
created_at: "2026-04-08 08:44:07",
customer_number: defaultCustomer.customerNumber,
customer_name: defaultCustomer.name,
seen_before: true,
barred: false,
},
{
id: 802,
department_id: 12,
plate: "CD67890",
plate_scanner_id: 2,
created_at: "2026-04-08 08:31:05",
customer_number: null,
customer_name: "",
seen_before: false,
barred: false,
},
],
motorApiLookupByPlate: {
AB12345: {
make: "RENAULT",
model: "Captur",
variant: "dCi 90",
type: "Personbil",
use: "Privat personkørsel",
},
CD67890: {
make: "SCANIA",
model: "R500",
variant: "Highline",
type: "Lastbil",
use: "Godstransport",
},
},
ordersById: {
54518: {
id: 54518,
customer_id: defaultCustomer.customerNumber,
department_id: 12,
reference: "EC21233 - Test Ref. / Intern nummer",
po: "",
notes: "",
reg_1: "EC21235",
reg_2: "",
reg_3: "",
invoice_collection_id: null,
booking_id: null,
completed_at: null,
closed_at: null,
created_at: "2026-04-08 08:44:07",
include_in_invoice: null,
},
},
orderItemsByOrderId: {
54518: [
{
id: 9101,
order_id: 54518,
product_id: 53,
product: products[0],
quantity: 1,
notes: "",
reference: "",
related_item_id: null,
price: 649,
},
{
id: 9102,
order_id: 54518,
product_id: 63,
product: products[1],
quantity: 1,
notes: "",
reference: "",
related_item_id: 9101,
price: 399,
},
{
id: 9103,
order_id: 54518,
product_id: 64,
product: products[2],
quantity: 1,
notes: "",
reference: "",
related_item_id: 9101,
price: 25,
},
{
id: 9104,
order_id: 54518,
product_id: 65,
product: products[3],
quantity: 1,
notes: "",
reference: "",
related_item_id: 9101,
price: 299,
},
],
},
attachmentsByOrderId: {
54518: [
{
id: 301,
object_type: "orders",
object_id: 54518,
content: {
image: null,
document: null,
relation: null,
other: "safety-seal.pdf",
src: null,
},
created_at: "2026-04-08 08:44:07",
updated_at: "2026-04-08 08:44:07",
deleted_at: null,
},
],
},
economicModuleOrdersByOrderId: {
54518: {
invoice_id: null,
invoice_draft_id: null,
},
},
stripeModuleOrdersByOrderId: {
54518: {},
},
paymentIntentsByOrderId: {},
readers: [{ id: "reader_online_1", label: "Mobile Reader", status: "online", action: null }],
nextOrderId: 54519,
nextOrderItemId: 9200,
nextAttachmentId: 400,
};
return {
...baseFixture,
...overrides,
customersByNumber: {
...baseFixture.customersByNumber,
...(overrides.customersByNumber || {}),
},
customerAttributesByNumber: {
...baseFixture.customerAttributesByNumber,
...(overrides.customerAttributesByNumber || {}),
},
customerNotesByNumber: {
...baseFixture.customerNotesByNumber,
...(overrides.customerNotesByNumber || {}),
},
numberPlateScanners: overrides.numberPlateScanners || baseFixture.numberPlateScanners,
numberPlateScans: overrides.numberPlateScans || baseFixture.numberPlateScans,
motorApiLookupByPlate: {
...baseFixture.motorApiLookupByPlate,
...(overrides.motorApiLookupByPlate || {}),
},
ordersById: {
...baseFixture.ordersById,
...(overrides.ordersById || {}),
},
orderItemsByOrderId: {
...baseFixture.orderItemsByOrderId,
...(overrides.orderItemsByOrderId || {}),
},
attachmentsByOrderId: {
...baseFixture.attachmentsByOrderId,
...(overrides.attachmentsByOrderId || {}),
},
economicModuleOrdersByOrderId: {
...baseFixture.economicModuleOrdersByOrderId,
...(overrides.economicModuleOrdersByOrderId || {}),
},
stripeModuleOrdersByOrderId: {
...baseFixture.stripeModuleOrdersByOrderId,
...(overrides.stripeModuleOrdersByOrderId || {}),
},
paymentIntentsByOrderId: {
...baseFixture.paymentIntentsByOrderId,
...(overrides.paymentIntentsByOrderId || {}),
},
};
}
function buildPosOrderItem(product, body, id) {
const quantity = Number(body.quantity || 1);
return {
id,
order_id: Number(body.order_id),
product_id: product.id,
product,
quantity,
notes: body.notes || "",
reference: body.reference || "",
related_item_id: body.related_item_id ?? null,
price: Number(body.price ?? product.price ?? 0),
};
}
function matchesProductCategory(product, category) {
const normalizedCategory = Number(category || 0);
const productCategory = Number(product?.category || 0);
const productName = String(product?.name || "");
if (normalizedCategory === 6) {
return productCategory === 6 || (product?.is_wash && productCategory === 4);
}
if (normalizedCategory === 2) {
return productCategory === 2 || /indvendig/i.test(productName);
}
return productCategory === normalizedCategory;
}
function buildPosOrderResponse(posFixture, orderId) {
const order = withEffectiveOrderState(posFixture, posFixture.ordersById[orderId] || null);
if (!order) {
return { success: true, data: null, includes: {} };
}
const customer = posFixture.customersByNumber[order.customer_id] || null;
return {
success: true,
data: order,
includes: {
orderItems: posFixture.orderItemsByOrderId[orderId] || [],
customer: customer ? { ...customer, economic_customer: customer.customerNumber } : null,
cashier: { id: 7, display_name: "Backoffice User" },
economicModuleOrders: posFixture.economicModuleOrdersByOrderId[orderId] || {},
stripeModuleOrders: posFixture.stripeModuleOrdersByOrderId[orderId] || {},
},
};
}
async function handlePosRoute({ route, request, parsedUrl, pathname, method, posFixture }) {
if (!posFixture) {
return false;
}
if (pathname.endsWith("/departments") && method === "GET") {
await route.fulfill(json({ success: true, data: posFixture.departments }));
return true;
}
if (pathname.endsWith("/departments/categories") && method === "GET") {
await route.fulfill(json({ success: true, data: posFixture.departmentCategories || [] }));
return true;
}
if (pathname.endsWith("/bookings") && method === "GET") {
await route.fulfill(json({ success: true, data: Array.isArray(posFixture.orderBookings) ? posFixture.orderBookings : [] }));
return true;
}
if (pathname.endsWith("/order-bookings") && method === "POST") {
const body = request.postDataJSON?.() || {};
const bookingId = Number(posFixture.nextOrderBookingId || 9001);
const customerNumber = Number(body.customer_number || 0);
const departmentId = Number(body.department || 0);
const createdAt = toSqlDateTime();
const bookingDate = String(body.datetime || createdAt).slice(0, 10);
const customer = posFixture.customersByNumber?.[customerNumber] || null;
const serviceNames = Array.isArray(body.items)
? body.items
.map((item) => String(item?.name || "").trim())
.filter(Boolean)
: [];
const createdBooking = {
id: bookingId,
customer_number: customerNumber,
customer_name: customer?.name || "E2E User",
department: departmentId,
date: bookingDate,
datetime: body.datetime || createdAt,
regNrTraekker: body.reg_1 || "",
regNrTrailer: body.reg_2 || "",
reg_1: body.reg_1 || "",
reg_2: body.reg_2 || "",
reg_3: body.reg_3 || "",
reference_number: body.reference || "",
reference: body.reference || "",
notes: body.note || "",
note: body.note || "",
po: body.po || "",
pickup_bool: body.pickup ? 1 : 0,
pickup: Boolean(body.pickup),
created_at: createdAt,
status: "pending",
wash_type: serviceNames.join(", "),
parsed_services: {
string: serviceNames.join(", "),
array: serviceNames,
},
wash_certificate_pdf: null,
washCertificateStatus: null,
};
posFixture.nextOrderBookingId = bookingId + 1;
posFixture.orderBookings = [createdBooking, ...(posFixture.orderBookings || [])];
await route.fulfill(json({ success: true, data: createdBooking }));
return true;
}
if (pathname.endsWith("/order-bookings") && method === "GET") {
const id = Number(parsedUrl.searchParams.get("id") || 0);
const filters = String(parsedUrl.searchParams.get("filters") || "");
let bookings = Array.isArray(posFixture.orderBookings) ? posFixture.orderBookings : [];
if (id > 0) {
await route.fulfill(json({ success: true, data: bookings.find((booking) => booking.id === id) || null }));
return true;
}
if (filters.includes("reg_1:")) {
const reg = filters.split("reg_1:")[1]?.split(",")[0] || "";
bookings = bookings.filter((booking) => String(booking.reg_1 || "").toUpperCase() === String(reg).toUpperCase());
}
await route.fulfill(json({ success: true, data: bookings }));
return true;
}
if (pathname.endsWith("/order-bookings") && method === "PUT") {
const body = request.postDataJSON?.() || {};
const bookingId = Number(body.id || 0);
const bookings = Array.isArray(posFixture.orderBookings) ? posFixture.orderBookings : [];
const bookingIndex = bookings.findIndex((booking) => Number(booking.id) === bookingId);
if (bookingIndex >= 0) {
bookings[bookingIndex] = {
...bookings[bookingIndex],
order_id: body.order_id ?? body.value ?? null,
};
posFixture.bookingOrderAssignments = Array.isArray(posFixture.bookingOrderAssignments)
? posFixture.bookingOrderAssignments
: [];
posFixture.bookingOrderAssignments.push({
id: bookingId,
order_id: bookings[bookingIndex].order_id,
});
}
await route.fulfill(json({ success: true, data: bookingIndex >= 0 ? bookings[bookingIndex] : null }));
return true;
}
if (/\/order-bookings\/\d+$/.test(pathname) && method === "GET") {
const bookingId = Number(pathname.split("/").pop());
const booking = (posFixture.orderBookings || []).find((entry) => entry.id === bookingId) || null;
await route.fulfill(json({ success: true, data: booking }));
return true;
}
if (pathname.endsWith("/order-bookings/complete") && method === "POST") {
const body = request.postDataJSON?.() || {};
const bookingId = Number(body.id || 0);
const bookings = Array.isArray(posFixture.orderBookings) ? posFixture.orderBookings : [];
const bookingIndex = bookings.findIndex((booking) => Number(booking.id) === bookingId);
if (bookingIndex >= 0) {
bookings[bookingIndex] = {
...bookings[bookingIndex],
status: "completed",
};
posFixture.completedBookingIds = Array.isArray(posFixture.completedBookingIds)
? posFixture.completedBookingIds
: [];
posFixture.completedBookingIds.push(bookingId);
}
await route.fulfill(
json({
success: true,
data: bookingIndex >= 0 ? bookings[bookingIndex] : { id: bookingId, status: "completed" },
})
);
return true;
}
if (pathname.endsWith("/department/numberplatescanners") && method === "GET") {
await route.fulfill(json({ success: true, data: posFixture.numberPlateScanners || [] }));
return true;
}
if (pathname.endsWith("/numberplatescans") && method === "GET") {
const filteredScans = filterNumberPlateScans(posFixture, {
filters: parsedUrl.searchParams.get("filters") || "",
search: parsedUrl.searchParams.get("search") || "",
order: parsedUrl.searchParams.get("order") || "created_at:desc",
});
const paginated = paginateRows(
filteredScans,
parsedUrl.searchParams.get("page") || 1,
parsedUrl.searchParams.get("limit") || 10
);
await route.fulfill(
json({
success: true,
data: paginated.rows,
meta: paginated.meta,
})
);
return true;
}
if (pathname.endsWith("/vehicles") && method === "GET") {
const search = String(parsedUrl.searchParams.get("search") || "").toUpperCase();
const vehicles = (posFixture.vehicles || []).filter(
(vehicle) =>
!search ||
String(vehicle.reg || "")
.toUpperCase()
.includes(search)
);
await route.fulfill(json({ success: true, data: vehicles }));
return true;
}
if (pathname.endsWith("/department/vehicles/unknown-customer") && method === "GET") {
await route.fulfill(json({ success: true, data: posFixture.unknownVehicles || [] }));
return true;
}
if (pathname.endsWith("/users/customer") && method === "GET") {
const customerNumber = Number(parsedUrl.searchParams.get("customer_number") || 0);
const customer = posFixture.customersByNumber[customerNumber] || Object.values(posFixture.customersByNumber)[0];
await route.fulfill(
json({
success: true,
data: {
customer_name: customer?.name || "",
economic_customer: customer || null,
},
})
);
return true;
}
if (pathname.endsWith("/modules/motorapi/lookup") && method === "GET") {
const licensePlate = String(parsedUrl.searchParams.get("license_plate") || "").toUpperCase();
const vehicleData = posFixture.motorApiLookupByPlate?.[licensePlate];
if (!vehicleData) {
await route.fulfill(
json(
{
success: false,
data: null,
},
404
)
);
return true;
}
await route.fulfill(
json({
success: true,
data: vehicleData,
})
);
return true;
}
if (pathname.endsWith("/customers") && method === "GET") {
const search = String(parsedUrl.searchParams.get("search") || "").toLowerCase();
const customers = Object.values(posFixture.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: customers,
meta: {
pagination: {
page: 1,
limit: 10,
total: customers.length,
},
},
})
);
return true;
}
if (pathname.endsWith("/customer/attributes") && method === "GET") {
const customerNumber = Number(parsedUrl.searchParams.get("customer_number") || 0);
await route.fulfill(json({ success: true, data: posFixture.customerAttributesByNumber[customerNumber] || [] }));
return true;
}
if (pathname.endsWith("/customer/notes") && method === "GET") {
const customerNumber = Number(parsedUrl.searchParams.get("customer_number") || 0);
await route.fulfill(json({ success: true, data: posFixture.customerNotesByNumber[customerNumber] || [] }));
return true;
}
if (pathname.endsWith("/superuser/user/discounts") && method === "GET") {
await route.fulfill(json({ success: true, data: [] }));
return true;
}
if (pathname.endsWith("/products") && method === "GET") {
const productId = Number(parsedUrl.searchParams.get("id") || 0);
const category = Number(parsedUrl.searchParams.get("category") || 0);
if (productId > 0) {
await route.fulfill(
json({ success: true, data: (posFixture.products || []).find((product) => product.id === productId) || null })
);
return true;
}
const products =
category > 0
? (posFixture.products || []).filter((product) => matchesProductCategory(product, category))
: posFixture.products || [];
await route.fulfill(json({ success: true, data: products }));
return true;
}
if (pathname.endsWith("/orders") && method === "GET") {
const filters = String(parsedUrl.searchParams.get("filters") || "");
const orders = filterPosOrders(posFixture, filters)
.map((order) => withEffectiveOrderState(posFixture, order))
.sort((a, b) => Number(b.id) - Number(a.id));
await route.fulfill(
json({
success: true,
data: orders,
meta: {
pagination: {
page: 1,
limit: orders.length || 20,
total: orders.length,
},
},
})
);
return true;
}
if (pathname.endsWith("/orders") && method === "POST") {
const body = request.postDataJSON?.() || {};
const orderId = posFixture.nextOrderId++;
posFixture.ordersById[orderId] = {
id: orderId,
customer_id: Number(body.customer_id),
department_id: Number(body.department_id || body.department || 12),
reference: body.reference || "",
po: body.po || "",
notes: body.notes || "",
reg_1: normalizeRegistrationValue(body.reg_1),
reg_2: normalizeRegistrationValue(body.reg_2),
reg_3: normalizeRegistrationValue(body.reg_3),
invoice_collection_id: null,
booking_id: null,
completed_at: null,
closed_at: null,
created_at: normalizeCreatedAtValue(body.created_at) || toSqlDateTime(),
include_in_invoice: normalizeIncludeInInvoiceValue(body.include_in_invoice),
};
posFixture.orderItemsByOrderId[orderId] = [];
posFixture.economicModuleOrdersByOrderId[orderId] = { invoice_id: null, invoice_draft_id: null };
posFixture.stripeModuleOrdersByOrderId[orderId] = {};
await route.fulfill(json({ success: true, data: { id: orderId } }));
return true;
}
if (pathname.endsWith("/order") && method === "GET") {
const orderId = Number(parsedUrl.searchParams.get("id") || 0);
await route.fulfill(json(buildPosOrderResponse(posFixture, orderId)));
return true;
}
if (pathname.endsWith("/orders") && method === "PUT") {
const body = request.postDataJSON?.() || {};
const orderId = Number(body.id || 0);
if (posFixture.ordersById[orderId]) {
posFixture.ordersById[orderId] = {
...posFixture.ordersById[orderId],
...body,
...(Object.prototype.hasOwnProperty.call(body, "reg_1")
? { reg_1: normalizeRegistrationValue(body.reg_1) }
: {}),
...(Object.prototype.hasOwnProperty.call(body, "reg_2")
? { reg_2: normalizeRegistrationValue(body.reg_2) }
: {}),
...(Object.prototype.hasOwnProperty.call(body, "reg_3")
? { reg_3: normalizeRegistrationValue(body.reg_3) }
: {}),
...(Object.prototype.hasOwnProperty.call(body, "created_at")
? { created_at: normalizeCreatedAtValue(body.created_at) }
: {}),
...(Object.prototype.hasOwnProperty.call(body, "include_in_invoice")
? { include_in_invoice: normalizeIncludeInInvoiceValue(body.include_in_invoice) }
: {}),
};
}
await route.fulfill(
json({ success: true, data: withEffectiveOrderState(posFixture, posFixture.ordersById[orderId] || null) })
);
return true;
}
if (pathname.endsWith("/order") && method === "PUT") {
const body = request.postDataJSON?.() || {};
const orderId = Number(body.id || 0);
if (posFixture.ordersById[orderId]) {
if (body.field) {
posFixture.ordersById[orderId][body.field] =
body.field === "include_in_invoice"
? normalizeIncludeInInvoiceValue(body.value)
: body.field === "reg_1" || body.field === "reg_2" || body.field === "reg_3"
? normalizeRegistrationValue(body.value)
: body.field === "created_at"
? normalizeCreatedAtValue(body.value)
: body.value;
} else {
posFixture.ordersById[orderId] = {
...posFixture.ordersById[orderId],
...body,
...(Object.prototype.hasOwnProperty.call(body, "reg_1")
? { reg_1: normalizeRegistrationValue(body.reg_1) }
: {}),
...(Object.prototype.hasOwnProperty.call(body, "reg_2")
? { reg_2: normalizeRegistrationValue(body.reg_2) }
: {}),
...(Object.prototype.hasOwnProperty.call(body, "reg_3")
? { reg_3: normalizeRegistrationValue(body.reg_3) }
: {}),
...(Object.prototype.hasOwnProperty.call(body, "created_at")
? { created_at: normalizeCreatedAtValue(body.created_at) }
: {}),
...(Object.prototype.hasOwnProperty.call(body, "include_in_invoice")
? { include_in_invoice: normalizeIncludeInInvoiceValue(body.include_in_invoice) }
: {}),
};
}
}
await route.fulfill(
json({ success: true, data: withEffectiveOrderState(posFixture, posFixture.ordersById[orderId] || null) })
);
return true;
}
if (pathname.endsWith("/order/items") && method === "GET") {
const orderId = Number(parsedUrl.searchParams.get("order_id") || 0);
await route.fulfill(json({ success: true, data: posFixture.orderItemsByOrderId[orderId] || [] }));
return true;
}
if (pathname.endsWith("/order/items") && method === "POST") {
const body = request.postDataJSON?.() || {};
const orderId = Number(body.order_id || 0);
const productId = Number(body.product_id || 0);
const product = (posFixture.products || []).find((entry) => entry.id === productId);
if (!product || !posFixture.ordersById[orderId]) {
await route.fulfill(json({ success: false, data: { message: "Order or product not found" } }, 422));
return true;
}
const orderItem = buildPosOrderItem(product, body, posFixture.nextOrderItemId++);
if (!Array.isArray(posFixture.orderItemsByOrderId[orderId])) {
posFixture.orderItemsByOrderId[orderId] = [];
}
posFixture.orderItemsByOrderId[orderId].push(orderItem);
await route.fulfill(json({ success: true, data: orderItem }));
return true;
}
if (pathname.endsWith("/order/items") && method === "PUT") {
const body = request.postDataJSON?.() || {};
const targetId = Number(body.id || 0);
Object.keys(posFixture.orderItemsByOrderId).forEach((orderIdKey) => {
posFixture.orderItemsByOrderId[orderIdKey] = (posFixture.orderItemsByOrderId[orderIdKey] || []).map((item) => {
if (item.id !== targetId) {
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: targetId } }));
return true;
}
if (pathname.endsWith("/order/items") && method === "DELETE") {
const orderItemId = Number(parsedUrl.searchParams.get("id") || 0);
Object.keys(posFixture.orderItemsByOrderId).forEach((orderIdKey) => {
posFixture.orderItemsByOrderId[orderIdKey] = (posFixture.orderItemsByOrderId[orderIdKey] || []).filter(
(item) => item.id !== orderItemId
);
});
await route.fulfill(json({ success: true, data: true }));
return true;
}
if (pathname.endsWith("/orders/attachments") && method === "GET") {
const orderId = Number(parsedUrl.searchParams.get("id") || 0);
await route.fulfill(json({ success: true, data: posFixture.attachmentsByOrderId[orderId] || [] }));
return true;
}
if (pathname.endsWith("/orders/attachments/upload") && method === "POST") {
const body = request.postDataJSON?.() || {};
const orderId = Number(body.order_id || body.id || 0);
if (!Array.isArray(posFixture.attachmentsByOrderId[orderId])) {
posFixture.attachmentsByOrderId[orderId] = [];
}
const attachmentId = posFixture.nextAttachmentId++;
const attachment = {
id: attachmentId,
object_type: "orders",
object_id: orderId,
content: {
image: null,
document: null,
relation: null,
other: `attachment-${attachmentId}.jpg`,
src: null,
},
created_at: toSqlDateTime(),
updated_at: toSqlDateTime(),
deleted_at: null,
};
posFixture.attachmentsByOrderId[orderId].push(attachment);
await route.fulfill(json({ success: true, data: attachment }));
return true;
}
if (pathname.endsWith("/orders/attachments") && method === "DELETE") {
const orderId = Number(parsedUrl.searchParams.get("order_id") || 0);
const attachmentId = Number(parsedUrl.searchParams.get("attachment_id") || 0);
posFixture.attachmentsByOrderId[orderId] = (posFixture.attachmentsByOrderId[orderId] || []).filter(
(attachment) => attachment.id !== attachmentId
);
await route.fulfill(json({ success: true, data: true }));
return true;
}
if (pathname.endsWith("/orders/attachments/download") && method === "GET") {
const orderId = Number(parsedUrl.searchParams.get("order_id") || 0);
const attachmentId = Number(parsedUrl.searchParams.get("attachment_id") || 0);
await route.fulfill(
json({
success: true,
data: {
download_link: `https://cdn.example.test/orders/${orderId}/attachments/${attachmentId}`,
},
})
);
return true;
}
if (pathname.endsWith("/order/wash-certificate") && method === "POST") {
const body = request.postDataJSON?.() || {};
const orderId = Number(body.id || 0);
if (!Array.isArray(posFixture.attachmentsByOrderId[orderId])) {
posFixture.attachmentsByOrderId[orderId] = [];
}
const existingAttachment =
(posFixture.attachmentsByOrderId[orderId] || []).find((attachment) => {
return String(attachment?.content?.other || "").toUpperCase() === "WASH_CERTIFICATE";
}) || null;
if (existingAttachment) {
await route.fulfill(
json({
success: true,
data: {
order_id: orderId,
created: false,
already_existed: true,
attachment_id: existingAttachment.id,
},
})
);
return true;
}
const attachmentId = posFixture.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: toSqlDateTime(),
updated_at: toSqlDateTime(),
deleted_at: null,
};
posFixture.attachmentsByOrderId[orderId].push(attachment);
await route.fulfill(
json({
success: true,
data: {
order_id: orderId,
created: true,
already_existed: false,
attachment_id: attachmentId,
},
})
);
return true;
}
if (pathname.endsWith("/departments/order/recommended") && method === "GET") {
await route.fulfill(
json({
success: true,
data: {
reg_1: {
motorapi: [53],
order_history: { 2: [], 3: [], 4: [], 5: [] },
},
},
})
);
return true;
}
if (pathname.endsWith("/orders/mark_as_completed") && method === "POST") {
const body = request.postDataJSON?.() || {};
const orderId = Number(body.id || 0);
if (posFixture.ordersById[orderId]) {
posFixture.ordersById[orderId].completed_at = new Date().toISOString();
}
await route.fulfill(json({ success: true, data: { id: orderId } }));
return true;
}
if (pathname.endsWith("/modules/stripe/department/terminal/readers") && method === "GET") {
await route.fulfill(json({ success: true, data: { data: posFixture.readers || [] } }));
return true;
}
if (pathname.endsWith("/orders/module/stripe/payment_intent") && method === "GET") {
const orderId = Number(parsedUrl.searchParams.get("id") || 0);
const paymentIntent = posFixture.paymentIntentsByOrderId[orderId] || null;
await route.fulfill(
json({
success: true,
data: {
payment_intent: paymentIntent,
has_payment_intent: !!paymentIntent,
},
})
);
return true;
}
if (pathname.endsWith("/orders/module/stripe/payment_intent") && method === "POST") {
const body = request.postDataJSON?.() || {};
const orderId = Number(body.id || 0);
const paymentIntent = posFixture.paymentIntentsByOrderId[orderId] || {
id: `pi_${orderId}`,
amount: 123400,
amount_capturable: 123400,
amount_received: 0,
currency: "dkk",
status: "requires_capture",
metadata: {
order_id: String(orderId),
reader_id: String(body.reader || "reader_online_1"),
tax_percentage: String(body.tax_percentage ?? 25),
},
};
posFixture.paymentIntentsByOrderId[orderId] = paymentIntent;
await route.fulfill(
json({
success: true,
data: {
payment_intent: paymentIntent,
has_payment_intent: true,
},
})
);
return true;
}
if (pathname.endsWith("/orders/module/stripe/payment_intent") && method === "DELETE") {
const body = request.postDataJSON?.() || {};
const orderId = Number(body.id || 0);
delete posFixture.paymentIntentsByOrderId[orderId];
await route.fulfill(json({ success: true, data: { payment_intent: null, has_payment_intent: false } }));
return true;
}
if (pathname.endsWith("/orders/module/stripe/payment_intent/capture") && method === "POST") {
const body = request.postDataJSON?.() || {};
const orderId = Number(body.id || 0);
const paymentIntent = {
...(posFixture.paymentIntentsByOrderId[orderId] || {}),
id: `pi_${orderId}`,
amount: 123400,
amount_capturable: 0,
amount_received: 123400,
currency: "dkk",
status: "succeeded",
metadata: {
order_id: String(orderId),
reader_id: "reader_online_1",
tax_percentage: "25",
},
};
posFixture.paymentIntentsByOrderId[orderId] = paymentIntent;
await route.fulfill(
json({
success: true,
data: {
payment_intent: paymentIntent,
has_payment_intent: true,
},
})
);
return true;
}
return false;
}
async function handleEdgeGatewayRoute({
route,
request,
parsedUrl,
pathname,
method,
edgeGatewayFixture,
shellSocketConfig,
}) {
if (!edgeGatewayFixture) {
return false;
}
if (pathname.endsWith("/edge-gateways") && method === "GET") {
const departmentId = Number(parsedUrl.searchParams.get("department_id") || 0);
const gateways =
departmentId > 0
? edgeGatewayFixture.gateways.filter((gateway) => gateway.department_id === departmentId)
: edgeGatewayFixture.gateways;
await route.fulfill(json({ data: gateways }));
return true;
}
if (/\/edge-gateways\/\d+$/.test(pathname) && method === "GET") {
const gatewayId = Number(pathname.split("/").pop());
await route.fulfill(json({ data: settleEdgeGatewayWork(edgeGatewayFixture, gatewayId) }));
return true;
}
if (pathname.endsWith("/edge-gateways/install-token") && method === "POST") {
const body = request.postDataJSON?.() || {};
await route.fulfill(
json(
{
data: {
claim_token_id: 9001,
token: "edge-install-token",
expires_at: "2026-04-08 08:45:00",
install_command:
"curl -fsSL https://api.truckwash.test/edge-agent/install.sh?token=edge-install-token | sudo bash",
install_url: "https://api.truckwash.test/edge-agent/install.sh?token=edge-install-token",
department_id: body.department_id ?? 1,
},
},
201
)
);
return true;
}
if (/\/edge-gateways\/\d+\/discovery$/.test(pathname) && method === "POST") {
const gatewayId = Number(pathname.split("/")[2]);
const gateway = edgeGatewayFixture.gateways.find((item) => item.id === gatewayId);
if (gateway) {
const commandId = Date.now();
gateway.discovery_status = "PENDING";
gateway.recent_commands = [
{
id: commandId,
command_type: "DISCOVER_SHELLY",
status: "PENDING",
},
...(gateway.recent_commands || []),
];
edgeGatewayFixture.pendingDiscoveryByGatewayId[gatewayId] = {
jobId: commandId,
fetchCount: 0,
device: {
id: 3,
device_id: "shelly-plus-new",
local_ip: "10.1.0.33",
model: "Shelly Plus 1PM",
channel_count: 1,
online: true,
capabilities: { generation: 2 },
},
};
}
await route.fulfill(json({ data: gateway }));
return true;
}
if (/\/edge-gateways\/\d+\/bindings$/.test(pathname) && method === "PUT") {
const gatewayId = Number(pathname.split("/")[2]);
const gateway = edgeGatewayFixture.gateways.find((item) => item.id === gatewayId);
const body = request.postDataJSON?.() || {};
if (gateway) {
gateway.bindings = (body.bindings || []).map((binding, index) => ({
id: index + 1,
...binding,
}));
}
await route.fulfill(json({ data: gateway?.bindings || [] }));
return true;
}
if (/\/edge-gateways\/\d+\/update-jobs$/.test(pathname) && method === "POST") {
const gatewayId = Number(pathname.split("/")[2]);
const gateway = edgeGatewayFixture.gateways.find((item) => item.id === gatewayId);
const body = request.postDataJSON?.() || {};
const job = {
id: Date.now(),
target_version: body.target_version || "1.2.1",
status: "PENDING",
};
if (gateway) {
gateway.recent_updates.unshift(job);
gateway.target_version = job.target_version;
edgeGatewayFixture.pendingUpdateByGatewayId[gatewayId] = {
jobId: job.id,
fetchCount: 0,
};
}
await route.fulfill(json({ data: job }, 201));
return true;
}
if (/\/edge-gateways\/\d+\/shell-sessions$/.test(pathname) && method === "POST") {
const gatewayId = Number(pathname.split("/")[2]);
const gateway = edgeGatewayFixture.gateways.find((item) => item.id === gatewayId);
const body = request.postDataJSON?.() || {};
const session = {
id: 12345,
gateway_id: gatewayId,
reason: body.reason || "Maintenance",
expires_at: "2026-04-09 12:45:00",
metadata: {
transport: "API_POLLING",
},
};
if (gateway) {
gateway.recent_shell_sessions.unshift(session);
}
createShellSessionFixture(
edgeGatewayFixture,
gatewayId,
session,
shellSocketConfig || buildEdgeGatewayShellSocketConfig()
);
await route.fulfill(
json(
{
data: {
session,
session_token: "shell-session-token",
transport: "API_POLLING",
},
},
201
)
);
return true;
}
if (/\/edge-gateways\/\d+\/shell-sessions\/\d+\/events$/.test(pathname) && method === "GET") {
const segments = pathname.split("/");
const sessionId = Number(segments[4]);
const shellSession = edgeGatewayFixture.shellSessionsById[sessionId];
const afterId = Number(parsedUrl.searchParams.get("after_id") || 0);
await route.fulfill(
json({
data: {
session: shellSession?.session || null,
events: (shellSession?.events || []).filter((event) => Number(event.id) > afterId),
},
})
);
return true;
}
if (/\/edge-gateways\/\d+\/shell-sessions\/\d+\/input$/.test(pathname) && method === "POST") {
const segments = pathname.split("/");
const sessionId = Number(segments[4]);
const shellSession = edgeGatewayFixture.shellSessionsById[sessionId];
const body = request.postDataJSON?.() || {};
if (shellSession) {
shellSession.inputBuffer += String(body.data || "");
if (/[\r\n]/.test(String(body.data || ""))) {
const command = shellSession.inputBuffer.replace(/[\r\n]+/g, "").trim();
shellSession.inputBuffer = "";
if (command !== "") {
shellSession.events.push({
id: shellSession.nextEventId,
session_id: sessionId,
event_type: "OUTPUT",
payload: {
data: `${command}\r\n${shellSession.directoryListing}\r\n${shellSession.prompt}`,
},
created_at: `2026-04-09 12:${String(40 + shellSession.nextEventId).padStart(2, "0")}:00`,
});
shellSession.nextEventId += 1;
}
}
}
await route.fulfill(json({ data: shellSession?.session || null }));
return true;
}
if (/\/edge-gateways\/\d+\/shell-sessions\/\d+\/resize$/.test(pathname) && method === "POST") {
const segments = pathname.split("/");
const sessionId = Number(segments[4]);
await route.fulfill(json({ data: edgeGatewayFixture.shellSessionsById[sessionId]?.session || null }));
return true;
}
if (/\/edge-gateways\/\d+\/shell-sessions\/\d+\/close$/.test(pathname) && method === "POST") {
const segments = pathname.split("/");
const sessionId = Number(segments[4]);
const shellSession = edgeGatewayFixture.shellSessionsById[sessionId];
if (shellSession) {
shellSession.session = {
...shellSession.session,
closed_at: "2026-04-09 12:46:00",
metadata: {
...(shellSession.session.metadata || {}),
closed_reason: "browser_requested",
},
};
shellSession.events.push({
id: shellSession.nextEventId,
session_id: sessionId,
event_type: "CLOSED",
payload: {
reason: "browser_requested",
code: 0,
},
created_at: `2026-04-09 12:${String(40 + shellSession.nextEventId).padStart(2, "0")}:00`,
});
shellSession.nextEventId += 1;
}
await route.fulfill(json({ data: shellSession?.session || null }));
return true;
}
if (/\/edge-gateways\/\d+\/rotate-credentials$/.test(pathname) && method === "POST") {
const gatewayId = Number(pathname.split("/")[2]);
const gateway = edgeGatewayFixture.gateways.find((item) => item.id === gatewayId);
await route.fulfill(
json({
data: {
gateway,
agent_token: `rotated-token-${gatewayId}`,
},
})
);
return true;
}
if (/\/departments\/\d+\/gateway-cutover$/.test(pathname) && method === "POST") {
const departmentId = Number(pathname.split("/")[2]);
const body = request.postDataJSON?.() || {};
edgeGatewayFixture.gateways
.filter((gateway) => gateway.department_id === departmentId)
.forEach((gateway) => {
gateway.department_transport_mode = body.transport_mode || "gateway";
});
await route.fulfill(
json({
data: {
department_id: departmentId,
transport_mode: body.transport_mode || "gateway",
},
})
);
return true;
}
return false;
}
export async function mockApi(page, options = {}) {
const shouldMockPosFixture = Boolean(options.pos || options.invoiceDistribution);
const posFixture = shouldMockPosFixture
? createPosFixture(options.pos && options.pos !== true ? options.pos : {})
: null;
const selfServe = options.selfServe
? createSelfServeFixture(options.selfServe === true ? {} : options.selfServe)
: null;
const edgeGatewayOptions =
options.edgeGateways && typeof options.edgeGateways === "object" ? options.edgeGateways : {};
const edgeGatewayFixture = options.edgeGateways === false ? null : createEdgeGatewayFixture(edgeGatewayOptions);
const shellSocketConfig = edgeGatewayFixture
? buildEdgeGatewayShellSocketConfig(edgeGatewayOptions.shellSocket || {})
: null;
await page.route(/https:\/\/cdn\.example\.test\/orders\/\d+\/attachments\/\d+$/i, async (route) => {
const request = route.request();
if (request.method() !== "GET" || !posFixture) {
await route.continue();
return;
}
const url = new URL(request.url());
const pathSegments = url.pathname.split("/").filter(Boolean);
const orderId = Number(pathSegments[1] || 0);
const attachmentId = Number(pathSegments[3] || 0);
const attachment =
(posFixture.attachmentsByOrderId?.[orderId] || []).find((entry) => Number(entry.id) === attachmentId) || null;
const previewResponse = getAttachmentPreviewContentType(attachment);
await route.fulfill(binary(previewResponse.body, previewResponse.contentType));
});
await page.route(API_HOST, async (route) => {
const request = route.request();
const url = request.url();
const parsedUrl = new URL(url);
const pathname = parsedUrl.pathname;
const method = request.method();
if (url.includes("/auth/recaptcha/pre-check") && method === "GET") {
await route.fulfill(
json({
data: {
recaptcha: {
enabled: false,
site_key: "",
},
rate_limit: {
enabled: false,
limit: 0,
remaining: 0,
reset: 0,
warning: null,
},
},
})
);
return;
}
if (url.includes("/auth/reCAPTCHA/public") && method === "GET") {
await route.fulfill(
json({
data: {
recaptcha: {
enabled: false,
site_key: "",
},
rate_limit: {
enabled: false,
limit: 0,
remaining: 0,
reset: 0,
warning: null,
},
},
})
);
return;
}
if (pathname.endsWith("/ping") && method === "GET") {
await route.fulfill(
json({
data: {
ok: true,
},
})
);
return;
}
if (pathname.endsWith("/worker/version") && method === "GET") {
await route.fulfill(
json({
data: {
version: options.workerVersion || "unknown",
},
})
);
return;
}
if (url.includes("/auth/login") && method === "POST") {
await route.fulfill(
json({
data: {
token: options.loginToken || "e2e-token",
},
})
);
return;
}
if (url.includes("/auth/session") && method === "GET") {
if (!options.authenticated) {
await route.fulfill(json({ message: "Unauthenticated" }, 401));
return;
}
const defaultSession = {
id: 1,
customer_number: 12345,
group_id: 1,
email: "e2e@example.com",
phone: {
number: "12345678",
country_code: 45,
},
notifications: {
wash_certificate_email: null,
email_notifications_enabled: true,
sms_notifications_enabled: false,
},
created_at: "2026-01-01T00:00:00.000Z",
updated_at: "2026-01-01T00:00:00.000Z",
display_name: "E2E User",
permissions: options.permissions || ["user"],
economic_customer: [],
};
const sessionData = {
...defaultSession,
...(options.sessionData || {}),
};
if (options.permissions) {
sessionData.permissions = options.permissions;
}
await route.fulfill(
json({
data: sessionData,
})
);
return;
}
if (pathname.endsWith("/guest/validation/customer-number") && method === "POST") {
const body = request.postDataJSON?.() || {};
const customerNumber = Number(body.customer_number || 0);
const sessionCustomerNumber = Number(options.sessionData?.customer_number || 0);
const exists = Boolean(
customerNumber &&
(customerNumber === sessionCustomerNumber ||
customerNumber === Number(posFixture?.defaultCustomer?.customerNumber || 0) ||
posFixture?.customersByNumber?.[customerNumber])
);
await route.fulfill(
json({
data: {
exists,
},
})
);
return;
}
if (pathname.endsWith("/workfeed/config")) {
const workfeedConfig = [
{ module: "workfeed", variable: "enabled", type: "bool", value: true },
{ module: "workfeed", variable: "api_url", type: "string", value: "https://api.workfeed.test" },
{ module: "workfeed", variable: "api_key", type: "string", value: "test-api-key" },
{ module: "workfeed", variable: "CompanyID", type: "string", value: "123456" },
];
if (method === "GET") {
const variable = parsedUrl.searchParams.get("variable");
if (variable) {
const match = workfeedConfig.find((entry) => entry.variable === variable);
await route.fulfill(json({ data: match ? match.value : null }));
return;
}
await route.fulfill(json({ data: workfeedConfig }));
return;
}
if (method === "POST") {
await route.fulfill(json({ data: { updated: true } }));
return;
}
}
if (pathname.endsWith("/modules/workfeed/departments") && method === "GET") {
const departments = [
{
id: "dep_01J5P2B0BC9Q2X7H8Y7JQ1M2N3",
name: "North Facility",
timezone: "Europe/Copenhagen",
active: true,
},
{
id: "dep_01J5P2B0BC9Q2X7H8Y7JQ1M2N4",
name: "South Facility",
timezone: "Europe/Copenhagen",
active: true,
},
];
await route.fulfill(
json({
data: {
items: departments,
pagination: { cursor: null, nextCursor: null, limit: 20, total: departments.length },
},
})
);
return;
}
if (pathname.endsWith("/modules/workfeed/employees") && method === "GET") {
const employees = [
{
id: "emp_01J5P2F2A0CQKBBX4P8M3Y7R5P",
firstName: "Anne",
lastName: "Nielsen",
fullName: "Anne Nielsen",
email: "anne.nielsen@example.com",
departmentId: "dep_01J5P2B0BC9Q2X7H8Y7JQ1M2N3",
active: true,
},
{
id: "emp_01J5P2F2A0CQKBBX4P8M3Y7R5Q",
firstName: "Mads",
lastName: "Jensen",
fullName: "Mads Jensen",
email: "mads.jensen@example.com",
departmentId: "dep_01J5P2B0BC9Q2X7H8Y7JQ1M2N4",
active: true,
},
];
await route.fulfill(
json({
data: {
items: employees,
pagination: { cursor: null, nextCursor: null, limit: 20, total: employees.length },
},
})
);
return;
}
if (/\/modules\/workfeed\/employees\/[^/]+$/i.test(pathname) && method === "GET") {
const id = pathname.split("/").pop();
await route.fulfill(
json({
data: {
id,
firstName: "Anne",
lastName: "Nielsen",
fullName: "Anne Nielsen",
email: "anne.nielsen@example.com",
departmentId: "dep_01J5P2B0BC9Q2X7H8Y7JQ1M2N3",
active: true,
},
})
);
return;
}
if (pathname.endsWith("/modules/workfeed/shifts") && method === "GET") {
const startFrom = parsedUrl.searchParams.get("startFrom");
const startTo = parsedUrl.searchParams.get("startTo");
const employeeID = parsedUrl.searchParams.get("employeeID");
const releasedRaw = parsedUrl.searchParams.get("released");
if (!startFrom) {
await route.fulfill(json({ message: "Missing required query parameter: startFrom" }, 422));
return;
}
if (!startTo) {
await route.fulfill(json({ message: "Missing required query parameter: startTo" }, 422));
return;
}
const shifts = [
{
id: "shf_01J5P3X9D35R9C6BDZ1S0R4V6Q",
title: "Morning Shift",
status: "published",
employeeID: "emp_01J5P2F2A0CQKBBX4P8M3Y7R5P",
released: true,
startAt: "2026-03-24T06:00:00Z",
endAt: "2026-03-24T14:00:00Z",
},
{
id: "shf_01J5P3X9D35R9C6BDZ1S0R4V6R",
title: "Evening Shift",
status: "draft",
employeeID: "emp_01J5P2F2A0CQKBBX4P8M3Y7R5Q",
released: false,
startAt: "2026-03-24T14:00:00Z",
endAt: "2026-03-24T22:00:00Z",
},
];
const released = releasedRaw === null ? undefined : releasedRaw.toLowerCase() === "true";
const filtered = shifts.filter((entry) => {
if (employeeID && entry.employeeID !== employeeID) {
return false;
}
if (released !== undefined && entry.released !== released) {
return false;
}
return true;
});
await route.fulfill(
json({
data: {
items: filtered,
pagination: { cursor: null, nextCursor: null, limit: 20, total: filtered.length },
},
})
);
return;
}
if (/\/modules\/workfeed\/shifts\/[^/]+$/i.test(pathname) && method === "GET") {
const id = pathname.split("/").pop();
await route.fulfill(
json({
data: {
id,
title: "Morning Shift",
status: "published",
employeeID: "emp_01J5P2F2A0CQKBBX4P8M3Y7R5P",
released: true,
startAt: "2026-03-24T06:00:00Z",
endAt: "2026-03-24T14:00:00Z",
},
})
);
return;
}
if (
await handleEdgeGatewayRoute({
route,
request,
parsedUrl,
pathname,
method,
edgeGatewayFixture,
shellSocketConfig,
})
) {
return;
}
if (await handlePosRoute({ route, request, parsedUrl, pathname, method, posFixture })) {
return;
}
if (selfServe) {
if (pathname.endsWith("/guest/departments") && method === "GET") {
await route.fulfill(json({ data: selfServe.departments }));
return;
}
if (pathname.endsWith("/departments") && method === "GET") {
await route.fulfill(json({ data: selfServe.departments.map(({ lanes, ...department }) => department) }));
return;
}
if (pathname.endsWith("/products") && method === "GET") {
await route.fulfill(json({ data: selfServe.products }));
return;
}
if (pathname.endsWith("/vehicles") && method === "GET") {
await route.fulfill(json({ data: selfServe.customerVehicles }));
return;
}
if (pathname.endsWith("/department/lanes") && method === "GET") {
const laneId = parsedUrl.searchParams.get("id");
if (laneId) {
await route.fulfill(json({ data: selfServe.laneById[laneId] || null }));
return;
}
await route.fulfill(json({ data: selfServe.departmentLanes }));
return;
}
if (pathname.endsWith("/department/selfserve/vehicle/allowed") && method === "GET") {
const laneId = parsedUrl.searchParams.get("lane_id");
const reg = (parsedUrl.searchParams.get("reg") || "").toUpperCase();
const previewData = selfServe.previewByKey[`${laneId}:${reg}`] || null;
await route.fulfill(
json({ data: previewData || { allowed: false, questions: [], tasks: [], conditions: [], rules: [] } })
);
return;
}
if (pathname.endsWith("/department/selfserve/washes/summary") && method === "GET") {
const sessionId = parsedUrl.searchParams.get("session_id");
const laneId = parsedUrl.searchParams.get("lane_id");
const reg = (parsedUrl.searchParams.get("reg") || "").toUpperCase();
const summaryData =
(sessionId ? selfServe.summaryBySessionId[sessionId] : null) ||
selfServe.summaryByKey[`${laneId}:${reg}`] ||
null;
await route.fulfill(
json({ data: summaryData || { events: [], questions: [], tasks: [], conditions: [], rules: [] } })
);
return;
}
if (pathname.endsWith("/department/selfserve/vehicle/conditions") && method === "POST") {
const body = request.postDataJSON?.() || {};
const key = `${body.lane}:${String(body.reg || "").toUpperCase()}:${body.question}:${body.value}`;
const responseSummary = selfServe.answerResponseByKey[key] || null;
await route.fulfill(
json({
data: {
selfserve: responseSummary,
},
})
);
return;
}
if (pathname.endsWith("/department/selfserve/tasks/attachments") && method === "GET") {
const taskId = parsedUrl.searchParams.get("id");
await route.fulfill(
json({
data: selfServe.attachmentsByTaskId[taskId] || [],
})
);
return;
}
if (pathname.endsWith("/department/selfserve/tasks/attachments/download") && method === "GET") {
const taskId = parsedUrl.searchParams.get("task_id");
const attachmentId = parsedUrl.searchParams.get("attachment_id");
await route.fulfill(
json(selfServe.attachmentDownloadByKey[`${taskId}:${attachmentId}`] || { download_link: null })
);
return;
}
if (pathname.endsWith("/modules/self-serve/lane/services/allowed") && method === "POST") {
await route.fulfill(
json({
data: {
allowed_services: selfServe.laneAllowedServices,
},
})
);
return;
}
if (pathname.endsWith("/modules/self-serve/lane/relay/machine/enable") && method === "POST") {
await route.fulfill(json(selfServe.relayResponse));
return;
}
if (pathname.endsWith("/modules/self-serve/lane/command") && method === "POST") {
await route.fulfill(json(selfServe.commandResponse));
return;
}
if (pathname.endsWith("/department/lanes/dynamic-image") && method === "GET") {
await route.fulfill(binary(selfServe.dynamicImage));
return;
}
}
if (pathname.endsWith("/departments") && method === "GET") {
await route.fulfill(
json({
data: [
{ id: 1, name: "Copenhagen" },
{ id: 2, name: "Odense" },
],
})
);
return;
}
if (pathname.endsWith("/collected-invoices") && method === "GET") {
const invoiceFixture = posFixture || { collectedInvoices: [] };
const filters = parseFilterExpressions(parsedUrl.searchParams.get("filters") || "");
const customerNumberFilter = Number(filters.customer_number || 0);
const collectedInvoices = (invoiceFixture.collectedInvoices || []).filter((invoice) => {
if (!customerNumberFilter) {
return true;
}
return Number(invoice.customer_number) === customerNumberFilter;
});
await route.fulfill(
json({
data: collectedInvoices,
})
);
return;
}
if (pathname.endsWith("/collected-invoices") && method === "POST") {
const invoiceFixture = posFixture || {
collectedInvoices: [],
customersByNumber: {},
nextCollectedInvoiceId: 200,
};
const body = request.postDataJSON?.() || {};
const invoiceId = Number(invoiceFixture.nextCollectedInvoiceId || 200);
const customerNumber = Number(body.customer_number || 0);
const customer = invoiceFixture.customersByNumber?.[customerNumber] || null;
invoiceFixture.nextCollectedInvoiceId = invoiceId + 1;
invoiceFixture.collectedInvoices = [
{
id: invoiceId,
customer_number: customerNumber,
customer_name: customer?.name || `Customer ${invoiceId}`,
total_net_amount: 0,
created_at: body.closed_at || toSqlDateTime().slice(0, 10),
closed_at: body.closed_at || null,
},
...(invoiceFixture.collectedInvoices || []),
];
await route.fulfill(
json({
success: true,
data: {
id: invoiceId,
},
})
);
return;
}
if (options.invoiceDistribution) {
const monthFromDate = parsedUrl.searchParams.get("dateFrom");
const monthNumber = monthFromDate ? Number(monthFromDate.split("-")[1]) : 1;
const monthBase = Number.isFinite(monthNumber) ? monthNumber * 10 : 10;
const forceDistributionLegacy = Boolean(options.invoiceDistributionForceLegacyFallback);
const forceCompareLegacy = Boolean(options.invoiceDistributionForceCompareFallback);
if (pathname.endsWith("/orders") && method === "GET") {
await route.fulfill(
json({
data: [
{
id: 1,
created_at: "2026-01-01T00:00:00.000Z",
},
],
})
);
return;
}
if (pathname.endsWith("/superuser/invoicing/period") && method === "GET") {
await route.fulfill(
json({
data: {
types: {
all: [
{
transactions: [
{ amount: 100 + monthBase, booked: true, excluded: false },
{ amount: 50 + monthBase, booked: true, excluded: false },
{ amount: 25, booked: false, excluded: false },
],
},
],
},
},
})
);
return;
}
if (pathname.endsWith("/superuser/invoicing/period/distribution/v2/all") && method === "GET") {
if (forceDistributionLegacy) {
await route.fulfill(json({ message: "v2 distribution temporarily unavailable" }, 500));
return;
}
await route.fulfill(
json({
fixed_pricing: {
customers: [
{
id: 10,
customer_number: 1001,
customer_name: "Acme Transport",
requires_action: false,
transactions: [
{
id: 1,
date: "2026-01-10T00:00:00.000Z",
amount: 80 + monthBase,
booked: true,
department_id: 1,
excluded: false,
},
],
meta: {
fixed_pricing: {
created_at: "2026-01-10T00:00:00.000Z",
price: 80 + monthBase,
original_price: 120 + monthBase,
department_totals_relative: { 1: 40 + monthBase, 2: 40 },
},
},
},
],
collective_results: {
total_fixed_price: 80 + monthBase,
total_department_totals_relative_parsed: {
Copenhagen: 40 + monthBase,
Odense: 40,
},
},
warnings: [],
},
wash_subscriptions: {
customers: [
{
id: 11,
customer_number: 1002,
customer_name: "Nordic Haul",
requires_action: false,
transactions: [
{
id: 2,
date: "2026-01-05T00:00:00.000Z",
amount: 40 + monthBase,
booked: true,
department_id: 1,
excluded: false,
},
],
meta: {
wash_subscription: {
created_at: "2026-01-05T00:00:00.000Z",
price: 40 + monthBase,
original_price: 55 + monthBase,
department_totals_relative: { 1: 20 + monthBase, 2: 20 },
},
},
},
],
collective_results: {
total_subscription_price: 40 + monthBase,
subscription_price_department_distribution_parsed: {
Copenhagen: 20 + monthBase,
Odense: 20,
},
},
warnings: [],
},
customer_prices: {
customers: [
{
id: 12,
customer_number: 1003,
customer_name: "Discount Fleet",
requires_action: false,
transactions: [
{
id: 3,
date: "2026-01-08T00:00:00.000Z",
amount: 15 + monthBase,
booked: true,
department_id: 2,
excluded: false,
},
],
meta: {
customer_price: {
created_at: "2026-01-08T00:00:00.000Z",
price: 15 + monthBase,
department_totals_relative: { 2: 15 + monthBase },
},
},
},
],
collective_results: {
total_customer_price: 15 + monthBase,
customer_price_department_distribution_parsed: {
Odense: 15 + monthBase,
},
},
warnings: [],
},
})
);
return;
}
if (pathname.endsWith("/superuser/invoicing/period/distribution/v2/fixed-pricing") && method === "GET") {
if (forceDistributionLegacy) {
await route.fulfill(json({ message: "v2 fixed pricing distribution temporarily unavailable" }, 500));
return;
}
await route.fulfill(
json({
customers: [
{
id: 10,
customer_number: 1001,
customer_name: "Acme Transport",
requires_action: false,
transactions: [
{
id: 1,
date: "2026-01-10T00:00:00.000Z",
amount: 80 + monthBase,
booked: true,
department_id: 1,
excluded: false,
},
],
meta: {
fixed_pricing: {
created_at: "2026-01-10T00:00:00.000Z",
price: 80 + monthBase,
original_price: 120 + monthBase,
department_totals_relative: { 1: 40 + monthBase, 2: 40 },
},
},
},
],
collective_results: {
total_fixed_price: 80 + monthBase,
total_department_totals_relative_parsed: {
Copenhagen: 40 + monthBase,
Odense: 40,
},
},
warnings: [],
})
);
return;
}
if (pathname.endsWith("/superuser/invoicing/period/distribution/v2/wash-subscriptions") && method === "GET") {
if (forceDistributionLegacy) {
await route.fulfill(json({ message: "v2 wash subscriptions distribution temporarily unavailable" }, 500));
return;
}
await route.fulfill(
json({
customers: [
{
id: 11,
customer_number: 1002,
customer_name: "Nordic Haul",
requires_action: false,
transactions: [
{
id: 2,
date: "2026-01-05T00:00:00.000Z",
amount: 40 + monthBase,
booked: true,
department_id: 1,
excluded: false,
},
],
meta: {
wash_subscription: {
created_at: "2026-01-05T00:00:00.000Z",
price: 40 + monthBase,
original_price: 55 + monthBase,
department_totals_relative: { 1: 20 + monthBase, 2: 20 },
},
},
},
],
collective_results: {
total_subscription_price: 40 + monthBase,
subscription_price_department_distribution_parsed: {
Copenhagen: 20 + monthBase,
Odense: 20,
},
},
warnings: [],
})
);
return;
}
if (pathname.endsWith("/superuser/invoicing/period/distribution/v2/customer-prices") && method === "GET") {
if (forceDistributionLegacy) {
await route.fulfill(json({ message: "v2 customer prices distribution temporarily unavailable" }, 500));
return;
}
await route.fulfill(
json({
customers: [
{
id: 12,
customer_number: 1003,
customer_name: "Discount Fleet",
requires_action: false,
transactions: [
{
id: 3,
date: "2026-01-08T00:00:00.000Z",
amount: 15 + monthBase,
booked: true,
department_id: 2,
excluded: false,
},
],
meta: {
customer_price: {
created_at: "2026-01-08T00:00:00.000Z",
price: 15 + monthBase,
department_totals_relative: { 2: 15 + monthBase },
},
},
},
],
collective_results: {
total_customer_price: 15 + monthBase,
customer_price_department_distribution_parsed: {
Odense: 15 + monthBase,
},
},
warnings: [],
})
);
return;
}
if (pathname.endsWith("/superuser/invoicing/period/distribution/fixed-pricing") && method === "GET") {
await route.fulfill(
json({
data: [
{
id: 10,
customer_number: 1001,
customer_name: "Acme Transport",
meta: {
fixed_pricing: {
created_at: "2026-01-10T00:00:00.000Z",
price: 80 + monthBase,
original_price: 120 + monthBase,
department_totals_relative: { 1: 40 + monthBase, 2: 40 },
},
},
},
],
includes: {
collective_fixed_pricing_results: {
total_fixed_price: 80 + monthBase,
total_department_totals_relative_parsed: {
Copenhagen: 40 + monthBase,
Odense: 40,
},
},
},
})
);
return;
}
if (pathname.endsWith("/superuser/invoicing/period/distribution/wash-subscriptions") && method === "GET") {
await route.fulfill(
json({
data: [
{
id: 11,
customer_number: 1002,
customer_name: "Nordic Haul",
meta: {
wash_subscription: {
created_at: "2026-01-05T00:00:00.000Z",
price: 40 + monthBase,
original_price: 55 + monthBase,
department_totals_relative: { 1: 20 + monthBase, 2: 20 },
},
},
},
],
includes: {
collective_subscription_results: {
total_subscription_price: 40 + monthBase,
subscription_price_department_distribution_parsed: {
Copenhagen: 20 + monthBase,
Odense: 20,
},
},
},
})
);
return;
}
if (pathname.endsWith("/collected-invoices/economic/compare") && method === "GET") {
const invoiceId = Number(parsedUrl.searchParams.get("collected_invoice_id") || 0);
const mismatch = invoiceId === 101 || invoiceId === 103;
await new Promise((resolve) => setTimeout(resolve, 600));
await route.fulfill(
json({
data: {
collected_invoice_id: invoiceId,
warnings: mismatch ? ["Line mismatch found"] : [],
internal_total: mismatch ? 150 : 120,
booked_total: mismatch ? 145 : 120,
draft_total: null,
difference: mismatch ? 5 : 0,
order_ids: mismatch ? [1, 2] : [3],
},
})
);
return;
}
if (pathname.endsWith("/collected-invoices/economic/v2/compare/bulk") && method === "POST") {
if (forceCompareLegacy) {
await route.fulfill(json({ message: "v2 compare bulk temporarily unavailable" }, 500));
return;
}
const body = request.postDataJSON?.() || {};
const ids = Array.isArray(body.collected_invoice_ids) ? body.collected_invoice_ids : [];
const results = ids.map((invoiceId) => {
const mismatch = Number(invoiceId) === 101 || Number(invoiceId) === 103;
const internalTotal = mismatch ? 150 : 120;
const targetTotal = mismatch ? 145 : 120;
return {
collected_invoice_id: Number(invoiceId),
warnings: mismatch ? ["Line mismatch found"] : [],
details: {
order_ids: mismatch ? [1, 2] : [3],
customer: {
internal_customer_number: 4000 + Number(invoiceId),
name: `Customer ${invoiceId}`,
},
internal: {
normalized: {
totals: {
net_total: internalTotal,
billable_line_count: mismatch ? 2 : 1,
},
},
},
},
comparison: {
totals: {
internal_net_total: internalTotal,
},
targets: {
booked: {
target: "booked",
status: mismatch ? "partial_mismatch" : "exact_match",
overall_match: !mismatch,
totals: {
target_net_total: targetTotal,
},
mismatch_reasons: mismatch ? ["department_total_mismatch"] : [],
warnings: mismatch ? ["Line mismatch found"] : [],
lines: {
summary: {
internal_billable_count: mismatch ? 2 : 1,
target_billable_count: mismatch ? 2 : 1,
mismatch_count: mismatch ? 1 : 0,
},
},
},
},
warnings: mismatch ? ["comparison warning"] : [],
},
};
});
await route.fulfill(
json({
requested: ids.length,
compared: ids.length,
failed: 0,
results,
errors: [],
})
);
return;
}
}
if (options.fallbackPassthrough) {
await route.fallback();
return;
}
await route.fulfill(json({ data: [] }));
});
}
export async function seedAuthenticatedState(page, token = "e2e-token") {
await page.addInitScript((value) => {
window.localStorage.setItem("token", value);
}, token);
}
export async function primeMockSession(page, { token = "e2e-token", bootPath = "/redirect" } = {}) {
await seedAuthenticatedState(page, token);
const sessionRequest = page
.waitForResponse(
(response) => {
return response.request().method() === "GET" && response.url().includes("/auth/session");
},
{ timeout: 10_000 }
)
.catch(() => null);
await page.goto(bootPath);
await sessionRequest;
}