- Introduced `.prettierrc.json` to enforce consistent code formatting across the project. - Updated unit and e2e test files to address formatting issues, improve readability, and ensure alignment with the new Prettier configuration.
2860 lines
86 KiB
JavaScript
2860 lines
86 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"
|
|
);
|
|
|
|
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 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 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);
|
|
return {
|
|
...order,
|
|
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,
|
|
},
|
|
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: [],
|
|
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",
|
|
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 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: [] }));
|
|
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 (/\/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("/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) => Number(product.category) === 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 || "",
|
|
notes: body.notes || "",
|
|
reg_1: body.reg_1 || "",
|
|
reg_2: body.reg_2 || "",
|
|
reg_3: body.reg_3 || "",
|
|
invoice_collection_id: null,
|
|
booking_id: null,
|
|
completed_at: null,
|
|
closed_at: null,
|
|
created_at: 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, "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.value;
|
|
} else {
|
|
posFixture.ordersById[orderId] = {
|
|
...posFixture.ordersById[orderId],
|
|
...body,
|
|
...(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 posFixture = options.pos ? createPosFixture(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(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("/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 (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") && method === "GET") {
|
|
const invoices = [101, 102, 103, 104, 105, 106].map((invoiceId, index) => ({
|
|
id: invoiceId,
|
|
customer_number: 1001 + index,
|
|
customer_name: `Customer ${invoiceId}`,
|
|
total_net_amount: 100 + index * 10,
|
|
}));
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: invoices,
|
|
})
|
|
);
|
|
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);
|
|
}
|