Files
pleno-vue/tests/unit/invoicing-period-flag-list.spec.js
T

468 lines
16 KiB
JavaScript

// @vitest-environment jsdom
import { mount } from "@vue/test-utils";
import { beforeEach, describe, expect, it, vi } from "vitest";
import InvoicingPeriodFlagList from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/components/InvoicingPeriodFlagList.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { createTestI18n } from "./helpers/mountWithApp.js";
import daMessages from "@/i18n/locales/da.json";
const swalFireMock = vi.hoisted(() => vi.fn(() => Promise.resolve({ isConfirmed: true, value: "Checked" })));
const showPopperMock = vi.hoisted(() => vi.fn());
const removePopperIfOpenMock = vi.hoisted(() => vi.fn());
const popperBoxMock = vi.hoisted(() => vi.fn((title, body) => ({ title, body })));
vi.mock("sweetalert2", () => ({
default: {
fire: swalFireMock,
},
}));
vi.mock("@/components/displays/PopperDefault.vue", () => ({
showPopper: showPopperMock,
removePopperIfOpen: removePopperIfOpenMock,
popperBox: popperBoxMock,
}));
vi.mock("@/components/session/token/SessionUser.vue", () => ({
SessionUser: {
request: vi.fn(() => Promise.resolve({ data: { data: null } })),
functions: {
redirectTo: {
department: vi.fn(),
superUser: vi.fn(),
},
},
},
}));
const i18n = createTestI18n({
en: {
global: {
cancel: "Cancel",
},
invoice_period: {
flags: {
automatic: {
price_mismatch: "{product} product price differs from expected.",
multiple_identical_primary_vehicle_items: "Order contains multiple identical primary vehicle items.",
wash_certificate_item_without_certificate: "Wash certificate item is present without a wash certificate.",
customer_rule_requires_reference: "Order is missing a required reference.",
customer_rule_requires_po_number: "Order is missing a required PO number.",
xlvask_missing_order_link: "XL Vask wash is neither ignored nor linked to an order in the selected period.",
},
preview: {
no_order_items: "No order items available.",
no_xlvask_usage_log: "No XL Vask details available.",
product: "Product",
quantity: "Qty",
price: "Price",
order_items: "Order items",
expected_price: "Expected price",
xlvask_usage_log: "XL Vask registration",
wash_id: "Wash ID",
registration_number: "Registration",
start_time: "Start time",
customer: "Customer",
},
tokens: {
expected: "expected",
order: "Order",
wash_certificate_item: "Wash certificate item",
wash_certificate: "Wash certificate",
xlvask_usage_log: "XL Vask wash",
},
status: {
resolved: "Resolved",
ignored: "Ignored",
false_positive: "False positive",
reason_placeholder: "Optional reason",
},
tooltip: {
created_at: "Created",
created_by: "By",
unknown: "Unknown",
},
},
},
},
});
const TooltipStub = {
props: {
position: String,
type: String,
multilined: Boolean,
appendToBody: Boolean,
},
template: `
<div
class="b-tooltip-stub"
:data-position="position"
:data-type="type"
:data-multilined="String(multilined)"
:data-append-to-body="String(appendToBody)"
>
<div class="b-tooltip-content-stub"><slot name="content" /></div>
<slot />
</div>
`,
};
const mountList = (flags, plugin = i18n) =>
mount(InvoicingPeriodFlagList, {
props: {
flags,
},
global: {
plugins: [plugin],
stubs: {
BTooltip: TooltipStub,
},
},
});
describe("InvoicingPeriodFlagList", () => {
beforeEach(() => {
swalFireMock.mockReset();
swalFireMock.mockResolvedValue({ isConfirmed: true, value: "Checked" });
showPopperMock.mockClear();
removePopperIfOpenMock.mockClear();
popperBoxMock.mockClear();
SessionUser.request.mockReset();
SessionUser.request.mockResolvedValue({ data: { data: null } });
SessionUser.functions.redirectTo.department.mockClear();
SessionUser.functions.redirectTo.superUser.mockClear();
});
it("renders manual flags before localized automatic warnings with matching flag colors", () => {
const wrapper = mountList([
{
id: 1,
source: "manual",
reason: "Customer asked for manual review.",
},
{
id: "auto-price-1",
source: "automatic",
definition_key: "price_mismatch",
message_key: "invoice_period.flags.automatic.price_mismatch",
message_params: {
product: "Spot Free",
},
},
]);
const rows = wrapper.findAll(".invoice-period-flag-row");
expect(rows).toHaveLength(2);
expect(rows[0].text()).toContain("Customer asked for manual review.");
expect(rows[0].find("i").classes()).toContain("has-text-danger");
expect(rows[1].text()).toContain("Spot Free product price differs from expected.");
expect(rows[1].find("i").classes()).toContain("has-text-warning");
});
it("does not render inactive flags", () => {
const wrapper = mountList([
{
id: 1,
source: "manual",
status: "resolved",
reason: "Already resolved.",
},
{
id: "auto-price-1",
source: "automatic",
status: "active",
definition_key: "price_mismatch",
message_key: "invoice_period.flags.automatic.price_mismatch",
message_params: {
product: "Spot Free",
},
},
]);
const rows = wrapper.findAll(".invoice-period-flag-row");
expect(rows).toHaveLength(1);
expect(wrapper.text()).not.toContain("Already resolved.");
expect(wrapper.text()).toContain("Spot Free product price differs from expected.");
});
it("shows manual flag creation metadata in a buefy tooltip", () => {
const wrapper = mountList([
{
id: 1,
source: "manual",
reason: "Customer asked for manual review.",
created_at: "2026-05-11 10:00:00",
created_by: 42,
created_by_name: "Jeppe",
},
{
id: "auto-price-1",
source: "automatic",
definition_key: "price_mismatch",
message_key: "invoice_period.flags.automatic.price_mismatch",
message_params: {
product: "Spot Free",
},
},
]);
const tooltip = wrapper.get('[data-testid="invoice-period-flag-created-tooltip-1"]');
expect(wrapper.get(".b-tooltip-stub").attributes("data-multilined")).toBe("true");
expect(wrapper.get(".b-tooltip-stub").attributes("data-append-to-body")).toBe("true");
expect(tooltip.text()).toContain("Created");
expect(tooltip.text()).toContain("By");
expect(tooltip.text()).toContain("Jeppe");
expect(tooltip.text()).toMatch(/10|2026|5\/11|11\/5/);
expect(wrapper.find('[data-testid="invoice-period-flag-created-tooltip-auto-price-1"]').exists()).toBe(false);
});
it("opens highlighted order items and previews expected price details from automatic messages", async () => {
const wrapper = mountList([
{
id: "auto-price-1",
source: "automatic",
fingerprint: "price-fingerprint",
target_type: "order_item_field",
target_id: 77,
field: "price",
order_id: 42,
order_item_id: 77,
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: 3,
order_id: 42,
order_item_id: 77,
order_items: [
{ id: 77, product_name: "Spot Free", quantity: 1, price: 99 },
{ id: 78, product_name: "Wash", quantity: 1, price: 199 },
],
expected_price_breakdown: {
product_price: 100,
department_price: 90,
product_discount_percentage: 10,
applied_discount_percentage: 10,
expected_price: 81,
},
},
},
]);
await wrapper.get(".invoice-period-flag-token").trigger("click");
expect(SessionUser.functions.redirectTo.department).toHaveBeenCalledWith(
3,
"modules/pos/orders/42?highlightOrderItem=77",
true
);
await wrapper.findAll(".invoice-period-flag-token")[1].trigger("mouseover");
expect(popperBoxMock).toHaveBeenCalledWith("Expected price", expect.stringContaining("81"));
expect(showPopperMock).toHaveBeenCalledTimes(1);
});
it("makes order and wash certificate item warning tokens hoverable and clickable", async () => {
const wrapper = mountList([
{
id: "auto-order-1",
source: "automatic",
fingerprint: "order-fingerprint",
definition_key: "multiple_identical_primary_vehicle_items",
message_key: "invoice_period.flags.automatic.multiple_identical_primary_vehicle_items",
order_id: 42,
context: {
department_id: 3,
order_id: 42,
order_items: [
{ id: 71, product_name: "Truck wash", quantity: 1, price: 199 },
{ id: 72, product_name: "Truck wash", quantity: 1, price: 199 },
],
},
},
{
id: "auto-certificate-1",
source: "automatic",
fingerprint: "certificate-fingerprint",
definition_key: "wash_certificate_item_without_certificate",
message_key: "invoice_period.flags.automatic.wash_certificate_item_without_certificate",
order_id: 43,
order_item_id: 88,
context: {
department_id: 4,
order_id: 43,
order_item_id: 88,
order_items: [
{ id: 88, product_name: "Wash certificate", quantity: 1, price: 0 },
{ id: 89, product_name: "Wash", quantity: 1, price: 199 },
],
},
},
]);
const tokens = wrapper.findAll(".invoice-period-flag-token");
expect(tokens.map((token) => token.text())).toEqual(["Order", "Wash certificate item"]);
await tokens[0].trigger("mouseover");
expect(popperBoxMock).toHaveBeenLastCalledWith("Order items", expect.stringContaining("Truck wash"));
await tokens[0].trigger("click");
expect(SessionUser.functions.redirectTo.department).toHaveBeenLastCalledWith(3, "modules/pos/orders/42", true);
await tokens[1].trigger("mouseover");
expect(popperBoxMock).toHaveBeenLastCalledWith(
"Order items",
expect.stringContaining("has-background-warning-light")
);
await tokens[1].trigger("click");
expect(SessionUser.functions.redirectTo.department).toHaveBeenLastCalledWith(
4,
"modules/pos/orders/43?highlightOrderItem=88",
true
);
});
it("makes required order field warning tokens hoverable and clickable", async () => {
const wrapper = mountList([
{
id: "auto-po-1",
source: "automatic",
fingerprint: "po-fingerprint",
definition_key: "customer_rule_requires_po_number",
message_key: "invoice_period.flags.automatic.customer_rule_requires_po_number",
order_id: 44,
context: {
department_id: 5,
order_id: 44,
order_items: [
{ id: 91, product_name: "Forvogn", quantity: 1, price: 649 },
{ id: 92, product_name: "Trailer", quantity: 1, price: 599 },
],
},
},
]);
const token = wrapper.get(".invoice-period-flag-token");
expect(token.text()).toBe("Order");
await token.trigger("mouseover");
expect(popperBoxMock).toHaveBeenLastCalledWith("Order items", expect.stringContaining("Forvogn"));
await token.trigger("click");
expect(SessionUser.functions.redirectTo.department).toHaveBeenLastCalledWith(5, "modules/pos/orders/44", true);
});
it("makes XL Vask registration warning tokens hoverable and clickable", async () => {
const wrapper = mountList([
{
id: "auto-xlvask-1",
source: "automatic",
fingerprint: "xlvask-fingerprint",
definition_key: "xlvask_missing_order_link",
message_key: "invoice_period.flags.automatic.xlvask_missing_order_link",
target_type: "xlvask_usage_log",
target_id: 55,
xlvask_usage_log_id: 55,
message_params: {
wash_id: "wash-55",
registration_number: "AB12345",
},
context: {
customer_name: "XL Customer",
xlvask_usage_log_id: 55,
wash_id: "wash-55",
registration_number: "AB12345",
start_time: "2026-05-11 10:00:00",
},
},
]);
const token = wrapper.get(".invoice-period-flag-token");
expect(token.text()).toBe("XL Vask wash");
await token.trigger("mouseover");
expect(popperBoxMock).toHaveBeenLastCalledWith("XL Vask registration", expect.stringContaining("AB12345"));
expect(showPopperMock).toHaveBeenCalledTimes(1);
await token.trigger("click");
expect(SessionUser.functions.redirectTo.superUser).toHaveBeenCalledWith(
"/invoices?activeTab=period&periodView=self_wash&xlvaskUsageLogId=55",
true
);
});
it("renders invoice period warnings in proper Danish", () => {
const wrapper = mountList(
[
{
id: "auto-order-1",
source: "automatic",
definition_key: "multiple_identical_primary_vehicle_items",
message_key: "invoice_period.flags.automatic.multiple_identical_primary_vehicle_items",
},
{
id: "auto-certificate-1",
source: "automatic",
definition_key: "wash_certificate_item_without_certificate",
message_key: "invoice_period.flags.automatic.wash_certificate_item_without_certificate",
},
{
id: "auto-price-1",
source: "automatic",
definition_key: "price_mismatch",
message_key: "invoice_period.flags.automatic.price_mismatch",
message_params: {
product: "Forevogn",
},
},
],
createTestI18n({ en: daMessages })
);
expect(wrapper.text()).toContain("Ordren indeholder flere ens primære køretøjsprodukter.");
expect(wrapper.text()).toContain("Vaskecertifikatlinjen findes uden et vaskecertifikat.");
expect(wrapper.text()).toContain("Forevogn: Produktprisen afviger fra den forventede pris.");
expect(wrapper.find('button[title="Løst"]').exists()).toBe(true);
});
it("persists manual and automatic status decisions through the correct endpoints", async () => {
const wrapper = mountList([
{
id: 12,
source: "manual",
reason: "Manual review",
},
{
id: "auto-price-1",
source: "automatic",
fingerprint: "price-fingerprint",
target_type: "order_item_field",
target_id: 77,
field: "price",
definition_key: "price_mismatch",
message: "Automatic review",
},
]);
await wrapper.get('[data-testid="invoice-period-flag-12"] button[title="Resolved"]').trigger("click");
expect(SessionUser.request).toHaveBeenCalledWith("/superuser/invoicing/period/flags/12/status", "PATCH", {
status: "resolved",
reason: "Checked",
});
await wrapper.get('[data-testid="invoice-period-flag-auto-price-1"] button[title="Ignored"]').trigger("click");
expect(SessionUser.request).toHaveBeenCalledWith("/superuser/invoicing/period/flags/automatic/status", "POST", {
fingerprint: "price-fingerprint",
status: "ignored",
target_type: "order_item_field",
target_id: 77,
field: "price",
definition_key: "price_mismatch",
reason: "Checked",
});
expect(wrapper.emitted("statusChanged")).toHaveLength(2);
});
});