@lacspace/invoice 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,51 @@
1
+ Lacspace Free Licence
2
+ Version 1.0, August 2026
3
+
4
+ Copyright (c) 2026 Lacspace
5
+
6
+ PREAMBLE
7
+
8
+ This software is published by Lacspace under the Lacspace Free Licence โ€” a free,
9
+ permissive licence that lets you use this software for any purpose, including in
10
+ commercial products and services, at no cost. It grants the same freedoms as
11
+ common permissive open-source licences; the only condition is that this notice
12
+ travels with the software. The canonical, always-current text of this licence is
13
+ maintained at https://lacspace.com/licenses/lacspace-free-1.0
14
+
15
+ GRANT OF RIGHTS
16
+
17
+ Permission is hereby granted, free of charge, to any person or organisation
18
+ obtaining a copy of this software and its associated documentation and data files
19
+ (the "Software"), to deal in the Software without restriction, including without
20
+ limitation the rights to use, copy, modify, merge, publish, distribute,
21
+ sublicense, and/or sell copies of the Software, and to permit persons to whom the
22
+ Software is furnished to do so, subject to the conditions below. These rights are
23
+ granted for any purpose, personal or commercial, and are perpetual, worldwide,
24
+ non-exclusive, and royalty-free.
25
+
26
+ CONDITIONS
27
+
28
+ The above copyright notice, this permission notice, and the name of this licence
29
+ ("Lacspace Free Licence") shall be included in all copies or substantial portions
30
+ of the Software.
31
+
32
+ TRADEMARKS
33
+
34
+ This licence does not grant permission to use the trade names, trademarks, service
35
+ marks, logos, or product names of Lacspace, except as required to reproduce the
36
+ notice above or to describe the origin of the Software in a truthful manner.
37
+
38
+ DISCLAIMER OF WARRANTY AND LIMITATION OF LIABILITY
39
+
40
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
42
+ FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
43
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN
44
+ AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION
45
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
46
+
47
+ ---
48
+
49
+ The Lacspace Free Licence is a source-available, permissive licence and is not (as
50
+ of this version) an OSI-approved licence. In substance it grants the same freedoms
51
+ as the MIT Licence. Learn more at https://lacspace.com/licenses
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ <div align="center">
2
+
3
+ # @lacspace/invoice
4
+
5
+ **An invoice model, numbering & tax-rollup engine โ€” pure, immutable & serializable.**
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@lacspace/invoice?color=%2316a34a&label=npm)](https://www.npmjs.com/package/@lacspace/invoice)
8
+ [![minzipped](https://img.shields.io/bundlephobia/minzip/@lacspace/invoice?label=minzip)](https://bundlephobia.com/package/@lacspace/invoice)
9
+ [![types](https://img.shields.io/badge/types-included-blue)](https://www.npmjs.com/package/@lacspace/invoice)
10
+ [![license](https://img.shields.io/npm/l/@lacspace/invoice?color=green)](https://github.com/lacspace/npm-packages/blob/main/LICENSE)
11
+
12
+ </div>
13
+
14
+ > The invoicing math every billing system re-implements badly. A tiny set of **pure functions** over a plain `Invoice` object โ€” compute per-line and total amounts, roll tax up by rate, generate sequential invoice numbers, track payments, and emit a render-ready row table. No PDF library, no floats.
15
+
16
+ - ๐ŸงŠ **Immutable** โ€” payments and status transitions return a brand-new invoice; your input is never mutated
17
+ - ๐Ÿ’พ **Serializable** โ€” `Invoice` is plain data, safe to `JSON.stringify` and persist
18
+ - ๐Ÿช™ **Exact money** โ€” integer **minor units** everywhere, so tax never loses a penny
19
+ - ๐Ÿงพ **Tax rollup** โ€” lines grouped by rate into a summary, consistent with the totals
20
+ - ๐Ÿ–จ๏ธ **Render-ready** โ€” `renderRows` hands a normalized table to `@lacspace/pdf` or `@lacspace/xlsx` โ€” with **zero dependency** on them
21
+ - โšก Isomorphic โ€” Node, edge runtimes & browsers ยท ๐Ÿ“ฆ ESM + CJS ยท fully typed ยท zero deps
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ npm i @lacspace/invoice # or pnpm add / yarn add / bun add
27
+ ```
28
+
29
+ ## Build an invoice
30
+
31
+ ```ts
32
+ import { createInvoice, invoiceNumber } from "@lacspace/invoice";
33
+
34
+ const inv = createInvoice({
35
+ number: invoiceNumber(1, { year: 2026 }), // "INV-2026-000001"
36
+ currency: "USD",
37
+ seller: { name: "Lacspace", taxId: "T-1" },
38
+ buyer: { name: "Acme Co", email: "ap@acme.test" },
39
+ lines: [
40
+ { description: "Widget", qty: 2, unitPrice: 1000, taxRate: 0.13, discount: 200 },
41
+ { description: "Sticker", qty: 1, unitPrice: 500, taxRate: 0 },
42
+ ],
43
+ });
44
+
45
+ inv.lines[0]; // { ..., net: 1800, tax: 234, total: 2034 }
46
+ inv.totals; // { subtotal: 2300, discount: 200, taxTotal: 234, total: 2534, amountPaid: 0, balanceDue: 2534 }
47
+ ```
48
+
49
+ ## Tax grouped by rate
50
+
51
+ ```ts
52
+ inv.taxSummary;
53
+ // [{ rate: 0, net: 500, tax: 0 }, { rate: 0.13, net: 1800, tax: 234 }]
54
+ ```
55
+
56
+ **Rounding order:** each line's tax is rounded first (`round(net * taxRate)`), then the already-rounded per-line taxes are summed into both `taxSummary` and `totals.taxTotal` โ€” so the summary always reconciles with the total.
57
+
58
+ ## Track payments โ€” immutably
59
+
60
+ ```ts
61
+ import { recordPayment } from "@lacspace/invoice";
62
+
63
+ let cur = recordPayment(inv, 2000); // cur.status === "partial", balanceDue 534
64
+ cur = recordPayment(cur, 534); // cur.status === "paid", balanceDue 0
65
+
66
+ recordPayment(inv, 3000); // throws InvoiceError { code: "overpayment" }
67
+ ```
68
+
69
+ ## Status transitions & overdue
70
+
71
+ ```ts
72
+ import { markIssued, markVoid, isOverdue } from "@lacspace/invoice";
73
+
74
+ const issued = markIssued(inv, Date.now());
75
+ const voided = markVoid(inv);
76
+ isOverdue({ ...issued, dueAt: Date.now() - 1 }); // true โ€” past due with a balance
77
+ ```
78
+
79
+ ## Hand it to a renderer
80
+
81
+ ```ts
82
+ import { renderRows } from "@lacspace/invoice";
83
+
84
+ const { columns, rows } = renderRows(inv);
85
+ // columns: ["Description", "Qty", "Unit", "Tax %", "Line total"]
86
+ // rows: [["Widget", 2, 1000, 13, 2034], ["Sticker", 1, 500, 0, 500]]
87
+ // โ†’ feed straight into @lacspace/xlsx or a PDF table builder
88
+ ```
89
+
90
+ ## API
91
+
92
+ | Function | Description |
93
+ | --- | --- |
94
+ | `createInvoice(input)` | build a computed, serializable `Invoice` (lines, totals, tax summary) |
95
+ | `recordPayment(inv, amount, opts?)` | immutable; add a payment, recompute balance, set `paid`/`partial` |
96
+ | `markIssued(inv, at?)` | immutable transition to `issued` (throws on paid/void) |
97
+ | `markVoid(inv)` | immutable transition to `void` (throws on paid) |
98
+ | `isOverdue(inv, now?)` | `dueAt` in the past **and** a positive balance |
99
+ | `invoiceNumber(seq, opts?)` | deterministic number, default `"INV-2026-000123"` |
100
+ | `renderRows(inv)` | `{ columns, rows }` โ€” a normalized table for PDF / XLSX |
101
+
102
+ Types exported: `Party`, `InvoiceLineInput`, `InvoiceLine`, `TaxSummaryRow`, `InvoiceTotals`, `InvoiceStatus`, `Invoice`, and the `InvoiceError` class.
103
+
104
+ All amounts are integer minor units; `taxRate` is a fraction (`0.13` = 13%). `net = qty * unitPrice - discount`, `tax = round(net * taxRate)`, `total = net + tax`.
105
+
106
+ ## Licensing
107
+
108
+ This package is **free** under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** โ€” permissive freedoms. Use it in personal and commercial projects at no cost; just keep the notice.
109
+
110
+ Not every Lacspace package is free. We also offer **Commercial** (paid), **Client-specific**, and **Private** (proprietary) packages under separate terms. See the full **[Lacspace Licence Centre](https://lacspace.com/licenses)**.
111
+
112
+ <!-- LACSPACE-DEV-PLATFORM -->
113
+
114
+ ---
115
+
116
+ ## The Lacspace Developer Platform
117
+
118
+ `@lacspace/invoice` is part of **63+ zero-dependency, isomorphic TypeScript packages**. Explore the ecosystem:
119
+
120
+ - ๐Ÿ—‚๏ธ **All packages** โ€” https://developer.lacspace.com/packages
121
+ - ๐Ÿงญ **Developer handbook** โ€” https://developer.lacspace.com/handbook
122
+ - ๐Ÿงช **Live playground** โ€” https://developer.lacspace.com/playground
123
+ - ๐Ÿ–ฅ๏ธ **Finished app templates** โ€” https://templates.lacspace.com
124
+ - ๐Ÿš€ **Scaffold a full app** โ€” `npm create lacspace-app@latest`
125
+
126
+ Free under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** โ€” a permissive, free-to-use licence.
package/dist/index.cjs ADDED
@@ -0,0 +1,149 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ var InvoiceError = class _InvoiceError extends Error {
5
+ constructor(message, code) {
6
+ super(message);
7
+ this.name = "InvoiceError";
8
+ this.code = code;
9
+ Object.setPrototypeOf(this, _InvoiceError.prototype);
10
+ }
11
+ };
12
+ function roundMinor(n) {
13
+ return n < 0 ? -Math.round(-n) : Math.round(n);
14
+ }
15
+ function computeLine(input) {
16
+ if (!Number.isFinite(input.qty) || input.qty < 0) {
17
+ throw new InvoiceError(
18
+ `Line "${input.description}" has a negative or invalid qty`,
19
+ "invalid_qty"
20
+ );
21
+ }
22
+ const discount = input.discount ?? 0;
23
+ const taxRate = input.taxRate ?? 0;
24
+ const net = input.qty * input.unitPrice - discount;
25
+ const tax = roundMinor(net * taxRate);
26
+ return {
27
+ ...input,
28
+ net,
29
+ tax,
30
+ total: net + tax
31
+ };
32
+ }
33
+ function summarizeTax(lines) {
34
+ const byRate = /* @__PURE__ */ new Map();
35
+ for (const line of lines) {
36
+ const rate = line.taxRate ?? 0;
37
+ const row = byRate.get(rate) ?? { rate, net: 0, tax: 0 };
38
+ row.net += line.net;
39
+ row.tax += line.tax;
40
+ byRate.set(rate, row);
41
+ }
42
+ return [...byRate.values()].sort((a, b) => a.rate - b.rate);
43
+ }
44
+ function createInvoice(input) {
45
+ if (!input.lines || input.lines.length === 0) {
46
+ throw new InvoiceError("An invoice must have at least one line", "no_lines");
47
+ }
48
+ const lines = input.lines.map(computeLine);
49
+ const subtotal = lines.reduce((s, l) => s + l.net, 0);
50
+ const discount = lines.reduce((s, l) => s + (l.discount ?? 0), 0);
51
+ const taxTotal = lines.reduce((s, l) => s + l.tax, 0);
52
+ const total = subtotal + taxTotal;
53
+ const amountPaid = input.amountPaid ?? 0;
54
+ const balanceDue = total - amountPaid;
55
+ const totals = {
56
+ subtotal,
57
+ discount,
58
+ taxTotal,
59
+ total,
60
+ amountPaid,
61
+ balanceDue
62
+ };
63
+ return {
64
+ number: input.number,
65
+ status: input.status ?? "draft",
66
+ currency: input.currency,
67
+ seller: input.seller,
68
+ buyer: input.buyer,
69
+ lines,
70
+ totals,
71
+ taxSummary: summarizeTax(lines),
72
+ issuedAt: input.issuedAt,
73
+ dueAt: input.dueAt,
74
+ notes: input.notes,
75
+ meta: input.meta
76
+ };
77
+ }
78
+ function recordPayment(inv, amount, opts) {
79
+ if (!Number.isFinite(amount) || amount <= 0) {
80
+ throw new InvoiceError("Payment amount must be positive", "invalid_amount");
81
+ }
82
+ const amountPaid = inv.totals.amountPaid + amount;
83
+ if (amountPaid > inv.totals.total) {
84
+ throw new InvoiceError(
85
+ "Payment exceeds the amount due",
86
+ "overpayment"
87
+ );
88
+ }
89
+ const balanceDue = inv.totals.total - amountPaid;
90
+ const status = balanceDue === 0 ? "paid" : "partial";
91
+ return {
92
+ ...inv,
93
+ status,
94
+ totals: { ...inv.totals, amountPaid, balanceDue },
95
+ meta: opts?.at !== void 0 ? { ...inv.meta ?? {}, lastPaymentAt: opts.at } : inv.meta
96
+ };
97
+ }
98
+ function markVoid(inv) {
99
+ if (inv.status === "paid") {
100
+ throw new InvoiceError("Cannot void a paid invoice", "already_paid");
101
+ }
102
+ return { ...inv, status: "void" };
103
+ }
104
+ function markIssued(inv, at) {
105
+ if (inv.status === "paid") {
106
+ throw new InvoiceError("Cannot re-issue a paid invoice", "already_paid");
107
+ }
108
+ if (inv.status === "void") {
109
+ throw new InvoiceError("Cannot issue a void invoice", "void");
110
+ }
111
+ return {
112
+ ...inv,
113
+ status: "issued",
114
+ issuedAt: at ?? inv.issuedAt
115
+ };
116
+ }
117
+ function isOverdue(inv, now = Date.now()) {
118
+ return inv.dueAt !== void 0 && inv.dueAt < now && inv.totals.balanceDue > 0;
119
+ }
120
+ function invoiceNumber(seq, opts) {
121
+ const prefix = opts?.prefix ?? "INV";
122
+ const year = opts?.year ?? (/* @__PURE__ */ new Date()).getFullYear();
123
+ const pad = opts?.pad ?? 6;
124
+ const sep = opts?.separator ?? "-";
125
+ const num = String(Math.trunc(seq)).padStart(pad, "0");
126
+ return `${prefix}${sep}${year}${sep}${num}`;
127
+ }
128
+ function renderRows(inv) {
129
+ const columns = ["Description", "Qty", "Unit", "Tax %", "Line total"];
130
+ const rows = inv.lines.map((l) => [
131
+ l.description,
132
+ l.qty,
133
+ l.unitPrice,
134
+ (l.taxRate ?? 0) * 100,
135
+ l.total
136
+ ]);
137
+ return { columns, rows };
138
+ }
139
+
140
+ exports.InvoiceError = InvoiceError;
141
+ exports.createInvoice = createInvoice;
142
+ exports.invoiceNumber = invoiceNumber;
143
+ exports.isOverdue = isOverdue;
144
+ exports.markIssued = markIssued;
145
+ exports.markVoid = markVoid;
146
+ exports.recordPayment = recordPayment;
147
+ exports.renderRows = renderRows;
148
+ //# sourceMappingURL=index.cjs.map
149
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @lacspace/invoice\n *\n * Invoice MODEL + numbering + tax rollup. Pure, immutable, isomorphic and\n * zero-dependency. All money is expressed in integer **minor units** (e.g.\n * cents / paisa) so nothing is ever lost to floating-point drift.\n *\n * This package is model-only: it computes a structured, serializable invoice\n * and can emit a normalized row table ready to feed a renderer such as\n * `@lacspace/pdf` or `@lacspace/xlsx` โ€” but it imports NEITHER of them.\n */\n\n// ---------------------------------------------------------------------------\n// Model\n// ---------------------------------------------------------------------------\n\n/** A billing party โ€” the seller or the buyer on an invoice. */\nexport interface Party {\n name: string;\n address?: string;\n email?: string;\n taxId?: string;\n meta?: Record<string, unknown>;\n}\n\n/**\n * Raw input for a single invoice line. Amounts are integer minor units;\n * `taxRate` is a fraction (e.g. `0.13` for 13%).\n */\nexport interface InvoiceLineInput {\n description: string;\n qty: number;\n unitPrice: number;\n taxRate?: number;\n discount?: number;\n sku?: string;\n}\n\n/**\n * A computed invoice line.\n * net = qty * unitPrice - discount\n * tax = round(net * taxRate)\n * total = net + tax\n */\nexport interface InvoiceLine extends InvoiceLineInput {\n net: number;\n tax: number;\n total: number;\n}\n\n/** One row of the tax summary: all net/tax that share a single rate. */\nexport interface TaxSummaryRow {\n rate: number;\n net: number;\n tax: number;\n}\n\n/** Invoice-level money rollup, all integer minor units. */\nexport interface InvoiceTotals {\n subtotal: number;\n discount: number;\n taxTotal: number;\n total: number;\n amountPaid: number;\n balanceDue: number;\n}\n\nexport type InvoiceStatus =\n | \"draft\"\n | \"issued\"\n | \"paid\"\n | \"partial\"\n | \"void\"\n | \"overdue\";\n\n/** A complete, serializable invoice. */\nexport interface Invoice {\n number: string;\n status: InvoiceStatus;\n currency: string;\n seller: Party;\n buyer: Party;\n lines: InvoiceLine[];\n totals: InvoiceTotals;\n taxSummary: TaxSummaryRow[];\n issuedAt?: number;\n dueAt?: number;\n notes?: string;\n meta?: Record<string, unknown>;\n}\n\n/** Error thrown for any invalid invoice operation. */\nexport class InvoiceError extends Error {\n code?: string;\n constructor(message: string, code?: string) {\n super(message);\n this.name = \"InvoiceError\";\n this.code = code;\n // Restore prototype chain for downlevel ES targets.\n Object.setPrototypeOf(this, InvoiceError.prototype);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/** Round to the nearest integer minor unit (half away from zero). */\nfunction roundMinor(n: number): number {\n return n < 0 ? -Math.round(-n) : Math.round(n);\n}\n\nfunction computeLine(input: InvoiceLineInput): InvoiceLine {\n if (!Number.isFinite(input.qty) || input.qty < 0) {\n throw new InvoiceError(\n `Line \"${input.description}\" has a negative or invalid qty`,\n \"invalid_qty\",\n );\n }\n const discount = input.discount ?? 0;\n const taxRate = input.taxRate ?? 0;\n const net = input.qty * input.unitPrice - discount;\n const tax = roundMinor(net * taxRate);\n return {\n ...input,\n net,\n tax,\n total: net + tax,\n };\n}\n\n/**\n * Group computed lines into tax-summary rows by rate.\n *\n * Rounding order: each LINE's tax is rounded first (in `computeLine`), then the\n * already-rounded per-line tax amounts are summed into the summary row. Net is\n * likewise summed from per-line nets. This keeps the tax summary consistent\n * with `totals.taxTotal` (both are sums of the same rounded per-line taxes).\n */\nfunction summarizeTax(lines: InvoiceLine[]): TaxSummaryRow[] {\n const byRate = new Map<number, TaxSummaryRow>();\n for (const line of lines) {\n const rate = line.taxRate ?? 0;\n const row = byRate.get(rate) ?? { rate, net: 0, tax: 0 };\n row.net += line.net;\n row.tax += line.tax;\n byRate.set(rate, row);\n }\n // Deterministic ascending order by rate.\n return [...byRate.values()].sort((a, b) => a.rate - b.rate);\n}\n\n// ---------------------------------------------------------------------------\n// Build & compute\n// ---------------------------------------------------------------------------\n\n/**\n * Build a fully computed invoice from raw input.\n *\n * Computes each line (net / tax / total), the invoice totals, and the tax\n * summary grouped by rate. Integer minor units throughout.\n *\n * @throws InvoiceError when there are no lines.\n */\nexport function createInvoice(input: {\n number: string;\n currency: string;\n seller: Party;\n buyer: Party;\n lines: InvoiceLineInput[];\n status?: InvoiceStatus;\n issuedAt?: number;\n dueAt?: number;\n amountPaid?: number;\n notes?: string;\n meta?: Record<string, unknown>;\n}): Invoice {\n if (!input.lines || input.lines.length === 0) {\n throw new InvoiceError(\"An invoice must have at least one line\", \"no_lines\");\n }\n\n const lines = input.lines.map(computeLine);\n\n const subtotal = lines.reduce((s, l) => s + l.net, 0);\n const discount = lines.reduce((s, l) => s + (l.discount ?? 0), 0);\n const taxTotal = lines.reduce((s, l) => s + l.tax, 0);\n const total = subtotal + taxTotal;\n const amountPaid = input.amountPaid ?? 0;\n const balanceDue = total - amountPaid;\n\n const totals: InvoiceTotals = {\n subtotal,\n discount,\n taxTotal,\n total,\n amountPaid,\n balanceDue,\n };\n\n return {\n number: input.number,\n status: input.status ?? \"draft\",\n currency: input.currency,\n seller: input.seller,\n buyer: input.buyer,\n lines,\n totals,\n taxSummary: summarizeTax(lines),\n issuedAt: input.issuedAt,\n dueAt: input.dueAt,\n notes: input.notes,\n meta: input.meta,\n };\n}\n\n/**\n * Record a payment against an invoice. Immutable โ€” returns a new invoice.\n *\n * Increases `amountPaid`, recomputes `balanceDue`, and sets `status` to\n * `\"paid\"` (balance 0) or `\"partial\"` (balance > 0).\n *\n * @throws InvoiceError when `amount <= 0` (\"invalid_amount\") or the payment\n * would take total paid past the invoice total (\"overpayment\").\n */\nexport function recordPayment(\n inv: Invoice,\n amount: number,\n opts?: { at?: number },\n): Invoice {\n if (!Number.isFinite(amount) || amount <= 0) {\n throw new InvoiceError(\"Payment amount must be positive\", \"invalid_amount\");\n }\n const amountPaid = inv.totals.amountPaid + amount;\n if (amountPaid > inv.totals.total) {\n throw new InvoiceError(\n \"Payment exceeds the amount due\",\n \"overpayment\",\n );\n }\n const balanceDue = inv.totals.total - amountPaid;\n const status: InvoiceStatus = balanceDue === 0 ? \"paid\" : \"partial\";\n return {\n ...inv,\n status,\n totals: { ...inv.totals, amountPaid, balanceDue },\n meta:\n opts?.at !== undefined\n ? { ...(inv.meta ?? {}), lastPaymentAt: opts.at }\n : inv.meta,\n };\n}\n\n/**\n * Transition an invoice to `\"void\"`. Immutable.\n *\n * @throws InvoiceError when the invoice is already paid (\"already_paid\").\n */\nexport function markVoid(inv: Invoice): Invoice {\n if (inv.status === \"paid\") {\n throw new InvoiceError(\"Cannot void a paid invoice\", \"already_paid\");\n }\n return { ...inv, status: \"void\" };\n}\n\n/**\n * Transition an invoice to `\"issued\"` (optionally stamping `issuedAt`).\n * Immutable.\n *\n * @throws InvoiceError when the invoice is already paid (\"already_paid\") or\n * void (\"void\").\n */\nexport function markIssued(inv: Invoice, at?: number): Invoice {\n if (inv.status === \"paid\") {\n throw new InvoiceError(\"Cannot re-issue a paid invoice\", \"already_paid\");\n }\n if (inv.status === \"void\") {\n throw new InvoiceError(\"Cannot issue a void invoice\", \"void\");\n }\n return {\n ...inv,\n status: \"issued\",\n issuedAt: at ?? inv.issuedAt,\n };\n}\n\n/**\n * Whether an invoice is overdue: it has a `dueAt` in the past and a positive\n * balance due.\n */\nexport function isOverdue(inv: Invoice, now: number = Date.now()): boolean {\n return (\n inv.dueAt !== undefined &&\n inv.dueAt < now &&\n inv.totals.balanceDue > 0\n );\n}\n\n// ---------------------------------------------------------------------------\n// Numbering & render helper\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a deterministic sequential invoice number.\n *\n * Default shape: `\"INV-2026-000123\"` (prefix `\"INV\"`, current year, pad 6).\n */\nexport function invoiceNumber(\n seq: number,\n opts?: { prefix?: string; year?: number; pad?: number; separator?: string },\n): string {\n const prefix = opts?.prefix ?? \"INV\";\n const year = opts?.year ?? new Date().getFullYear();\n const pad = opts?.pad ?? 6;\n const sep = opts?.separator ?? \"-\";\n const num = String(Math.trunc(seq)).padStart(pad, \"0\");\n return `${prefix}${sep}${year}${sep}${num}`;\n}\n\n/**\n * Emit a normalized table from an invoice's lines, ready to hand to a renderer\n * such as `@lacspace/xlsx` or a PDF table builder. Amounts stay in integer\n * minor units โ€” formatting is the renderer's job. No renderer is imported.\n *\n * Columns: `Description`, `Qty`, `Unit`, `Tax %`, `Line total`.\n */\nexport function renderRows(inv: Invoice): {\n columns: string[];\n rows: (string | number)[][];\n} {\n const columns = [\"Description\", \"Qty\", \"Unit\", \"Tax %\", \"Line total\"];\n const rows = inv.lines.map((l) => [\n l.description,\n l.qty,\n l.unitPrice,\n (l.taxRate ?? 0) * 100,\n l.total,\n ]);\n return { columns, rows };\n}\n"]}
@@ -0,0 +1,154 @@
1
+ /**
2
+ * @lacspace/invoice
3
+ *
4
+ * Invoice MODEL + numbering + tax rollup. Pure, immutable, isomorphic and
5
+ * zero-dependency. All money is expressed in integer **minor units** (e.g.
6
+ * cents / paisa) so nothing is ever lost to floating-point drift.
7
+ *
8
+ * This package is model-only: it computes a structured, serializable invoice
9
+ * and can emit a normalized row table ready to feed a renderer such as
10
+ * `@lacspace/pdf` or `@lacspace/xlsx` โ€” but it imports NEITHER of them.
11
+ */
12
+ /** A billing party โ€” the seller or the buyer on an invoice. */
13
+ interface Party {
14
+ name: string;
15
+ address?: string;
16
+ email?: string;
17
+ taxId?: string;
18
+ meta?: Record<string, unknown>;
19
+ }
20
+ /**
21
+ * Raw input for a single invoice line. Amounts are integer minor units;
22
+ * `taxRate` is a fraction (e.g. `0.13` for 13%).
23
+ */
24
+ interface InvoiceLineInput {
25
+ description: string;
26
+ qty: number;
27
+ unitPrice: number;
28
+ taxRate?: number;
29
+ discount?: number;
30
+ sku?: string;
31
+ }
32
+ /**
33
+ * A computed invoice line.
34
+ * net = qty * unitPrice - discount
35
+ * tax = round(net * taxRate)
36
+ * total = net + tax
37
+ */
38
+ interface InvoiceLine extends InvoiceLineInput {
39
+ net: number;
40
+ tax: number;
41
+ total: number;
42
+ }
43
+ /** One row of the tax summary: all net/tax that share a single rate. */
44
+ interface TaxSummaryRow {
45
+ rate: number;
46
+ net: number;
47
+ tax: number;
48
+ }
49
+ /** Invoice-level money rollup, all integer minor units. */
50
+ interface InvoiceTotals {
51
+ subtotal: number;
52
+ discount: number;
53
+ taxTotal: number;
54
+ total: number;
55
+ amountPaid: number;
56
+ balanceDue: number;
57
+ }
58
+ type InvoiceStatus = "draft" | "issued" | "paid" | "partial" | "void" | "overdue";
59
+ /** A complete, serializable invoice. */
60
+ interface Invoice {
61
+ number: string;
62
+ status: InvoiceStatus;
63
+ currency: string;
64
+ seller: Party;
65
+ buyer: Party;
66
+ lines: InvoiceLine[];
67
+ totals: InvoiceTotals;
68
+ taxSummary: TaxSummaryRow[];
69
+ issuedAt?: number;
70
+ dueAt?: number;
71
+ notes?: string;
72
+ meta?: Record<string, unknown>;
73
+ }
74
+ /** Error thrown for any invalid invoice operation. */
75
+ declare class InvoiceError extends Error {
76
+ code?: string;
77
+ constructor(message: string, code?: string);
78
+ }
79
+ /**
80
+ * Build a fully computed invoice from raw input.
81
+ *
82
+ * Computes each line (net / tax / total), the invoice totals, and the tax
83
+ * summary grouped by rate. Integer minor units throughout.
84
+ *
85
+ * @throws InvoiceError when there are no lines.
86
+ */
87
+ declare function createInvoice(input: {
88
+ number: string;
89
+ currency: string;
90
+ seller: Party;
91
+ buyer: Party;
92
+ lines: InvoiceLineInput[];
93
+ status?: InvoiceStatus;
94
+ issuedAt?: number;
95
+ dueAt?: number;
96
+ amountPaid?: number;
97
+ notes?: string;
98
+ meta?: Record<string, unknown>;
99
+ }): Invoice;
100
+ /**
101
+ * Record a payment against an invoice. Immutable โ€” returns a new invoice.
102
+ *
103
+ * Increases `amountPaid`, recomputes `balanceDue`, and sets `status` to
104
+ * `"paid"` (balance 0) or `"partial"` (balance > 0).
105
+ *
106
+ * @throws InvoiceError when `amount <= 0` ("invalid_amount") or the payment
107
+ * would take total paid past the invoice total ("overpayment").
108
+ */
109
+ declare function recordPayment(inv: Invoice, amount: number, opts?: {
110
+ at?: number;
111
+ }): Invoice;
112
+ /**
113
+ * Transition an invoice to `"void"`. Immutable.
114
+ *
115
+ * @throws InvoiceError when the invoice is already paid ("already_paid").
116
+ */
117
+ declare function markVoid(inv: Invoice): Invoice;
118
+ /**
119
+ * Transition an invoice to `"issued"` (optionally stamping `issuedAt`).
120
+ * Immutable.
121
+ *
122
+ * @throws InvoiceError when the invoice is already paid ("already_paid") or
123
+ * void ("void").
124
+ */
125
+ declare function markIssued(inv: Invoice, at?: number): Invoice;
126
+ /**
127
+ * Whether an invoice is overdue: it has a `dueAt` in the past and a positive
128
+ * balance due.
129
+ */
130
+ declare function isOverdue(inv: Invoice, now?: number): boolean;
131
+ /**
132
+ * Generate a deterministic sequential invoice number.
133
+ *
134
+ * Default shape: `"INV-2026-000123"` (prefix `"INV"`, current year, pad 6).
135
+ */
136
+ declare function invoiceNumber(seq: number, opts?: {
137
+ prefix?: string;
138
+ year?: number;
139
+ pad?: number;
140
+ separator?: string;
141
+ }): string;
142
+ /**
143
+ * Emit a normalized table from an invoice's lines, ready to hand to a renderer
144
+ * such as `@lacspace/xlsx` or a PDF table builder. Amounts stay in integer
145
+ * minor units โ€” formatting is the renderer's job. No renderer is imported.
146
+ *
147
+ * Columns: `Description`, `Qty`, `Unit`, `Tax %`, `Line total`.
148
+ */
149
+ declare function renderRows(inv: Invoice): {
150
+ columns: string[];
151
+ rows: (string | number)[][];
152
+ };
153
+
154
+ export { type Invoice, InvoiceError, type InvoiceLine, type InvoiceLineInput, type InvoiceStatus, type InvoiceTotals, type Party, type TaxSummaryRow, createInvoice, invoiceNumber, isOverdue, markIssued, markVoid, recordPayment, renderRows };
@@ -0,0 +1,154 @@
1
+ /**
2
+ * @lacspace/invoice
3
+ *
4
+ * Invoice MODEL + numbering + tax rollup. Pure, immutable, isomorphic and
5
+ * zero-dependency. All money is expressed in integer **minor units** (e.g.
6
+ * cents / paisa) so nothing is ever lost to floating-point drift.
7
+ *
8
+ * This package is model-only: it computes a structured, serializable invoice
9
+ * and can emit a normalized row table ready to feed a renderer such as
10
+ * `@lacspace/pdf` or `@lacspace/xlsx` โ€” but it imports NEITHER of them.
11
+ */
12
+ /** A billing party โ€” the seller or the buyer on an invoice. */
13
+ interface Party {
14
+ name: string;
15
+ address?: string;
16
+ email?: string;
17
+ taxId?: string;
18
+ meta?: Record<string, unknown>;
19
+ }
20
+ /**
21
+ * Raw input for a single invoice line. Amounts are integer minor units;
22
+ * `taxRate` is a fraction (e.g. `0.13` for 13%).
23
+ */
24
+ interface InvoiceLineInput {
25
+ description: string;
26
+ qty: number;
27
+ unitPrice: number;
28
+ taxRate?: number;
29
+ discount?: number;
30
+ sku?: string;
31
+ }
32
+ /**
33
+ * A computed invoice line.
34
+ * net = qty * unitPrice - discount
35
+ * tax = round(net * taxRate)
36
+ * total = net + tax
37
+ */
38
+ interface InvoiceLine extends InvoiceLineInput {
39
+ net: number;
40
+ tax: number;
41
+ total: number;
42
+ }
43
+ /** One row of the tax summary: all net/tax that share a single rate. */
44
+ interface TaxSummaryRow {
45
+ rate: number;
46
+ net: number;
47
+ tax: number;
48
+ }
49
+ /** Invoice-level money rollup, all integer minor units. */
50
+ interface InvoiceTotals {
51
+ subtotal: number;
52
+ discount: number;
53
+ taxTotal: number;
54
+ total: number;
55
+ amountPaid: number;
56
+ balanceDue: number;
57
+ }
58
+ type InvoiceStatus = "draft" | "issued" | "paid" | "partial" | "void" | "overdue";
59
+ /** A complete, serializable invoice. */
60
+ interface Invoice {
61
+ number: string;
62
+ status: InvoiceStatus;
63
+ currency: string;
64
+ seller: Party;
65
+ buyer: Party;
66
+ lines: InvoiceLine[];
67
+ totals: InvoiceTotals;
68
+ taxSummary: TaxSummaryRow[];
69
+ issuedAt?: number;
70
+ dueAt?: number;
71
+ notes?: string;
72
+ meta?: Record<string, unknown>;
73
+ }
74
+ /** Error thrown for any invalid invoice operation. */
75
+ declare class InvoiceError extends Error {
76
+ code?: string;
77
+ constructor(message: string, code?: string);
78
+ }
79
+ /**
80
+ * Build a fully computed invoice from raw input.
81
+ *
82
+ * Computes each line (net / tax / total), the invoice totals, and the tax
83
+ * summary grouped by rate. Integer minor units throughout.
84
+ *
85
+ * @throws InvoiceError when there are no lines.
86
+ */
87
+ declare function createInvoice(input: {
88
+ number: string;
89
+ currency: string;
90
+ seller: Party;
91
+ buyer: Party;
92
+ lines: InvoiceLineInput[];
93
+ status?: InvoiceStatus;
94
+ issuedAt?: number;
95
+ dueAt?: number;
96
+ amountPaid?: number;
97
+ notes?: string;
98
+ meta?: Record<string, unknown>;
99
+ }): Invoice;
100
+ /**
101
+ * Record a payment against an invoice. Immutable โ€” returns a new invoice.
102
+ *
103
+ * Increases `amountPaid`, recomputes `balanceDue`, and sets `status` to
104
+ * `"paid"` (balance 0) or `"partial"` (balance > 0).
105
+ *
106
+ * @throws InvoiceError when `amount <= 0` ("invalid_amount") or the payment
107
+ * would take total paid past the invoice total ("overpayment").
108
+ */
109
+ declare function recordPayment(inv: Invoice, amount: number, opts?: {
110
+ at?: number;
111
+ }): Invoice;
112
+ /**
113
+ * Transition an invoice to `"void"`. Immutable.
114
+ *
115
+ * @throws InvoiceError when the invoice is already paid ("already_paid").
116
+ */
117
+ declare function markVoid(inv: Invoice): Invoice;
118
+ /**
119
+ * Transition an invoice to `"issued"` (optionally stamping `issuedAt`).
120
+ * Immutable.
121
+ *
122
+ * @throws InvoiceError when the invoice is already paid ("already_paid") or
123
+ * void ("void").
124
+ */
125
+ declare function markIssued(inv: Invoice, at?: number): Invoice;
126
+ /**
127
+ * Whether an invoice is overdue: it has a `dueAt` in the past and a positive
128
+ * balance due.
129
+ */
130
+ declare function isOverdue(inv: Invoice, now?: number): boolean;
131
+ /**
132
+ * Generate a deterministic sequential invoice number.
133
+ *
134
+ * Default shape: `"INV-2026-000123"` (prefix `"INV"`, current year, pad 6).
135
+ */
136
+ declare function invoiceNumber(seq: number, opts?: {
137
+ prefix?: string;
138
+ year?: number;
139
+ pad?: number;
140
+ separator?: string;
141
+ }): string;
142
+ /**
143
+ * Emit a normalized table from an invoice's lines, ready to hand to a renderer
144
+ * such as `@lacspace/xlsx` or a PDF table builder. Amounts stay in integer
145
+ * minor units โ€” formatting is the renderer's job. No renderer is imported.
146
+ *
147
+ * Columns: `Description`, `Qty`, `Unit`, `Tax %`, `Line total`.
148
+ */
149
+ declare function renderRows(inv: Invoice): {
150
+ columns: string[];
151
+ rows: (string | number)[][];
152
+ };
153
+
154
+ export { type Invoice, InvoiceError, type InvoiceLine, type InvoiceLineInput, type InvoiceStatus, type InvoiceTotals, type Party, type TaxSummaryRow, createInvoice, invoiceNumber, isOverdue, markIssued, markVoid, recordPayment, renderRows };
package/dist/index.js ADDED
@@ -0,0 +1,140 @@
1
+ // src/index.ts
2
+ var InvoiceError = class _InvoiceError extends Error {
3
+ constructor(message, code) {
4
+ super(message);
5
+ this.name = "InvoiceError";
6
+ this.code = code;
7
+ Object.setPrototypeOf(this, _InvoiceError.prototype);
8
+ }
9
+ };
10
+ function roundMinor(n) {
11
+ return n < 0 ? -Math.round(-n) : Math.round(n);
12
+ }
13
+ function computeLine(input) {
14
+ if (!Number.isFinite(input.qty) || input.qty < 0) {
15
+ throw new InvoiceError(
16
+ `Line "${input.description}" has a negative or invalid qty`,
17
+ "invalid_qty"
18
+ );
19
+ }
20
+ const discount = input.discount ?? 0;
21
+ const taxRate = input.taxRate ?? 0;
22
+ const net = input.qty * input.unitPrice - discount;
23
+ const tax = roundMinor(net * taxRate);
24
+ return {
25
+ ...input,
26
+ net,
27
+ tax,
28
+ total: net + tax
29
+ };
30
+ }
31
+ function summarizeTax(lines) {
32
+ const byRate = /* @__PURE__ */ new Map();
33
+ for (const line of lines) {
34
+ const rate = line.taxRate ?? 0;
35
+ const row = byRate.get(rate) ?? { rate, net: 0, tax: 0 };
36
+ row.net += line.net;
37
+ row.tax += line.tax;
38
+ byRate.set(rate, row);
39
+ }
40
+ return [...byRate.values()].sort((a, b) => a.rate - b.rate);
41
+ }
42
+ function createInvoice(input) {
43
+ if (!input.lines || input.lines.length === 0) {
44
+ throw new InvoiceError("An invoice must have at least one line", "no_lines");
45
+ }
46
+ const lines = input.lines.map(computeLine);
47
+ const subtotal = lines.reduce((s, l) => s + l.net, 0);
48
+ const discount = lines.reduce((s, l) => s + (l.discount ?? 0), 0);
49
+ const taxTotal = lines.reduce((s, l) => s + l.tax, 0);
50
+ const total = subtotal + taxTotal;
51
+ const amountPaid = input.amountPaid ?? 0;
52
+ const balanceDue = total - amountPaid;
53
+ const totals = {
54
+ subtotal,
55
+ discount,
56
+ taxTotal,
57
+ total,
58
+ amountPaid,
59
+ balanceDue
60
+ };
61
+ return {
62
+ number: input.number,
63
+ status: input.status ?? "draft",
64
+ currency: input.currency,
65
+ seller: input.seller,
66
+ buyer: input.buyer,
67
+ lines,
68
+ totals,
69
+ taxSummary: summarizeTax(lines),
70
+ issuedAt: input.issuedAt,
71
+ dueAt: input.dueAt,
72
+ notes: input.notes,
73
+ meta: input.meta
74
+ };
75
+ }
76
+ function recordPayment(inv, amount, opts) {
77
+ if (!Number.isFinite(amount) || amount <= 0) {
78
+ throw new InvoiceError("Payment amount must be positive", "invalid_amount");
79
+ }
80
+ const amountPaid = inv.totals.amountPaid + amount;
81
+ if (amountPaid > inv.totals.total) {
82
+ throw new InvoiceError(
83
+ "Payment exceeds the amount due",
84
+ "overpayment"
85
+ );
86
+ }
87
+ const balanceDue = inv.totals.total - amountPaid;
88
+ const status = balanceDue === 0 ? "paid" : "partial";
89
+ return {
90
+ ...inv,
91
+ status,
92
+ totals: { ...inv.totals, amountPaid, balanceDue },
93
+ meta: opts?.at !== void 0 ? { ...inv.meta ?? {}, lastPaymentAt: opts.at } : inv.meta
94
+ };
95
+ }
96
+ function markVoid(inv) {
97
+ if (inv.status === "paid") {
98
+ throw new InvoiceError("Cannot void a paid invoice", "already_paid");
99
+ }
100
+ return { ...inv, status: "void" };
101
+ }
102
+ function markIssued(inv, at) {
103
+ if (inv.status === "paid") {
104
+ throw new InvoiceError("Cannot re-issue a paid invoice", "already_paid");
105
+ }
106
+ if (inv.status === "void") {
107
+ throw new InvoiceError("Cannot issue a void invoice", "void");
108
+ }
109
+ return {
110
+ ...inv,
111
+ status: "issued",
112
+ issuedAt: at ?? inv.issuedAt
113
+ };
114
+ }
115
+ function isOverdue(inv, now = Date.now()) {
116
+ return inv.dueAt !== void 0 && inv.dueAt < now && inv.totals.balanceDue > 0;
117
+ }
118
+ function invoiceNumber(seq, opts) {
119
+ const prefix = opts?.prefix ?? "INV";
120
+ const year = opts?.year ?? (/* @__PURE__ */ new Date()).getFullYear();
121
+ const pad = opts?.pad ?? 6;
122
+ const sep = opts?.separator ?? "-";
123
+ const num = String(Math.trunc(seq)).padStart(pad, "0");
124
+ return `${prefix}${sep}${year}${sep}${num}`;
125
+ }
126
+ function renderRows(inv) {
127
+ const columns = ["Description", "Qty", "Unit", "Tax %", "Line total"];
128
+ const rows = inv.lines.map((l) => [
129
+ l.description,
130
+ l.qty,
131
+ l.unitPrice,
132
+ (l.taxRate ?? 0) * 100,
133
+ l.total
134
+ ]);
135
+ return { columns, rows };
136
+ }
137
+
138
+ export { InvoiceError, createInvoice, invoiceNumber, isOverdue, markIssued, markVoid, recordPayment, renderRows };
139
+ //# sourceMappingURL=index.js.map
140
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";AA4FO,IAAM,YAAA,GAAN,MAAM,aAAA,SAAqB,KAAA,CAAM;AAAA,EAEtC,WAAA,CAAY,SAAiB,IAAA,EAAe;AAC1C,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,cAAA;AACZ,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AAEZ,IAAA,MAAA,CAAO,cAAA,CAAe,IAAA,EAAM,aAAA,CAAa,SAAS,CAAA;AAAA,EACpD;AACF;AAOA,SAAS,WAAW,CAAA,EAAmB;AACrC,EAAA,OAAO,CAAA,GAAI,CAAA,GAAI,CAAC,IAAA,CAAK,KAAA,CAAM,CAAC,CAAC,CAAA,GAAI,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA;AAC/C;AAEA,SAAS,YAAY,KAAA,EAAsC;AACzD,EAAA,IAAI,CAAC,OAAO,QAAA,CAAS,KAAA,CAAM,GAAG,CAAA,IAAK,KAAA,CAAM,MAAM,CAAA,EAAG;AAChD,IAAA,MAAM,IAAI,YAAA;AAAA,MACR,CAAA,MAAA,EAAS,MAAM,WAAW,CAAA,+BAAA,CAAA;AAAA,MAC1B;AAAA,KACF;AAAA,EACF;AACA,EAAA,MAAM,QAAA,GAAW,MAAM,QAAA,IAAY,CAAA;AACnC,EAAA,MAAM,OAAA,GAAU,MAAM,OAAA,IAAW,CAAA;AACjC,EAAA,MAAM,GAAA,GAAM,KAAA,CAAM,GAAA,GAAM,KAAA,CAAM,SAAA,GAAY,QAAA;AAC1C,EAAA,MAAM,GAAA,GAAM,UAAA,CAAW,GAAA,GAAM,OAAO,CAAA;AACpC,EAAA,OAAO;AAAA,IACL,GAAG,KAAA;AAAA,IACH,GAAA;AAAA,IACA,GAAA;AAAA,IACA,OAAO,GAAA,GAAM;AAAA,GACf;AACF;AAUA,SAAS,aAAa,KAAA,EAAuC;AAC3D,EAAA,MAAM,MAAA,uBAAa,GAAA,EAA2B;AAC9C,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,MAAM,IAAA,GAAO,KAAK,OAAA,IAAW,CAAA;AAC7B,IAAA,MAAM,GAAA,GAAM,MAAA,CAAO,GAAA,CAAI,IAAI,CAAA,IAAK,EAAE,IAAA,EAAM,GAAA,EAAK,CAAA,EAAG,GAAA,EAAK,CAAA,EAAE;AACvD,IAAA,GAAA,CAAI,OAAO,IAAA,CAAK,GAAA;AAChB,IAAA,GAAA,CAAI,OAAO,IAAA,CAAK,GAAA;AAChB,IAAA,MAAA,CAAO,GAAA,CAAI,MAAM,GAAG,CAAA;AAAA,EACtB;AAEA,EAAA,OAAO,CAAC,GAAG,MAAA,CAAO,MAAA,EAAQ,CAAA,CAAE,IAAA,CAAK,CAAC,CAAA,EAAG,CAAA,KAAM,CAAA,CAAE,IAAA,GAAO,EAAE,IAAI,CAAA;AAC5D;AAcO,SAAS,cAAc,KAAA,EAYlB;AACV,EAAA,IAAI,CAAC,KAAA,CAAM,KAAA,IAAS,KAAA,CAAM,KAAA,CAAM,WAAW,CAAA,EAAG;AAC5C,IAAA,MAAM,IAAI,YAAA,CAAa,wCAAA,EAA0C,UAAU,CAAA;AAAA,EAC7E;AAEA,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,KAAA,CAAM,GAAA,CAAI,WAAW,CAAA;AAEzC,EAAA,MAAM,QAAA,GAAW,MAAM,MAAA,CAAO,CAAC,GAAG,CAAA,KAAM,CAAA,GAAI,CAAA,CAAE,GAAA,EAAK,CAAC,CAAA;AACpD,EAAA,MAAM,QAAA,GAAW,KAAA,CAAM,MAAA,CAAO,CAAC,CAAA,EAAG,MAAM,CAAA,IAAK,CAAA,CAAE,QAAA,IAAY,CAAA,CAAA,EAAI,CAAC,CAAA;AAChE,EAAA,MAAM,QAAA,GAAW,MAAM,MAAA,CAAO,CAAC,GAAG,CAAA,KAAM,CAAA,GAAI,CAAA,CAAE,GAAA,EAAK,CAAC,CAAA;AACpD,EAAA,MAAM,QAAQ,QAAA,GAAW,QAAA;AACzB,EAAA,MAAM,UAAA,GAAa,MAAM,UAAA,IAAc,CAAA;AACvC,EAAA,MAAM,aAAa,KAAA,GAAQ,UAAA;AAE3B,EAAA,MAAM,MAAA,GAAwB;AAAA,IAC5B,QAAA;AAAA,IACA,QAAA;AAAA,IACA,QAAA;AAAA,IACA,KAAA;AAAA,IACA,UAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,OAAO;AAAA,IACL,QAAQ,KAAA,CAAM,MAAA;AAAA,IACd,MAAA,EAAQ,MAAM,MAAA,IAAU,OAAA;AAAA,IACxB,UAAU,KAAA,CAAM,QAAA;AAAA,IAChB,QAAQ,KAAA,CAAM,MAAA;AAAA,IACd,OAAO,KAAA,CAAM,KAAA;AAAA,IACb,KAAA;AAAA,IACA,MAAA;AAAA,IACA,UAAA,EAAY,aAAa,KAAK,CAAA;AAAA,IAC9B,UAAU,KAAA,CAAM,QAAA;AAAA,IAChB,OAAO,KAAA,CAAM,KAAA;AAAA,IACb,OAAO,KAAA,CAAM,KAAA;AAAA,IACb,MAAM,KAAA,CAAM;AAAA,GACd;AACF;AAWO,SAAS,aAAA,CACd,GAAA,EACA,MAAA,EACA,IAAA,EACS;AACT,EAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,MAAM,CAAA,IAAK,UAAU,CAAA,EAAG;AAC3C,IAAA,MAAM,IAAI,YAAA,CAAa,iCAAA,EAAmC,gBAAgB,CAAA;AAAA,EAC5E;AACA,EAAA,MAAM,UAAA,GAAa,GAAA,CAAI,MAAA,CAAO,UAAA,GAAa,MAAA;AAC3C,EAAA,IAAI,UAAA,GAAa,GAAA,CAAI,MAAA,CAAO,KAAA,EAAO;AACjC,IAAA,MAAM,IAAI,YAAA;AAAA,MACR,gCAAA;AAAA,MACA;AAAA,KACF;AAAA,EACF;AACA,EAAA,MAAM,UAAA,GAAa,GAAA,CAAI,MAAA,CAAO,KAAA,GAAQ,UAAA;AACtC,EAAA,MAAM,MAAA,GAAwB,UAAA,KAAe,CAAA,GAAI,MAAA,GAAS,SAAA;AAC1D,EAAA,OAAO;AAAA,IACL,GAAG,GAAA;AAAA,IACH,MAAA;AAAA,IACA,QAAQ,EAAE,GAAG,GAAA,CAAI,MAAA,EAAQ,YAAY,UAAA,EAAW;AAAA,IAChD,IAAA,EACE,IAAA,EAAM,EAAA,KAAO,MAAA,GACT,EAAE,GAAI,GAAA,CAAI,IAAA,IAAQ,EAAC,EAAI,aAAA,EAAe,IAAA,CAAK,EAAA,KAC3C,GAAA,CAAI;AAAA,GACZ;AACF;AAOO,SAAS,SAAS,GAAA,EAAuB;AAC9C,EAAA,IAAI,GAAA,CAAI,WAAW,MAAA,EAAQ;AACzB,IAAA,MAAM,IAAI,YAAA,CAAa,4BAAA,EAA8B,cAAc,CAAA;AAAA,EACrE;AACA,EAAA,OAAO,EAAE,GAAG,GAAA,EAAK,MAAA,EAAQ,MAAA,EAAO;AAClC;AASO,SAAS,UAAA,CAAW,KAAc,EAAA,EAAsB;AAC7D,EAAA,IAAI,GAAA,CAAI,WAAW,MAAA,EAAQ;AACzB,IAAA,MAAM,IAAI,YAAA,CAAa,gCAAA,EAAkC,cAAc,CAAA;AAAA,EACzE;AACA,EAAA,IAAI,GAAA,CAAI,WAAW,MAAA,EAAQ;AACzB,IAAA,MAAM,IAAI,YAAA,CAAa,6BAAA,EAA+B,MAAM,CAAA;AAAA,EAC9D;AACA,EAAA,OAAO;AAAA,IACL,GAAG,GAAA;AAAA,IACH,MAAA,EAAQ,QAAA;AAAA,IACR,QAAA,EAAU,MAAM,GAAA,CAAI;AAAA,GACtB;AACF;AAMO,SAAS,SAAA,CAAU,GAAA,EAAc,GAAA,GAAc,IAAA,CAAK,KAAI,EAAY;AACzE,EAAA,OACE,GAAA,CAAI,UAAU,MAAA,IACd,GAAA,CAAI,QAAQ,GAAA,IACZ,GAAA,CAAI,OAAO,UAAA,GAAa,CAAA;AAE5B;AAWO,SAAS,aAAA,CACd,KACA,IAAA,EACQ;AACR,EAAA,MAAM,MAAA,GAAS,MAAM,MAAA,IAAU,KAAA;AAC/B,EAAA,MAAM,OAAO,IAAA,EAAM,IAAA,IAAA,iBAAQ,IAAI,IAAA,IAAO,WAAA,EAAY;AAClD,EAAA,MAAM,GAAA,GAAM,MAAM,GAAA,IAAO,CAAA;AACzB,EAAA,MAAM,GAAA,GAAM,MAAM,SAAA,IAAa,GAAA;AAC/B,EAAA,MAAM,GAAA,GAAM,OAAO,IAAA,CAAK,KAAA,CAAM,GAAG,CAAC,CAAA,CAAE,QAAA,CAAS,GAAA,EAAK,GAAG,CAAA;AACrD,EAAA,OAAO,CAAA,EAAG,MAAM,CAAA,EAAG,GAAG,GAAG,IAAI,CAAA,EAAG,GAAG,CAAA,EAAG,GAAG,CAAA,CAAA;AAC3C;AASO,SAAS,WAAW,GAAA,EAGzB;AACA,EAAA,MAAM,UAAU,CAAC,aAAA,EAAe,KAAA,EAAO,MAAA,EAAQ,SAAS,YAAY,CAAA;AACpE,EAAA,MAAM,IAAA,GAAO,GAAA,CAAI,KAAA,CAAM,GAAA,CAAI,CAAC,CAAA,KAAM;AAAA,IAChC,CAAA,CAAE,WAAA;AAAA,IACF,CAAA,CAAE,GAAA;AAAA,IACF,CAAA,CAAE,SAAA;AAAA,IAAA,CACD,CAAA,CAAE,WAAW,CAAA,IAAK,GAAA;AAAA,IACnB,CAAA,CAAE;AAAA,GACH,CAAA;AACD,EAAA,OAAO,EAAE,SAAS,IAAA,EAAK;AACzB","file":"index.js","sourcesContent":["/**\n * @lacspace/invoice\n *\n * Invoice MODEL + numbering + tax rollup. Pure, immutable, isomorphic and\n * zero-dependency. All money is expressed in integer **minor units** (e.g.\n * cents / paisa) so nothing is ever lost to floating-point drift.\n *\n * This package is model-only: it computes a structured, serializable invoice\n * and can emit a normalized row table ready to feed a renderer such as\n * `@lacspace/pdf` or `@lacspace/xlsx` โ€” but it imports NEITHER of them.\n */\n\n// ---------------------------------------------------------------------------\n// Model\n// ---------------------------------------------------------------------------\n\n/** A billing party โ€” the seller or the buyer on an invoice. */\nexport interface Party {\n name: string;\n address?: string;\n email?: string;\n taxId?: string;\n meta?: Record<string, unknown>;\n}\n\n/**\n * Raw input for a single invoice line. Amounts are integer minor units;\n * `taxRate` is a fraction (e.g. `0.13` for 13%).\n */\nexport interface InvoiceLineInput {\n description: string;\n qty: number;\n unitPrice: number;\n taxRate?: number;\n discount?: number;\n sku?: string;\n}\n\n/**\n * A computed invoice line.\n * net = qty * unitPrice - discount\n * tax = round(net * taxRate)\n * total = net + tax\n */\nexport interface InvoiceLine extends InvoiceLineInput {\n net: number;\n tax: number;\n total: number;\n}\n\n/** One row of the tax summary: all net/tax that share a single rate. */\nexport interface TaxSummaryRow {\n rate: number;\n net: number;\n tax: number;\n}\n\n/** Invoice-level money rollup, all integer minor units. */\nexport interface InvoiceTotals {\n subtotal: number;\n discount: number;\n taxTotal: number;\n total: number;\n amountPaid: number;\n balanceDue: number;\n}\n\nexport type InvoiceStatus =\n | \"draft\"\n | \"issued\"\n | \"paid\"\n | \"partial\"\n | \"void\"\n | \"overdue\";\n\n/** A complete, serializable invoice. */\nexport interface Invoice {\n number: string;\n status: InvoiceStatus;\n currency: string;\n seller: Party;\n buyer: Party;\n lines: InvoiceLine[];\n totals: InvoiceTotals;\n taxSummary: TaxSummaryRow[];\n issuedAt?: number;\n dueAt?: number;\n notes?: string;\n meta?: Record<string, unknown>;\n}\n\n/** Error thrown for any invalid invoice operation. */\nexport class InvoiceError extends Error {\n code?: string;\n constructor(message: string, code?: string) {\n super(message);\n this.name = \"InvoiceError\";\n this.code = code;\n // Restore prototype chain for downlevel ES targets.\n Object.setPrototypeOf(this, InvoiceError.prototype);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/** Round to the nearest integer minor unit (half away from zero). */\nfunction roundMinor(n: number): number {\n return n < 0 ? -Math.round(-n) : Math.round(n);\n}\n\nfunction computeLine(input: InvoiceLineInput): InvoiceLine {\n if (!Number.isFinite(input.qty) || input.qty < 0) {\n throw new InvoiceError(\n `Line \"${input.description}\" has a negative or invalid qty`,\n \"invalid_qty\",\n );\n }\n const discount = input.discount ?? 0;\n const taxRate = input.taxRate ?? 0;\n const net = input.qty * input.unitPrice - discount;\n const tax = roundMinor(net * taxRate);\n return {\n ...input,\n net,\n tax,\n total: net + tax,\n };\n}\n\n/**\n * Group computed lines into tax-summary rows by rate.\n *\n * Rounding order: each LINE's tax is rounded first (in `computeLine`), then the\n * already-rounded per-line tax amounts are summed into the summary row. Net is\n * likewise summed from per-line nets. This keeps the tax summary consistent\n * with `totals.taxTotal` (both are sums of the same rounded per-line taxes).\n */\nfunction summarizeTax(lines: InvoiceLine[]): TaxSummaryRow[] {\n const byRate = new Map<number, TaxSummaryRow>();\n for (const line of lines) {\n const rate = line.taxRate ?? 0;\n const row = byRate.get(rate) ?? { rate, net: 0, tax: 0 };\n row.net += line.net;\n row.tax += line.tax;\n byRate.set(rate, row);\n }\n // Deterministic ascending order by rate.\n return [...byRate.values()].sort((a, b) => a.rate - b.rate);\n}\n\n// ---------------------------------------------------------------------------\n// Build & compute\n// ---------------------------------------------------------------------------\n\n/**\n * Build a fully computed invoice from raw input.\n *\n * Computes each line (net / tax / total), the invoice totals, and the tax\n * summary grouped by rate. Integer minor units throughout.\n *\n * @throws InvoiceError when there are no lines.\n */\nexport function createInvoice(input: {\n number: string;\n currency: string;\n seller: Party;\n buyer: Party;\n lines: InvoiceLineInput[];\n status?: InvoiceStatus;\n issuedAt?: number;\n dueAt?: number;\n amountPaid?: number;\n notes?: string;\n meta?: Record<string, unknown>;\n}): Invoice {\n if (!input.lines || input.lines.length === 0) {\n throw new InvoiceError(\"An invoice must have at least one line\", \"no_lines\");\n }\n\n const lines = input.lines.map(computeLine);\n\n const subtotal = lines.reduce((s, l) => s + l.net, 0);\n const discount = lines.reduce((s, l) => s + (l.discount ?? 0), 0);\n const taxTotal = lines.reduce((s, l) => s + l.tax, 0);\n const total = subtotal + taxTotal;\n const amountPaid = input.amountPaid ?? 0;\n const balanceDue = total - amountPaid;\n\n const totals: InvoiceTotals = {\n subtotal,\n discount,\n taxTotal,\n total,\n amountPaid,\n balanceDue,\n };\n\n return {\n number: input.number,\n status: input.status ?? \"draft\",\n currency: input.currency,\n seller: input.seller,\n buyer: input.buyer,\n lines,\n totals,\n taxSummary: summarizeTax(lines),\n issuedAt: input.issuedAt,\n dueAt: input.dueAt,\n notes: input.notes,\n meta: input.meta,\n };\n}\n\n/**\n * Record a payment against an invoice. Immutable โ€” returns a new invoice.\n *\n * Increases `amountPaid`, recomputes `balanceDue`, and sets `status` to\n * `\"paid\"` (balance 0) or `\"partial\"` (balance > 0).\n *\n * @throws InvoiceError when `amount <= 0` (\"invalid_amount\") or the payment\n * would take total paid past the invoice total (\"overpayment\").\n */\nexport function recordPayment(\n inv: Invoice,\n amount: number,\n opts?: { at?: number },\n): Invoice {\n if (!Number.isFinite(amount) || amount <= 0) {\n throw new InvoiceError(\"Payment amount must be positive\", \"invalid_amount\");\n }\n const amountPaid = inv.totals.amountPaid + amount;\n if (amountPaid > inv.totals.total) {\n throw new InvoiceError(\n \"Payment exceeds the amount due\",\n \"overpayment\",\n );\n }\n const balanceDue = inv.totals.total - amountPaid;\n const status: InvoiceStatus = balanceDue === 0 ? \"paid\" : \"partial\";\n return {\n ...inv,\n status,\n totals: { ...inv.totals, amountPaid, balanceDue },\n meta:\n opts?.at !== undefined\n ? { ...(inv.meta ?? {}), lastPaymentAt: opts.at }\n : inv.meta,\n };\n}\n\n/**\n * Transition an invoice to `\"void\"`. Immutable.\n *\n * @throws InvoiceError when the invoice is already paid (\"already_paid\").\n */\nexport function markVoid(inv: Invoice): Invoice {\n if (inv.status === \"paid\") {\n throw new InvoiceError(\"Cannot void a paid invoice\", \"already_paid\");\n }\n return { ...inv, status: \"void\" };\n}\n\n/**\n * Transition an invoice to `\"issued\"` (optionally stamping `issuedAt`).\n * Immutable.\n *\n * @throws InvoiceError when the invoice is already paid (\"already_paid\") or\n * void (\"void\").\n */\nexport function markIssued(inv: Invoice, at?: number): Invoice {\n if (inv.status === \"paid\") {\n throw new InvoiceError(\"Cannot re-issue a paid invoice\", \"already_paid\");\n }\n if (inv.status === \"void\") {\n throw new InvoiceError(\"Cannot issue a void invoice\", \"void\");\n }\n return {\n ...inv,\n status: \"issued\",\n issuedAt: at ?? inv.issuedAt,\n };\n}\n\n/**\n * Whether an invoice is overdue: it has a `dueAt` in the past and a positive\n * balance due.\n */\nexport function isOverdue(inv: Invoice, now: number = Date.now()): boolean {\n return (\n inv.dueAt !== undefined &&\n inv.dueAt < now &&\n inv.totals.balanceDue > 0\n );\n}\n\n// ---------------------------------------------------------------------------\n// Numbering & render helper\n// ---------------------------------------------------------------------------\n\n/**\n * Generate a deterministic sequential invoice number.\n *\n * Default shape: `\"INV-2026-000123\"` (prefix `\"INV\"`, current year, pad 6).\n */\nexport function invoiceNumber(\n seq: number,\n opts?: { prefix?: string; year?: number; pad?: number; separator?: string },\n): string {\n const prefix = opts?.prefix ?? \"INV\";\n const year = opts?.year ?? new Date().getFullYear();\n const pad = opts?.pad ?? 6;\n const sep = opts?.separator ?? \"-\";\n const num = String(Math.trunc(seq)).padStart(pad, \"0\");\n return `${prefix}${sep}${year}${sep}${num}`;\n}\n\n/**\n * Emit a normalized table from an invoice's lines, ready to hand to a renderer\n * such as `@lacspace/xlsx` or a PDF table builder. Amounts stay in integer\n * minor units โ€” formatting is the renderer's job. No renderer is imported.\n *\n * Columns: `Description`, `Qty`, `Unit`, `Tax %`, `Line total`.\n */\nexport function renderRows(inv: Invoice): {\n columns: string[];\n rows: (string | number)[][];\n} {\n const columns = [\"Description\", \"Qty\", \"Unit\", \"Tax %\", \"Line total\"];\n const rows = inv.lines.map((l) => [\n l.description,\n l.qty,\n l.unitPrice,\n (l.taxRate ?? 0) * 100,\n l.total,\n ]);\n return { columns, rows };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@lacspace/invoice",
3
+ "version": "1.0.0",
4
+ "description": "Invoice model, numbering and tax-rollup engine โ€” per-line and total calculation, tax grouped by rate, sequential invoice numbers, payment tracking and a render-ready row structure for PDF/XLSX. Integer minor units, zero-dependency, isomorphic.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "sideEffects": false,
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "prepublishOnly": "npm run build"
28
+ },
29
+ "keywords": [
30
+ "invoice",
31
+ "invoicing",
32
+ "billing",
33
+ "tax",
34
+ "invoice-number",
35
+ "accounting",
36
+ "ecommerce",
37
+ "vat",
38
+ "minor-units",
39
+ "isomorphic",
40
+ "typescript"
41
+ ],
42
+ "author": "Lacspace <contact@lacspace.com>",
43
+ "license": "SEE LICENSE IN LICENSE",
44
+ "homepage": "https://developer.lacspace.com/packages/invoice",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/lacspace/npm-packages.git",
48
+ "directory": "invoice"
49
+ },
50
+ "bugs": {
51
+ "url": "https://github.com/lacspace/npm-packages/issues"
52
+ },
53
+ "engines": {
54
+ "node": ">=18"
55
+ },
56
+ "dependencies": {},
57
+ "publishConfig": {
58
+ "access": "public"
59
+ }
60
+ }