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:
2026-10-04 04:15:58 +05:30
parent d6ec6b2a17
commit 5597fb791d
19 changed files with 2403 additions and 327 deletions
+58
View File
@@ -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);
});
});