@lacspace/invoice 1.0.0 → 1.0.2
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/README.md +4 -4
- package/dist/index.cjs +6 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -105,9 +105,9 @@ All amounts are integer minor units; `taxRate` is a fraction (`0.13` = 13%). `ne
|
|
|
105
105
|
|
|
106
106
|
## Licensing
|
|
107
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.
|
|
108
|
+
This package is **free** under the **[Lacspace Free Licence](https://developer.lacspace.com/licenses/lacspace-free-1.0)** — permissive freedoms. Use it in personal and commercial projects at no cost; just keep the notice.
|
|
109
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)**.
|
|
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://developer.lacspace.com/licenses)**.
|
|
111
111
|
|
|
112
112
|
<!-- LACSPACE-DEV-PLATFORM -->
|
|
113
113
|
|
|
@@ -115,7 +115,7 @@ Not every Lacspace package is free. We also offer **Commercial** (paid), **Clien
|
|
|
115
115
|
|
|
116
116
|
## The Lacspace Developer Platform
|
|
117
117
|
|
|
118
|
-
`@lacspace/invoice` is part of **
|
|
118
|
+
`@lacspace/invoice` is part of **80+ zero-dependency, isomorphic TypeScript packages**. Explore the ecosystem:
|
|
119
119
|
|
|
120
120
|
- 🗂️ **All packages** — https://developer.lacspace.com/packages
|
|
121
121
|
- 🧭 **Developer handbook** — https://developer.lacspace.com/handbook
|
|
@@ -123,4 +123,4 @@ Not every Lacspace package is free. We also offer **Commercial** (paid), **Clien
|
|
|
123
123
|
- 🖥️ **Finished app templates** — https://templates.lacspace.com
|
|
124
124
|
- 🚀 **Scaffold a full app** — `npm create lacspace-app@latest`
|
|
125
125
|
|
|
126
|
-
Free under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — a permissive, free-to-use licence.
|
|
126
|
+
Free under the **[Lacspace Free Licence](https://developer.lacspace.com/licenses/lacspace-free-1.0)** — a permissive, free-to-use licence.
|
package/dist/index.cjs
CHANGED
|
@@ -79,6 +79,12 @@ function recordPayment(inv, amount, opts) {
|
|
|
79
79
|
if (!Number.isFinite(amount) || amount <= 0) {
|
|
80
80
|
throw new InvoiceError("Payment amount must be positive", "invalid_amount");
|
|
81
81
|
}
|
|
82
|
+
if (inv.status === "void") {
|
|
83
|
+
throw new InvoiceError(
|
|
84
|
+
"Cannot record a payment on a void invoice",
|
|
85
|
+
"void"
|
|
86
|
+
);
|
|
87
|
+
}
|
|
82
88
|
const amountPaid = inv.totals.amountPaid + amount;
|
|
83
89
|
if (amountPaid > inv.totals.total) {
|
|
84
90
|
throw new InvoiceError(
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +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"]}
|
|
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 if (inv.status === \"void\") {\n throw new InvoiceError(\n \"Cannot record a payment on a void invoice\",\n \"void\",\n );\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/dist/index.js
CHANGED
|
@@ -77,6 +77,12 @@ function recordPayment(inv, amount, opts) {
|
|
|
77
77
|
if (!Number.isFinite(amount) || amount <= 0) {
|
|
78
78
|
throw new InvoiceError("Payment amount must be positive", "invalid_amount");
|
|
79
79
|
}
|
|
80
|
+
if (inv.status === "void") {
|
|
81
|
+
throw new InvoiceError(
|
|
82
|
+
"Cannot record a payment on a void invoice",
|
|
83
|
+
"void"
|
|
84
|
+
);
|
|
85
|
+
}
|
|
80
86
|
const amountPaid = inv.totals.amountPaid + amount;
|
|
81
87
|
if (amountPaid > inv.totals.total) {
|
|
82
88
|
throw new InvoiceError(
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","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"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","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 if (inv.status === \"void\") {\n throw new InvoiceError(\n \"Cannot record a payment on a void invoice\",\n \"void\",\n );\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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lacspace/invoice",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|