Add issue/cancel lifecycle, GST derivation and integer-paise totals
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.
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user