issue_invoice allocates the number, freezes vendor and bank details and stores server-computed totals in one transaction. Issued invoices are cancelled, not deleted. Tax heads derive from supplier state and place of supply (a disagreeing choice is rejected); unregistered suppliers issue a plain Invoice. Adds GSTIN checksum validation, canonical India Compliance state list, server-side drafts, series validation, relative and magic-byte-checked asset paths, and GST settings. PDF re-exports use the frozen vendor snapshot.
59 lines
2.0 KiB
TypeScript
59 lines
2.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { computeLineAmount, computeTotals } from "./invoice";
|
|
import { EMPTY_ITEM, type InvoiceItem } from "./types";
|
|
|
|
// These cases mirror the unit tests in src-tauri/src/gst.rs, so the on-screen preview
|
|
// and the stored totals stay identical.
|
|
|
|
const fixed = (amount: number): InvoiceItem => ({ ...EMPTY_ITEM, mode: "fixed", amount });
|
|
const rate = (r: number, quantity: number): InvoiceItem => ({
|
|
...EMPTY_ITEM,
|
|
mode: "rate",
|
|
rate: r,
|
|
quantity,
|
|
unit: "hour",
|
|
});
|
|
|
|
describe("computeTotals", () => {
|
|
it("splits 18% on 7310.00 into 657.90 CGST and SGST", () => {
|
|
const t = computeTotals([fixed(7310)], 0, "cgst_sgst", 18);
|
|
expect(t.subtotal).toBe(7310);
|
|
expect(t.cgst).toBe(657.9);
|
|
expect(t.sgst).toBe(657.9);
|
|
expect(t.igst).toBe(0);
|
|
expect(t.total).toBe(8625.8);
|
|
});
|
|
|
|
it("rounds odd paise half up for IGST and per head for CGST/SGST", () => {
|
|
const igst = computeTotals([fixed(33.33)], 0, "igst", 5);
|
|
expect(igst.igst).toBe(1.67);
|
|
expect(igst.total).toBe(35);
|
|
const split = computeTotals([fixed(33.33)], 0, "cgst_sgst", 5);
|
|
expect([split.cgst, split.sgst]).toEqual([0.83, 0.83]);
|
|
expect(split.total).toBe(34.99);
|
|
});
|
|
|
|
it("handles fractional quantities", () => {
|
|
const t = computeTotals([rate(1200, 1.5)], 0, "none", 0);
|
|
expect(t.subtotal).toBe(1800);
|
|
expect(t.total).toBe(1800);
|
|
expect(computeLineAmount(rate(99.99, 2.5))).toBe(249.98);
|
|
});
|
|
|
|
it("clamps the discount to the subtotal", () => {
|
|
const items = [fixed(100), fixed(50.5)];
|
|
const over = computeTotals(items, 999.99, "igst", 18);
|
|
expect(over.discount).toBe(150.5);
|
|
expect(over.taxable).toBe(0);
|
|
expect(over.total).toBe(0);
|
|
expect(computeTotals(items, -5, "none", 0).discount).toBe(0);
|
|
expect(computeTotals(items, 50.5, "none", 0).taxable).toBe(100);
|
|
});
|
|
|
|
it("ignores the rate when no tax applies", () => {
|
|
const t = computeTotals([fixed(1000)], 0, "none", 18);
|
|
expect(t.taxTotal).toBe(0);
|
|
expect(t.total).toBe(1000);
|
|
});
|
|
});
|