Files
pleno-vue/tests/unit/invoice-distribution-api.spec.js
T
Jeppe Bundgaard b39b458f4f Add .prettierrc.json and refactor test files for improved formatting consistency:
- Introduced `.prettierrc.json` to enforce consistent code formatting across the project.
- Updated unit and e2e test files to address formatting issues, improve readability, and ensure alignment with the new Prettier configuration.
2026-04-13 09:13:27 +02:00

537 lines
16 KiB
JavaScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@/components/session/token/SessionUser.vue", () => {
const request = vi.fn();
const parseErrorMessage = vi.fn((error) => error?.message || String(error || "Unknown error"));
return {
SessionUser: {
request,
functions: {
parseErrorMessage,
},
},
};
});
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import {
compareCollectedInvoicesForMonth,
fetchMonthData,
} from "@/views/dashboards/superUserDashboard/invoiceDistribution/imports/invoiceDistributionApi.js";
const buildInvoice = (id) => ({
id,
customer_number: 2000 + id,
customer_name: `Customer ${id}`,
total_net_amount: 100,
});
const buildV2Category = (amount) => ({
customers: [
{
id: 1,
customer_number: 5001,
customer_name: "ACME",
requires_action: false,
transactions: [{ id: 1, amount, booked: true, excluded: false, department_id: 1, date: "2026-03-01T00:00:00Z" }],
meta: {
fixed_pricing: {
price: amount,
department_totals_relative: { 1: amount },
},
},
},
],
collective_results: {
total_amount: amount,
department_distribution_parsed: { Ops: amount },
},
warnings: [],
});
const buildV2CompareResult = (invoiceId, { internalTotal = 100, bookedTotal = 100, mismatch = false } = {}) => ({
collected_invoice_id: invoiceId,
warnings: mismatch ? ["Top level warning"] : [],
details: {
order_ids: mismatch ? [1, 2] : [1],
customer: {
internal_customer_number: 7000 + invoiceId,
name: `Customer ${invoiceId}`,
},
internal: {
normalized: {
totals: {
net_total: internalTotal,
billable_line_count: mismatch ? 2 : 1,
},
},
},
},
comparison: {
totals: {
internal_net_total: internalTotal,
},
targets: {
booked: {
target: "booked",
status: mismatch ? "partial_mismatch" : "exact_match",
overall_match: !mismatch,
totals: {
target_net_total: bookedTotal,
difference: internalTotal - bookedTotal,
},
mismatch_reasons: mismatch ? ["department_total_mismatch"] : [],
warnings: mismatch ? ["Line mismatch"] : [],
lines: {
summary: {
internal_billable_count: mismatch ? 2 : 1,
target_billable_count: mismatch ? 2 : 1,
mismatch_count: mismatch ? 1 : 0,
},
diff: [],
},
departments: {
matches: !mismatch,
diff: [],
},
},
},
warnings: [],
},
});
describe("invoice distribution api adapter", () => {
let consoleWarnSpy;
beforeEach(() => {
consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
SessionUser.request.mockReset();
SessionUser.functions.parseErrorMessage.mockReset();
SessionUser.functions.parseErrorMessage.mockImplementation(
(error) => error?.message || String(error || "Unknown error")
);
});
afterEach(() => {
consoleWarnSpy?.mockRestore();
});
it("uses v2 all endpoint for month distribution when available", async () => {
SessionUser.request.mockImplementation(async (endpoint) => {
if (endpoint === "/superuser/invoicing/period") {
return {
data: {
data: {
types: {
all: [
{
transactions: [{ amount: 200, booked: true, excluded: false }],
},
],
},
},
},
};
}
if (endpoint === "/superuser/invoicing/period/distribution/v2/all") {
return {
data: {
fixed_pricing: buildV2Category(80),
wash_subscriptions: buildV2Category(70),
customer_prices: buildV2Category(50),
},
};
}
throw new Error(`Unexpected endpoint: ${endpoint}`);
});
const result = await fetchMonthData(2026, 3);
expect(result.fallback.usedLegacyDistribution).toBe(false);
expect(result.fallback.usedSplitV2Distribution).toBe(false);
expect(result.summary.distributionAmount).toBe(150);
expect(result.summary.customerPriceAmount).toBe(50);
expect(result.summary.totalAmount).toBe(350);
expect(SessionUser.request).toHaveBeenCalledWith(
"/superuser/invoicing/period/distribution/v2/all",
"GET",
expect.any(Object)
);
});
it("falls back to v2 split endpoints when v2 all fails", async () => {
SessionUser.request.mockImplementation(async (endpoint) => {
if (endpoint === "/superuser/invoicing/period") {
return {
data: {
data: {
types: {
all: [{ transactions: [{ amount: 120, booked: true, excluded: false }] }],
},
},
},
};
}
if (endpoint === "/superuser/invoicing/period/distribution/v2/all") {
throw new Error("v2 all unavailable");
}
if (endpoint === "/superuser/invoicing/period/distribution/v2/fixed-pricing") {
return { data: buildV2Category(40) };
}
if (endpoint === "/superuser/invoicing/period/distribution/v2/wash-subscriptions") {
return { data: buildV2Category(30) };
}
if (endpoint === "/superuser/invoicing/period/distribution/v2/customer-prices") {
return { data: buildV2Category(20) };
}
throw new Error(`Unexpected endpoint: ${endpoint}`);
});
const result = await fetchMonthData(2026, 3);
expect(result.fallback.usedLegacyDistribution).toBe(false);
expect(result.fallback.usedSplitV2Distribution).toBe(true);
expect(result.fallback.distributionFallbackReason).toContain("v2 all unavailable");
expect(result.summary.distributionAmount).toBe(70);
expect(result.summary.customerPriceAmount).toBe(20);
expect(result.summary.totalAmount).toBe(190);
});
it("falls back to legacy distribution endpoints when all v2 paths fail", async () => {
SessionUser.request.mockImplementation(async (endpoint) => {
if (endpoint === "/superuser/invoicing/period") {
return {
data: {
data: {
types: {
all: [{ transactions: [{ amount: 120, booked: true, excluded: false }] }],
},
},
},
};
}
if (
endpoint === "/superuser/invoicing/period/distribution/v2/all" ||
endpoint === "/superuser/invoicing/period/distribution/v2/fixed-pricing" ||
endpoint === "/superuser/invoicing/period/distribution/v2/wash-subscriptions" ||
endpoint === "/superuser/invoicing/period/distribution/v2/customer-prices"
) {
throw new Error(`${endpoint} unavailable`);
}
if (endpoint === "/superuser/invoicing/period/distribution/fixed-pricing") {
return {
data: {
customers: [
{
id: 1,
customer_number: 1001,
customer_name: "Legacy Fixed",
meta: { fixed_pricing: { price: 50 } },
},
],
collective_results: {
total_fixed_price: 50,
total_department_totals_relative_parsed: { Ops: 50 },
},
warnings: [],
},
};
}
if (endpoint === "/superuser/invoicing/period/distribution/wash-subscriptions") {
return {
data: {
customers: [
{
id: 2,
customer_number: 1002,
customer_name: "Legacy Subscription",
meta: { wash_subscription: { price: 25 } },
},
],
collective_results: {
total_subscription_price: 25,
subscription_price_department_distribution_parsed: { Ops: 25 },
},
warnings: [],
},
};
}
throw new Error(`Unexpected endpoint: ${endpoint}`);
});
const result = await fetchMonthData(2026, 3);
expect(result.fallback.usedLegacyDistribution).toBe(true);
expect(result.fallback.distributionFallbackReason).toContain(
"/superuser/invoicing/period/distribution/v2/all unavailable"
);
expect(result.summary.fixedPricingAmount).toBe(50);
expect(result.summary.subscriptionAmount).toBe(25);
expect(result.summary.distributionAmount).toBe(75);
expect(result.summary.totalAmount).toBe(195);
});
it("chunks v2 bulk compare requests to max 200 ids", async () => {
const invoices = Array.from({ length: 250 }, (_, index) => buildInvoice(index + 1));
SessionUser.request.mockImplementation(async (endpoint, method, params) => {
if (endpoint === "/collected-invoices" && method === "GET") {
const page = Number(params?.page || 1);
const perPage = 100;
const start = (page - 1) * perPage;
const data = invoices.slice(start, start + perPage);
return {
data: {
data,
meta: {
pagination: {
total: invoices.length,
per_page: perPage,
},
},
},
};
}
if (endpoint === "/collected-invoices/economic/v2/compare/bulk" && method === "POST") {
const ids = params?.collected_invoice_ids || [];
return {
data: {
data: {
requested: ids.length,
compared: ids.length,
failed: 0,
results: ids.map((id) => buildV2CompareResult(id)),
errors: [],
},
},
};
}
throw new Error(`Unexpected endpoint: ${endpoint}`);
});
const result = await compareCollectedInvoicesForMonth({
year: 2026,
month: 3,
mode: "invoice_total",
batchSize: 500,
});
const bulkCalls = SessionUser.request.mock.calls.filter(
([endpoint]) => endpoint === "/collected-invoices/economic/v2/compare/bulk"
);
expect(bulkCalls).toHaveLength(2);
expect(bulkCalls[0][2].collected_invoice_ids).toHaveLength(200);
expect(bulkCalls[1][2].collected_invoice_ids).toHaveLength(50);
expect(result.invoiceCount).toBe(250);
expect(result.fallback.usedLegacyCompare).toBe(false);
});
it("emits incremental compare progress and row chunks while v2 compare runs", async () => {
const invoices = [buildInvoice(1), buildInvoice(2), buildInvoice(3), buildInvoice(4), buildInvoice(5)];
const onRows = vi.fn();
const onProgress = vi.fn();
SessionUser.request.mockImplementation(async (endpoint, method, params) => {
if (endpoint === "/collected-invoices" && method === "GET") {
return {
data: {
data: invoices,
},
};
}
if (endpoint === "/collected-invoices/economic/v2/compare/bulk" && method === "POST") {
const ids = params?.collected_invoice_ids || [];
return {
data: {
data: {
requested: ids.length,
compared: ids.length,
failed: 0,
results: ids.map((id) => buildV2CompareResult(id)),
errors: [],
},
},
};
}
throw new Error(`Unexpected endpoint: ${endpoint}`);
});
const result = await compareCollectedInvoicesForMonth({
year: 2026,
month: 3,
mode: "invoice_total",
batchSize: 2,
onRows,
onProgress,
});
expect(result.rows).toHaveLength(5);
expect(onRows).toHaveBeenCalledTimes(5);
expect(onRows.mock.calls.map(([rows]) => rows.length)).toEqual([1, 1, 1, 1, 1]);
expect(onProgress.mock.calls.map(([progress]) => `${progress.processed}/${progress.total}`)).toEqual([
"0/5",
"1/5",
"2/5",
"3/5",
"4/5",
"5/5",
]);
});
it("recovers missing bulk compare rows via v2 single endpoint", async () => {
const invoices = [buildInvoice(101), buildInvoice(102)];
SessionUser.request.mockImplementation(async (endpoint, method, params) => {
if (endpoint === "/collected-invoices" && method === "GET") {
return {
data: {
data: invoices,
},
};
}
if (endpoint === "/collected-invoices/economic/v2/compare/bulk" && method === "POST") {
return {
data: {
data: {
requested: params?.collected_invoice_ids?.length || 0,
compared: 1,
failed: 1,
results: [buildV2CompareResult(101)],
errors: [{ collected_invoice_id: 102, error: "temporary mismatch in bulk compare" }],
},
},
};
}
if (endpoint === "/collected-invoices/economic/v2/compare" && method === "GET") {
return {
data: {
data: buildV2CompareResult(102, { mismatch: true, internalTotal: 110, bookedTotal: 90 }),
},
};
}
throw new Error(`Unexpected endpoint: ${endpoint}`);
});
const result = await compareCollectedInvoicesForMonth({
year: 2026,
month: 3,
mode: "line_by_line",
});
expect(result.fallback.usedLegacyCompare).toBe(false);
expect(result.fallback.usedSingleCompareFallback).toBe(true);
expect(result.fallback.compareFallbackReason).toContain("temporary mismatch in bulk compare");
expect(result.rows).toHaveLength(2);
expect(result.rows.some((row) => row.invoiceId === 102)).toBe(true);
});
it("falls back to legacy compare when v2 bulk compare fails", async () => {
const invoices = [buildInvoice(101), buildInvoice(102)];
SessionUser.request.mockImplementation(async (endpoint, method, params) => {
if (endpoint === "/collected-invoices" && method === "GET") {
return {
data: {
data: invoices,
},
};
}
if (endpoint === "/collected-invoices/economic/v2/compare/bulk" && method === "POST") {
throw new Error("v2 compare failed");
}
if (endpoint === "/collected-invoices/economic/compare" && method === "GET") {
const invoiceId = Number(params?.collected_invoice_id);
return {
data: {
data: {
collected_invoice_id: invoiceId,
internal_total: 100,
booked_total: 95,
difference: 5,
warnings: [],
order_ids: [invoiceId],
},
},
};
}
throw new Error(`Unexpected endpoint: ${endpoint}`);
});
const result = await compareCollectedInvoicesForMonth({
year: 2026,
month: 3,
mode: "invoice_total",
legacyBatchSize: 10,
});
const legacyCalls = SessionUser.request.mock.calls.filter(
([endpoint]) => endpoint === "/collected-invoices/economic/compare"
);
expect(legacyCalls).toHaveLength(2);
expect(result.fallback.usedLegacyCompare).toBe(true);
expect(result.fallback.compareFallbackReason).toContain("v2 compare failed");
expect(result.rows).toHaveLength(2);
});
it("creates mismatch fallback row when both bulk and single v2 data are missing", async () => {
const invoices = [buildInvoice(301)];
SessionUser.request.mockImplementation(async (endpoint, method) => {
if (endpoint === "/collected-invoices" && method === "GET") {
return { data: { data: invoices } };
}
if (endpoint === "/collected-invoices/economic/v2/compare/bulk" && method === "POST") {
return {
data: {
data: {
requested: 1,
compared: 0,
failed: 1,
results: [],
errors: [{ collected_invoice_id: 301, error: "no bulk result" }],
},
},
};
}
if (endpoint === "/collected-invoices/economic/v2/compare" && method === "GET") {
throw new Error("single compare down");
}
throw new Error(`Unexpected endpoint: ${endpoint}`);
});
const result = await compareCollectedInvoicesForMonth({
year: 2026,
month: 3,
mode: "invoice_total",
});
expect(result.fallback.usedLegacyCompare).toBe(false);
expect(result.fallback.usedSingleCompareFallback).toBe(true);
expect(result.rows).toHaveLength(1);
expect(result.rows[0].status).toBe("mismatch");
expect(result.rows[0].warnings.join(" ")).toContain("single compare down");
});
});