The new ProductAggregatesCard child component calls useI18n() during setup. The existing invoicing-period-queue-state.behavior.spec.js mounts the parent InvoicingBillingPeriodViewAll view without providing a vue-i18n instance, which causes the Serial unit suite to fail with: SyntaxError: Need to install with `app.use` function Wire createTestI18n() into the mount() global plugins so the new card and any other future i18n-using children render correctly in this serial spec.
1263 lines
41 KiB
JavaScript
1263 lines
41 KiB
JavaScript
// @vitest-environment jsdom
|
|
import { computed, nextTick } from "vue";
|
|
import { flushPromises, mount } from "@vue/test-utils";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const {
|
|
sharedVariablesRef,
|
|
currentViewRef,
|
|
startDateRef,
|
|
endDateRef,
|
|
queueRef,
|
|
inProgressRef,
|
|
loadingCustomerNumbersRef,
|
|
} = vi.hoisted(() => {
|
|
const { ref } = require("vue");
|
|
|
|
return {
|
|
sharedVariablesRef: ref({ types: { all: [] } }),
|
|
currentViewRef: ref("all"),
|
|
startDateRef: ref(new Date("2026-04-01T00:00:00.000Z")),
|
|
endDateRef: ref(new Date("2026-04-30T23:59:59.000Z")),
|
|
queueRef: ref([]),
|
|
inProgressRef: ref([]),
|
|
loadingCustomerNumbersRef: ref([]),
|
|
};
|
|
});
|
|
|
|
vi.mock("@/services/economicTransferQueue.js", () => ({
|
|
ECONOMIC_QUEUE_STATUS: {
|
|
QUEUED: "QUEUED",
|
|
PROCESSING: "PROCESSING",
|
|
COMPLETED: "COMPLETED",
|
|
FAILED: "FAILED",
|
|
},
|
|
}));
|
|
|
|
vi.mock("sweetalert2", () => ({
|
|
default: {
|
|
fire: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
|
SessionUser: {
|
|
objects: {
|
|
global: {
|
|
language: {
|
|
processing: "Processing",
|
|
queued: "Queued",
|
|
all_booked: "All booked",
|
|
none: "None",
|
|
invoice_now: "Invoice now",
|
|
},
|
|
},
|
|
orders: {
|
|
meta: {
|
|
labels: {
|
|
single: "order",
|
|
multiple: "orders",
|
|
},
|
|
},
|
|
get: {
|
|
multiple: vi.fn().mockResolvedValue([]),
|
|
},
|
|
},
|
|
collectedOrderInvoices: {
|
|
functions: {
|
|
createVehicleSubscriptionInvoice: vi.fn(),
|
|
add_fixed_pricing: vi.fn(),
|
|
add_vehicle_subscriptions: vi.fn(),
|
|
split_by_month: vi.fn(),
|
|
},
|
|
},
|
|
vehicles: {
|
|
columns: {
|
|
wash_subscription: {
|
|
label: "Subscription",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
functions: {
|
|
currency: {
|
|
toLocal: (value) => String(value),
|
|
},
|
|
parseErrorMessage: (error) => error?.message ?? String(error),
|
|
},
|
|
},
|
|
}));
|
|
|
|
vi.mock(
|
|
"@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportView.vue",
|
|
() => ({
|
|
view: {
|
|
variables: {
|
|
currentView: currentViewRef,
|
|
sharedVariables: sharedVariablesRef,
|
|
},
|
|
computed: {
|
|
componentName: computed(() => currentViewRef.value),
|
|
},
|
|
functions: {
|
|
filterExcluded: (transactions = []) => transactions.filter((transaction) => !transaction?.excluded),
|
|
getCustomerViewTotalNetAmount: (customer) => {
|
|
const transactions = Array.isArray(customer?.transactions) ? customer.transactions : [];
|
|
return transactions.reduce((sum, transaction) => sum + Number(transaction?.amount ?? 0), 0);
|
|
},
|
|
},
|
|
},
|
|
})
|
|
);
|
|
|
|
vi.mock(
|
|
"@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportDates.vue",
|
|
() => ({
|
|
dates: {
|
|
variables: {
|
|
start: startDateRef,
|
|
end: endDateRef,
|
|
},
|
|
computed: {
|
|
formattedStartDate: require("vue").computed(() => startDateRef.value.toISOString().split("T")[0]),
|
|
formattedEndDate: require("vue").computed(() => endDateRef.value.toISOString().split("T")[0]),
|
|
},
|
|
},
|
|
})
|
|
);
|
|
|
|
vi.mock(
|
|
"@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportInvoiceQueue.vue",
|
|
() => ({
|
|
invoiceQueue: {
|
|
addInvoiceCollectionsToQueue: vi.fn(),
|
|
processInvoiceCollectionQueue: vi.fn(),
|
|
invoiceCollectionQueue: queueRef,
|
|
invoiceCollectionQueueInProgress: inProgressRef,
|
|
invoiceCollectionQueueFailed: require("vue").ref([]),
|
|
invoiceCollectionQueueSuccess: require("vue").ref([]),
|
|
markPeriodRefreshLoading: vi.fn((customerNumbers = []) => {
|
|
loadingCustomerNumbersRef.value = Array.from(
|
|
new Set([...loadingCustomerNumbersRef.value, ...customerNumbers.filter(Boolean)])
|
|
);
|
|
}),
|
|
finishPeriodRefresh: vi.fn((customerNumbers = []) => {
|
|
loadingCustomerNumbersRef.value = loadingCustomerNumbersRef.value.filter(
|
|
(customerNumber) => !customerNumbers.includes(customerNumber)
|
|
);
|
|
}),
|
|
isPeriodCustomerRefreshLoading: vi.fn((customerNumber) =>
|
|
loadingCustomerNumbersRef.value.includes(Number(customerNumber))
|
|
),
|
|
},
|
|
})
|
|
);
|
|
|
|
import InvoicingBillingPeriodViewAll from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewAll.vue";
|
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
|
import { invoiceQueue } from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportInvoiceQueue.vue";
|
|
import Swal from "sweetalert2";
|
|
import {
|
|
periodPaging,
|
|
resetPeriodPagingState,
|
|
} from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportPaging.js";
|
|
import { createTestI18n } from "./helpers/mountWithApp.js";
|
|
|
|
const viewI18n = createTestI18n();
|
|
|
|
const mountView = () =>
|
|
mount(InvoicingBillingPeriodViewAll, {
|
|
global: {
|
|
plugins: [viewI18n],
|
|
stubs: {
|
|
"b-tabs": {
|
|
template: "<div data-testid='b-tabs-stub'><slot /></div>",
|
|
},
|
|
"b-tab-item": {
|
|
template:
|
|
"<div class='b-tab-item-stub' v-bind='$attrs'><div class='b-tab-item-header'><slot name='header' /></div><slot /></div>",
|
|
},
|
|
InvoicingBillingPeriodStatistics: { template: "<div />" },
|
|
InvoicingBillingPeriodFilters: { template: "<div />" },
|
|
PaginationNavigation: {
|
|
props: ["currentPage", "totalPages", "setPage", "loadFunction"],
|
|
template: `
|
|
<div data-testid="pagination-navigation-stub">
|
|
<button data-testid="invoicing-period-page-prev" @click="setPage(currentPage - 1); loadFunction()">Prev</button>
|
|
<button data-testid="invoicing-period-page-next" @click="setPage(currentPage + 1); loadFunction()">Next</button>
|
|
</div>
|
|
`,
|
|
},
|
|
WhiteBox: { template: "<div><slot /></div>" },
|
|
ColorIndicator: {
|
|
props: ["icon_class", "color_class"],
|
|
template:
|
|
"<div class='color-indicator-stub' :data-icon='icon_class' :data-color='color_class'><slot /></div>",
|
|
},
|
|
ActionSettingsWheelButton: { template: "<div><slot name='actions' /></div>" },
|
|
InvoicingPeriodFlagList: {
|
|
emits: ["statusChanged"],
|
|
props: ["flags"],
|
|
methods: {
|
|
markStatus(flag) {
|
|
this.$emit("statusChanged", { ...flag, status: "ignored" });
|
|
},
|
|
},
|
|
template: `
|
|
<div data-testid='invoice-period-flags-stub'>
|
|
{{ flags.length }}
|
|
<button
|
|
v-for="flag in flags"
|
|
:key="flag.id || flag.fingerprint"
|
|
:data-testid="'invoice-period-flag-status-' + (flag.id || flag.fingerprint)"
|
|
@click="markStatus(flag)"
|
|
></button>
|
|
</div>
|
|
`,
|
|
},
|
|
InvoiceOrdersPagination: {
|
|
props: ["setCustomerFilter", "showOnlyWithIds", "groupInvoiceCollection"],
|
|
template:
|
|
'<div class="invoice-orders-pagination-stub" :data-customer="setCustomerFilter" :data-order-ids="(showOnlyWithIds || []).join(\',\')" :data-group-invoice-collection="groupInvoiceCollection ? \'true\' : \'false\'" />',
|
|
},
|
|
InvoicingPeriodObjectTree: {
|
|
props: ["customer", "transactions"],
|
|
template:
|
|
'<div data-testid="invoicing-period-object-tree-stub" :data-customer="customer && customer.customer_number" :data-transaction-count="(transactions || []).length" />',
|
|
},
|
|
SmallCustomerActivityChart: { template: "<div />" },
|
|
InvoicingBillingPeriodCustomerAttributes: { template: "<div />" },
|
|
InvoicingBillingPeriodInvoiceProgressBar: { template: "<div />" },
|
|
},
|
|
},
|
|
});
|
|
|
|
describe("Invoicing period queue state", () => {
|
|
beforeEach(() => {
|
|
currentViewRef.value = "all";
|
|
queueRef.value = [];
|
|
inProgressRef.value = [];
|
|
loadingCustomerNumbersRef.value = [];
|
|
SessionUser.objects.orders.get.multiple.mockReset();
|
|
SessionUser.objects.orders.get.multiple.mockResolvedValue([]);
|
|
SessionUser.objects.collectedOrderInvoices.functions.split_by_month.mockReset();
|
|
SessionUser.objects.collectedOrderInvoices.functions.split_by_month.mockResolvedValue({
|
|
data: {
|
|
data: {
|
|
processed_count: 1,
|
|
changed_count: 1,
|
|
skipped_count: 0,
|
|
},
|
|
},
|
|
});
|
|
invoiceQueue.addInvoiceCollectionsToQueue.mockReset();
|
|
invoiceQueue.processInvoiceCollectionQueue.mockReset();
|
|
invoiceQueue.markPeriodRefreshLoading.mockClear();
|
|
invoiceQueue.finishPeriodRefresh.mockClear();
|
|
invoiceQueue.isPeriodCustomerRefreshLoading.mockClear();
|
|
Swal.fire.mockReset();
|
|
Swal.fire.mockResolvedValue({ isDenied: true });
|
|
resetPeriodPagingState();
|
|
sharedVariablesRef.value = {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 1,
|
|
customer_number: 1001,
|
|
customer_name: "Queued Customer",
|
|
requires_action: false,
|
|
queue: {
|
|
has_active_job: true,
|
|
statuses: ["QUEUED"],
|
|
invoice_collection_ids: [14578],
|
|
is_action_blocked: true,
|
|
},
|
|
transactions: [
|
|
{
|
|
id: 5001,
|
|
amount: 100,
|
|
booked: false,
|
|
excluded: false,
|
|
queue_status: "QUEUED",
|
|
queue_job_id: 9,
|
|
invoice_collection_id: 14578,
|
|
date: "2026-04-10T10:00:00.000Z",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 2,
|
|
customer_number: 1002,
|
|
customer_name: "Mixed Customer",
|
|
requires_action: true,
|
|
queue: {
|
|
has_active_job: true,
|
|
statuses: ["PROCESSING"],
|
|
invoice_collection_ids: [2001],
|
|
is_action_blocked: false,
|
|
},
|
|
transactions: [
|
|
{
|
|
id: 5002,
|
|
amount: 120,
|
|
booked: false,
|
|
excluded: false,
|
|
queue_status: "PROCESSING",
|
|
queue_job_id: 10,
|
|
invoice_collection_id: 2001,
|
|
date: "2026-04-11T10:00:00.000Z",
|
|
},
|
|
{
|
|
id: 5003,
|
|
amount: 80,
|
|
booked: false,
|
|
excluded: false,
|
|
queue_status: null,
|
|
queue_job_id: null,
|
|
invoice_collection_id: null,
|
|
date: "2026-04-12T10:00:00.000Z",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it("renders a disabled queued CTA when the backend marks the customer as action-blocked", async () => {
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
const queuedButton = wrapper.get("[data-testid='invoicing-period-customer-queue-1001']");
|
|
expect(queuedButton.text()).toContain("Queued");
|
|
expect(queuedButton.attributes("disabled")).toBeDefined();
|
|
expect(wrapper.find("[data-testid='invoicing-period-customer-invoice-1001']").exists()).toBe(false);
|
|
});
|
|
|
|
it("preserves a routed customer selection until the initial period data arrives", async () => {
|
|
sharedVariablesRef.value = null;
|
|
periodPaging.selectedCustomerNumber = 1002;
|
|
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
expect(periodPaging.selectedCustomerNumber).toBe(1002);
|
|
expect(wrapper.find("[data-testid='invoicing-period-review-detail']").exists()).toBe(false);
|
|
|
|
sharedVariablesRef.value = {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 2,
|
|
customer_number: 1002,
|
|
customer_name: "Routed Customer",
|
|
requires_action: true,
|
|
transactions: [],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
await nextTick();
|
|
|
|
expect(periodPaging.selectedCustomerNumber).toBe(1002);
|
|
expect(wrapper.get("[data-testid='invoicing-period-review-detail'] h2").text()).toBe("Routed Customer");
|
|
});
|
|
|
|
it("keeps retained refresh errors visible for empty and filtered-empty results", async () => {
|
|
sharedVariablesRef.value = { types: { all: [] } };
|
|
periodPaging.loadError = true;
|
|
periodPaging.loadErrorMessage = "Refresh failed";
|
|
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
expect(wrapper.find("[data-testid='invoicing-period-retained-error']").exists()).toBe(true);
|
|
expect(wrapper.find("[data-testid='invoicing-period-true-empty']").exists()).toBe(true);
|
|
|
|
sharedVariablesRef.value = {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 3,
|
|
customer_number: 1003,
|
|
customer_name: "Ready Customer",
|
|
requires_action: true,
|
|
review: { state: "ready", severity: "green", reasons: [], next_action: "create_invoice" },
|
|
transactions: [],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
periodPaging.reviewState = "blocked";
|
|
await nextTick();
|
|
|
|
expect(wrapper.find("[data-testid='invoicing-period-retained-error']").exists()).toBe(true);
|
|
expect(wrapper.find("[data-testid='invoicing-period-filtered-empty']").exists()).toBe(true);
|
|
});
|
|
|
|
it("lets local queue state override stale review state while a refresh is pending", async () => {
|
|
queueRef.value = [3001];
|
|
periodPaging.reviewState = "queued";
|
|
sharedVariablesRef.value = {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 3,
|
|
customer_number: 1003,
|
|
customer_name: "Locally Queued Customer",
|
|
requires_action: false,
|
|
review: {
|
|
state: "completed",
|
|
severity: "green",
|
|
reasons: [],
|
|
next_action: "none",
|
|
is_actionable: false,
|
|
},
|
|
transactions: [
|
|
{
|
|
id: 6001,
|
|
amount: 75,
|
|
booked: false,
|
|
excluded: false,
|
|
invoice_collection_id: 3001,
|
|
date: "2026-04-13T10:00:00.000Z",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
const customer = wrapper.get("[data-testid='invoicing-period-customer-1003']");
|
|
expect(customer.get(".period-customer-review").classes()).toContain("review-state--blue");
|
|
expect(customer.get(".period-customer-review").text()).toContain("I kø");
|
|
expect(wrapper.find("[data-testid='invoicing-period-filtered-empty']").exists()).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
state: "blocked",
|
|
severity: "red",
|
|
reason: "manual_flags",
|
|
nextAction: "resolve_manual_flags",
|
|
expectedClass: "review-state--red",
|
|
expectedLabel: "Blokeret",
|
|
},
|
|
{
|
|
state: "attention",
|
|
severity: "yellow",
|
|
reason: "automatic_flags",
|
|
nextAction: "review_warnings",
|
|
expectedClass: "review-state--yellow",
|
|
expectedLabel: "Kræver opmærksomhed",
|
|
},
|
|
])(
|
|
"preserves $state review priority while a local export is queued",
|
|
async ({ state, severity, reason, nextAction, expectedClass, expectedLabel }) => {
|
|
queueRef.value = [3001];
|
|
sharedVariablesRef.value = {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 3,
|
|
customer_number: 1003,
|
|
customer_name: "Blocked Customer",
|
|
requires_action: true,
|
|
review: {
|
|
state,
|
|
severity,
|
|
reasons: [{ code: reason, count: 1 }],
|
|
next_action: nextAction,
|
|
is_actionable: false,
|
|
},
|
|
transactions: [
|
|
{
|
|
id: 6001,
|
|
amount: 75,
|
|
booked: false,
|
|
excluded: false,
|
|
invoice_collection_id: 3001,
|
|
date: "2026-04-13T10:00:00.000Z",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
const customer = wrapper.get("[data-testid='invoicing-period-customer-1003']");
|
|
expect(customer.get(".period-customer-review").classes()).toContain(expectedClass);
|
|
expect(customer.get(".period-customer-review").text()).toContain(expectedLabel);
|
|
expect(customer.get(".period-customer-review").text()).not.toContain("I kø");
|
|
}
|
|
);
|
|
|
|
it("moves from a completed selection to the first reviewable customer", async () => {
|
|
periodPaging.selectedCustomerNumber = 1001;
|
|
sharedVariablesRef.value = {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 1,
|
|
customer_number: 1001,
|
|
customer_name: "Completed Customer",
|
|
requires_action: false,
|
|
review: { state: "completed", severity: "green", reasons: [], next_action: "none" },
|
|
transactions: [],
|
|
},
|
|
{
|
|
id: 2,
|
|
customer_number: 1002,
|
|
customer_name: "First Reviewable Customer",
|
|
requires_action: true,
|
|
review: { state: "ready", severity: "green", reasons: [], next_action: "create_invoice" },
|
|
transactions: [],
|
|
},
|
|
{
|
|
id: 3,
|
|
customer_number: 1003,
|
|
customer_name: "Second Reviewable Customer",
|
|
requires_action: true,
|
|
review: { state: "attention", severity: "yellow", reasons: [], next_action: "review_warnings" },
|
|
transactions: [],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
expect(wrapper.get("[data-testid='invoicing-period-review-detail'] h2").text()).toBe("Completed Customer");
|
|
await wrapper.findAll(".period-review-detail__navigation button")[1].trigger("click");
|
|
await nextTick();
|
|
|
|
expect(periodPaging.selectedCustomerNumber).toBe(1002);
|
|
expect(wrapper.get("[data-testid='invoicing-period-review-detail'] h2").text()).toBe("First Reviewable Customer");
|
|
});
|
|
|
|
it("debounces period search input and resets pagination to the first page", async () => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
periodPaging.page = 3;
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
await wrapper.get("[data-testid='invoicing-period-search-input']").setValue("Solaris");
|
|
expect(periodPaging.search).toBe("");
|
|
expect(periodPaging.page).toBe(3);
|
|
|
|
await vi.advanceTimersByTimeAsync(299);
|
|
expect(periodPaging.search).toBe("");
|
|
|
|
await vi.advanceTimersByTimeAsync(1);
|
|
await nextTick();
|
|
|
|
expect(periodPaging.search).toBe("Solaris");
|
|
expect(periodPaging.page).toBe(1);
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it("reuses the list reload button loading state instead of rendering a separate refreshing badge", async () => {
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
expect(wrapper.get("[data-testid='invoicing-period-page-reload-button']").exists()).toBe(true);
|
|
expect(wrapper.find("[data-testid='invoicing-period-refreshing']").exists()).toBe(false);
|
|
|
|
periodPaging.isRefreshing = true;
|
|
await nextTick();
|
|
|
|
expect(wrapper.get("[data-testid='invoicing-period-page-reload-button']").classes("is-loading")).toBe(true);
|
|
expect(wrapper.find("[data-testid='invoicing-period-refreshing']").exists()).toBe(false);
|
|
});
|
|
|
|
it("offers large period page sizes and all rows", async () => {
|
|
periodPaging.page = 4;
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
const select = wrapper.get("[data-testid='invoicing-period-limit-select']");
|
|
const optionValues = Array.from(select.element.options).map((option) => option.value);
|
|
const optionLabels = Array.from(select.element.options).map((option) => option.textContent.trim());
|
|
|
|
expect(select.element.value).toBe("100");
|
|
expect(optionValues).toEqual(["10", "25", "50", "100", "200", "500", "all"]);
|
|
expect(optionLabels.at(-1)).toBe("All");
|
|
|
|
await select.setValue("all");
|
|
expect(periodPaging.limit).toBe("all");
|
|
expect(periodPaging.page).toBe(1);
|
|
|
|
await select.setValue("500");
|
|
expect(periodPaging.limit).toBe(500);
|
|
});
|
|
|
|
it("stores table filters in the Filters tab for the standard view", async () => {
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
const filtersTab = wrapper.get("[data-testid='invoicing-period-filters-tab']");
|
|
expect(filtersTab.find("[data-testid='invoicing-period-table-filters']").exists()).toBe(true);
|
|
expect(filtersTab.find("[data-testid='invoicing-period-limit-select']").exists()).toBe(true);
|
|
});
|
|
|
|
it("keeps flag tab counts stable when only the page size changes", async () => {
|
|
periodPaging.typeCounts = {
|
|
all: {
|
|
total: 793,
|
|
manual_flags: 2,
|
|
automatic_flags: 0,
|
|
},
|
|
};
|
|
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
const getFlagTabCounts = () =>
|
|
wrapper
|
|
.findAll("[data-testid='invoicing-period-flag-tabs'] .period-flag-tab-header .tag")
|
|
.map((tag) => Number.parseInt(tag.text(), 10));
|
|
|
|
expect(getFlagTabCounts()).toEqual([793, 2, 0, 791]);
|
|
|
|
const select = wrapper.get("[data-testid='invoicing-period-limit-select']");
|
|
await select.setValue("10");
|
|
await nextTick();
|
|
|
|
expect(periodPaging.limit).toBe(10);
|
|
expect(getFlagTabCounts()).toEqual([793, 2, 0, 791]);
|
|
});
|
|
|
|
it("keeps only one customer unfolded at a time", async () => {
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
await wrapper.get("[data-testid='invoicing-period-customer-select-1001']").trigger("click");
|
|
await nextTick();
|
|
|
|
expect(sharedVariablesRef.value.types.all[0].expanded).toBe(true);
|
|
expect(sharedVariablesRef.value.types.all[1].expanded).toBe(false);
|
|
expect(wrapper.find("[data-testid='invoicing-period-object-tree-stub'][data-customer='1001']").exists()).toBe(true);
|
|
expect(wrapper.find("[data-testid='invoicing-period-object-tree-stub'][data-customer='1002']").exists()).toBe(
|
|
false
|
|
);
|
|
|
|
await wrapper.get("[data-testid='invoicing-period-customer-select-1002']").trigger("click");
|
|
await nextTick();
|
|
|
|
expect(sharedVariablesRef.value.types.all[0].expanded).toBe(false);
|
|
expect(sharedVariablesRef.value.types.all[1].expanded).toBe(true);
|
|
expect(wrapper.find("[data-testid='invoicing-period-object-tree-stub'][data-customer='1001']").exists()).toBe(
|
|
false
|
|
);
|
|
expect(wrapper.find("[data-testid='invoicing-period-object-tree-stub'][data-customer='1002']").exists()).toBe(true);
|
|
});
|
|
|
|
it("keeps the invoice action visible for mixed queued and still-unqueued customers", async () => {
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
expect(wrapper.find("[data-testid='invoicing-period-customer-queue-1002']").exists()).toBe(false);
|
|
expect(wrapper.get("[data-testid='invoicing-period-customer-invoice-1002']").text()).toContain("Invoice now");
|
|
});
|
|
|
|
it("uses a flag icon and renders flag rows when the period response contains active flags", async () => {
|
|
sharedVariablesRef.value = {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 6,
|
|
customer_number: 1006,
|
|
customer_name: "Flagged Customer",
|
|
requires_action: false,
|
|
status_indicator: "flag_red",
|
|
flag_counts: {
|
|
manual: 1,
|
|
automatic: 1,
|
|
total: 2,
|
|
},
|
|
flags: [
|
|
{
|
|
id: 991,
|
|
source: "manual",
|
|
status: "active",
|
|
target_type: "customer",
|
|
target_id: 1006,
|
|
customer_number: 1006,
|
|
reason: "Manual check",
|
|
},
|
|
{
|
|
id: "auto:abc",
|
|
source: "automatic",
|
|
status: "active",
|
|
target_type: "customer",
|
|
target_id: 1006,
|
|
customer_number: 1006,
|
|
message: "Automatic check",
|
|
},
|
|
],
|
|
transactions: [
|
|
{
|
|
id: 7101,
|
|
amount: 75,
|
|
booked: false,
|
|
excluded: false,
|
|
invoice_collection_id: 4101,
|
|
date: "2026-04-13T10:00:00.000Z",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
const customer = wrapper.get("[data-testid='invoicing-period-customer-1006']");
|
|
expect(customer.get(".color-indicator-stub").attributes("data-icon")).toBe("fas fa-flag");
|
|
expect(customer.get(".color-indicator-stub").attributes("data-color")).toBe("has-text-danger");
|
|
expect(customer.get("[data-testid='invoice-period-flags-stub']").text()).toBe("2");
|
|
});
|
|
|
|
it("keeps the Fakturer nu button visible for customers with red flags even when requires_action is false", async () => {
|
|
sharedVariablesRef.value = {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 11,
|
|
customer_number: 1011,
|
|
customer_name: "Red Flag Only Customer",
|
|
requires_action: false,
|
|
status_indicator: "flag_red",
|
|
flag_counts: {
|
|
manual: 1,
|
|
automatic: 0,
|
|
total: 1,
|
|
},
|
|
flags: [
|
|
{
|
|
id: 1011,
|
|
source: "manual",
|
|
status: "active",
|
|
target_type: "customer",
|
|
target_id: 1011,
|
|
customer_number: 1011,
|
|
reason: "Manual flag — review required",
|
|
},
|
|
],
|
|
transactions: [
|
|
{
|
|
id: 8101,
|
|
amount: 60,
|
|
booked: false,
|
|
excluded: false,
|
|
invoice_collection_id: 6101,
|
|
date: "2026-04-15T10:00:00.000Z",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
const customer = wrapper.get("[data-testid='invoicing-period-customer-1011']");
|
|
expect(customer.get("[data-testid='invoicing-period-customer-invoice-1011']").text()).toContain("Invoice now");
|
|
expect(customer.find("[data-testid='invoicing-period-customer-multiple-red-flag-1011']").exists()).toBe(false);
|
|
});
|
|
|
|
it("keeps collected invoice flags visible even when linked transactions are excluded", async () => {
|
|
sharedVariablesRef.value = {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 7,
|
|
customer_number: 1007,
|
|
customer_name: "Collection Flag Customer",
|
|
requires_action: false,
|
|
status_indicator: "flag_red",
|
|
flag_counts: {
|
|
manual: 1,
|
|
automatic: 0,
|
|
total: 1,
|
|
},
|
|
flags: [
|
|
{
|
|
id: 992,
|
|
source: "manual",
|
|
status: "active",
|
|
target_type: "collected_order_invoice",
|
|
target_id: 5101,
|
|
customer_number: 1007,
|
|
reason: "Invoice collection needs review",
|
|
},
|
|
],
|
|
transactions: [
|
|
{
|
|
id: 7201,
|
|
amount: 95,
|
|
booked: false,
|
|
excluded: true,
|
|
invoice_collection_id: 5101,
|
|
date: "2026-04-14T10:00:00.000Z",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
const customer = wrapper.get("[data-testid='invoicing-period-customer-1007']");
|
|
expect(customer.get(".color-indicator-stub").attributes("data-icon")).toBe("fas fa-flag");
|
|
expect(customer.get("[data-testid='invoice-period-flags-stub']").text()).toBe("1");
|
|
});
|
|
|
|
it("removes the customer flag icon after the last active flag is marked inactive", async () => {
|
|
sharedVariablesRef.value = {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 6,
|
|
customer_number: 1006,
|
|
customer_name: "Flagged Customer",
|
|
requires_action: true,
|
|
status_indicator: "flag_yellow",
|
|
flag_counts: {
|
|
manual: 0,
|
|
automatic: 1,
|
|
total: 1,
|
|
},
|
|
flags: [
|
|
{
|
|
id: "auto:abc",
|
|
source: "automatic",
|
|
status: "active",
|
|
target_type: "customer",
|
|
target_id: 1006,
|
|
customer_number: 1006,
|
|
message: "Automatic check",
|
|
},
|
|
],
|
|
transactions: [
|
|
{
|
|
id: 7101,
|
|
amount: 75,
|
|
booked: false,
|
|
excluded: false,
|
|
invoice_collection_id: 4101,
|
|
date: "2026-04-13T10:00:00.000Z",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
const customer = wrapper.get("[data-testid='invoicing-period-customer-1006']");
|
|
expect(customer.get(".color-indicator-stub").attributes("data-icon")).toBe("fas fa-flag");
|
|
|
|
await customer.get("[data-testid='invoice-period-flag-status-auto:abc']").trigger("click");
|
|
await nextTick();
|
|
|
|
expect(customer.get(".color-indicator-stub").attributes("data-icon")).toBe("fas fa-circle");
|
|
expect(customer.get(".color-indicator-stub").attributes("data-color")).toBe("has-text-danger");
|
|
expect(customer.get("[data-testid='invoice-period-flags-stub']").text()).toBe("0");
|
|
expect(sharedVariablesRef.value.types.all[0].flag_counts).toEqual({
|
|
manual: 0,
|
|
automatic: 0,
|
|
total: 0,
|
|
});
|
|
expect(periodPaging.reloadSequence).toBe(1);
|
|
});
|
|
|
|
it("queues period invoice collections without opening the invoice collection page", async () => {
|
|
const windowOpenSpy = vi.spyOn(window, "open").mockImplementation(() => null);
|
|
sharedVariablesRef.value = {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 3,
|
|
customer_number: 1003,
|
|
customer_name: "Ready Customer",
|
|
requires_action: true,
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
transactions: [
|
|
{
|
|
id: 6001,
|
|
amount: 75,
|
|
booked: false,
|
|
excluded: false,
|
|
invoice_collection_id: 3001,
|
|
date: "2026-04-13T10:00:00.000Z",
|
|
},
|
|
{
|
|
id: 6002,
|
|
amount: 125,
|
|
booked: false,
|
|
excluded: false,
|
|
invoice_collection_id: 3001,
|
|
date: "2026-04-14T10:00:00.000Z",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
await wrapper.get("[data-testid='invoicing-period-customer-invoice-1003']").trigger("click");
|
|
await nextTick();
|
|
|
|
expect(SessionUser.objects.orders.get.multiple).not.toHaveBeenCalled();
|
|
expect(invoiceQueue.addInvoiceCollectionsToQueue).toHaveBeenCalledWith([3001], {
|
|
customerNumber: 1003,
|
|
});
|
|
expect(invoiceQueue.processInvoiceCollectionQueue).toHaveBeenCalledTimes(1);
|
|
expect(windowOpenSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("splits instead of queueing when multi-month invoicing warning is confirmed", async () => {
|
|
sharedVariablesRef.value = {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 8,
|
|
customer_number: 1008,
|
|
customer_name: "Multi Month Customer",
|
|
requires_action: true,
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
transactions: [
|
|
{
|
|
id: 8101,
|
|
amount: 75,
|
|
booked: false,
|
|
excluded: false,
|
|
invoice_collection_id: 8100,
|
|
date: "2026-03-28T10:00:00.000Z",
|
|
},
|
|
{
|
|
id: 8102,
|
|
amount: 125,
|
|
booked: false,
|
|
excluded: false,
|
|
invoice_collection_id: 8100,
|
|
date: "2026-04-02T10:00:00.000Z",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
Swal.fire.mockResolvedValueOnce({ isConfirmed: true }).mockResolvedValueOnce({ isConfirmed: true });
|
|
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
await wrapper.get("[data-testid='invoicing-period-customer-invoice-1008']").trigger("click");
|
|
await flushPromises();
|
|
await nextTick();
|
|
|
|
expect(SessionUser.objects.collectedOrderInvoices.functions.split_by_month).toHaveBeenCalledWith(
|
|
"2026-03-28",
|
|
"2026-04-02",
|
|
{
|
|
invoiceCollectionIds: [8100],
|
|
preview: false,
|
|
}
|
|
);
|
|
expect(invoiceQueue.addInvoiceCollectionsToQueue).not.toHaveBeenCalled();
|
|
expect(invoiceQueue.processInvoiceCollectionQueue).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("continues queueing together when multi-month invoicing warning is denied", async () => {
|
|
sharedVariablesRef.value = {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 9,
|
|
customer_number: 1009,
|
|
customer_name: "Invoice Together Customer",
|
|
requires_action: true,
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
transactions: [
|
|
{
|
|
id: 8201,
|
|
amount: 75,
|
|
booked: false,
|
|
excluded: false,
|
|
invoice_collection_id: 8200,
|
|
date: "2026-03-28T10:00:00.000Z",
|
|
},
|
|
{
|
|
id: 8202,
|
|
amount: 125,
|
|
booked: false,
|
|
excluded: false,
|
|
invoice_collection_id: 8200,
|
|
date: "2026-04-02T10:00:00.000Z",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
Swal.fire.mockResolvedValueOnce({ isDenied: true });
|
|
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
await wrapper.get("[data-testid='invoicing-period-customer-invoice-1009']").trigger("click");
|
|
await flushPromises();
|
|
await nextTick();
|
|
|
|
expect(SessionUser.objects.collectedOrderInvoices.functions.split_by_month).not.toHaveBeenCalled();
|
|
expect(invoiceQueue.addInvoiceCollectionsToQueue).toHaveBeenCalledWith([8200], {
|
|
customerNumber: 1009,
|
|
});
|
|
expect(invoiceQueue.processInvoiceCollectionQueue).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("does not queue when multi-month invoicing warning is dismissed", async () => {
|
|
sharedVariablesRef.value = {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 10,
|
|
customer_number: 1010,
|
|
customer_name: "Cancel Multi Month Customer",
|
|
requires_action: true,
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
transactions: [
|
|
{
|
|
id: 8301,
|
|
amount: 75,
|
|
booked: false,
|
|
excluded: false,
|
|
invoice_collection_id: 8300,
|
|
date: "2026-03-28T10:00:00.000Z",
|
|
},
|
|
{
|
|
id: 8302,
|
|
amount: 125,
|
|
booked: false,
|
|
excluded: false,
|
|
invoice_collection_id: 8300,
|
|
date: "2026-04-02T10:00:00.000Z",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
Swal.fire.mockResolvedValueOnce({ isDismissed: true });
|
|
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
await wrapper.get("[data-testid='invoicing-period-customer-invoice-1010']").trigger("click");
|
|
await flushPromises();
|
|
await nextTick();
|
|
|
|
expect(SessionUser.objects.collectedOrderInvoices.functions.split_by_month).not.toHaveBeenCalled();
|
|
expect(invoiceQueue.addInvoiceCollectionsToQueue).not.toHaveBeenCalled();
|
|
expect(invoiceQueue.processInvoiceCollectionQueue).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("applies invoice-now loading only to the affected customer", async () => {
|
|
sharedVariablesRef.value = {
|
|
types: {
|
|
all: [
|
|
{
|
|
id: 4,
|
|
customer_number: 1004,
|
|
customer_name: "Loading Customer",
|
|
requires_action: true,
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
transactions: [
|
|
{
|
|
id: 7001,
|
|
amount: 75,
|
|
booked: false,
|
|
excluded: false,
|
|
invoice_collection_id: 4001,
|
|
date: "2026-04-13T10:00:00.000Z",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 5,
|
|
customer_number: 1005,
|
|
customer_name: "Independent Customer",
|
|
requires_action: true,
|
|
queue: {
|
|
has_active_job: false,
|
|
statuses: [],
|
|
invoice_collection_ids: [],
|
|
is_action_blocked: false,
|
|
},
|
|
transactions: [
|
|
{
|
|
id: 7002,
|
|
amount: 125,
|
|
booked: false,
|
|
excluded: false,
|
|
invoice_collection_id: 4002,
|
|
date: "2026-04-14T10:00:00.000Z",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
loadingCustomerNumbersRef.value = [1004];
|
|
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
const loadingButton = wrapper.get("[data-testid='invoicing-period-customer-invoice-1004']");
|
|
const independentButton = wrapper.get("[data-testid='invoicing-period-customer-invoice-1005']");
|
|
|
|
expect(loadingButton.classes()).toContain("is-loading");
|
|
expect(loadingButton.attributes("disabled")).toBeDefined();
|
|
expect(independentButton.classes()).not.toContain("is-loading");
|
|
expect(independentButton.attributes("disabled")).toBeUndefined();
|
|
});
|
|
|
|
it("groups possible duplicates by vehicle plate and date across customers", async () => {
|
|
currentViewRef.value = "possible_duplicates";
|
|
sharedVariablesRef.value = {
|
|
types: {
|
|
possible_duplicates: [
|
|
{
|
|
id: 11,
|
|
customer_number: 2101,
|
|
customer_name: "Pleno Vognmandsforretning",
|
|
requires_action: true,
|
|
transactions: [
|
|
{
|
|
id: 8101,
|
|
amount: 100,
|
|
booked: false,
|
|
excluded: false,
|
|
reg_1: "EC 21233",
|
|
date: "2026-04-10T10:00:00.000Z",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 12,
|
|
customer_number: 2102,
|
|
customer_name: "Estland Alle ApS",
|
|
requires_action: true,
|
|
transactions: [
|
|
{
|
|
id: 8102,
|
|
amount: 125,
|
|
booked: false,
|
|
excluded: false,
|
|
reg_1: "EC21233",
|
|
created_at: "2026-04-10 12:15:00",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 13,
|
|
customer_number: 2103,
|
|
customer_name: "Single Wash ApS",
|
|
requires_action: true,
|
|
transactions: [
|
|
{
|
|
id: 8103,
|
|
amount: 90,
|
|
booked: false,
|
|
excluded: false,
|
|
reg_1: "SINGLE1",
|
|
created_at: "2026-04-10 12:15:00",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const wrapper = mountView();
|
|
await nextTick();
|
|
|
|
expect(wrapper.find("[data-testid='invoicing-period-customer-2101']").exists()).toBe(false);
|
|
const group = wrapper.get("[data-testid='invoicing-period-duplicate-group-EC21233-2026-04-10']");
|
|
expect(wrapper.find("[data-testid='invoicing-period-duplicate-group-SINGLE1-2026-04-10']").exists()).toBe(false);
|
|
|
|
expect(group.text()).toContain("2 kunder");
|
|
expect(group.text()).toContain("2 vaskelog");
|
|
expect(group.text()).not.toContain("EC21233 - 10/04/2026");
|
|
expect(group.text()).toContain("Pleno Vognmandsforretning #2101");
|
|
expect(group.text()).toContain("Estland Alle ApS #2102");
|
|
const duplicateTags = group.find(".duplicate-group-tags");
|
|
expect(duplicateTags.exists()).toBe(true);
|
|
expect(duplicateTags.findAll(".duplicate-group-chip")).toHaveLength(2);
|
|
|
|
await group.find(".duplicate-group-header").trigger("click");
|
|
await nextTick();
|
|
|
|
const expanded = wrapper.get("[data-testid='invoicing-period-duplicate-group-expanded-EC21233-2026-04-10']");
|
|
expect(expanded.text()).toContain("Sammenligning");
|
|
expect(expanded.text()).toContain("EC21233");
|
|
expect(expanded.text()).toContain("10/04/2026");
|
|
expect(expanded.findAll(".duplicate-comparison-row")).toHaveLength(2);
|
|
|
|
const pagination = expanded.get(".invoice-orders-pagination-stub");
|
|
expect(pagination.attributes("data-customer")).toBeUndefined();
|
|
expect(pagination.attributes("data-order-ids")).toBe("8101,8102");
|
|
expect(pagination.attributes("data-group-invoice-collection")).toBe("false");
|
|
});
|
|
});
|