@hasna/invoices 0.1.2 → 0.1.4
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/dist/cli/index.js +92 -20
- package/dist/db/invoices.d.ts.map +1 -1
- package/dist/index.js +89 -19
- package/dist/lib/version.d.ts +1 -1
- package/dist/mcp/index.js +88 -18
- package/dist/server/index.js +88 -18
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2088,12 +2088,83 @@ function normalizePagination(options, defaults) {
|
|
|
2088
2088
|
const offset = Number.isFinite(options.offset) ? Math.max(Math.trunc(options.offset), 0) : 0;
|
|
2089
2089
|
return { limit, offset };
|
|
2090
2090
|
}
|
|
2091
|
-
function
|
|
2091
|
+
function assertInvoiceStatus(status) {
|
|
2092
|
+
if (!INVOICE_STATUSES.has(status)) {
|
|
2093
|
+
throw new RangeError(`Invalid invoice status: ${String(status)}`);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
function assertSafeCents(value, field) {
|
|
2097
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
2098
|
+
throw new RangeError(`${field} must be a safe nonnegative integer`);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
function normalizeInvoiceLine(line, index) {
|
|
2102
|
+
const prefix = `Invoice line ${index + 1}`;
|
|
2103
|
+
if (!line || typeof line !== "object") {
|
|
2104
|
+
throw new TypeError(`${prefix} must be an object`);
|
|
2105
|
+
}
|
|
2106
|
+
const id = line.id;
|
|
2107
|
+
const description = line.description;
|
|
2108
|
+
const quantity = line.quantity;
|
|
2109
|
+
const unitPriceCents = line.unitPriceCents;
|
|
2110
|
+
const taxRateBasisPoints = line.taxRateBasisPoints;
|
|
2111
|
+
if (id !== undefined && typeof id !== "string") {
|
|
2112
|
+
throw new TypeError(`${prefix} id must be a string`);
|
|
2113
|
+
}
|
|
2114
|
+
if (typeof description !== "string" || description.trim().length === 0) {
|
|
2115
|
+
throw new TypeError(`${prefix} description must not be blank`);
|
|
2116
|
+
}
|
|
2117
|
+
if (!Number.isFinite(quantity) || quantity <= 0) {
|
|
2118
|
+
throw new RangeError(`${prefix} quantity must be a finite positive number`);
|
|
2119
|
+
}
|
|
2120
|
+
if (!Number.isSafeInteger(unitPriceCents) || unitPriceCents < 0) {
|
|
2121
|
+
throw new RangeError(`${prefix} unitPriceCents must be a safe nonnegative integer`);
|
|
2122
|
+
}
|
|
2123
|
+
if (taxRateBasisPoints !== undefined && (!Number.isSafeInteger(taxRateBasisPoints) || taxRateBasisPoints < 0)) {
|
|
2124
|
+
throw new RangeError(`${prefix} taxRateBasisPoints must be a safe nonnegative integer`);
|
|
2125
|
+
}
|
|
2126
|
+
return {
|
|
2127
|
+
id,
|
|
2128
|
+
description,
|
|
2129
|
+
quantity,
|
|
2130
|
+
unitPriceCents,
|
|
2131
|
+
taxRateBasisPoints
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
function toLineTotalCents(line, index) {
|
|
2092
2135
|
const subtotal = Math.round(line.quantity * line.unitPriceCents);
|
|
2093
2136
|
const taxRate = line.taxRateBasisPoints ?? 0;
|
|
2094
2137
|
const tax = Math.round(subtotal * taxRate / 1e4);
|
|
2138
|
+
const prefix = `Invoice line ${index + 1}`;
|
|
2139
|
+
assertSafeCents(subtotal, `${prefix} lineTotalCents`);
|
|
2140
|
+
assertSafeCents(tax, `${prefix} lineTaxCents`);
|
|
2095
2141
|
return { lineTotalCents: subtotal, lineTaxCents: tax };
|
|
2096
2142
|
}
|
|
2143
|
+
function prepareInvoiceLines(lines) {
|
|
2144
|
+
if (!Array.isArray(lines)) {
|
|
2145
|
+
throw new RangeError("Invoice must include at least one line");
|
|
2146
|
+
}
|
|
2147
|
+
const lineCount = lines.length;
|
|
2148
|
+
if (!Number.isSafeInteger(lineCount) || lineCount <= 0) {
|
|
2149
|
+
throw new RangeError("Invoice must include at least one line");
|
|
2150
|
+
}
|
|
2151
|
+
const preparedLines = [];
|
|
2152
|
+
for (let index = 0;index < lineCount; index += 1) {
|
|
2153
|
+
const line = normalizeInvoiceLine(lines[index], index);
|
|
2154
|
+
const { lineTotalCents, lineTaxCents } = toLineTotalCents(line, index);
|
|
2155
|
+
preparedLines.push({
|
|
2156
|
+
id: line.id ?? randomUUID(),
|
|
2157
|
+
position: index,
|
|
2158
|
+
description: line.description,
|
|
2159
|
+
quantity: line.quantity,
|
|
2160
|
+
unitPriceCents: line.unitPriceCents,
|
|
2161
|
+
taxRateBasisPoints: line.taxRateBasisPoints ?? 0,
|
|
2162
|
+
lineTotalCents,
|
|
2163
|
+
lineTaxCents
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
return preparedLines;
|
|
2167
|
+
}
|
|
2097
2168
|
function mapPartyRow(row) {
|
|
2098
2169
|
return {
|
|
2099
2170
|
id: row.id,
|
|
@@ -2157,23 +2228,18 @@ function listParties(db, kind) {
|
|
|
2157
2228
|
return db.query("SELECT * FROM parties ORDER BY legal_name ASC").all().map(mapPartyRow);
|
|
2158
2229
|
}
|
|
2159
2230
|
function createInvoice(db, input) {
|
|
2231
|
+
const preparedLines = prepareInvoiceLines(input.lines);
|
|
2160
2232
|
const invoiceId = input.id ?? randomUUID();
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
taxRateBasisPoints: line.taxRateBasisPoints ?? 0,
|
|
2170
|
-
lineTotalCents,
|
|
2171
|
-
lineTaxCents
|
|
2172
|
-
};
|
|
2173
|
-
});
|
|
2174
|
-
const subtotalCents = preparedLines.reduce((sum, line) => sum + line.lineTotalCents, 0);
|
|
2175
|
-
const taxCents = preparedLines.reduce((sum, line) => sum + line.lineTaxCents, 0);
|
|
2233
|
+
let subtotalCents = 0;
|
|
2234
|
+
let taxCents = 0;
|
|
2235
|
+
for (const line of preparedLines) {
|
|
2236
|
+
subtotalCents += line.lineTotalCents;
|
|
2237
|
+
assertSafeCents(subtotalCents, "subtotalCents");
|
|
2238
|
+
taxCents += line.lineTaxCents;
|
|
2239
|
+
assertSafeCents(taxCents, "taxCents");
|
|
2240
|
+
}
|
|
2176
2241
|
const totalCents = subtotalCents + taxCents;
|
|
2242
|
+
assertSafeCents(totalCents, "totalCents");
|
|
2177
2243
|
db.transaction(() => {
|
|
2178
2244
|
db.query(`
|
|
2179
2245
|
INSERT INTO invoices (
|
|
@@ -2206,12 +2272,14 @@ function getInvoiceById(db, id) {
|
|
|
2206
2272
|
}
|
|
2207
2273
|
function listInvoices(db, options = {}) {
|
|
2208
2274
|
const { limit, offset } = normalizePagination(options, { limit: 50 });
|
|
2209
|
-
if (options.status) {
|
|
2275
|
+
if (options.status !== undefined) {
|
|
2276
|
+
assertInvoiceStatus(options.status);
|
|
2210
2277
|
return db.query("SELECT * FROM invoices WHERE status = ?1 ORDER BY issued_at DESC, created_at DESC LIMIT ?2 OFFSET ?3").all(options.status, limit, offset).map(mapInvoiceRow);
|
|
2211
2278
|
}
|
|
2212
2279
|
return db.query("SELECT * FROM invoices ORDER BY issued_at DESC, created_at DESC LIMIT ?1 OFFSET ?2").all(limit, offset).map(mapInvoiceRow);
|
|
2213
2280
|
}
|
|
2214
2281
|
function updateInvoiceStatus(db, id, status) {
|
|
2282
|
+
assertInvoiceStatus(status);
|
|
2215
2283
|
db.query("UPDATE invoices SET status = ?2, updated_at = datetime('now') WHERE id = ?1").run(id, status);
|
|
2216
2284
|
const row = db.query("SELECT * FROM invoices WHERE id = ?1").get(id);
|
|
2217
2285
|
return row ? mapInvoiceRow(row) : null;
|
|
@@ -2219,7 +2287,8 @@ function updateInvoiceStatus(db, id, status) {
|
|
|
2219
2287
|
function searchInvoices(db, query, options = {}) {
|
|
2220
2288
|
const { limit, offset } = normalizePagination(options, { limit: 20 });
|
|
2221
2289
|
const escapedPhrase = `"${query.replace(/"/g, '""')}"`;
|
|
2222
|
-
if (options.status) {
|
|
2290
|
+
if (options.status !== undefined) {
|
|
2291
|
+
assertInvoiceStatus(options.status);
|
|
2223
2292
|
return db.query(`
|
|
2224
2293
|
SELECT i.*
|
|
2225
2294
|
FROM invoices_fts f
|
|
@@ -2241,7 +2310,10 @@ LIMIT ?2
|
|
|
2241
2310
|
OFFSET ?3
|
|
2242
2311
|
`).all(escapedPhrase, limit, offset).map(mapInvoiceRow);
|
|
2243
2312
|
}
|
|
2244
|
-
var
|
|
2313
|
+
var INVOICE_STATUSES;
|
|
2314
|
+
var init_invoices = __esm(() => {
|
|
2315
|
+
INVOICE_STATUSES = new Set(["draft", "sent", "partially_paid", "paid", "void", "overdue"]);
|
|
2316
|
+
});
|
|
2245
2317
|
|
|
2246
2318
|
// src/db/database.ts
|
|
2247
2319
|
import { mkdirSync } from "fs";
|
|
@@ -2410,7 +2482,7 @@ function listAgents(db) {
|
|
|
2410
2482
|
}
|
|
2411
2483
|
|
|
2412
2484
|
// src/lib/version.ts
|
|
2413
|
-
var VERSION = "0.
|
|
2485
|
+
var VERSION = "0.1.4";
|
|
2414
2486
|
|
|
2415
2487
|
// src/cli/index.ts
|
|
2416
2488
|
init_database();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"invoices.d.ts","sourceRoot":"","sources":["../../src/db/invoices.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAG3C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,gBAAgB,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAC1E,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACrD,KAAK,EAAE,iBAAiB,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,mBAAmB,EAAE,CAAC;CAC9B;
|
|
1
|
+
{"version":3,"file":"invoices.d.ts","sourceRoot":"","sources":["../../src/db/invoices.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/lib/version.ts
|
|
3
|
-
var VERSION = "0.
|
|
3
|
+
var VERSION = "0.1.4";
|
|
4
4
|
// src/db/database.ts
|
|
5
5
|
import { mkdirSync } from "fs";
|
|
6
6
|
import { dirname, join } from "path";
|
|
@@ -205,17 +205,89 @@ function migrateDatabase(options = {}) {
|
|
|
205
205
|
}
|
|
206
206
|
// src/db/invoices.ts
|
|
207
207
|
import { randomUUID } from "crypto";
|
|
208
|
+
var INVOICE_STATUSES = new Set(["draft", "sent", "partially_paid", "paid", "void", "overdue"]);
|
|
208
209
|
function normalizePagination(options, defaults) {
|
|
209
210
|
const limit = Number.isFinite(options.limit) ? Math.max(Math.trunc(options.limit), 0) : defaults.limit;
|
|
210
211
|
const offset = Number.isFinite(options.offset) ? Math.max(Math.trunc(options.offset), 0) : 0;
|
|
211
212
|
return { limit, offset };
|
|
212
213
|
}
|
|
213
|
-
function
|
|
214
|
+
function assertInvoiceStatus(status) {
|
|
215
|
+
if (!INVOICE_STATUSES.has(status)) {
|
|
216
|
+
throw new RangeError(`Invalid invoice status: ${String(status)}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function assertSafeCents(value, field) {
|
|
220
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
221
|
+
throw new RangeError(`${field} must be a safe nonnegative integer`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function normalizeInvoiceLine(line, index) {
|
|
225
|
+
const prefix = `Invoice line ${index + 1}`;
|
|
226
|
+
if (!line || typeof line !== "object") {
|
|
227
|
+
throw new TypeError(`${prefix} must be an object`);
|
|
228
|
+
}
|
|
229
|
+
const id = line.id;
|
|
230
|
+
const description = line.description;
|
|
231
|
+
const quantity = line.quantity;
|
|
232
|
+
const unitPriceCents = line.unitPriceCents;
|
|
233
|
+
const taxRateBasisPoints = line.taxRateBasisPoints;
|
|
234
|
+
if (id !== undefined && typeof id !== "string") {
|
|
235
|
+
throw new TypeError(`${prefix} id must be a string`);
|
|
236
|
+
}
|
|
237
|
+
if (typeof description !== "string" || description.trim().length === 0) {
|
|
238
|
+
throw new TypeError(`${prefix} description must not be blank`);
|
|
239
|
+
}
|
|
240
|
+
if (!Number.isFinite(quantity) || quantity <= 0) {
|
|
241
|
+
throw new RangeError(`${prefix} quantity must be a finite positive number`);
|
|
242
|
+
}
|
|
243
|
+
if (!Number.isSafeInteger(unitPriceCents) || unitPriceCents < 0) {
|
|
244
|
+
throw new RangeError(`${prefix} unitPriceCents must be a safe nonnegative integer`);
|
|
245
|
+
}
|
|
246
|
+
if (taxRateBasisPoints !== undefined && (!Number.isSafeInteger(taxRateBasisPoints) || taxRateBasisPoints < 0)) {
|
|
247
|
+
throw new RangeError(`${prefix} taxRateBasisPoints must be a safe nonnegative integer`);
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
id,
|
|
251
|
+
description,
|
|
252
|
+
quantity,
|
|
253
|
+
unitPriceCents,
|
|
254
|
+
taxRateBasisPoints
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function toLineTotalCents(line, index) {
|
|
214
258
|
const subtotal = Math.round(line.quantity * line.unitPriceCents);
|
|
215
259
|
const taxRate = line.taxRateBasisPoints ?? 0;
|
|
216
260
|
const tax = Math.round(subtotal * taxRate / 1e4);
|
|
261
|
+
const prefix = `Invoice line ${index + 1}`;
|
|
262
|
+
assertSafeCents(subtotal, `${prefix} lineTotalCents`);
|
|
263
|
+
assertSafeCents(tax, `${prefix} lineTaxCents`);
|
|
217
264
|
return { lineTotalCents: subtotal, lineTaxCents: tax };
|
|
218
265
|
}
|
|
266
|
+
function prepareInvoiceLines(lines) {
|
|
267
|
+
if (!Array.isArray(lines)) {
|
|
268
|
+
throw new RangeError("Invoice must include at least one line");
|
|
269
|
+
}
|
|
270
|
+
const lineCount = lines.length;
|
|
271
|
+
if (!Number.isSafeInteger(lineCount) || lineCount <= 0) {
|
|
272
|
+
throw new RangeError("Invoice must include at least one line");
|
|
273
|
+
}
|
|
274
|
+
const preparedLines = [];
|
|
275
|
+
for (let index = 0;index < lineCount; index += 1) {
|
|
276
|
+
const line = normalizeInvoiceLine(lines[index], index);
|
|
277
|
+
const { lineTotalCents, lineTaxCents } = toLineTotalCents(line, index);
|
|
278
|
+
preparedLines.push({
|
|
279
|
+
id: line.id ?? randomUUID(),
|
|
280
|
+
position: index,
|
|
281
|
+
description: line.description,
|
|
282
|
+
quantity: line.quantity,
|
|
283
|
+
unitPriceCents: line.unitPriceCents,
|
|
284
|
+
taxRateBasisPoints: line.taxRateBasisPoints ?? 0,
|
|
285
|
+
lineTotalCents,
|
|
286
|
+
lineTaxCents
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return preparedLines;
|
|
290
|
+
}
|
|
219
291
|
function mapPartyRow(row) {
|
|
220
292
|
return {
|
|
221
293
|
id: row.id,
|
|
@@ -292,23 +364,18 @@ function listParties(db, kind) {
|
|
|
292
364
|
return db.query("SELECT * FROM parties ORDER BY legal_name ASC").all().map(mapPartyRow);
|
|
293
365
|
}
|
|
294
366
|
function createInvoice(db, input) {
|
|
367
|
+
const preparedLines = prepareInvoiceLines(input.lines);
|
|
295
368
|
const invoiceId = input.id ?? randomUUID();
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
taxRateBasisPoints: line.taxRateBasisPoints ?? 0,
|
|
305
|
-
lineTotalCents,
|
|
306
|
-
lineTaxCents
|
|
307
|
-
};
|
|
308
|
-
});
|
|
309
|
-
const subtotalCents = preparedLines.reduce((sum, line) => sum + line.lineTotalCents, 0);
|
|
310
|
-
const taxCents = preparedLines.reduce((sum, line) => sum + line.lineTaxCents, 0);
|
|
369
|
+
let subtotalCents = 0;
|
|
370
|
+
let taxCents = 0;
|
|
371
|
+
for (const line of preparedLines) {
|
|
372
|
+
subtotalCents += line.lineTotalCents;
|
|
373
|
+
assertSafeCents(subtotalCents, "subtotalCents");
|
|
374
|
+
taxCents += line.lineTaxCents;
|
|
375
|
+
assertSafeCents(taxCents, "taxCents");
|
|
376
|
+
}
|
|
311
377
|
const totalCents = subtotalCents + taxCents;
|
|
378
|
+
assertSafeCents(totalCents, "totalCents");
|
|
312
379
|
db.transaction(() => {
|
|
313
380
|
db.query(`
|
|
314
381
|
INSERT INTO invoices (
|
|
@@ -341,12 +408,14 @@ function getInvoiceById(db, id) {
|
|
|
341
408
|
}
|
|
342
409
|
function listInvoices(db, options = {}) {
|
|
343
410
|
const { limit, offset } = normalizePagination(options, { limit: 50 });
|
|
344
|
-
if (options.status) {
|
|
411
|
+
if (options.status !== undefined) {
|
|
412
|
+
assertInvoiceStatus(options.status);
|
|
345
413
|
return db.query("SELECT * FROM invoices WHERE status = ?1 ORDER BY issued_at DESC, created_at DESC LIMIT ?2 OFFSET ?3").all(options.status, limit, offset).map(mapInvoiceRow);
|
|
346
414
|
}
|
|
347
415
|
return db.query("SELECT * FROM invoices ORDER BY issued_at DESC, created_at DESC LIMIT ?1 OFFSET ?2").all(limit, offset).map(mapInvoiceRow);
|
|
348
416
|
}
|
|
349
417
|
function updateInvoiceStatus(db, id, status) {
|
|
418
|
+
assertInvoiceStatus(status);
|
|
350
419
|
db.query("UPDATE invoices SET status = ?2, updated_at = datetime('now') WHERE id = ?1").run(id, status);
|
|
351
420
|
const row = db.query("SELECT * FROM invoices WHERE id = ?1").get(id);
|
|
352
421
|
return row ? mapInvoiceRow(row) : null;
|
|
@@ -371,7 +440,8 @@ FROM invoices i;
|
|
|
371
440
|
function searchInvoices(db, query, options = {}) {
|
|
372
441
|
const { limit, offset } = normalizePagination(options, { limit: 20 });
|
|
373
442
|
const escapedPhrase = `"${query.replace(/"/g, '""')}"`;
|
|
374
|
-
if (options.status) {
|
|
443
|
+
if (options.status !== undefined) {
|
|
444
|
+
assertInvoiceStatus(options.status);
|
|
375
445
|
return db.query(`
|
|
376
446
|
SELECT i.*
|
|
377
447
|
FROM invoices_fts f
|
package/dist/lib/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.1.4";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/mcp/index.js
CHANGED
|
@@ -4201,17 +4201,89 @@ function migrateDatabase(options = {}) {
|
|
|
4201
4201
|
|
|
4202
4202
|
// src/db/invoices.ts
|
|
4203
4203
|
import { randomUUID } from "crypto";
|
|
4204
|
+
var INVOICE_STATUSES = new Set(["draft", "sent", "partially_paid", "paid", "void", "overdue"]);
|
|
4204
4205
|
function normalizePagination(options, defaults) {
|
|
4205
4206
|
const limit = Number.isFinite(options.limit) ? Math.max(Math.trunc(options.limit), 0) : defaults.limit;
|
|
4206
4207
|
const offset = Number.isFinite(options.offset) ? Math.max(Math.trunc(options.offset), 0) : 0;
|
|
4207
4208
|
return { limit, offset };
|
|
4208
4209
|
}
|
|
4209
|
-
function
|
|
4210
|
+
function assertInvoiceStatus(status) {
|
|
4211
|
+
if (!INVOICE_STATUSES.has(status)) {
|
|
4212
|
+
throw new RangeError(`Invalid invoice status: ${String(status)}`);
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
function assertSafeCents(value, field) {
|
|
4216
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
4217
|
+
throw new RangeError(`${field} must be a safe nonnegative integer`);
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
function normalizeInvoiceLine(line, index) {
|
|
4221
|
+
const prefix = `Invoice line ${index + 1}`;
|
|
4222
|
+
if (!line || typeof line !== "object") {
|
|
4223
|
+
throw new TypeError(`${prefix} must be an object`);
|
|
4224
|
+
}
|
|
4225
|
+
const id = line.id;
|
|
4226
|
+
const description = line.description;
|
|
4227
|
+
const quantity = line.quantity;
|
|
4228
|
+
const unitPriceCents = line.unitPriceCents;
|
|
4229
|
+
const taxRateBasisPoints = line.taxRateBasisPoints;
|
|
4230
|
+
if (id !== undefined && typeof id !== "string") {
|
|
4231
|
+
throw new TypeError(`${prefix} id must be a string`);
|
|
4232
|
+
}
|
|
4233
|
+
if (typeof description !== "string" || description.trim().length === 0) {
|
|
4234
|
+
throw new TypeError(`${prefix} description must not be blank`);
|
|
4235
|
+
}
|
|
4236
|
+
if (!Number.isFinite(quantity) || quantity <= 0) {
|
|
4237
|
+
throw new RangeError(`${prefix} quantity must be a finite positive number`);
|
|
4238
|
+
}
|
|
4239
|
+
if (!Number.isSafeInteger(unitPriceCents) || unitPriceCents < 0) {
|
|
4240
|
+
throw new RangeError(`${prefix} unitPriceCents must be a safe nonnegative integer`);
|
|
4241
|
+
}
|
|
4242
|
+
if (taxRateBasisPoints !== undefined && (!Number.isSafeInteger(taxRateBasisPoints) || taxRateBasisPoints < 0)) {
|
|
4243
|
+
throw new RangeError(`${prefix} taxRateBasisPoints must be a safe nonnegative integer`);
|
|
4244
|
+
}
|
|
4245
|
+
return {
|
|
4246
|
+
id,
|
|
4247
|
+
description,
|
|
4248
|
+
quantity,
|
|
4249
|
+
unitPriceCents,
|
|
4250
|
+
taxRateBasisPoints
|
|
4251
|
+
};
|
|
4252
|
+
}
|
|
4253
|
+
function toLineTotalCents(line, index) {
|
|
4210
4254
|
const subtotal = Math.round(line.quantity * line.unitPriceCents);
|
|
4211
4255
|
const taxRate = line.taxRateBasisPoints ?? 0;
|
|
4212
4256
|
const tax = Math.round(subtotal * taxRate / 1e4);
|
|
4257
|
+
const prefix = `Invoice line ${index + 1}`;
|
|
4258
|
+
assertSafeCents(subtotal, `${prefix} lineTotalCents`);
|
|
4259
|
+
assertSafeCents(tax, `${prefix} lineTaxCents`);
|
|
4213
4260
|
return { lineTotalCents: subtotal, lineTaxCents: tax };
|
|
4214
4261
|
}
|
|
4262
|
+
function prepareInvoiceLines(lines) {
|
|
4263
|
+
if (!Array.isArray(lines)) {
|
|
4264
|
+
throw new RangeError("Invoice must include at least one line");
|
|
4265
|
+
}
|
|
4266
|
+
const lineCount = lines.length;
|
|
4267
|
+
if (!Number.isSafeInteger(lineCount) || lineCount <= 0) {
|
|
4268
|
+
throw new RangeError("Invoice must include at least one line");
|
|
4269
|
+
}
|
|
4270
|
+
const preparedLines = [];
|
|
4271
|
+
for (let index = 0;index < lineCount; index += 1) {
|
|
4272
|
+
const line = normalizeInvoiceLine(lines[index], index);
|
|
4273
|
+
const { lineTotalCents, lineTaxCents } = toLineTotalCents(line, index);
|
|
4274
|
+
preparedLines.push({
|
|
4275
|
+
id: line.id ?? randomUUID(),
|
|
4276
|
+
position: index,
|
|
4277
|
+
description: line.description,
|
|
4278
|
+
quantity: line.quantity,
|
|
4279
|
+
unitPriceCents: line.unitPriceCents,
|
|
4280
|
+
taxRateBasisPoints: line.taxRateBasisPoints ?? 0,
|
|
4281
|
+
lineTotalCents,
|
|
4282
|
+
lineTaxCents
|
|
4283
|
+
});
|
|
4284
|
+
}
|
|
4285
|
+
return preparedLines;
|
|
4286
|
+
}
|
|
4215
4287
|
function mapPartyRow(row) {
|
|
4216
4288
|
return {
|
|
4217
4289
|
id: row.id,
|
|
@@ -4275,23 +4347,18 @@ function listParties(db, kind) {
|
|
|
4275
4347
|
return db.query("SELECT * FROM parties ORDER BY legal_name ASC").all().map(mapPartyRow);
|
|
4276
4348
|
}
|
|
4277
4349
|
function createInvoice(db, input) {
|
|
4350
|
+
const preparedLines = prepareInvoiceLines(input.lines);
|
|
4278
4351
|
const invoiceId = input.id ?? randomUUID();
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
taxRateBasisPoints: line.taxRateBasisPoints ?? 0,
|
|
4288
|
-
lineTotalCents,
|
|
4289
|
-
lineTaxCents
|
|
4290
|
-
};
|
|
4291
|
-
});
|
|
4292
|
-
const subtotalCents = preparedLines.reduce((sum, line) => sum + line.lineTotalCents, 0);
|
|
4293
|
-
const taxCents = preparedLines.reduce((sum, line) => sum + line.lineTaxCents, 0);
|
|
4352
|
+
let subtotalCents = 0;
|
|
4353
|
+
let taxCents = 0;
|
|
4354
|
+
for (const line of preparedLines) {
|
|
4355
|
+
subtotalCents += line.lineTotalCents;
|
|
4356
|
+
assertSafeCents(subtotalCents, "subtotalCents");
|
|
4357
|
+
taxCents += line.lineTaxCents;
|
|
4358
|
+
assertSafeCents(taxCents, "taxCents");
|
|
4359
|
+
}
|
|
4294
4360
|
const totalCents = subtotalCents + taxCents;
|
|
4361
|
+
assertSafeCents(totalCents, "totalCents");
|
|
4295
4362
|
db.transaction(() => {
|
|
4296
4363
|
db.query(`
|
|
4297
4364
|
INSERT INTO invoices (
|
|
@@ -4324,12 +4391,14 @@ function getInvoiceById(db, id) {
|
|
|
4324
4391
|
}
|
|
4325
4392
|
function listInvoices(db, options = {}) {
|
|
4326
4393
|
const { limit, offset } = normalizePagination(options, { limit: 50 });
|
|
4327
|
-
if (options.status) {
|
|
4394
|
+
if (options.status !== undefined) {
|
|
4395
|
+
assertInvoiceStatus(options.status);
|
|
4328
4396
|
return db.query("SELECT * FROM invoices WHERE status = ?1 ORDER BY issued_at DESC, created_at DESC LIMIT ?2 OFFSET ?3").all(options.status, limit, offset).map(mapInvoiceRow);
|
|
4329
4397
|
}
|
|
4330
4398
|
return db.query("SELECT * FROM invoices ORDER BY issued_at DESC, created_at DESC LIMIT ?1 OFFSET ?2").all(limit, offset).map(mapInvoiceRow);
|
|
4331
4399
|
}
|
|
4332
4400
|
function updateInvoiceStatus(db, id, status) {
|
|
4401
|
+
assertInvoiceStatus(status);
|
|
4333
4402
|
db.query("UPDATE invoices SET status = ?2, updated_at = datetime('now') WHERE id = ?1").run(id, status);
|
|
4334
4403
|
const row = db.query("SELECT * FROM invoices WHERE id = ?1").get(id);
|
|
4335
4404
|
return row ? mapInvoiceRow(row) : null;
|
|
@@ -4341,7 +4410,8 @@ function deleteInvoice(db, id) {
|
|
|
4341
4410
|
function searchInvoices(db, query, options = {}) {
|
|
4342
4411
|
const { limit, offset } = normalizePagination(options, { limit: 20 });
|
|
4343
4412
|
const escapedPhrase = `"${query.replace(/"/g, '""')}"`;
|
|
4344
|
-
if (options.status) {
|
|
4413
|
+
if (options.status !== undefined) {
|
|
4414
|
+
assertInvoiceStatus(options.status);
|
|
4345
4415
|
return db.query(`
|
|
4346
4416
|
SELECT i.*
|
|
4347
4417
|
FROM invoices_fts f
|
package/dist/server/index.js
CHANGED
|
@@ -4009,17 +4009,89 @@ function openInvoiceDatabase(options = {}) {
|
|
|
4009
4009
|
|
|
4010
4010
|
// src/db/invoices.ts
|
|
4011
4011
|
import { randomUUID } from "crypto";
|
|
4012
|
+
var INVOICE_STATUSES = new Set(["draft", "sent", "partially_paid", "paid", "void", "overdue"]);
|
|
4012
4013
|
function normalizePagination(options, defaults) {
|
|
4013
4014
|
const limit = Number.isFinite(options.limit) ? Math.max(Math.trunc(options.limit), 0) : defaults.limit;
|
|
4014
4015
|
const offset = Number.isFinite(options.offset) ? Math.max(Math.trunc(options.offset), 0) : 0;
|
|
4015
4016
|
return { limit, offset };
|
|
4016
4017
|
}
|
|
4017
|
-
function
|
|
4018
|
+
function assertInvoiceStatus(status) {
|
|
4019
|
+
if (!INVOICE_STATUSES.has(status)) {
|
|
4020
|
+
throw new RangeError(`Invalid invoice status: ${String(status)}`);
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
function assertSafeCents(value, field) {
|
|
4024
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
4025
|
+
throw new RangeError(`${field} must be a safe nonnegative integer`);
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
function normalizeInvoiceLine(line, index) {
|
|
4029
|
+
const prefix = `Invoice line ${index + 1}`;
|
|
4030
|
+
if (!line || typeof line !== "object") {
|
|
4031
|
+
throw new TypeError(`${prefix} must be an object`);
|
|
4032
|
+
}
|
|
4033
|
+
const id = line.id;
|
|
4034
|
+
const description = line.description;
|
|
4035
|
+
const quantity = line.quantity;
|
|
4036
|
+
const unitPriceCents = line.unitPriceCents;
|
|
4037
|
+
const taxRateBasisPoints = line.taxRateBasisPoints;
|
|
4038
|
+
if (id !== undefined && typeof id !== "string") {
|
|
4039
|
+
throw new TypeError(`${prefix} id must be a string`);
|
|
4040
|
+
}
|
|
4041
|
+
if (typeof description !== "string" || description.trim().length === 0) {
|
|
4042
|
+
throw new TypeError(`${prefix} description must not be blank`);
|
|
4043
|
+
}
|
|
4044
|
+
if (!Number.isFinite(quantity) || quantity <= 0) {
|
|
4045
|
+
throw new RangeError(`${prefix} quantity must be a finite positive number`);
|
|
4046
|
+
}
|
|
4047
|
+
if (!Number.isSafeInteger(unitPriceCents) || unitPriceCents < 0) {
|
|
4048
|
+
throw new RangeError(`${prefix} unitPriceCents must be a safe nonnegative integer`);
|
|
4049
|
+
}
|
|
4050
|
+
if (taxRateBasisPoints !== undefined && (!Number.isSafeInteger(taxRateBasisPoints) || taxRateBasisPoints < 0)) {
|
|
4051
|
+
throw new RangeError(`${prefix} taxRateBasisPoints must be a safe nonnegative integer`);
|
|
4052
|
+
}
|
|
4053
|
+
return {
|
|
4054
|
+
id,
|
|
4055
|
+
description,
|
|
4056
|
+
quantity,
|
|
4057
|
+
unitPriceCents,
|
|
4058
|
+
taxRateBasisPoints
|
|
4059
|
+
};
|
|
4060
|
+
}
|
|
4061
|
+
function toLineTotalCents(line, index) {
|
|
4018
4062
|
const subtotal = Math.round(line.quantity * line.unitPriceCents);
|
|
4019
4063
|
const taxRate = line.taxRateBasisPoints ?? 0;
|
|
4020
4064
|
const tax = Math.round(subtotal * taxRate / 1e4);
|
|
4065
|
+
const prefix = `Invoice line ${index + 1}`;
|
|
4066
|
+
assertSafeCents(subtotal, `${prefix} lineTotalCents`);
|
|
4067
|
+
assertSafeCents(tax, `${prefix} lineTaxCents`);
|
|
4021
4068
|
return { lineTotalCents: subtotal, lineTaxCents: tax };
|
|
4022
4069
|
}
|
|
4070
|
+
function prepareInvoiceLines(lines) {
|
|
4071
|
+
if (!Array.isArray(lines)) {
|
|
4072
|
+
throw new RangeError("Invoice must include at least one line");
|
|
4073
|
+
}
|
|
4074
|
+
const lineCount = lines.length;
|
|
4075
|
+
if (!Number.isSafeInteger(lineCount) || lineCount <= 0) {
|
|
4076
|
+
throw new RangeError("Invoice must include at least one line");
|
|
4077
|
+
}
|
|
4078
|
+
const preparedLines = [];
|
|
4079
|
+
for (let index = 0;index < lineCount; index += 1) {
|
|
4080
|
+
const line = normalizeInvoiceLine(lines[index], index);
|
|
4081
|
+
const { lineTotalCents, lineTaxCents } = toLineTotalCents(line, index);
|
|
4082
|
+
preparedLines.push({
|
|
4083
|
+
id: line.id ?? randomUUID(),
|
|
4084
|
+
position: index,
|
|
4085
|
+
description: line.description,
|
|
4086
|
+
quantity: line.quantity,
|
|
4087
|
+
unitPriceCents: line.unitPriceCents,
|
|
4088
|
+
taxRateBasisPoints: line.taxRateBasisPoints ?? 0,
|
|
4089
|
+
lineTotalCents,
|
|
4090
|
+
lineTaxCents
|
|
4091
|
+
});
|
|
4092
|
+
}
|
|
4093
|
+
return preparedLines;
|
|
4094
|
+
}
|
|
4023
4095
|
function mapPartyRow(row) {
|
|
4024
4096
|
return {
|
|
4025
4097
|
id: row.id,
|
|
@@ -4083,23 +4155,18 @@ function listParties(db, kind) {
|
|
|
4083
4155
|
return db.query("SELECT * FROM parties ORDER BY legal_name ASC").all().map(mapPartyRow);
|
|
4084
4156
|
}
|
|
4085
4157
|
function createInvoice(db, input) {
|
|
4158
|
+
const preparedLines = prepareInvoiceLines(input.lines);
|
|
4086
4159
|
const invoiceId = input.id ?? randomUUID();
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
taxRateBasisPoints: line.taxRateBasisPoints ?? 0,
|
|
4096
|
-
lineTotalCents,
|
|
4097
|
-
lineTaxCents
|
|
4098
|
-
};
|
|
4099
|
-
});
|
|
4100
|
-
const subtotalCents = preparedLines.reduce((sum, line) => sum + line.lineTotalCents, 0);
|
|
4101
|
-
const taxCents = preparedLines.reduce((sum, line) => sum + line.lineTaxCents, 0);
|
|
4160
|
+
let subtotalCents = 0;
|
|
4161
|
+
let taxCents = 0;
|
|
4162
|
+
for (const line of preparedLines) {
|
|
4163
|
+
subtotalCents += line.lineTotalCents;
|
|
4164
|
+
assertSafeCents(subtotalCents, "subtotalCents");
|
|
4165
|
+
taxCents += line.lineTaxCents;
|
|
4166
|
+
assertSafeCents(taxCents, "taxCents");
|
|
4167
|
+
}
|
|
4102
4168
|
const totalCents = subtotalCents + taxCents;
|
|
4169
|
+
assertSafeCents(totalCents, "totalCents");
|
|
4103
4170
|
db.transaction(() => {
|
|
4104
4171
|
db.query(`
|
|
4105
4172
|
INSERT INTO invoices (
|
|
@@ -4132,12 +4199,14 @@ function getInvoiceById(db, id) {
|
|
|
4132
4199
|
}
|
|
4133
4200
|
function listInvoices(db, options = {}) {
|
|
4134
4201
|
const { limit, offset } = normalizePagination(options, { limit: 50 });
|
|
4135
|
-
if (options.status) {
|
|
4202
|
+
if (options.status !== undefined) {
|
|
4203
|
+
assertInvoiceStatus(options.status);
|
|
4136
4204
|
return db.query("SELECT * FROM invoices WHERE status = ?1 ORDER BY issued_at DESC, created_at DESC LIMIT ?2 OFFSET ?3").all(options.status, limit, offset).map(mapInvoiceRow);
|
|
4137
4205
|
}
|
|
4138
4206
|
return db.query("SELECT * FROM invoices ORDER BY issued_at DESC, created_at DESC LIMIT ?1 OFFSET ?2").all(limit, offset).map(mapInvoiceRow);
|
|
4139
4207
|
}
|
|
4140
4208
|
function updateInvoiceStatus(db, id, status) {
|
|
4209
|
+
assertInvoiceStatus(status);
|
|
4141
4210
|
db.query("UPDATE invoices SET status = ?2, updated_at = datetime('now') WHERE id = ?1").run(id, status);
|
|
4142
4211
|
const row = db.query("SELECT * FROM invoices WHERE id = ?1").get(id);
|
|
4143
4212
|
return row ? mapInvoiceRow(row) : null;
|
|
@@ -4145,7 +4214,8 @@ function updateInvoiceStatus(db, id, status) {
|
|
|
4145
4214
|
function searchInvoices(db, query, options = {}) {
|
|
4146
4215
|
const { limit, offset } = normalizePagination(options, { limit: 20 });
|
|
4147
4216
|
const escapedPhrase = `"${query.replace(/"/g, '""')}"`;
|
|
4148
|
-
if (options.status) {
|
|
4217
|
+
if (options.status !== undefined) {
|
|
4218
|
+
assertInvoiceStatus(options.status);
|
|
4149
4219
|
return db.query(`
|
|
4150
4220
|
SELECT i.*
|
|
4151
4221
|
FROM invoices_fts f
|