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); }); });