import { describe, it, expect, afterEach } from "vitest"; import prisma from "../config/database"; import { createOrder, getOrderTotals } from "../services/orders.service"; import { createIssuedOrder, getIssuedOrderTotals, } from "../services/issued-orders.service"; // Per-currency total aggregation for both order lists. These hit the real // `app_test` DB via the service layer (suite convention) and prove the /stats // endpoints sum order TOTAL (incl. VAT) per currency across the WHOLE filtered // set, and that the where (month/year + status) is respected. // // A far-future month/year is used so seeded/other-test rows can't fall in the // window and skew the deterministic sums (mirrors drafts-aggregation.test.ts). const STATS_YEAR = 2097; const STATS_MONTH = 8; // -> window [2097-08-01, 2097-09-01) const createdOrderIds: number[] = []; const createdIssuedIds: number[] = []; const createdCustomerIds: number[] = []; afterEach(async () => { for (const id of createdOrderIds) await prisma.orders.deleteMany({ where: { id } }); for (const id of createdIssuedIds) await prisma.issued_orders.deleteMany({ where: { id } }); for (const id of createdCustomerIds) await prisma.customers.deleteMany({ where: { id } }); createdOrderIds.length = 0; createdIssuedIds.length = 0; createdCustomerIds.length = 0; // Finalized orders consume the shared/issued sequences — reset so we don't // leak a consumed number into sibling test files. await prisma.number_sequences.deleteMany({ where: { type: { in: ["shared", "issued_order", "project"] } }, }); }); function amountFor( totals: { currency: string; amount: number }[], currency: string, ): number | undefined { return totals.find((t) => t.currency === currency)?.amount; } describe("getOrderTotals (received orders) per-currency aggregation", () => { it("sums TOTAL incl. VAT per currency over the full filtered set", async () => { // Two CZK orders + one EUR, all in the same far-future month. 21% VAT, // applied on the whole subtotal (enrichOrder math). // CZK #1: 2 x 1000 = 2000 net -> +21% = 2420 // CZK #2: 1 x 500 = 500 net -> +21% = 605 => CZK total 3025 // EUR : 3 x 100 = 300 net -> +21% = 363 => EUR total 363 const mk = async (currency: string, qty: number, price: number) => { const res = await createOrder({ status: "prijata", currency, language: "cs", vat_rate: 21, apply_vat: true, create_project: false, items: [{ description: "X", quantity: qty, unit_price: price }], }); if (!("id" in res)) throw new Error(JSON.stringify(res)); createdOrderIds.push(res.id!); // created_at is auto-managed; pin it into the stats window so the // month/year filter is deterministic. await prisma.orders.update({ where: { id: res.id }, data: { created_at: new Date(STATS_YEAR, STATS_MONTH - 1, 15, 12, 0, 0), }, }); return res.id!; }; await mk("CZK", 2, 1000); await mk("CZK", 1, 500); await mk("EUR", 3, 100); const { totals } = await getOrderTotals({ month: STATS_MONTH, year: STATS_YEAR, }); expect(amountFor(totals, "CZK")).toBe(3025); expect(amountFor(totals, "EUR")).toBe(363); }); it("respects the where: a different month and a status filter change the result", async () => { const mk = async (status: string, monthOffset: number) => { const res = await createOrder({ status, currency: "CZK", language: "cs", vat_rate: 21, apply_vat: true, create_project: false, items: [{ description: "X", quantity: 1, unit_price: 1000 }], }); if (!("id" in res)) throw new Error(JSON.stringify(res)); createdOrderIds.push(res.id!); await prisma.orders.update({ where: { id: res.id }, data: { created_at: new Date( STATS_YEAR, STATS_MONTH - 1 + monthOffset, 15, 12, 0, 0, ), }, }); return res.id!; }; // One 'prijata' in the target month, one 'stornovana' in the target month, // one 'prijata' in the NEXT month. await mk("prijata", 0); await mk("stornovana", 0); await mk("prijata", 1); // Whole month: both same-month orders count -> 2 x 1210 = 2420. const whole = await getOrderTotals({ month: STATS_MONTH, year: STATS_YEAR, }); expect(amountFor(whole.totals, "CZK")).toBe(2420); // Status filter narrows to the single 'prijata' in the target month -> 1210. const onlyPrijata = await getOrderTotals({ month: STATS_MONTH, year: STATS_YEAR, status: "prijata", }); expect(amountFor(onlyPrijata.totals, "CZK")).toBe(1210); // Next month sees only the one 'prijata' there -> 1210. const nextMonth = await getOrderTotals({ month: STATS_MONTH + 1, year: STATS_YEAR, }); expect(amountFor(nextMonth.totals, "CZK")).toBe(1210); }); }); describe("getIssuedOrderTotals (issued orders) per-currency aggregation", () => { it("sums TOTAL incl. VAT per currency over the full filtered set", async () => { // order_date is settable at create, so no post-create pin needed. VAT is // applied per-line (computeIssuedOrderTotals) but with a single line per // order here the result matches the on-the-whole subtotal math. // CZK #1: 2 x 1000 @21% = 2420 // CZK #2: 1 x 500 @21% = 605 => CZK total 3025 // EUR : 3 x 100 @21% = 363 => EUR total 363 const dateStr = `${STATS_YEAR}-0${STATS_MONTH}-15`; const mk = async (currency: string, qty: number, price: number) => { const o = await createIssuedOrder({ currency, vat_rate: 21, apply_vat: true, order_date: dateStr, items: [ { description: "X", quantity: qty, unit_price: price, vat_rate: 21 }, ], }); if ("error" in o) throw new Error(`createIssuedOrder failed: ${o.error}`); createdIssuedIds.push(o.id); return o.id; }; await mk("CZK", 2, 1000); await mk("CZK", 1, 500); await mk("EUR", 3, 100); const { totals } = await getIssuedOrderTotals({ month: STATS_MONTH, year: STATS_YEAR, }); expect(amountFor(totals, "CZK")).toBe(3025); expect(amountFor(totals, "EUR")).toBe(363); }); it("respects the where: a different month and a status filter change the result", async () => { const inMonth = `${STATS_YEAR}-0${STATS_MONTH}-15`; const nextMonth = `${STATS_YEAR}-0${STATS_MONTH + 1}-15`; // Target month: one draft, one already-sent (both 1210). Next month: one. const draft = await createIssuedOrder({ currency: "CZK", vat_rate: 21, apply_vat: true, order_date: inMonth, items: [ { description: "X", quantity: 1, unit_price: 1000, vat_rate: 21 }, ], }); if ("error" in draft) throw new Error(`createIssuedOrder failed: ${draft.error}`); createdIssuedIds.push(draft.id); const sent = await createIssuedOrder({ status: "sent", currency: "CZK", vat_rate: 21, apply_vat: true, order_date: inMonth, items: [ { description: "X", quantity: 1, unit_price: 1000, vat_rate: 21 }, ], }); if ("error" in sent) throw new Error(`createIssuedOrder failed: ${sent.error}`); createdIssuedIds.push(sent.id); const next = await createIssuedOrder({ currency: "CZK", vat_rate: 21, apply_vat: true, order_date: nextMonth, items: [ { description: "X", quantity: 1, unit_price: 1000, vat_rate: 21 }, ], }); if ("error" in next) throw new Error(`createIssuedOrder failed: ${next.error}`); createdIssuedIds.push(next.id); // Whole target month: both count -> 2 x 1210 = 2420. const whole = await getIssuedOrderTotals({ month: STATS_MONTH, year: STATS_YEAR, }); expect(amountFor(whole.totals, "CZK")).toBe(2420); // Status filter narrows to the single 'sent' in the target month -> 1210. const onlySent = await getIssuedOrderTotals({ month: STATS_MONTH, year: STATS_YEAR, status: "sent", }); expect(amountFor(onlySent.totals, "CZK")).toBe(1210); // Next month sees only the one order there -> 1210. const nextStats = await getIssuedOrderTotals({ month: STATS_MONTH + 1, year: STATS_YEAR, }); expect(amountFor(nextStats.totals, "CZK")).toBe(1210); }); });