2672 lines
94 KiB
JavaScript
2672 lines
94 KiB
JavaScript
import { expect, test } from "@playwright/test";
|
|
import { mockApi, seedAuthenticatedState } from "./support/network.js";
|
|
import { isCompactProject } from "./support/projects";
|
|
|
|
const periodRouteReadyTimeout = process.env.CI ? 30_000 : 15_000;
|
|
|
|
function json(body, status = 200) {
|
|
return {
|
|
status,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(body),
|
|
};
|
|
}
|
|
|
|
function matchesApiPath(urlString, expectedPath) {
|
|
const url = new URL(urlString);
|
|
return url.pathname === expectedPath || url.pathname === `/api${expectedPath}`;
|
|
}
|
|
|
|
async function suppressVueDevtoolsOverlay(page) {
|
|
await page.addInitScript(() => {
|
|
const STYLE_ID = "__e2e-hide-vue-devtools";
|
|
localStorage.setItem("lastVersionCheck", String(Date.now()));
|
|
|
|
const apply = () => {
|
|
const target = document.head || document.documentElement;
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
if (!document.getElementById(STYLE_ID)) {
|
|
const style = document.createElement("style");
|
|
style.id = STYLE_ID;
|
|
style.textContent =
|
|
"#__vue-devtools-container__, .vue-devtools__anchor-btn, .vue-devtools__panel-content { display: none !important; visibility: hidden !important; pointer-events: none !important; }";
|
|
target.appendChild(style);
|
|
}
|
|
|
|
const container = document.getElementById("__vue-devtools-container__");
|
|
if (container) {
|
|
container.style.display = "none";
|
|
container.style.pointerEvents = "none";
|
|
}
|
|
};
|
|
|
|
apply();
|
|
const startObserving = () => {
|
|
if (!document.documentElement) {
|
|
requestAnimationFrame(startObserving);
|
|
return;
|
|
}
|
|
|
|
const observer = new MutationObserver(apply);
|
|
observer.observe(document.documentElement, { childList: true, subtree: true });
|
|
};
|
|
|
|
startObserving();
|
|
});
|
|
}
|
|
|
|
function createPeriodPayload() {
|
|
return {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 11,
|
|
customer_number: 4001,
|
|
customer_name: "Acme Fleet",
|
|
requires_action: true,
|
|
transactions: [{ id: 9001, date: "2026-03-10T10:00:00.000Z", amount: 120, booked: false, excluded: false }],
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
meta: {
|
|
fixed_pricing: {
|
|
price: 500,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: 12,
|
|
customer_number: 4002,
|
|
customer_name: "Nordic Transport",
|
|
requires_action: false,
|
|
transactions: [{ id: 9002, date: "2026-03-11T10:00:00.000Z", amount: 80, booked: true, excluded: false }],
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
meta: {},
|
|
},
|
|
],
|
|
invoice_per_order: [
|
|
{
|
|
id: 13,
|
|
customer_number: 7001,
|
|
customer_name: "Invoice Per Order Co",
|
|
requires_action: true,
|
|
transactions: [{ id: 9003, date: "2026-03-12T10:00:00.000Z", amount: 75, booked: false, excluded: false }],
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
meta: {},
|
|
},
|
|
],
|
|
fixed_pricing: [
|
|
{
|
|
id: 11,
|
|
customer_number: 4001,
|
|
customer_name: "Acme Fleet",
|
|
requires_action: true,
|
|
transactions: [{ id: 9005, date: "2026-03-31T10:00:00.000Z", amount: 1000, booked: true, excluded: false }],
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
meta: {
|
|
fixed_pricing: {
|
|
price: 1000,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
tank_cleaning: [],
|
|
special_arrangements: [],
|
|
vehicle_subscriptions: [
|
|
{
|
|
id: 14,
|
|
customer_number: 8001,
|
|
customer_name: "Subscription Movers",
|
|
requires_action: true,
|
|
transactions: [{ id: 9004, date: "2026-03-13T10:00:00.000Z", amount: 60, booked: false, excluded: false }],
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
meta: {},
|
|
},
|
|
],
|
|
possible_duplicates: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
function createFlaggedPeriodPayload({
|
|
resolvedManualFlagIds = [],
|
|
resolvedAutomaticFingerprints = [],
|
|
flaggedTransactionBooked = false,
|
|
flaggedCustomerRequiresAction = true,
|
|
includeHiddenFilteredFlag = false,
|
|
} = {}) {
|
|
const payload = createPeriodPayload();
|
|
payload.types.all[0].requires_action = flaggedCustomerRequiresAction;
|
|
payload.types.all[0].transactions = payload.types.all[0].transactions.map((transaction) => ({
|
|
...transaction,
|
|
booked: flaggedTransactionBooked,
|
|
date: "2026-06-09T10:00:00.000Z",
|
|
}));
|
|
const flags = [
|
|
{
|
|
id: 501,
|
|
source: "manual",
|
|
severity: "red",
|
|
status: "active",
|
|
target_type: "customer",
|
|
target_id: 4001,
|
|
customer_number: 4001,
|
|
reason: "Manual red flag for customer review.",
|
|
message: "Manual red flag for customer review.",
|
|
created_at: "2026-05-11 10:00:00",
|
|
created_by: 7,
|
|
created_by_name: "Jeppe",
|
|
},
|
|
{
|
|
id: "auto-order-reference-1",
|
|
source: "automatic",
|
|
severity: "yellow",
|
|
status: "active",
|
|
target_type: "order_field",
|
|
target_id: 9001,
|
|
field: "reference",
|
|
customer_number: 4001,
|
|
order_id: 9001,
|
|
order_item_id: null,
|
|
definition_key: "customer_rule_requires_reference",
|
|
fingerprint: "order-reference-fingerprint-1",
|
|
message_key: "invoice_period.flags.automatic.customer_rule_requires_reference",
|
|
message_params: {},
|
|
message: "Order is missing a required reference.",
|
|
context: {
|
|
department_id: 1,
|
|
order_id: 9001,
|
|
order_items: [
|
|
{ id: 7701, product_name: "Spot Free", quantity: 1, price: 99 },
|
|
{ id: 7702, product_name: "Wash", quantity: 1, price: 199 },
|
|
],
|
|
},
|
|
},
|
|
{
|
|
id: "auto-price-1",
|
|
source: "automatic",
|
|
severity: "yellow",
|
|
status: "active",
|
|
target_type: "order_item_field",
|
|
target_id: 7701,
|
|
field: "price",
|
|
customer_number: 4001,
|
|
order_id: 9001,
|
|
order_item_id: 7701,
|
|
definition_key: "price_mismatch",
|
|
fingerprint: "price-fingerprint-1",
|
|
message_key: "invoice_period.flags.automatic.price_mismatch",
|
|
message_params: {
|
|
product: "Spot Free",
|
|
},
|
|
message: "Spot Free product price differs from expected.",
|
|
message_parts: [
|
|
{ type: "order_item", text: "Spot Free" },
|
|
{ type: "text", text: " product price differs from " },
|
|
{ type: "expected_price", text: "expected" },
|
|
{ type: "text", text: "." },
|
|
],
|
|
context: {
|
|
department_id: 1,
|
|
order_id: 9001,
|
|
order_item_id: 7701,
|
|
order_items: [
|
|
{ id: 7701, product_name: "Spot Free", quantity: 1, price: 99 },
|
|
{ id: 7702, product_name: "Wash", quantity: 1, price: 199 },
|
|
],
|
|
expected_price_breakdown: {
|
|
product_price: 100,
|
|
department_price: 90,
|
|
product_discount_percentage: 10,
|
|
category_discount_percentage: 0,
|
|
applied_discount_percentage: 10,
|
|
expected_price: 81,
|
|
},
|
|
},
|
|
},
|
|
];
|
|
if (includeHiddenFilteredFlag) {
|
|
flags.push({
|
|
id: "auto-hidden-order-reference-1",
|
|
source: "automatic",
|
|
severity: "yellow",
|
|
status: "active",
|
|
target_type: "order_field",
|
|
target_id: 9006,
|
|
field: "reference",
|
|
customer_number: 4001,
|
|
order_id: 9006,
|
|
order_item_id: null,
|
|
invoice_collection_id: 16891,
|
|
definition_key: "customer_rule_requires_reference",
|
|
fingerprint: "hidden-order-reference-fingerprint-1",
|
|
message_key: "invoice_period.flags.automatic.customer_rule_requires_reference",
|
|
message_params: {},
|
|
message: "Hidden booked order is missing a required reference.",
|
|
context: {
|
|
department_id: 1,
|
|
order_id: 9006,
|
|
invoice_collection_id: 16891,
|
|
order_items: [{ id: 7716, product_name: "Hidden Wash", quantity: 1, price: 75 }],
|
|
},
|
|
});
|
|
}
|
|
const resolvedManualFlagIdSet = new Set(resolvedManualFlagIds.map((id) => String(id)));
|
|
const resolvedAutomaticFingerprintSet = new Set(
|
|
resolvedAutomaticFingerprints.map((fingerprint) => String(fingerprint))
|
|
);
|
|
const activeFlags = flags.filter((flag) => {
|
|
if (flag.source === "manual") {
|
|
return !resolvedManualFlagIdSet.has(String(flag.id));
|
|
}
|
|
return !resolvedAutomaticFingerprintSet.has(String(flag.fingerprint || flag.id));
|
|
});
|
|
const manualFlagCount = activeFlags.filter((flag) => flag.source === "manual").length;
|
|
const automaticFlagCount = activeFlags.filter((flag) => flag.source === "automatic").length;
|
|
const flaggedMetadata = {
|
|
status_indicator: manualFlagCount > 0 ? "flag_red" : automaticFlagCount > 0 ? "flag_yellow" : "circle_red",
|
|
flag_counts: {
|
|
manual: manualFlagCount,
|
|
automatic: automaticFlagCount,
|
|
total: manualFlagCount + automaticFlagCount,
|
|
},
|
|
flags: activeFlags,
|
|
};
|
|
|
|
payload.types.all[0] = {
|
|
...payload.types.all[0],
|
|
...flaggedMetadata,
|
|
};
|
|
payload.types.fixed_pricing[0] = {
|
|
...payload.types.fixed_pricing[0],
|
|
...flaggedMetadata,
|
|
};
|
|
|
|
return payload;
|
|
}
|
|
|
|
function createPossibleDuplicatesPeriodPayload({ dateFrom = "2026-05-11" } = {}) {
|
|
const duplicateDate = dateFrom || "2026-05-11";
|
|
const duplicateTransaction = (id, customerNumber, customerName, amount, hour) => ({
|
|
id,
|
|
amount,
|
|
booked: false,
|
|
excluded: false,
|
|
reg_1: customerNumber === 7201 ? "EC 21233" : "EC21233",
|
|
date: `${duplicateDate}T${hour}:00:00.000Z`,
|
|
created_at: `${duplicateDate} ${hour}:00:00`,
|
|
invoice_collection_id: id + 1000,
|
|
queue_status: null,
|
|
queue_job_id: null,
|
|
customer_number: customerNumber,
|
|
customer_name: customerName,
|
|
});
|
|
const duplicateCustomer = (id, customerNumber, customerName, transaction) => ({
|
|
id,
|
|
customer_number: customerNumber,
|
|
customer_name: customerName,
|
|
requires_action: true,
|
|
transactions: [transaction],
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
meta: {},
|
|
});
|
|
|
|
return {
|
|
types: {
|
|
all: [],
|
|
invoice_per_order: [],
|
|
fixed_pricing: [],
|
|
tank_cleaning: [],
|
|
special_arrangements: [],
|
|
vehicle_subscriptions: [],
|
|
possible_duplicates: [
|
|
duplicateCustomer(
|
|
71,
|
|
7201,
|
|
"Pleno Vognmandsforretning",
|
|
duplicateTransaction(9701, 7201, "Pleno Vognmandsforretning", 1523, "10")
|
|
),
|
|
duplicateCustomer(
|
|
72,
|
|
7202,
|
|
"Estland Alle ApS",
|
|
duplicateTransaction(9702, 7202, "Estland Alle ApS", 399, "11")
|
|
),
|
|
duplicateCustomer(73, 7203, "Single Wash ApS", {
|
|
...duplicateTransaction(9703, 7203, "Single Wash ApS", 50, "12"),
|
|
reg_1: "SINGLE1",
|
|
}),
|
|
],
|
|
},
|
|
};
|
|
}
|
|
|
|
function createPeriodPayloadForChangedRange() {
|
|
return {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 21,
|
|
customer_number: 5001,
|
|
customer_name: "April Logistics",
|
|
requires_action: true,
|
|
transactions: [{ id: 9101, date: "2026-04-10T10:00:00.000Z", amount: 250, booked: false, excluded: false }],
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
meta: {},
|
|
},
|
|
],
|
|
invoice_per_order: [
|
|
{
|
|
id: 22,
|
|
customer_number: 5002,
|
|
customer_name: "April Per Order",
|
|
requires_action: true,
|
|
transactions: [{ id: 9102, date: "2026-04-11T11:00:00.000Z", amount: 150, booked: false, excluded: false }],
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
meta: {},
|
|
},
|
|
],
|
|
fixed_pricing: [],
|
|
tank_cleaning: [],
|
|
special_arrangements: [],
|
|
vehicle_subscriptions: [],
|
|
possible_duplicates: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
function createQueuedPeriodPayload() {
|
|
return {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 31,
|
|
customer_number: 6001,
|
|
customer_name: "Queued Fleet",
|
|
requires_action: false,
|
|
transactions: [
|
|
{
|
|
id: 9201,
|
|
date: "2026-04-14T10:00:00.000Z",
|
|
amount: 210,
|
|
booked: false,
|
|
excluded: false,
|
|
invoice_collection_id: 14578,
|
|
queue_status: "QUEUED",
|
|
queue_job_id: 88,
|
|
},
|
|
],
|
|
queue: {
|
|
has_active_job: true,
|
|
statuses: ["QUEUED"],
|
|
invoice_collection_ids: [14578],
|
|
is_action_blocked: true,
|
|
},
|
|
meta: {},
|
|
},
|
|
],
|
|
invoice_per_order: [],
|
|
fixed_pricing: [],
|
|
tank_cleaning: [],
|
|
special_arrangements: [],
|
|
vehicle_subscriptions: [],
|
|
possible_duplicates: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
function createDraftBlockedPeriodPayload() {
|
|
return {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 41,
|
|
customer_number: 6101,
|
|
customer_name: "Drafted Fleet",
|
|
requires_action: false,
|
|
transactions: [
|
|
{
|
|
id: 9301,
|
|
date: "2026-04-14T10:00:00.000Z",
|
|
amount: 210,
|
|
booked: false,
|
|
excluded: false,
|
|
invoice_collection_id: 16501,
|
|
queue_status: null,
|
|
queue_job_id: null,
|
|
},
|
|
],
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
draft: {
|
|
has_valid_draft: true,
|
|
invoice_collection_ids: [16501],
|
|
is_action_blocked: true,
|
|
},
|
|
meta: {},
|
|
},
|
|
],
|
|
invoice_per_order: [],
|
|
fixed_pricing: [],
|
|
tank_cleaning: [],
|
|
special_arrangements: [],
|
|
vehicle_subscriptions: [],
|
|
possible_duplicates: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
function createStaleDraftPeriodPayload() {
|
|
return {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 42,
|
|
customer_number: 6102,
|
|
customer_name: "Deleted Draft Fleet",
|
|
requires_action: true,
|
|
transactions: [
|
|
{
|
|
id: 9302,
|
|
date: "2026-04-14T10:00:00.000Z",
|
|
amount: 210,
|
|
booked: false,
|
|
excluded: false,
|
|
invoice_collection_id: 16502,
|
|
queue_status: null,
|
|
queue_job_id: null,
|
|
},
|
|
],
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
draft: {
|
|
has_valid_draft: false,
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
meta: {},
|
|
},
|
|
],
|
|
invoice_per_order: [],
|
|
fixed_pricing: [],
|
|
tank_cleaning: [],
|
|
special_arrangements: [],
|
|
vehicle_subscriptions: [],
|
|
possible_duplicates: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
function createAttributeStackPeriodPayload() {
|
|
const customer = (customerNumber, customerName, transactionId, amount = 210) => ({
|
|
id: customerNumber,
|
|
customer_number: customerNumber,
|
|
customer_name: customerName,
|
|
requires_action: true,
|
|
transactions: [
|
|
{
|
|
id: transactionId,
|
|
date: "2026-03-14T10:00:00.000Z",
|
|
amount,
|
|
booked: false,
|
|
excluded: false,
|
|
invoice_collection_id: transactionId + 1000,
|
|
queue_status: null,
|
|
queue_job_id: null,
|
|
},
|
|
],
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
meta: {},
|
|
});
|
|
|
|
return {
|
|
types: {
|
|
all: [customer(4101, "Stacked Attributes Transport", 9401), customer(4102, "Single Attribute Logistics", 9402)],
|
|
invoice_per_order: [customer(4101, "Stacked Attributes Transport", 9401)],
|
|
fixed_pricing: [
|
|
customer(4101, "Stacked Attributes Transport", 9401),
|
|
customer(4102, "Single Attribute Logistics", 9402),
|
|
],
|
|
tank_cleaning: [customer(4101, "Stacked Attributes Transport", 9401)],
|
|
special_arrangements: [],
|
|
vehicle_subscriptions: [],
|
|
possible_duplicates: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
function createParallelInvoicePeriodPayload() {
|
|
const readyCustomer = (customerNumber, invoiceCollectionId, name) => ({
|
|
id: customerNumber,
|
|
customer_number: customerNumber,
|
|
customer_name: name,
|
|
requires_action: true,
|
|
transactions: [
|
|
{
|
|
id: invoiceCollectionId - 1000,
|
|
date: "2026-04-14T10:00:00.000Z",
|
|
amount: 210,
|
|
booked: false,
|
|
excluded: false,
|
|
invoice_collection_id: invoiceCollectionId,
|
|
queue_status: null,
|
|
queue_job_id: null,
|
|
},
|
|
],
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
meta: {},
|
|
});
|
|
|
|
return {
|
|
types: {
|
|
all: [
|
|
readyCustomer(4001, 55501, "Acme Fleet"),
|
|
readyCustomer(4003, 55503, "Baltic Freight"),
|
|
readyCustomer(4004, 55504, "Open Haulage"),
|
|
],
|
|
invoice_per_order: [],
|
|
fixed_pricing: [],
|
|
tank_cleaning: [],
|
|
special_arrangements: [],
|
|
vehicle_subscriptions: [],
|
|
possible_duplicates: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
function createParallelInvoiceTargetedPayload(customerNumbers) {
|
|
const completedCustomer = (customerNumber, name) => ({
|
|
id: customerNumber,
|
|
customer_number: customerNumber,
|
|
customer_name: name,
|
|
requires_action: false,
|
|
transactions: [
|
|
{
|
|
id: customerNumber + 100,
|
|
date: "2026-04-14T10:00:00.000Z",
|
|
amount: 210,
|
|
booked: true,
|
|
excluded: false,
|
|
invoice_collection_id: null,
|
|
queue_status: null,
|
|
queue_job_id: null,
|
|
},
|
|
],
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
meta: {},
|
|
});
|
|
|
|
const customersByNumber = {
|
|
4001: completedCustomer(4001, "Acme Fleet"),
|
|
4003: completedCustomer(4003, "Baltic Freight"),
|
|
};
|
|
|
|
return {
|
|
types: {
|
|
all: customerNumbers.map((customerNumber) => customersByNumber[customerNumber]).filter(Boolean),
|
|
invoice_per_order: [],
|
|
fixed_pricing: [],
|
|
tank_cleaning: [],
|
|
special_arrangements: [],
|
|
vehicle_subscriptions: [],
|
|
possible_duplicates: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
async function setupPeriodEndpoints(page, requests, options = {}) {
|
|
let initialRange = null;
|
|
|
|
await page.route("**/superuser/invoicing/period**", async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
const url = new URL(route.request().url());
|
|
if (!matchesApiPath(route.request().url(), "/superuser/invoicing/period")) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
const dateFrom = url.searchParams.get("dateFrom");
|
|
const dateTo = url.searchParams.get("dateTo");
|
|
const isWarmRequest = url.searchParams.get("periodWarm") === "1";
|
|
if (!isWarmRequest) {
|
|
requests.push({
|
|
dateFrom,
|
|
dateTo,
|
|
});
|
|
}
|
|
|
|
if (!initialRange && !isWarmRequest) {
|
|
initialRange = { dateFrom, dateTo };
|
|
}
|
|
|
|
const didChangeRange = !initialRange || dateFrom !== initialRange.dateFrom || dateTo !== initialRange.dateTo;
|
|
const isDistributionFixtureRange =
|
|
dateTo === "2026-03-31" && ["2026-02-28", "2026-03-01", "2026-03-02", "2026-03-03"].includes(dateFrom);
|
|
let payload;
|
|
|
|
if (options.payloadFactory) {
|
|
payload = await options.payloadFactory({
|
|
dateFrom,
|
|
dateTo,
|
|
didChangeRange,
|
|
isDistributionFixtureRange,
|
|
url,
|
|
});
|
|
} else if (didChangeRange && !isDistributionFixtureRange) {
|
|
payload = createPeriodPayloadForChangedRange();
|
|
} else {
|
|
payload = createPeriodPayload();
|
|
}
|
|
|
|
await route.fulfill(
|
|
json(
|
|
payload?.__rawResponse ?? {
|
|
data: payload,
|
|
}
|
|
)
|
|
);
|
|
});
|
|
|
|
await page.route("**/superuser/invoicing/period/distribution/fixed-pricing**", async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
includes: {
|
|
collective_fixed_pricing_results: {
|
|
total_fixed_price: 1000,
|
|
total_original_price: 400,
|
|
total_department_totals: {
|
|
1: 220,
|
|
2: 180,
|
|
},
|
|
total_department_totals_parsed: {
|
|
Copenhagen: 220,
|
|
Odense: 180,
|
|
},
|
|
total_department_totals_relative_parsed: {
|
|
Copenhagen: 600,
|
|
Odense: 400,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.route("**/superuser/invoicing/period/distribution/v2/booked-department-75**", async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
customers: [
|
|
{
|
|
customer_number: 4001,
|
|
customer_name: "Acme Fleet",
|
|
meta: {
|
|
booked_department_75: {
|
|
booked_net_amount: 1000,
|
|
distributed_net_amount: 1000,
|
|
undistributed_net_amount: 0,
|
|
department_distribution: {
|
|
1: 700,
|
|
2: 300,
|
|
},
|
|
booked_groups: [
|
|
{
|
|
month: "2026-03",
|
|
source_category: "fixed_pricing",
|
|
booked_net_amount: 1000,
|
|
department_distribution: {
|
|
1: 700,
|
|
2: 300,
|
|
},
|
|
undistributed_net_amount: 0,
|
|
},
|
|
{
|
|
month: "2026-03",
|
|
source_category: "wash_subscriptions",
|
|
booked_net_amount: 600,
|
|
department_distribution: {
|
|
1: 500,
|
|
2: 100,
|
|
},
|
|
undistributed_net_amount: 0,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
collective_results: {
|
|
booked_net_amount: 1600,
|
|
distributed_net_amount: 1600,
|
|
undistributed_net_amount: 0,
|
|
department_distribution: {
|
|
1: 1200,
|
|
2: 400,
|
|
},
|
|
},
|
|
warnings: [],
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.route("**/superuser/invoicing/period/distribution/wash-subscriptions**", async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
includes: {
|
|
collective_subscription_results: {
|
|
total_subscription_price: 300,
|
|
subscription_price_department_distribution: {
|
|
1: 200,
|
|
2: 100,
|
|
},
|
|
subscription_price_department_distribution_parsed: {
|
|
Copenhagen: 200,
|
|
Odense: 100,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.route("**/departments**", async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
const url = new URL(route.request().url());
|
|
if (!(url.pathname === "/departments" || url.pathname === "/api/departments")) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: [
|
|
{ id: 1, name: "Copenhagen" },
|
|
{ id: 2, name: "Odense" },
|
|
],
|
|
})
|
|
);
|
|
});
|
|
}
|
|
|
|
async function openPeriodView(page, options = {}) {
|
|
const periodRequests = [];
|
|
const token = "superuser-period-e2e-token";
|
|
await suppressVueDevtoolsOverlay(page);
|
|
await seedAuthenticatedState(page, token);
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
loginToken: token,
|
|
invoiceDistribution: true,
|
|
});
|
|
await setupPeriodEndpoints(page, periodRequests, options);
|
|
await page.goto("/superuser/invoices?activeTab=period", { waitUntil: "domcontentloaded" });
|
|
await expect(page).toHaveURL(/activeTab=period/);
|
|
await expect(page.getByTestId("invoicing-period-view-selector-all")).toBeVisible({
|
|
timeout: periodRouteReadyTimeout,
|
|
});
|
|
return { periodRequests };
|
|
}
|
|
|
|
async function setEntireMarchPeriod(page) {
|
|
const dateInputs = page.locator("[data-testid='invoicing-period-view'] input[type='date']:visible");
|
|
await expect(dateInputs.first()).toBeVisible();
|
|
await dateInputs.nth(0).fill("2026-03-01");
|
|
await dateInputs.nth(0).dispatchEvent("change");
|
|
await dateInputs.nth(1).fill("2026-03-31");
|
|
await dateInputs.nth(1).dispatchEvent("change");
|
|
await expect(page.getByTestId("invoicing-period-view-selector-fixed_pricing")).toBeVisible();
|
|
}
|
|
|
|
async function selectPeriodView(page, viewName) {
|
|
await page.getByTestId(`invoicing-period-view-selector-${viewName}`).click();
|
|
await expect(page.getByTestId("invoicing-period-view-all")).toHaveAttribute("data-current-view", viewName);
|
|
}
|
|
|
|
async function seedInvoicesPage(page, token = "superuser-period-e2e-token") {
|
|
await suppressVueDevtoolsOverlay(page);
|
|
await seedAuthenticatedState(page, token);
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
loginToken: token,
|
|
invoiceDistribution: true,
|
|
});
|
|
}
|
|
|
|
async function routeFlaggedPeriodOrders(
|
|
page,
|
|
{ invoiceCollectionId = null, includeHiddenCollectionOrder = false } = {}
|
|
) {
|
|
await page.route("**/orders**", async (route) => {
|
|
if (route.request().method() !== "GET" || !matchesApiPath(route.request().url(), "/orders")) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: [
|
|
{
|
|
id: 9001,
|
|
customer_id: 4001,
|
|
customer_name: "Acme Fleet",
|
|
cashier_id: 7,
|
|
cashier_name: "Copenhagen",
|
|
department_id: 1,
|
|
reference: "",
|
|
notes: "",
|
|
po: "",
|
|
reg_1: "AC4001",
|
|
reg_2: "",
|
|
reg_3: "",
|
|
created_at: "2026-03-10 10:00:00",
|
|
total_net_amount: 120,
|
|
invoice_collection_id: invoiceCollectionId,
|
|
invoice_collection: invoiceCollectionId
|
|
? {
|
|
id: invoiceCollectionId,
|
|
closed_at: "2026-03-10 10:30:00",
|
|
booked_invoice_id: 28368,
|
|
}
|
|
: null,
|
|
economic_invoice_module: null,
|
|
stripe_invoice_module: null,
|
|
error_message: null,
|
|
completed_at: null,
|
|
pending_handheld: false,
|
|
user_id: 11,
|
|
},
|
|
],
|
|
meta: {
|
|
pagination: {
|
|
limit: 20,
|
|
total: 1,
|
|
},
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
if (includeHiddenCollectionOrder && invoiceCollectionId) {
|
|
await page.route("**/collected-invoices**", async (route) => {
|
|
const url = new URL(route.request().url());
|
|
if (
|
|
route.request().method() !== "GET" ||
|
|
!matchesApiPath(route.request().url(), "/collected-invoices") ||
|
|
url.searchParams.get("id") !== String(invoiceCollectionId)
|
|
) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
id: invoiceCollectionId,
|
|
orders: [
|
|
{ id: 9001, invoice_collection_id: invoiceCollectionId },
|
|
{ id: 9006, invoice_collection_id: invoiceCollectionId },
|
|
],
|
|
},
|
|
})
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
async function routeFlaggedPeriodOrderItems(page) {
|
|
await page.route("**/order/items**", async (route) => {
|
|
if (route.request().method() !== "GET" || !matchesApiPath(route.request().url(), "/order/items")) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: [
|
|
{
|
|
id: 7701,
|
|
order_id: 9001,
|
|
product_id: 101,
|
|
reference: "",
|
|
notes: "",
|
|
price: 99,
|
|
quantity: 1,
|
|
related_item_id: 0,
|
|
include_in_invoice: true,
|
|
product: {
|
|
id: 101,
|
|
name: "Spot Free",
|
|
price: 100,
|
|
},
|
|
},
|
|
{
|
|
id: 7702,
|
|
order_id: 9001,
|
|
product_id: 102,
|
|
reference: "",
|
|
notes: "",
|
|
price: 199,
|
|
quantity: 1,
|
|
related_item_id: 0,
|
|
include_in_invoice: true,
|
|
product: {
|
|
id: 102,
|
|
name: "Wash",
|
|
price: 199,
|
|
},
|
|
},
|
|
],
|
|
})
|
|
);
|
|
});
|
|
}
|
|
|
|
async function getBoundingBox(locator, label) {
|
|
await expect(locator).toBeVisible();
|
|
const box = await locator.boundingBox();
|
|
if (!box) {
|
|
throw new Error(`${label} did not produce a bounding box`);
|
|
}
|
|
return box;
|
|
}
|
|
|
|
function boxesOverlap(firstBox, secondBox, tolerance = 1) {
|
|
const horizontalOverlap =
|
|
Math.max(firstBox.x, secondBox.x) <
|
|
Math.min(firstBox.x + firstBox.width, secondBox.x + secondBox.width) - tolerance;
|
|
const verticalOverlap =
|
|
Math.max(firstBox.y, secondBox.y) <
|
|
Math.min(firstBox.y + firstBox.height, secondBox.y + secondBox.height) - tolerance;
|
|
|
|
return horizontalOverlap && verticalOverlap;
|
|
}
|
|
|
|
function expectBoxInside(innerBox, outerBox, label, tolerance = 1) {
|
|
expect(innerBox.x, `${label} left edge`).toBeGreaterThanOrEqual(outerBox.x - tolerance);
|
|
expect(innerBox.y, `${label} top edge`).toBeGreaterThanOrEqual(outerBox.y - tolerance);
|
|
expect(innerBox.x + innerBox.width, `${label} right edge`).toBeLessThanOrEqual(
|
|
outerBox.x + outerBox.width + tolerance
|
|
);
|
|
expect(innerBox.y + innerBox.height, `${label} bottom edge`).toBeLessThanOrEqual(
|
|
outerBox.y + outerBox.height + tolerance
|
|
);
|
|
}
|
|
|
|
test.describe("Invoicing period tab", () => {
|
|
test("@smoke @pr period view does not throw queue refresh errors on load", async ({ page }) => {
|
|
const pageErrors = [];
|
|
page.on("pageerror", (error) => {
|
|
pageErrors.push(error.message);
|
|
});
|
|
|
|
await openPeriodView(page);
|
|
await expect(page.getByTestId("invoicing-period-view-selector-all")).toBeVisible();
|
|
await page.waitForTimeout(250);
|
|
|
|
expect(pageErrors).not.toEqual(expect.arrayContaining([expect.stringContaining("reading 'value'")]));
|
|
});
|
|
|
|
test("@smoke period view loads selectors and displays all-customer list", async ({ page }) => {
|
|
const { periodRequests } = await openPeriodView(page);
|
|
|
|
await expect(page.getByTestId("invoicing-period-view-selector-all")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-view-selector-invoice_per_order")).toBeVisible();
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
|
await expect(page.getByTestId("invoicing-period-view-all")).toHaveAttribute("data-current-view", "all");
|
|
await expect(page.getByTestId("invoicing-period-customer-4001")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-customer-4002")).toBeVisible();
|
|
expect(periodRequests.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("@smoke period view opens Selvvask import and attaching view", async ({ page }) => {
|
|
const usageOrderRequests = [];
|
|
const fastLinkRequests = [];
|
|
const automationAcceptRequests = [];
|
|
let automation = {
|
|
id: 7101,
|
|
status: "suggested",
|
|
action: "attach_order",
|
|
confidence: 0.93,
|
|
source: "fuzzy",
|
|
reason: "Samme primære produkt og relaterede tillæg matcher en ordre fra samme dag. Ordre #7001.",
|
|
matched_order_id: 7001,
|
|
created_order_id: null,
|
|
candidate_order: {
|
|
id: 7001,
|
|
department_id: 1,
|
|
created_at: "2026-03-10 12:03:00",
|
|
},
|
|
proposed_order: null,
|
|
can_accept: true,
|
|
can_deny: true,
|
|
};
|
|
|
|
await openPeriodView(page);
|
|
await setEntireMarchPeriod(page);
|
|
|
|
await page.route("**/modules/xlvask/services/usage/orders**", async (route) => {
|
|
const path = new URL(route.request().url()).pathname;
|
|
if (
|
|
route.request().method() === "POST" &&
|
|
path.endsWith("/modules/xlvask/services/usage/orders/8101/automation/accept")
|
|
) {
|
|
automationAcceptRequests.push(JSON.parse(route.request().postData() || "{}"));
|
|
automation = {
|
|
...automation,
|
|
status: "accepted",
|
|
can_accept: false,
|
|
can_deny: false,
|
|
};
|
|
await route.fulfill(json({ data: automation }));
|
|
return;
|
|
}
|
|
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
if (matchesApiPath(route.request().url(), "/modules/xlvask/services/usage/orders/fast-link")) {
|
|
fastLinkRequests.push(route.request().url());
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
order_items: [
|
|
{
|
|
id: 9101,
|
|
product_id: 301,
|
|
product: { name: "Kassevogn/varevogn" },
|
|
quantity: 1,
|
|
price: 125,
|
|
},
|
|
],
|
|
potential_duplicates: [],
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (!matchesApiPath(route.request().url(), "/modules/xlvask/services/usage/orders")) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
const url = new URL(route.request().url());
|
|
usageOrderRequests.push({
|
|
filters: url.searchParams.get("filters") || "",
|
|
limit: url.searchParams.get("limit") || "",
|
|
});
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: [
|
|
{
|
|
id: 8101,
|
|
reg_1: "AB12345",
|
|
created_at: "2026-03-10 12:00:00",
|
|
customer_id: 4001,
|
|
customer_number: 4001,
|
|
customer_name: "DEKRA AMU Center Hovedstaden A/S",
|
|
department_id: 1,
|
|
lane: 1,
|
|
wash_id: "wash-selvvask-1",
|
|
duplicates: [],
|
|
fast_link_key: "temporary_cache_selfwash8101",
|
|
total_net_amount: 125,
|
|
xlvask_primary_product_name: "Kassevogn/varevogn",
|
|
automation,
|
|
},
|
|
{
|
|
id: 8102,
|
|
reg_1: "CD67890",
|
|
created_at: "2026-03-10 12:15:00",
|
|
customer_id: 4002,
|
|
customer_number: 4002,
|
|
customer_name: "Self Wash Transport",
|
|
department_id: 2,
|
|
lane: 2,
|
|
wash_id: "wash-selvvask-2",
|
|
duplicates: [],
|
|
fast_link_key: null,
|
|
total_net_amount: 88,
|
|
xlvask_primary_product_name: "Varebil",
|
|
automation: {
|
|
id: 7102,
|
|
status: "failed",
|
|
action: "attach_order",
|
|
error: "No safe match",
|
|
can_accept: false,
|
|
can_deny: false,
|
|
},
|
|
},
|
|
],
|
|
meta: {
|
|
pagination: {
|
|
page: 1,
|
|
per_page: 10000,
|
|
total: 2,
|
|
},
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await expect(page.getByTestId("invoicing-period-view-selector-self_wash")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-view-selector-self_wash")).toContainText("Selvvask (0/1)");
|
|
await expect(page.getByTestId("invoicing-period-view-selector-progress-self_wash")).toBeVisible();
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-self_wash").click();
|
|
await expect(page.getByTestId("invoicing-period-self-wash-view")).toBeVisible();
|
|
const selfWashView = page.getByTestId("invoicing-period-self-wash-view");
|
|
await expect(selfWashView.locator("input[type='date']")).toHaveCount(0);
|
|
await expect(selfWashView.locator("input[placeholder='Søg i transaktioner']")).toHaveCount(0);
|
|
await expect(selfWashView.getByText("Per side")).toHaveCount(0);
|
|
await expect(selfWashView.getByText("Rækkefølge")).toHaveCount(0);
|
|
await expect(selfWashView.getByText("Dato fra")).toHaveCount(0);
|
|
await expect(selfWashView.getByText("Dato til")).toHaveCount(0);
|
|
await expect(selfWashView.getByText("Vis kun ikke tilknyttede vaske")).toHaveCount(0);
|
|
await expect(page.getByRole("heading", { name: /Selvvask/ })).toBeVisible();
|
|
await expect(page.getByText("AB12345")).toBeVisible();
|
|
await expect(page.getByText("CD67890")).toBeVisible();
|
|
await expect(selfWashView.locator(".xlvask-usage-price").first()).toContainText("125");
|
|
await expect(selfWashView.getByText("Kunne ikke behandles")).toHaveCount(0);
|
|
expect(fastLinkRequests).toHaveLength(0);
|
|
await expect(page.getByTestId("xlvask-automation-suggestion-8101")).toContainText("Foreslået: Tilknyt ordre #7001");
|
|
await expect(selfWashView.locator(".xlvask-usage-card-header").first()).toBeVisible();
|
|
await expect
|
|
.poll(() => page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth))
|
|
.toBeLessThanOrEqual(1);
|
|
|
|
await page.getByText("AB12345").click();
|
|
await expect(page.getByTestId("xlvask-automation-accept-8101")).toBeVisible();
|
|
await page.getByTestId("xlvask-automation-accept-8101").click();
|
|
await expect(page.getByTestId("xlvask-automation-suggestion-8101")).toContainText("Accepteret #7001");
|
|
expect(automationAcceptRequests).toEqual([{ suggestion_id: 7101 }]);
|
|
|
|
await expect.poll(() => usageOrderRequests.length).toBeGreaterThan(0);
|
|
expect(usageOrderRequests.some((request) => request.limit === "10000")).toBe(true);
|
|
expect(
|
|
usageOrderRequests.some(
|
|
(request) =>
|
|
request.filters.includes("StartTime-date_from:2026-03-01") &&
|
|
request.filters.includes("StartTime-date_to:2026-03-31")
|
|
)
|
|
).toBe(true);
|
|
});
|
|
|
|
test("@smoke period view shows flags and saves automatic flag decisions", async ({ page }) => {
|
|
const automaticStatusRequests = [];
|
|
const manualStatusRequests = [];
|
|
const resolvedManualFlagIds = new Set();
|
|
const resolvedAutomaticFingerprints = new Set();
|
|
|
|
await openPeriodView(page, {
|
|
payloadFactory: () =>
|
|
createFlaggedPeriodPayload({
|
|
resolvedManualFlagIds: Array.from(resolvedManualFlagIds),
|
|
resolvedAutomaticFingerprints: Array.from(resolvedAutomaticFingerprints),
|
|
flaggedTransactionBooked: true,
|
|
flaggedCustomerRequiresAction: false,
|
|
}),
|
|
});
|
|
|
|
await page.route("**/*", async (route) => {
|
|
const request = route.request();
|
|
const path = new URL(request.url()).pathname;
|
|
if (request.method() === "PATCH" && /\/superuser\/invoicing\/period\/flags\/\d+\/status$/.test(path)) {
|
|
manualStatusRequests.push({
|
|
path,
|
|
payload: JSON.parse(request.postData() || "{}"),
|
|
});
|
|
const flagId = Number(path.match(/\/flags\/(\d+)\/status$/)?.[1] || 0);
|
|
if (flagId > 0) {
|
|
resolvedManualFlagIds.add(flagId);
|
|
}
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
id: 501,
|
|
source: "manual",
|
|
status: "resolved",
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (request.method() !== "POST" || !path.endsWith("/superuser/invoicing/period/flags/automatic/status")) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
const payload = JSON.parse(request.postData() || "{}");
|
|
automaticStatusRequests.push(payload);
|
|
if (payload.fingerprint) {
|
|
resolvedAutomaticFingerprints.add(String(payload.fingerprint));
|
|
}
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
id: 9001,
|
|
source: "automatic",
|
|
status: "ignored",
|
|
},
|
|
})
|
|
);
|
|
});
|
|
await routeFlaggedPeriodOrders(page);
|
|
await routeFlaggedPeriodOrderItems(page);
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
|
|
|
const customerRow = page.getByTestId("invoicing-period-customer-4001");
|
|
await expect(customerRow.locator(".color-indicator i.fa-flag").first()).toHaveClass(/has-text-danger/);
|
|
await expect(customerRow.getByText("Manual red flag for customer review.")).toBeVisible();
|
|
await expect(customerRow.getByText(/Alle bogført|All booked/i)).toHaveCount(0);
|
|
await expect(customerRow.getByText(/Spot Free/)).toHaveCount(0);
|
|
const manualFlag = page.getByTestId("invoice-period-flag-501");
|
|
await manualFlag.locator(".invoice-period-flag-row__icon").hover();
|
|
const manualFlagTooltip = page.getByTestId("invoice-period-flag-created-tooltip-501");
|
|
await expect(manualFlagTooltip).toContainText("Oprettet");
|
|
await expect(manualFlagTooltip).toContainText("Af");
|
|
await expect(manualFlagTooltip).toContainText("Jeppe");
|
|
await manualFlag.hover();
|
|
await manualFlag.getByTitle(/Resolved|Løst/i).click();
|
|
await page.locator(".swal2-textarea").fill("Handled manually");
|
|
await page.locator(".swal2-confirm").click();
|
|
await expect(manualFlag).toHaveCount(0);
|
|
await expect(customerRow.locator(".color-indicator i.fa-flag").first()).toHaveClass(/has-text-warning/);
|
|
|
|
await customerRow.getByText("Acme Fleet").click();
|
|
await expect(page.getByTestId("invoicing-period-customer-expanded-4001")).toBeVisible();
|
|
const orderFlag = page.getByTestId("invoice-period-flag-auto-order-reference-1");
|
|
await expect(orderFlag).toBeVisible();
|
|
await expect(orderFlag).toContainText("Ordren mangler en påkrævet reference.");
|
|
const orderFlagToken = orderFlag.locator(".invoice-period-flag-token", { hasText: "Ordren" });
|
|
await expect(orderFlagToken).toBeVisible();
|
|
await orderFlagToken.hover();
|
|
await expect(page.getByText("Ordrelinjer").last()).toBeVisible();
|
|
await expect(page.getByText("Spot Free").last()).toBeVisible();
|
|
const orderPopupPromise = page.waitForEvent("popup");
|
|
await orderFlagToken.click();
|
|
const orderPopup = await orderPopupPromise;
|
|
await expect(orderPopup).toHaveURL(/\/admin\/1\/modules\/pos\/orders\/9001/);
|
|
await orderPopup.close();
|
|
await orderFlag.hover();
|
|
await orderFlag.getByTitle(/Ignored|Ignoreret/i).click();
|
|
await page.locator(".swal2-textarea").fill("Accepted reference warning");
|
|
await page.locator(".swal2-confirm").click();
|
|
await expect(orderFlag).toHaveCount(0);
|
|
if ((await page.getByTestId("invoicing-period-customer-expanded-4001").count()) === 0) {
|
|
await customerRow.getByText("Acme Fleet").click();
|
|
await expect(page.getByTestId("invoicing-period-customer-expanded-4001")).toBeVisible();
|
|
}
|
|
|
|
const flaggedOrderRow = page
|
|
.getByTestId("pos-order-expand-9001")
|
|
.locator("xpath=ancestor::tr | ancestor::div[contains(@class, 'pos-orders-mobile-card')]");
|
|
await expect(flaggedOrderRow.locator(".color-indicator i.fa-flag").first()).toHaveClass(/has-text-warning/);
|
|
|
|
await page.getByTestId("pos-order-expand-9001").click();
|
|
|
|
const orderItemFlagIndicator = page.getByTestId("order-content-item-flag-indicator-7701").first();
|
|
await expect(orderItemFlagIndicator).toBeVisible();
|
|
await expect(orderItemFlagIndicator).toHaveClass(/has-text-warning/);
|
|
await expect(orderItemFlagIndicator.locator("i")).toHaveClass(/fa-flag/);
|
|
|
|
const automaticFlag = page.getByTestId("invoice-period-flag-auto-price-1");
|
|
await expect(automaticFlag).toBeVisible();
|
|
await automaticFlag.hover();
|
|
await automaticFlag.getByTitle(/Ignored|Ignoreret/i).click();
|
|
await page.locator(".swal2-textarea").fill("Accepted for this period");
|
|
await page.locator(".swal2-confirm").click();
|
|
await expect(automaticFlag).toHaveCount(0);
|
|
await expect(page.getByTestId("order-content-item-flag-indicator-7701")).toHaveCount(0);
|
|
await expect(customerRow.locator(".color-indicator i.fa-flag")).toHaveCount(0);
|
|
await expect(customerRow.locator(".color-indicator i.fa-circle").first()).toHaveClass(/has-text-success/);
|
|
|
|
await expect
|
|
.poll(() => manualStatusRequests.map((request) => request.payload))
|
|
.toEqual([
|
|
{
|
|
status: "resolved",
|
|
reason: "Handled manually",
|
|
},
|
|
]);
|
|
await expect
|
|
.poll(() => automaticStatusRequests)
|
|
.toEqual([
|
|
{
|
|
fingerprint: "order-reference-fingerprint-1",
|
|
status: "ignored",
|
|
target_type: "order_field",
|
|
target_id: 9001,
|
|
field: "reference",
|
|
definition_key: "customer_rule_requires_reference",
|
|
reason: "Accepted reference warning",
|
|
},
|
|
{
|
|
fingerprint: "price-fingerprint-1",
|
|
status: "ignored",
|
|
target_type: "order_item_field",
|
|
target_id: 7701,
|
|
field: "price",
|
|
definition_key: "price_mismatch",
|
|
reason: "Accepted for this period",
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("@smoke period expanded booked collection shows hidden flag reasons", async ({ page }) => {
|
|
await openPeriodView(page, {
|
|
payloadFactory: () =>
|
|
createFlaggedPeriodPayload({
|
|
resolvedManualFlagIds: [501],
|
|
resolvedAutomaticFingerprints: ["order-reference-fingerprint-1", "price-fingerprint-1"],
|
|
flaggedTransactionBooked: true,
|
|
flaggedCustomerRequiresAction: false,
|
|
includeHiddenFilteredFlag: true,
|
|
}),
|
|
});
|
|
await routeFlaggedPeriodOrders(page, {
|
|
invoiceCollectionId: 16891,
|
|
includeHiddenCollectionOrder: true,
|
|
});
|
|
await routeFlaggedPeriodOrderItems(page);
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
|
|
|
const customerRow = page.getByTestId("invoicing-period-customer-4001");
|
|
await expect(customerRow.locator(".color-indicator i.fa-flag").first()).toHaveClass(/has-text-warning/);
|
|
await expect(customerRow.getByText(/Alle bogført|All booked/i)).toHaveCount(0);
|
|
|
|
await customerRow.getByText("Acme Fleet").click();
|
|
await expect(page.getByTestId("invoicing-period-customer-expanded-4001")).toBeVisible();
|
|
await expect(page.getByTestId("pos-order-invoice-collection-summary-16891")).toContainText(
|
|
/Faktura samling ID: 16891|Invoice collection ID: 16891/i
|
|
);
|
|
await expect(page.getByTestId("invoicing-period-customer-expanded-4001")).toContainText(
|
|
/Viser 1 vaske|Showing 1 wash/i
|
|
);
|
|
});
|
|
|
|
test("@smoke period customer unfolding is single-open and action wheel shows active state", async ({ page }) => {
|
|
await page.setViewportSize({ width: 1900, height: 900 });
|
|
await openPeriodView(page);
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
|
await expect(page.getByTestId("invoicing-period-customer-4001")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-customer-4002")).toBeVisible();
|
|
|
|
const firstCustomer = page.getByTestId("invoicing-period-customer-4001");
|
|
const secondCustomer = page.getByTestId("invoicing-period-customer-4002");
|
|
|
|
await firstCustomer.getByText("Acme Fleet").click();
|
|
await expect(page.getByTestId("invoicing-period-customer-expanded-4001")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-customer-expanded-4002")).toHaveCount(0);
|
|
|
|
await secondCustomer.getByText("Nordic Transport").click();
|
|
await expect(page.getByTestId("invoicing-period-customer-expanded-4001")).toHaveCount(0);
|
|
await expect(page.getByTestId("invoicing-period-customer-expanded-4002")).toBeVisible();
|
|
|
|
const firstWheel = firstCustomer.locator(".action-settings-wheel-trigger").first();
|
|
const secondWheel = secondCustomer.locator(".action-settings-wheel-trigger").first();
|
|
|
|
await firstWheel.click();
|
|
await expect(firstWheel).toHaveClass(/action-settings-wheel-trigger--active/);
|
|
await expect(firstWheel).toHaveAttribute("aria-expanded", "true");
|
|
|
|
const sectionIds = await firstWheel
|
|
.locator("xpath=ancestor::div[contains(@class, 'dropdown')]")
|
|
.getByTestId("action-settings-wheel-sections")
|
|
.locator("[data-testid^='action-settings-wheel-section-']")
|
|
.evaluateAll((sections) => sections.map((section) => section.getAttribute("data-testid")));
|
|
expect(sectionIds.at(-1)).toBe("action-settings-wheel-section-invoice-period-flags");
|
|
|
|
const isFlatLayout = (await page.getByTestId("action-settings-wheel-flyout").count()) === 0;
|
|
|
|
if (isFlatLayout) {
|
|
const customerFlagAction = firstCustomer.locator("button.dropdown-item-action").filter({ hasText: /^Kunde$/ });
|
|
await expect(customerFlagAction).toBeVisible();
|
|
} else {
|
|
await firstCustomer.getByTestId("action-settings-wheel-section-invoice-period-flags").hover();
|
|
const flagSubmenu = page.getByTestId("action-settings-wheel-submenu-invoice-period-flags");
|
|
const customerFlagAction = flagSubmenu.locator("button.dropdown-item-action", { hasText: "Kunde" });
|
|
await expect(customerFlagAction).toBeVisible();
|
|
await expect(flagSubmenu).not.toContainText(/Dette m.*l/);
|
|
}
|
|
|
|
await secondWheel.click();
|
|
await expect(firstWheel).not.toHaveClass(/action-settings-wheel-trigger--active/);
|
|
await expect(firstWheel).toHaveAttribute("aria-expanded", "false");
|
|
await expect(secondWheel).toHaveClass(/action-settings-wheel-trigger--active/);
|
|
await expect(secondWheel).toHaveAttribute("aria-expanded", "true");
|
|
});
|
|
|
|
test("@smoke period expanded order filters are hidden until requested", async ({ page }) => {
|
|
await openPeriodView(page);
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
|
const firstCustomer = page.getByTestId("invoicing-period-customer-4001");
|
|
await expect(firstCustomer).toBeVisible();
|
|
|
|
await firstCustomer.getByText("Acme Fleet").click();
|
|
await expect(page.getByTestId("invoicing-period-customer-expanded-4001")).toBeVisible();
|
|
|
|
const filterToggle = page.getByTestId("invoice-orders-toggle-filters");
|
|
await expect(filterToggle).toBeVisible();
|
|
await expect(filterToggle).toHaveAttribute("aria-expanded", "false");
|
|
await expect(page.getByTestId("table-labeled-pagination-filters")).toHaveCount(0);
|
|
|
|
await filterToggle.click();
|
|
await expect(filterToggle).toHaveAttribute("aria-expanded", "true");
|
|
await expect(page.getByTestId("table-labeled-pagination-filters")).toBeVisible();
|
|
|
|
await filterToggle.click();
|
|
await expect(filterToggle).toHaveAttribute("aria-expanded", "false");
|
|
await expect(page.getByTestId("table-labeled-pagination-filters")).toHaveCount(0);
|
|
});
|
|
|
|
test("@smoke period possible duplicates are grouped by license plate and date", async ({ page }) => {
|
|
const pageErrors = [];
|
|
page.on("pageerror", (error) => {
|
|
pageErrors.push(error.message);
|
|
});
|
|
await page.clock.setFixedTime(new Date("2026-05-11T10:00:00.000Z"));
|
|
await openPeriodView(page, { payloadFactory: createPossibleDuplicatesPeriodPayload });
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-possible_duplicates").click({
|
|
position: { x: 12, y: 12 },
|
|
});
|
|
await page.waitForTimeout(100);
|
|
expect(pageErrors).toEqual([]);
|
|
await expect(page.getByTestId("invoicing-period-view-selector-possible_duplicates")).toHaveClass(
|
|
/is-selected-view/
|
|
);
|
|
await expect(page.getByTestId("invoicing-period-view-all")).toHaveAttribute(
|
|
"data-current-view",
|
|
"possible_duplicates"
|
|
);
|
|
|
|
const duplicateSelector = page.getByTestId("invoicing-period-view-selector-possible_duplicates");
|
|
await expect(duplicateSelector).toContainText(/Mulige|Possible/i);
|
|
await expect(duplicateSelector).toContainText("(0/1)");
|
|
|
|
const duplicateGroup = page.getByTestId("invoicing-period-duplicate-group-EC21233-2026-05-11");
|
|
await expect(duplicateGroup).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-customer-7201")).toHaveCount(0);
|
|
await expect(page.getByTestId("invoicing-period-customer-7202")).toHaveCount(0);
|
|
await expect(duplicateGroup).toContainText("EC21233");
|
|
await expect(duplicateGroup).toContainText("2 kunder");
|
|
await expect(duplicateGroup).toContainText("2 vaskelog");
|
|
await expect(duplicateGroup).not.toContainText("EC21233 - 11/05/2026");
|
|
await expect(duplicateGroup).toContainText("Pleno Vognmandsforretning #7201");
|
|
await expect(duplicateGroup).toContainText("Estland Alle ApS #7202");
|
|
await expect(page.getByTestId("invoicing-period-duplicate-group-SINGLE1-2026-05-11")).toHaveCount(0);
|
|
|
|
const plateTag = page.getByTestId("invoicing-period-duplicate-plate-tag-EC21233-2026-05-11");
|
|
const dateTag = page.getByTestId("invoicing-period-duplicate-date-tag-EC21233-2026-05-11");
|
|
const plateBox = await plateTag.boundingBox();
|
|
const dateBox = await dateTag.boundingBox();
|
|
expect(plateBox).not.toBeNull();
|
|
expect(dateBox).not.toBeNull();
|
|
expect(Math.abs((plateBox?.x ?? 0) - (dateBox?.x ?? 0))).toBeLessThan(4);
|
|
expect(dateBox?.y ?? 0).toBeGreaterThan(plateBox?.y ?? 0);
|
|
|
|
await duplicateGroup.click();
|
|
const expandedGroup = page.getByTestId("invoicing-period-duplicate-group-expanded-EC21233-2026-05-11");
|
|
await expect(expandedGroup.locator("[data-testid='pagination-search-input']")).toHaveCount(0);
|
|
await expect(expandedGroup.locator("[data-testid='pagination-reload-actions']")).toHaveCount(0);
|
|
await expect(expandedGroup.locator("nav.pagination")).toHaveCount(0);
|
|
await expect(expandedGroup.getByText(/Side\s+\d+\s+af/i)).toHaveCount(0);
|
|
const comparison = page.getByTestId("invoicing-period-duplicate-comparison-EC21233-2026-05-11");
|
|
await expect(comparison).toBeVisible();
|
|
await expect(comparison.getByTestId("invoicing-period-duplicate-comparison-row-9701")).toContainText(
|
|
"Pleno Vognmandsforretning #7201"
|
|
);
|
|
await expect(comparison.getByTestId("invoicing-period-duplicate-comparison-row-9702")).toContainText(
|
|
"Estland Alle ApS #7202"
|
|
);
|
|
await expect(comparison.getByText("EC21233")).toHaveCount(2);
|
|
});
|
|
|
|
test("@smoke period customer attribute tags stack vertically without resizing cards", async ({ page }) => {
|
|
await openPeriodView(page, { payloadFactory: createAttributeStackPeriodPayload });
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
|
|
|
const multiAttributeCard = page.getByTestId("invoicing-period-customer-4101");
|
|
const singleAttributeCard = page.getByTestId("invoicing-period-customer-4102");
|
|
const attributeStack = page.getByTestId("invoicing-period-customer-attributes-4101");
|
|
const tagLocators = ["invoice_per_order", "fixed_pricing", "tank_cleaning"].map((viewKey) =>
|
|
page.getByTestId(`invoicing-period-customer-attribute-4101-${viewKey}`)
|
|
);
|
|
|
|
const multiCardBox = await getBoundingBox(multiAttributeCard, "multi-attribute customer card");
|
|
const singleCardBox = await getBoundingBox(singleAttributeCard, "single-attribute customer card");
|
|
const stackBox = await getBoundingBox(attributeStack, "customer attribute stack");
|
|
const tagBoxes = [];
|
|
|
|
for (const [index, tagLocator] of tagLocators.entries()) {
|
|
const tagBox = await getBoundingBox(tagLocator, `customer attribute tag ${index + 1}`);
|
|
const fontSize = await tagLocator.evaluate((element) => {
|
|
return Number.parseFloat(window.getComputedStyle(element).fontSize || "0");
|
|
});
|
|
tagBoxes.push(tagBox);
|
|
expectBoxInside(tagBox, stackBox, `customer attribute tag ${index + 1}`);
|
|
expect(fontSize, `customer attribute tag ${index + 1} font size`).toBeGreaterThanOrEqual(10);
|
|
}
|
|
|
|
expect(Math.abs(multiCardBox.height - singleCardBox.height)).toBeLessThanOrEqual(3);
|
|
|
|
tagBoxes.slice(1).forEach((tagBox, index) => {
|
|
expect(Math.abs(tagBox.x - tagBoxes[0].x)).toBeLessThanOrEqual(1);
|
|
expect(tagBox.y).toBeGreaterThan(tagBoxes[index].y);
|
|
});
|
|
|
|
const controlLocators = [
|
|
multiAttributeCard.getByText(/1\s+(Transaktion|Transaction|order|orders|ordre|Vaskelog)/i).first(),
|
|
multiAttributeCard.getByText(/210/).first(),
|
|
page.getByTestId("invoicing-period-customer-invoice-4101"),
|
|
multiAttributeCard.locator(".action-settings-wheel-trigger").first(),
|
|
];
|
|
|
|
for (const [index, controlLocator] of controlLocators.entries()) {
|
|
const controlBox = await getBoundingBox(controlLocator, `customer row control ${index + 1}`);
|
|
expect(boxesOverlap(stackBox, controlBox), `attribute stack overlaps row control ${index + 1}`).toBe(false);
|
|
}
|
|
});
|
|
|
|
test("@smoke period view selector switch updates visible customer set", async ({ page }) => {
|
|
await openPeriodView(page);
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-invoice_per_order").click();
|
|
await expect(page.getByTestId("invoicing-period-view-all")).toHaveAttribute(
|
|
"data-current-view",
|
|
"invoice_per_order"
|
|
);
|
|
await expect(page.getByTestId("invoicing-period-customer-7001")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-customer-4001")).toHaveCount(0);
|
|
await expect(page).toHaveURL(/activeTab=period/);
|
|
});
|
|
|
|
test("@smoke period distribution calculations handle department 75 payloads in browser", async ({ page }) => {
|
|
await page.goto("/");
|
|
|
|
const result = await page.evaluate(async () => {
|
|
const module = await import(
|
|
"/src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/invoicingBillingPeriodDistributionCalculations.js"
|
|
);
|
|
|
|
const subscriptionDistribution = {
|
|
total_subscription_price: 300,
|
|
subscription_price_department_distribution: {
|
|
1: 200,
|
|
2: 100,
|
|
},
|
|
};
|
|
const bookedDepartment75Distribution = {
|
|
customers: [
|
|
{
|
|
meta: {
|
|
booked_department_75: {
|
|
booked_groups: [
|
|
{
|
|
source_category: "fixed_pricing",
|
|
booked_net_amount: 1000,
|
|
department_distribution: {
|
|
1: 700,
|
|
2: 300,
|
|
},
|
|
},
|
|
{
|
|
source_category: "wash_subscriptions",
|
|
booked_net_amount: 600,
|
|
department_distribution: {
|
|
1: 500,
|
|
2: 100,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
return {
|
|
subscriptionBookedCopenhagen: module.getDepartmentRelativeBookedAmount(
|
|
{ id: 1, name: "Copenhagen" },
|
|
subscriptionDistribution,
|
|
600
|
|
),
|
|
fixedPricingBookedCopenhagen: module.getBookedDepartment75SourceDepartmentAmount(
|
|
{ id: 1, name: "Copenhagen" },
|
|
bookedDepartment75Distribution,
|
|
"fixed_pricing"
|
|
),
|
|
washSubscriptionBookedOdense: module.getBookedDepartment75SourceDepartmentAmount(
|
|
{ id: 2, name: "Odense" },
|
|
bookedDepartment75Distribution,
|
|
"wash_subscriptions"
|
|
),
|
|
};
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
subscriptionBookedCopenhagen: 400,
|
|
fixedPricingBookedCopenhagen: 700,
|
|
washSubscriptionBookedOdense: 100,
|
|
});
|
|
});
|
|
|
|
test("@smoke period all view shows combined department 75 distribution", async ({ page }) => {
|
|
await openPeriodView(page);
|
|
|
|
await setEntireMarchPeriod(page);
|
|
await selectPeriodView(page, "all");
|
|
|
|
const combinedTable = page.getByTestId("department-75-combined-distribution-table");
|
|
await expect(combinedTable).toBeVisible();
|
|
const combinedTableContainer = page.getByTestId("department-75-combined-distribution-table-container");
|
|
await expect(combinedTableContainer).toBeVisible();
|
|
|
|
const copenhagenCells = combinedTable.locator("tr", { hasText: "Copenhagen" }).locator("td");
|
|
const odenseCells = combinedTable.locator("tr", { hasText: "Odense" }).locator("td");
|
|
const totalCells = combinedTable.locator("tr", { hasText: "Total" }).locator("td");
|
|
|
|
await expect(copenhagenCells.nth(1)).toContainText("220");
|
|
await expect(copenhagenCells.nth(2)).toContainText("600");
|
|
await expect(copenhagenCells.nth(3)).toContainText("200");
|
|
await expect(copenhagenCells.nth(4)).toContainText("800");
|
|
await expect(copenhagenCells.nth(5)).toContainText("700");
|
|
await expect(copenhagenCells.nth(6)).toContainText("500");
|
|
await expect(copenhagenCells.nth(7)).toContainText("1.200");
|
|
|
|
await expect(odenseCells.nth(5)).toContainText("300");
|
|
await expect(odenseCells.nth(6)).toContainText("100");
|
|
await expect(odenseCells.nth(7)).toContainText("400");
|
|
|
|
await expect(totalCells.nth(4)).toContainText("1.300");
|
|
await expect(totalCells.nth(7)).toContainText("1.600");
|
|
|
|
const isDesktop = page.viewportSize().width >= 1024;
|
|
if (isDesktop) {
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(() => {
|
|
const tableContainer = document.querySelector(
|
|
'[data-testid="department-75-combined-distribution-table-container"]'
|
|
);
|
|
const selector = document.querySelector('[data-testid="invoicing-period-view-selector-all"]');
|
|
if (!tableContainer || !selector) {
|
|
return false;
|
|
}
|
|
|
|
const tableContainerRect = tableContainer.getBoundingClientRect();
|
|
const selectorRect = selector.getBoundingClientRect();
|
|
return (
|
|
tableContainer.scrollWidth - tableContainer.clientWidth <= 2 &&
|
|
tableContainerRect.right <= selectorRect.left - 8
|
|
);
|
|
})
|
|
)
|
|
.toBe(true);
|
|
}
|
|
});
|
|
|
|
test("@smoke period subscription distribution uses e-conomic booked department 75 amounts", async ({ page }) => {
|
|
await openPeriodView(page);
|
|
|
|
await setEntireMarchPeriod(page);
|
|
await selectPeriodView(page, "vehicle_subscriptions");
|
|
|
|
const subscriptionTable = page.locator("table").filter({ hasText: "Abonnementspris" });
|
|
await expect(subscriptionTable).toBeVisible();
|
|
await expect(subscriptionTable.locator("tr", { hasText: "Copenhagen" }).locator("td").nth(3)).toContainText("500");
|
|
await expect(subscriptionTable.locator("tr", { hasText: "Odense" }).locator("td").nth(3)).toContainText("100");
|
|
});
|
|
|
|
test("@smoke period fixed pricing uses e-conomic booked department 75 amounts", async ({ page }) => {
|
|
await openPeriodView(page);
|
|
|
|
await setEntireMarchPeriod(page);
|
|
await selectPeriodView(page, "fixed_pricing");
|
|
|
|
const fixedPricingTable = page.locator("table").filter({ hasText: "Original pris" });
|
|
await expect(fixedPricingTable).toBeVisible();
|
|
|
|
const copenhagenBookedCell = fixedPricingTable.locator("tr", { hasText: "Copenhagen" }).locator("td").nth(5);
|
|
const odenseBookedCell = fixedPricingTable.locator("tr", { hasText: "Odense" }).locator("td").nth(5);
|
|
|
|
await expect(copenhagenBookedCell).toContainText("700");
|
|
await expect(odenseBookedCell).toContainText("300");
|
|
await expect(copenhagenBookedCell).not.toContainText("550");
|
|
await expect(odenseBookedCell).not.toContainText("450");
|
|
});
|
|
|
|
test("@smoke period view invoices required-action customers without opening collection tab", async ({ page }) => {
|
|
await openPeriodView(page);
|
|
|
|
const orderLookups = [];
|
|
const economicInvoiceRequests = [];
|
|
const popupUrls = [];
|
|
|
|
page.on("popup", (popup) => {
|
|
popupUrls.push(popup.url());
|
|
});
|
|
|
|
await page.route("**/order**", async (route) => {
|
|
if (route.request().method() !== "GET" || !matchesApiPath(route.request().url(), "/order")) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
const url = new URL(route.request().url());
|
|
const orderId = Number(url.searchParams.get("id") || 0);
|
|
orderLookups.push(orderId);
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: {
|
|
id: orderId,
|
|
invoice_collection_id: 55501,
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.route("**/collected-invoices/economic**", async (route) => {
|
|
if (matchesApiPath(route.request().url(), "/collected-invoices/economic/queue/status")) {
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
job: {
|
|
id: 77001,
|
|
status: "COMPLETED",
|
|
progress_percent: 100,
|
|
progress_message: "Done",
|
|
result: {},
|
|
},
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (
|
|
route.request().method() !== "POST" ||
|
|
!matchesApiPath(route.request().url(), "/collected-invoices/economic")
|
|
) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
const payload = JSON.parse(route.request().postData() || "{}");
|
|
economicInvoiceRequests.push(payload);
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
job: {
|
|
id: 77001,
|
|
status: "QUEUED",
|
|
progress_percent: 0,
|
|
progress_message: "Queued",
|
|
result: null,
|
|
},
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
|
await expect(page.getByTestId("invoicing-period-customer-4001")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-customer-invoice-4001")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-customer-4002")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-customer-invoice-4002")).toHaveCount(0);
|
|
|
|
await page.getByTestId("invoicing-period-customer-invoice-4001").click();
|
|
|
|
await expect.poll(() => orderLookups).toEqual([9001]);
|
|
await expect.poll(() => economicInvoiceRequests.length).toBe(1);
|
|
expect(economicInvoiceRequests[0]).toEqual({
|
|
id: 55501,
|
|
send_as_is: false,
|
|
});
|
|
await page.waitForTimeout(250);
|
|
expect(popupUrls).toEqual([]);
|
|
await expect(page).toHaveURL(/activeTab=period/);
|
|
});
|
|
|
|
test("@smoke period view invoices multiple customers with page refresh and independent loading", async ({ page }) => {
|
|
const token = "superuser-period-parallel-token";
|
|
const periodRequests = [];
|
|
const economicInvoiceRequests = [];
|
|
const statusResolvers = new Map();
|
|
|
|
await suppressVueDevtoolsOverlay(page);
|
|
await seedAuthenticatedState(page, token);
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: ["superuser", "user"],
|
|
loginToken: token,
|
|
invoiceDistribution: true,
|
|
});
|
|
|
|
await page.route("**/superuser/invoicing/period**", async (route) => {
|
|
if (route.request().method() !== "GET" || !matchesApiPath(route.request().url(), "/superuser/invoicing/period")) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
const url = new URL(route.request().url());
|
|
const customerNumbers =
|
|
url.searchParams
|
|
.get("customerNumbers")
|
|
?.split(",")
|
|
.map((value) => Number(value))
|
|
.filter((value) => Number.isInteger(value) && value > 0) ?? [];
|
|
|
|
periodRequests.push({
|
|
dateFrom: url.searchParams.get("dateFrom"),
|
|
dateTo: url.searchParams.get("dateTo"),
|
|
customerNumbers: url.searchParams.get("customerNumbers"),
|
|
});
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data:
|
|
customerNumbers.length > 0
|
|
? createParallelInvoiceTargetedPayload(customerNumbers)
|
|
: createParallelInvoicePeriodPayload(),
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.route("**/collected-invoices/economic/queue/status**", async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
const url = new URL(route.request().url());
|
|
const jobId = Number(url.searchParams.get("job_id") || 0);
|
|
await new Promise((resolve) => {
|
|
statusResolvers.set(jobId, resolve);
|
|
});
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
job: {
|
|
id: jobId,
|
|
status: "COMPLETED",
|
|
progress_percent: 100,
|
|
progress_message: "Done",
|
|
result: {},
|
|
},
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.route("**/collected-invoices/economic**", async (route) => {
|
|
if (
|
|
route.request().method() !== "POST" ||
|
|
!matchesApiPath(route.request().url(), "/collected-invoices/economic")
|
|
) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
const payload = JSON.parse(route.request().postData() || "{}");
|
|
economicInvoiceRequests.push(payload);
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
job: {
|
|
id: payload.id === 55501 ? 77001 : 77003,
|
|
status: "QUEUED",
|
|
progress_percent: 0,
|
|
progress_message: "Queued",
|
|
result: null,
|
|
},
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.route("**/departments**", async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
const url = new URL(route.request().url());
|
|
if (!(url.pathname === "/departments" || url.pathname === "/api/departments")) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: [
|
|
{ id: 1, name: "Copenhagen" },
|
|
{ id: 2, name: "Odense" },
|
|
],
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.goto("/superuser/invoices?activeTab=period");
|
|
await expect(page.getByTestId("invoicing-period-view-selector-all")).toBeVisible({ timeout: 15_000 });
|
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
|
const visibleCustomerOrder = () =>
|
|
page
|
|
.locator(
|
|
[
|
|
"[data-testid^='invoicing-period-customer-']",
|
|
":not([data-testid^='invoicing-period-customer-invoice-'])",
|
|
":not([data-testid^='invoicing-period-customer-queue-'])",
|
|
":not([data-testid^='invoicing-period-customer-draft-'])",
|
|
].join("")
|
|
)
|
|
.evaluateAll((elements) =>
|
|
elements.map((element) => element.getAttribute("data-testid")?.replace("invoicing-period-customer-", ""))
|
|
);
|
|
const fullRequestCountBeforeInvoiceClicks = periodRequests.filter((request) => !request.customerNumbers).length;
|
|
await expect.poll(visibleCustomerOrder).toEqual(["4001", "4003", "4004"]);
|
|
|
|
await page.getByTestId("invoicing-period-customer-invoice-4001").click();
|
|
await page.getByTestId("invoicing-period-customer-invoice-4003").click();
|
|
|
|
await expect.poll(() => economicInvoiceRequests.map((request) => request.id).sort()).toEqual([55501, 55503]);
|
|
expect(periodRequests.filter((request) => !request.customerNumbers)).toHaveLength(
|
|
fullRequestCountBeforeInvoiceClicks
|
|
);
|
|
expect(periodRequests.filter((request) => request.customerNumbers)).toHaveLength(0);
|
|
|
|
await expect(page.getByTestId("invoicing-period-customer-invoice-4001")).toBeDisabled();
|
|
await expect(page.getByTestId("invoicing-period-customer-invoice-4001")).toHaveClass(/is-loading/);
|
|
await expect(page.getByTestId("invoicing-period-customer-invoice-4003")).toBeDisabled();
|
|
await expect(page.getByTestId("invoicing-period-customer-invoice-4003")).toHaveClass(/is-loading/);
|
|
await expect(page.getByTestId("invoicing-period-customer-invoice-4004")).toBeEnabled();
|
|
await expect(page.getByTestId("invoicing-period-customer-invoice-4004")).not.toHaveClass(/is-loading/);
|
|
|
|
await expect.poll(() => statusResolvers.has(77001)).toBeTruthy();
|
|
statusResolvers.get(77001)?.();
|
|
await expect
|
|
.poll(() => periodRequests.filter((request) => !request.customerNumbers).length)
|
|
.toBeGreaterThan(fullRequestCountBeforeInvoiceClicks);
|
|
const fullRequestCountAfterFirstCompletion = periodRequests.filter((request) => !request.customerNumbers).length;
|
|
await expect(page.getByTestId("invoicing-period-customer-invoice-4003")).toBeDisabled();
|
|
await expect(page.getByTestId("invoicing-period-customer-invoice-4003")).toHaveClass(/is-loading/);
|
|
await expect(page.getByTestId("invoicing-period-customer-invoice-4004")).toBeEnabled();
|
|
await expect.poll(visibleCustomerOrder).toEqual(["4001", "4003", "4004"]);
|
|
|
|
await expect.poll(() => statusResolvers.has(77003)).toBeTruthy();
|
|
statusResolvers.get(77003)?.();
|
|
await expect
|
|
.poll(() => periodRequests.filter((request) => !request.customerNumbers).length)
|
|
.toBeGreaterThan(fullRequestCountAfterFirstCompletion);
|
|
await expect.poll(visibleCustomerOrder).toEqual(["4001", "4003", "4004"]);
|
|
expect(periodRequests.filter((request) => request.customerNumbers)).toHaveLength(0);
|
|
});
|
|
|
|
test("@smoke period view date changes trigger a refreshed period query", async ({ page }) => {
|
|
let didDelayDateRefresh = false;
|
|
let releaseDateRefresh;
|
|
const { periodRequests } = await openPeriodView(page);
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
|
await expect(page.getByTestId("invoicing-period-customer-4001")).toBeVisible();
|
|
|
|
await page.route("**/superuser/invoicing/period**", async (route) => {
|
|
if (route.request().method() !== "GET" || !matchesApiPath(route.request().url(), "/superuser/invoicing/period")) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
const url = new URL(route.request().url());
|
|
if (url.searchParams.get("dateFrom") !== "2026-04-02" || url.searchParams.get("periodWarm") === "1") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
periodRequests.push({
|
|
dateFrom: url.searchParams.get("dateFrom"),
|
|
dateTo: url.searchParams.get("dateTo"),
|
|
});
|
|
if (!didDelayDateRefresh) {
|
|
didDelayDateRefresh = true;
|
|
await new Promise((resolve) => {
|
|
releaseDateRefresh = resolve;
|
|
});
|
|
}
|
|
await route.fulfill(json({ data: createPeriodPayloadForChangedRange() }));
|
|
});
|
|
|
|
const dateInputs = page.locator("[data-testid='invoicing-period-view'] input[type='date']:visible");
|
|
await expect(dateInputs.first()).toBeVisible();
|
|
|
|
await dateInputs.nth(0).fill("2026-04-02");
|
|
await dateInputs.nth(0).dispatchEvent("change");
|
|
await expect(page.getByTestId("invoicing-period-view-selector-all").locator("progress").first()).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-customer-4001")).toBeVisible();
|
|
releaseDateRefresh?.();
|
|
await dateInputs.nth(1).fill("2026-04-30");
|
|
await dateInputs.nth(1).dispatchEvent("change");
|
|
|
|
const firstRange = periodRequests[0];
|
|
await expect
|
|
.poll(() =>
|
|
periodRequests.some(
|
|
(request) => request.dateFrom !== firstRange?.dateFrom || request.dateTo !== firstRange?.dateTo
|
|
)
|
|
)
|
|
.toBeTruthy();
|
|
await expect(page.getByTestId("invoicing-period-customer-5001")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-view-selector-all").locator("progress").first()).toHaveCount(0);
|
|
await expect(page).toHaveURL(/activeTab=period/);
|
|
});
|
|
|
|
test("@smoke period partial month warning selects the whole calendar month", async ({ page }) => {
|
|
const { periodRequests } = await openPeriodView(page);
|
|
const dateInputs = page.locator("[data-testid='invoicing-period-view'] input[type='date']:visible");
|
|
await expect(dateInputs.first()).toBeVisible();
|
|
|
|
await dateInputs.nth(0).fill("2026-05-04");
|
|
await dateInputs.nth(0).dispatchEvent("change");
|
|
await dateInputs.nth(1).fill("2026-05-05");
|
|
await dateInputs.nth(1).dispatchEvent("change");
|
|
|
|
const requestCountBeforeClick = periodRequests.length;
|
|
await page.getByTestId("date-period-set-entire-month").click();
|
|
|
|
await expect(dateInputs.nth(0)).toHaveValue("2026-05-01");
|
|
await expect(dateInputs.nth(1)).toHaveValue("2026-05-31");
|
|
await expect.poll(() => periodRequests.length).toBeGreaterThan(requestCountBeforeClick);
|
|
await expect
|
|
.poll(() => periodRequests.at(-1))
|
|
.toEqual({
|
|
dateFrom: "2026-05-01",
|
|
dateTo: "2026-05-31",
|
|
});
|
|
});
|
|
|
|
test("@smoke period month shortcuts select whole calendar months", async ({ page }, testInfo) => {
|
|
const isCompact = isCompactProject(testInfo);
|
|
|
|
await page.clock.setFixedTime(new Date("2026-05-04T10:00:00.000Z"));
|
|
const { periodRequests } = await openPeriodView(page);
|
|
const initialRequestCount = periodRequests.length;
|
|
const dateInputs = page.locator("[data-testid='invoicing-period-view'] input[type='date']:visible");
|
|
|
|
if (isCompact) {
|
|
const select = page.getByTestId("date-period-shortcuts");
|
|
const label = await select
|
|
.locator("option")
|
|
.filter({ hasText: /sidste måned|last month/i })
|
|
.first()
|
|
.textContent();
|
|
await select.selectOption({ label: label.trim() });
|
|
} else {
|
|
await page.getByRole("button", { name: /last month|sidste måned/i }).click();
|
|
}
|
|
|
|
await expect(dateInputs.nth(0)).toHaveValue("2026-04-01");
|
|
await expect(dateInputs.nth(1)).toHaveValue("2026-04-30");
|
|
await expect.poll(() => periodRequests.length).toBeGreaterThan(initialRequestCount);
|
|
await expect
|
|
.poll(() => periodRequests.at(-1))
|
|
.toEqual({
|
|
dateFrom: "2026-04-01",
|
|
dateTo: "2026-04-30",
|
|
});
|
|
});
|
|
|
|
test("@smoke period view reload button triggers a fresh period query", async ({ page }) => {
|
|
const pageErrors = [];
|
|
page.on("pageerror", (error) => {
|
|
pageErrors.push(error.message);
|
|
});
|
|
|
|
const { periodRequests } = await openPeriodView(page);
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
|
const initialRequestCount = periodRequests.length;
|
|
await page.getByTestId("invoicing-period-reload-button").click();
|
|
await expect.poll(() => periodRequests.length).toBeGreaterThan(initialRequestCount);
|
|
await page.waitForTimeout(250);
|
|
await expect(page.getByTestId("invoicing-period-view-all")).toHaveAttribute("data-current-view", "all");
|
|
await expect(page).toHaveURL(/activeTab=period/);
|
|
expect(pageErrors).not.toEqual(expect.arrayContaining([expect.stringContaining("reading 'types'")]));
|
|
});
|
|
|
|
test("@smoke period page keeps cached cards visible during refresh, search, and pagination", async ({ page }) => {
|
|
const periodRequests = [];
|
|
let delayNextPeriodResponse = false;
|
|
let releaseDelayedResponse = null;
|
|
|
|
const pageCustomer = (customerNumber, customerName, transactionId) => ({
|
|
id: customerNumber,
|
|
customer_number: customerNumber,
|
|
customer_name: customerName,
|
|
requires_action: true,
|
|
transactions: [
|
|
{
|
|
id: transactionId,
|
|
date: "2026-04-14T10:00:00.000Z",
|
|
amount: 210,
|
|
booked: false,
|
|
excluded: false,
|
|
invoice_collection_id: transactionId + 1000,
|
|
queue_status: null,
|
|
queue_job_id: null,
|
|
},
|
|
],
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
meta: {},
|
|
});
|
|
|
|
const paginatedPeriodResponse = ({ search, pageNumber }) => {
|
|
const isSearch = String(search || "")
|
|
.toLowerCase()
|
|
.includes("beta");
|
|
const customer = isSearch
|
|
? pageCustomer(9102, "Beta Search", 9912)
|
|
: pageNumber === 2
|
|
? pageCustomer(9103, "Gamma Page", 9913)
|
|
: pageCustomer(9101, "Alpha Cached", 9911);
|
|
const total = isSearch ? 1 : 30;
|
|
|
|
return {
|
|
__rawResponse: {
|
|
data: {
|
|
dateFrom: "2026-04-01 00:00:00",
|
|
dateTo: "2026-04-30 23:59:59",
|
|
types: {
|
|
all: [customer],
|
|
invoice_per_order: [],
|
|
fixed_pricing: [],
|
|
tank_cleaning: [],
|
|
special_arrangements: [],
|
|
vehicle_subscriptions: [],
|
|
possible_duplicates: [],
|
|
},
|
|
type_counts: {
|
|
all: {
|
|
requires_action: total,
|
|
draft: 0,
|
|
manual_flags: 0,
|
|
automatic_flags: 0,
|
|
completed: 0,
|
|
total,
|
|
},
|
|
},
|
|
type_totals: {
|
|
all: {
|
|
total: isSearch ? 1200 : 9000,
|
|
booked: isSearch ? 400 : 6000,
|
|
not_booked: isSearch ? 800 : 3000,
|
|
},
|
|
},
|
|
},
|
|
meta: {
|
|
pagination: {
|
|
page: pageNumber,
|
|
per_page: 1,
|
|
total,
|
|
search: search || "",
|
|
filters: {
|
|
includeRequiresAction: true,
|
|
includeBooked: true,
|
|
},
|
|
order: null,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
};
|
|
|
|
await openPeriodView(page, {
|
|
payloadFactory: async ({ url }) => {
|
|
const pageNumber = Number(url.searchParams.get("page") || 1);
|
|
const search = url.searchParams.get("search") || "";
|
|
const isWarmRequest = url.searchParams.get("periodWarm") === "1";
|
|
if (!isWarmRequest) {
|
|
periodRequests.push({
|
|
page: pageNumber,
|
|
search,
|
|
limit: url.searchParams.get("limit"),
|
|
periodView: url.searchParams.get("periodView"),
|
|
});
|
|
}
|
|
|
|
if (delayNextPeriodResponse && !isWarmRequest) {
|
|
delayNextPeriodResponse = false;
|
|
await new Promise((resolve) => {
|
|
releaseDelayedResponse = resolve;
|
|
});
|
|
}
|
|
|
|
return paginatedPeriodResponse({ search, pageNumber });
|
|
},
|
|
});
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
|
await expect(page.getByTestId("invoicing-period-customer-9101")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-view-selector-all")).toContainText("(0/30)");
|
|
await page.getByTestId("invoicing-period-flag-tabs").getByText("Filters").first().click();
|
|
await expect(page.getByTestId("invoicing-period-limit-select")).toHaveValue("100");
|
|
await expect.poll(() => periodRequests.some((request) => request.limit === "100")).toBeTruthy();
|
|
const statistics = page.getByTestId("invoicing-period-statistics");
|
|
await expect(statistics.getByTestId("invoicing-period-stat-total")).toContainText("9.000,00");
|
|
await expect(statistics.getByTestId("invoicing-period-stat-booked")).toContainText("6.000,00");
|
|
await expect(statistics.getByTestId("invoicing-period-stat-not-booked")).toContainText("3.000,00");
|
|
|
|
const periodPagination = page.getByTestId("invoicing-period-pagination-navigation");
|
|
await periodPagination.locator(".pagination-next").click();
|
|
await expect.poll(() => periodRequests.some((request) => request.page === 2)).toBeTruthy();
|
|
await expect(page.getByTestId("invoicing-period-customer-9103")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-customer-9101")).toHaveCount(0);
|
|
|
|
delayNextPeriodResponse = true;
|
|
await periodPagination.locator(".pagination-previous").click();
|
|
await expect(page.getByTestId("invoicing-period-customer-9101")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-refreshing")).toBeVisible();
|
|
releaseDelayedResponse?.();
|
|
await expect(page.getByTestId("invoicing-period-refreshing")).toHaveCount(0);
|
|
|
|
await page.getByTestId("invoicing-period-search-input").fill("Beta");
|
|
await expect.poll(() => periodRequests.some((request) => request.search === "Beta")).toBeTruthy();
|
|
await expect(page.getByTestId("invoicing-period-customer-9102")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-customer-9101")).toHaveCount(0);
|
|
await expect(page).toHaveURL(/periodSearch=Beta/);
|
|
await expect(page).toHaveURL(/periodLimit=100/);
|
|
|
|
await page.getByTestId("invoicing-period-limit-select").selectOption("all");
|
|
await expect.poll(() => periodRequests.some((request) => request.limit === "all")).toBeTruthy();
|
|
await expect(page).toHaveURL(/periodLimit=all/);
|
|
});
|
|
|
|
test("@smoke period monthly split posts selected range and refreshes period data", async ({ page }) => {
|
|
const { periodRequests } = await openPeriodView(page);
|
|
const splitRequests = [];
|
|
const initialRequestCount = periodRequests.length;
|
|
|
|
await page.route("**/collected-invoices/split-by-month**", async (route) => {
|
|
if (
|
|
route.request().method() !== "POST" ||
|
|
!matchesApiPath(route.request().url(), "/collected-invoices/split-by-month")
|
|
) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
const payload = JSON.parse(route.request().postData() || "{}");
|
|
splitRequests.push(payload);
|
|
if (payload.preview) {
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
preview: true,
|
|
processed_count: 2,
|
|
changed_count: 1,
|
|
skipped_count: 1,
|
|
changed: [
|
|
{
|
|
invoice_collection_id: 101,
|
|
months: [
|
|
{
|
|
month: "2026-03",
|
|
order_count: 2,
|
|
will_create_collection: false,
|
|
target_invoice_collection_id: 101,
|
|
},
|
|
{
|
|
month: "2026-04",
|
|
order_count: 1,
|
|
will_create_collection: true,
|
|
target_invoice_collection_id: null,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
skipped: [
|
|
{
|
|
invoice_collection_id: 202,
|
|
message: "Invoice collection already belongs to one month",
|
|
},
|
|
],
|
|
},
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
preview: false,
|
|
processed_count: 2,
|
|
changed_count: 1,
|
|
skipped_count: 1,
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.getByTestId("invoicing-period-split-by-month-button").click();
|
|
await expect(page.getByText(/Preview monthly split|Forhåndsvis månedsopdeling/i)).toBeVisible();
|
|
await expect(
|
|
page.locator(".swal2-html-container strong").filter({ hasText: /Collection #101|Fakturasamling #101/i })
|
|
).toBeVisible();
|
|
await expect(page.locator(".swal2-html-container td").filter({ hasText: /^2026-04$/ })).toBeVisible();
|
|
await expect(page.getByText(/Create new collection|Opret ny fakturasamling/i)).toBeVisible();
|
|
await page.getByRole("button", { name: /apply split|udfør opdeling/i }).click();
|
|
|
|
await expect.poll(() => splitRequests.length).toBe(2);
|
|
expect(splitRequests[0]).toEqual({
|
|
dateFrom: periodRequests[0]?.dateFrom,
|
|
dateTo: periodRequests[0]?.dateTo,
|
|
preview: true,
|
|
});
|
|
expect(splitRequests[1]).toEqual({
|
|
dateFrom: periodRequests[0]?.dateFrom,
|
|
dateTo: periodRequests[0]?.dateTo,
|
|
preview: false,
|
|
});
|
|
|
|
await expect(page.getByText(/Processed 2 invoice collections|Behandlede 2 fakturasamlinger/i)).toBeVisible();
|
|
await page.getByRole("button", { name: "OK" }).click();
|
|
await expect.poll(() => periodRequests.length).toBeGreaterThan(initialRequestCount);
|
|
});
|
|
|
|
test("@smoke period view shows a queued CTA when backend queue metadata blocks invoicing", async ({ page }) => {
|
|
const periodRequests = [];
|
|
await seedInvoicesPage(page);
|
|
await setupPeriodEndpoints(page, periodRequests);
|
|
|
|
await page.route("**/superuser/invoicing/period**", async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
const url = new URL(route.request().url());
|
|
periodRequests.push({
|
|
dateFrom: url.searchParams.get("dateFrom"),
|
|
dateTo: url.searchParams.get("dateTo"),
|
|
});
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: createQueuedPeriodPayload(),
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.goto("/superuser/invoices?activeTab=period");
|
|
await expect(page).toHaveURL(/activeTab=period/);
|
|
await expect(page.getByTestId("invoicing-period-view-selector-all")).toBeVisible({ timeout: 15_000 });
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
|
await expect(page.getByTestId("invoicing-period-customer-6001")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-customer-queue-6001")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-customer-queue-6001")).toBeDisabled();
|
|
await expect(page.getByTestId("invoicing-period-customer-invoice-6001")).toHaveCount(0);
|
|
expect(periodRequests.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("@smoke period view shows a disabled Kladde CTA when backend draft metadata blocks invoicing", async ({
|
|
page,
|
|
}) => {
|
|
await openPeriodView(page, { payloadFactory: createDraftBlockedPeriodPayload });
|
|
|
|
const draftedOrder = {
|
|
id: 9301,
|
|
customer_id: 6101,
|
|
customer_name: "Drafted Fleet",
|
|
cashier_id: 7,
|
|
cashier_name: "Jeppe",
|
|
department_id: 1,
|
|
reg_1: "EC21233",
|
|
reg_2: "",
|
|
reg_3: "",
|
|
reference: "EC21233",
|
|
notes: "",
|
|
po: "",
|
|
created_at: "2026-04-14 10:00:00",
|
|
total_net_amount: 210,
|
|
invoice_collection_id: 16501,
|
|
invoice_collection: {
|
|
id: 16501,
|
|
closed_at: "2026-04-14",
|
|
booked_invoice_id: null,
|
|
},
|
|
economic_invoice_module: null,
|
|
stripe_invoice_module: null,
|
|
error_message: null,
|
|
completed_at: null,
|
|
pending_handheld: false,
|
|
user_id: 41,
|
|
};
|
|
|
|
await page.route("**/orders**", async (route) => {
|
|
if (route.request().method() !== "GET" || !matchesApiPath(route.request().url(), "/orders")) {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: [draftedOrder],
|
|
meta: {
|
|
pagination: {
|
|
limit: 20,
|
|
total: 1,
|
|
},
|
|
},
|
|
})
|
|
);
|
|
});
|
|
await page.route("**/collected-invoices/16501**", async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: {
|
|
id: 16501,
|
|
orders: [draftedOrder],
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
|
const allSelector = page.getByTestId("invoicing-period-view-selector-all");
|
|
await expect(allSelector).toContainText("(0/1)");
|
|
const draftProgressSegment = page.getByTestId("invoicing-period-view-selector-progress-draft-all");
|
|
await expect(draftProgressSegment).toBeVisible();
|
|
await expect(draftProgressSegment).toHaveCSS("background-color", "rgb(246, 195, 68)");
|
|
|
|
const customerRow = page.getByTestId("invoicing-period-customer-6101");
|
|
await expect(customerRow).toBeVisible();
|
|
await expect(customerRow.locator(".color-indicator i.fa-circle").first()).toHaveClass(/has-text-warning/);
|
|
|
|
const draftTag = page.getByTestId("invoicing-period-customer-draft-6101");
|
|
await expect(draftTag).toBeVisible();
|
|
await expect(draftTag).toHaveClass(/tag/);
|
|
await expect(draftTag).toHaveClass(/is-warning/);
|
|
await expect(draftTag).toHaveAttribute("aria-disabled", "true");
|
|
await expect(draftTag.locator("i")).toHaveClass(/fa-file-alt/);
|
|
await expect(draftTag).toContainText("Kladde");
|
|
await expect(page.getByTestId("invoicing-period-customer-invoice-6101")).toHaveCount(0);
|
|
|
|
await customerRow.getByText("Drafted Fleet").click();
|
|
const collectionTotal = page.getByTestId("pos-order-invoice-collection-total-16501");
|
|
await expect(collectionTotal).toContainText("210");
|
|
|
|
const isDesktop = page.viewportSize().width >= 1024;
|
|
if (isDesktop) {
|
|
const amountHeader = page.getByRole("columnheader", { name: /Beløb|Amount/i }).last();
|
|
const amountHeaderBox = await getBoundingBox(amountHeader, "orders amount column header");
|
|
const collectionTotalBox = await getBoundingBox(collectionTotal, "invoice collection total");
|
|
expect(Math.abs(collectionTotalBox.x - amountHeaderBox.x)).toBeLessThanOrEqual(2);
|
|
expect(Math.abs(collectionTotalBox.width - amountHeaderBox.width)).toBeLessThanOrEqual(3);
|
|
}
|
|
});
|
|
|
|
test("@smoke period view restores Fakturer nu when draft metadata is stale or deleted", async ({ page }) => {
|
|
await openPeriodView(page, { payloadFactory: createStaleDraftPeriodPayload });
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
|
await expect(page.getByTestId("invoicing-period-customer-6102")).toBeVisible();
|
|
await expect(page.getByTestId("invoicing-period-customer-draft-6102")).toHaveCount(0);
|
|
|
|
const invoiceButton = page.getByTestId("invoicing-period-customer-invoice-6102");
|
|
await expect(invoiceButton).toBeVisible();
|
|
await expect(invoiceButton).toBeEnabled();
|
|
await expect(invoiceButton).toContainText(/Fakturer nu|Invoice now/i);
|
|
});
|
|
|
|
test("@smoke period view refreshes queue state when the Period route becomes active again", async ({ page }) => {
|
|
const periodRequests = [];
|
|
let requestCount = 0;
|
|
let returnQueuedPayload = false;
|
|
|
|
await seedInvoicesPage(page);
|
|
await setupPeriodEndpoints(page, periodRequests);
|
|
|
|
await page.route("**/superuser/invoicing/period**", async (route) => {
|
|
if (route.request().method() !== "GET") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
|
|
requestCount += 1;
|
|
const url = new URL(route.request().url());
|
|
periodRequests.push({
|
|
dateFrom: url.searchParams.get("dateFrom"),
|
|
dateTo: url.searchParams.get("dateTo"),
|
|
});
|
|
|
|
await route.fulfill(
|
|
json({
|
|
data: returnQueuedPayload ? createQueuedPeriodPayload() : createPeriodPayload(),
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.goto("/superuser/invoices?activeTab=overview");
|
|
await expect(page).toHaveURL(/activeTab=overview/);
|
|
returnQueuedPayload = true;
|
|
|
|
await page.goto("/superuser/invoices?activeTab=period");
|
|
await expect(page).toHaveURL(/activeTab=period/);
|
|
await expect.poll(() => requestCount).toBeGreaterThan(0);
|
|
|
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
|
await expect(page.getByTestId("invoicing-period-customer-queue-6001")).toBeVisible();
|
|
});
|
|
});
|