- 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.
409 lines
12 KiB
JavaScript
409 lines
12 KiB
JavaScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
buildDistributionQueryState,
|
|
getMonthRange,
|
|
buildMonthSummary,
|
|
buildDepartmentAllocations,
|
|
buildDepartmentAllocationFooterTotals,
|
|
buildCustomerAllocationRows,
|
|
buildMonthComparison,
|
|
buildSourceComposition,
|
|
normalizeDistributionQueryState,
|
|
normalizeCompareResult,
|
|
aggregateCompareRows,
|
|
sortCompareRows,
|
|
sortMonthSummaries,
|
|
} from "@/views/dashboards/superUserDashboard/invoiceDistribution/imports/invoiceDistributionCalculations.js";
|
|
|
|
describe("invoice distribution calculations", () => {
|
|
it("builds month ranges from first month to current month", () => {
|
|
const result = getMonthRange(new Date("2026-01-10T00:00:00Z"), new Date("2026-03-20T00:00:00Z"), true);
|
|
expect(result.map((item) => item.key)).toEqual(["2026-03", "2026-02", "2026-01"]);
|
|
});
|
|
|
|
it("builds month summary with booked and source split", () => {
|
|
const summary = buildMonthSummary({
|
|
year: 2026,
|
|
month: 2,
|
|
periodData: {
|
|
types: {
|
|
all: [
|
|
{
|
|
transactions: [
|
|
{ amount: 100, booked: true, excluded: false },
|
|
{ amount: 25, booked: false, excluded: false },
|
|
{ amount: 30, booked: true, excluded: true },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
fixedDistribution: {
|
|
includes: {
|
|
collective_fixed_pricing_results: {
|
|
total_fixed_price: 40,
|
|
},
|
|
},
|
|
},
|
|
subscriptionDistribution: {
|
|
includes: {
|
|
collective_subscription_results: {
|
|
total_subscription_price: 20,
|
|
},
|
|
},
|
|
},
|
|
customerPriceDistribution: {
|
|
collective_results: {
|
|
total_customer_price: 15,
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(summary.bookedAmount).toBe(100);
|
|
expect(summary.fixedPricingAmount).toBe(40);
|
|
expect(summary.subscriptionAmount).toBe(20);
|
|
expect(summary.customerPriceAmount).toBe(15);
|
|
expect(summary.distributionAmount).toBe(60);
|
|
expect(summary.otherBookedAmount).toBe(25);
|
|
expect(summary.totalAmount).toBe(160);
|
|
});
|
|
|
|
it("prefers actually distributed fixed and subscription amounts for summary totals", () => {
|
|
const summary = buildMonthSummary({
|
|
year: 2026,
|
|
month: 2,
|
|
periodData: {
|
|
types: {
|
|
all: [
|
|
{
|
|
transactions: [{ amount: 200, booked: true, excluded: false }],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
fixedDistribution: {
|
|
collective_results: {
|
|
total_fixed_price: 50,
|
|
total_department_totals_relative_parsed: {
|
|
Alpha: 30,
|
|
Beta: 10,
|
|
},
|
|
},
|
|
},
|
|
subscriptionDistribution: {
|
|
collective_results: {
|
|
total_subscription_price: 120,
|
|
subscription_price_department_distribution_parsed: {
|
|
Alpha: 70,
|
|
Beta: 20,
|
|
},
|
|
},
|
|
},
|
|
customerPriceDistribution: {
|
|
collective_results: {
|
|
total_customer_price: 15,
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(summary.fixedPricingAmount).toBe(40);
|
|
expect(summary.subscriptionAmount).toBe(90);
|
|
expect(summary.customerPriceAmount).toBe(15);
|
|
expect(summary.distributionAmount).toBe(130);
|
|
expect(summary.totalAmount).toBe(330);
|
|
});
|
|
|
|
it("builds department allocations with percentages and booked estimate", () => {
|
|
const rows = buildDepartmentAllocations({
|
|
departments: [
|
|
{ id: 1, name: "Alpha" },
|
|
{ id: 2, name: "Beta" },
|
|
],
|
|
bookedAmount: 300,
|
|
fixedDistribution: {
|
|
includes: {
|
|
collective_fixed_pricing_results: {
|
|
total_department_totals_relative_parsed: {
|
|
Alpha: 60,
|
|
Beta: 40,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
subscriptionDistribution: {
|
|
includes: {
|
|
collective_subscription_results: {
|
|
subscription_price_department_distribution_parsed: {
|
|
Alpha: 30,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
customerPriceDistribution: {
|
|
collective_results: {
|
|
customer_price_department_distribution_parsed: {
|
|
Beta: 10,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(rows).toHaveLength(2);
|
|
expect(rows[0].departmentName).toBe("Alpha");
|
|
expect(rows[0].totalAmount).toBe(90);
|
|
expect(rows[0].sharePercent).toBe(69.23);
|
|
expect(rows[0].estimatedBookedAllocation).toBe(207.69);
|
|
expect(rows[1].departmentName).toBe("Beta");
|
|
expect(rows[1].customerPriceAmount).toBe(10);
|
|
expect(rows[1].totalAmount).toBe(40);
|
|
});
|
|
|
|
it("builds department footer totals without adding customer prices to the total column", () => {
|
|
const totals = buildDepartmentAllocationFooterTotals([
|
|
{
|
|
fixedAmount: 120000,
|
|
subscriptionAmount: 80000,
|
|
customerPriceAmount: 15000,
|
|
totalAmount: 215000,
|
|
},
|
|
{
|
|
fixedAmount: 40000,
|
|
subscriptionAmount: 57000,
|
|
customerPriceAmount: 12000,
|
|
totalAmount: 109000,
|
|
},
|
|
]);
|
|
|
|
expect(totals.fixedAmount).toBe(160000);
|
|
expect(totals.subscriptionAmount).toBe(137000);
|
|
expect(totals.customerPriceAmount).toBe(27000);
|
|
expect(totals.totalAmount).toBe(297000);
|
|
});
|
|
|
|
it("builds customer rows and respects creation date filter", () => {
|
|
const rows = buildCustomerAllocationRows({
|
|
year: 2026,
|
|
month: 2,
|
|
departments: [{ id: 1, name: "Alpha" }],
|
|
fixedDistribution: {
|
|
data: [
|
|
{
|
|
id: 1,
|
|
customer_number: 10,
|
|
customer_name: "Customer A",
|
|
meta: {
|
|
fixed_pricing: {
|
|
created_at: "2026-02-01T00:00:00Z",
|
|
price: 100,
|
|
original_price: 140,
|
|
department_totals_relative: { 1: 100 },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
id: 2,
|
|
customer_number: 20,
|
|
customer_name: "Future Customer",
|
|
meta: {
|
|
fixed_pricing: {
|
|
created_at: "2026-03-01T00:00:00Z",
|
|
price: 100,
|
|
original_price: 140,
|
|
department_totals_relative: { 1: 100 },
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
subscriptionDistribution: {
|
|
data: [],
|
|
},
|
|
customerPriceDistribution: {
|
|
customers: [
|
|
{
|
|
id: 7,
|
|
customer_number: 99,
|
|
customer_name: "Discount Customer",
|
|
transactions: [{ id: 1, amount: 22, booked: true, excluded: false, date: "2026-02-05T00:00:00Z" }],
|
|
meta: {
|
|
customer_price: {
|
|
created_at: "2026-02-05T00:00:00Z",
|
|
price: 22,
|
|
department_totals_relative: { 1: 22 },
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
expect(rows).toHaveLength(2);
|
|
expect(rows[0].customerName).toBe("Customer A");
|
|
expect(rows[0].departmentAllocations).toEqual({ Alpha: 100 });
|
|
expect(rows[1].source).toBe("customer_prices");
|
|
});
|
|
|
|
it("builds month-over-month comparison deltas", () => {
|
|
const comparison = buildMonthComparison(
|
|
{ totalAmount: 200, bookedAmount: 140, distributionAmount: 60 },
|
|
{ totalAmount: 100, bookedAmount: 120, distributionAmount: 30 }
|
|
);
|
|
|
|
expect(comparison.totalAmount.delta).toBe(100);
|
|
expect(comparison.bookedAmount.delta).toBe(20);
|
|
expect(comparison.distributionAmount.delta).toBe(30);
|
|
});
|
|
|
|
it("normalizes and serializes distribution query state", () => {
|
|
const normalized = normalizeDistributionQueryState({
|
|
compareMonth: "2026-02",
|
|
compareMode: "line_by_line",
|
|
customerSearch: " acme ",
|
|
customerSource: "customer_prices",
|
|
customerDepartment: "Ops",
|
|
departmentSearch: " nord ",
|
|
});
|
|
|
|
expect(normalized).toEqual({
|
|
compareMonth: "2026-02",
|
|
compareMode: "line_by_line",
|
|
compareVisibility: "all",
|
|
customerSearch: "acme",
|
|
customerSource: "customer_prices",
|
|
customerDepartment: "Ops",
|
|
departmentSearch: "nord",
|
|
});
|
|
|
|
const serialized = buildDistributionQueryState(normalized);
|
|
expect(serialized).toEqual({
|
|
compareMonth: "2026-02",
|
|
compareMode: "line_by_line",
|
|
customerSearch: "acme",
|
|
customerSource: "customer_prices",
|
|
customerDepartment: "Ops",
|
|
departmentSearch: "nord",
|
|
});
|
|
});
|
|
|
|
it("sorts month summaries and compare rows for decision-first tables", () => {
|
|
const sortedMonths = sortMonthSummaries(
|
|
[
|
|
{ key: "2026-01", year: 2026, month: 1, totalAmount: 10 },
|
|
{ key: "2026-03", year: 2026, month: 3, totalAmount: 40 },
|
|
{ key: "2026-02", year: 2026, month: 2, totalAmount: 20 },
|
|
],
|
|
"month",
|
|
"desc"
|
|
);
|
|
expect(sortedMonths.map((row) => row.key)).toEqual(["2026-03", "2026-02", "2026-01"]);
|
|
|
|
const sortedTotals = sortMonthSummaries(sortedMonths, "total", "asc");
|
|
expect(sortedTotals.map((row) => row.totalAmount)).toEqual([10, 20, 40]);
|
|
|
|
const sortedCompare = sortCompareRows([
|
|
{ invoiceId: 3, status: "ok", warningCount: 0, difference: 0 },
|
|
{ invoiceId: 2, status: "ok", warningCount: 2, difference: 4 },
|
|
{ invoiceId: 1, status: "mismatch", warningCount: 1, difference: 10 },
|
|
]);
|
|
expect(sortedCompare.map((row) => row.invoiceId)).toEqual([1, 2, 3]);
|
|
});
|
|
|
|
it("builds source composition percentages for KPI cues", () => {
|
|
const composition = buildSourceComposition({
|
|
fixedPricingAmount: 60,
|
|
subscriptionAmount: 40,
|
|
customerPriceAmount: 20,
|
|
distributionAmount: 100,
|
|
});
|
|
expect(composition.fixedPercent).toBe(50);
|
|
expect(composition.subscriptionPercent).toBe(33.33);
|
|
expect(composition.customerPricePercent).toBe(16.67);
|
|
});
|
|
|
|
it("normalizes compare payloads for both modes and aggregates", () => {
|
|
const okRow = normalizeCompareResult(
|
|
{ id: 1, customer_name: "A", total_net_amount: 100 },
|
|
{
|
|
internal_total: 100,
|
|
booked_total: 100,
|
|
difference: 0,
|
|
warnings: [],
|
|
order_ids: [1, 2],
|
|
},
|
|
"invoice_total"
|
|
);
|
|
|
|
const mismatchRow = normalizeCompareResult(
|
|
{ id: 2, customer_name: "B", total_net_amount: 100 },
|
|
{
|
|
internal_total: 100,
|
|
booked_total: 90,
|
|
difference: 10,
|
|
warnings: ["Line mismatch"],
|
|
order_ids: [3],
|
|
},
|
|
"line_by_line"
|
|
);
|
|
|
|
const v2Row = normalizeCompareResult(
|
|
{ id: 3, customer_name: "C", total_net_amount: 200 },
|
|
{
|
|
collected_invoice_id: 3,
|
|
warnings: ["Top-level warning"],
|
|
details: {
|
|
order_ids: [10, 20],
|
|
internal: {
|
|
normalized: {
|
|
totals: {
|
|
net_total: 200,
|
|
billable_line_count: 2,
|
|
},
|
|
},
|
|
},
|
|
customer: {
|
|
name: "Customer V2",
|
|
},
|
|
},
|
|
comparison: {
|
|
totals: {
|
|
internal_net_total: 200,
|
|
},
|
|
targets: {
|
|
booked: {
|
|
status: "partial_mismatch",
|
|
overall_match: false,
|
|
totals: {
|
|
target_net_total: 180,
|
|
},
|
|
mismatch_reasons: ["department_total_mismatch"],
|
|
lines: {
|
|
summary: {
|
|
mismatch_count: 1,
|
|
target_billable_count: 2,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
warnings: ["comparison warning"],
|
|
},
|
|
},
|
|
"line_by_line"
|
|
);
|
|
|
|
expect(okRow.status).toBe("ok");
|
|
expect(mismatchRow.status).toBe("mismatch");
|
|
expect(mismatchRow.lineMismatchCount).toBe(1);
|
|
expect(v2Row.status).toBe("mismatch");
|
|
expect(v2Row.bookedTotal).toBe(180);
|
|
expect(v2Row.difference).toBe(20);
|
|
expect(v2Row.mismatchReasons).toContain("department_total_mismatch");
|
|
expect(v2Row.lineMismatchCount).toBe(1);
|
|
|
|
const aggregate = aggregateCompareRows([okRow, mismatchRow, v2Row]);
|
|
expect(aggregate.total).toBe(3);
|
|
expect(aggregate.mismatches).toBe(2);
|
|
expect(aggregate.warningCount).toBe(4);
|
|
expect(aggregate.totalDifference).toBe(30);
|
|
});
|
|
});
|