@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 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 toLineTotalCents(line) {
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
- const preparedLines = input.lines.map((line, index) => {
2162
- const { lineTotalCents, lineTaxCents } = toLineTotalCents(line);
2163
- return {
2164
- id: line.id ?? randomUUID(),
2165
- position: index,
2166
- description: line.description,
2167
- quantity: line.quantity,
2168
- unitPriceCents: line.unitPriceCents,
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 init_invoices = () => {};
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.0.1";
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":"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"}
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.0.1";
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 toLineTotalCents(line) {
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
- const preparedLines = input.lines.map((line, index) => {
297
- const { lineTotalCents, lineTaxCents } = toLineTotalCents(line);
298
- return {
299
- id: line.id ?? randomUUID(),
300
- position: index,
301
- description: line.description,
302
- quantity: line.quantity,
303
- unitPriceCents: line.unitPriceCents,
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
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.0.1";
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 toLineTotalCents(line) {
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
- const preparedLines = input.lines.map((line, index) => {
4280
- const { lineTotalCents, lineTaxCents } = toLineTotalCents(line);
4281
- return {
4282
- id: line.id ?? randomUUID(),
4283
- position: index,
4284
- description: line.description,
4285
- quantity: line.quantity,
4286
- unitPriceCents: line.unitPriceCents,
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
@@ -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 toLineTotalCents(line) {
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
- const preparedLines = input.lines.map((line, index) => {
4088
- const { lineTotalCents, lineTaxCents } = toLineTotalCents(line);
4089
- return {
4090
- id: line.id ?? randomUUID(),
4091
- position: index,
4092
- description: line.description,
4093
- quantity: line.quantity,
4094
- unitPriceCents: line.unitPriceCents,
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/invoices",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Invoice generation and management for AI agents - CLI + MCP server + REST API + dashboard + SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",