@hasna/invoices 0.1.2 → 0.1.3

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,78 @@ 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 assertSafeCents(value, field) {
2092
+ if (!Number.isSafeInteger(value) || value < 0) {
2093
+ throw new RangeError(`${field} must be a safe nonnegative integer`);
2094
+ }
2095
+ }
2096
+ function normalizeInvoiceLine(line, index) {
2097
+ const prefix = `Invoice line ${index + 1}`;
2098
+ if (!line || typeof line !== "object") {
2099
+ throw new TypeError(`${prefix} must be an object`);
2100
+ }
2101
+ const id = line.id;
2102
+ const description = line.description;
2103
+ const quantity = line.quantity;
2104
+ const unitPriceCents = line.unitPriceCents;
2105
+ const taxRateBasisPoints = line.taxRateBasisPoints;
2106
+ if (id !== undefined && typeof id !== "string") {
2107
+ throw new TypeError(`${prefix} id must be a string`);
2108
+ }
2109
+ if (typeof description !== "string" || description.trim().length === 0) {
2110
+ throw new TypeError(`${prefix} description must not be blank`);
2111
+ }
2112
+ if (!Number.isFinite(quantity) || quantity <= 0) {
2113
+ throw new RangeError(`${prefix} quantity must be a finite positive number`);
2114
+ }
2115
+ if (!Number.isSafeInteger(unitPriceCents) || unitPriceCents < 0) {
2116
+ throw new RangeError(`${prefix} unitPriceCents must be a safe nonnegative integer`);
2117
+ }
2118
+ if (taxRateBasisPoints !== undefined && (!Number.isSafeInteger(taxRateBasisPoints) || taxRateBasisPoints < 0)) {
2119
+ throw new RangeError(`${prefix} taxRateBasisPoints must be a safe nonnegative integer`);
2120
+ }
2121
+ return {
2122
+ id,
2123
+ description,
2124
+ quantity,
2125
+ unitPriceCents,
2126
+ taxRateBasisPoints
2127
+ };
2128
+ }
2129
+ function toLineTotalCents(line, index) {
2092
2130
  const subtotal = Math.round(line.quantity * line.unitPriceCents);
2093
2131
  const taxRate = line.taxRateBasisPoints ?? 0;
2094
2132
  const tax = Math.round(subtotal * taxRate / 1e4);
2133
+ const prefix = `Invoice line ${index + 1}`;
2134
+ assertSafeCents(subtotal, `${prefix} lineTotalCents`);
2135
+ assertSafeCents(tax, `${prefix} lineTaxCents`);
2095
2136
  return { lineTotalCents: subtotal, lineTaxCents: tax };
2096
2137
  }
2138
+ function prepareInvoiceLines(lines) {
2139
+ if (!Array.isArray(lines)) {
2140
+ throw new RangeError("Invoice must include at least one line");
2141
+ }
2142
+ const lineCount = lines.length;
2143
+ if (!Number.isSafeInteger(lineCount) || lineCount <= 0) {
2144
+ throw new RangeError("Invoice must include at least one line");
2145
+ }
2146
+ const preparedLines = [];
2147
+ for (let index = 0;index < lineCount; index += 1) {
2148
+ const line = normalizeInvoiceLine(lines[index], index);
2149
+ const { lineTotalCents, lineTaxCents } = toLineTotalCents(line, index);
2150
+ preparedLines.push({
2151
+ id: line.id ?? randomUUID(),
2152
+ position: index,
2153
+ description: line.description,
2154
+ quantity: line.quantity,
2155
+ unitPriceCents: line.unitPriceCents,
2156
+ taxRateBasisPoints: line.taxRateBasisPoints ?? 0,
2157
+ lineTotalCents,
2158
+ lineTaxCents
2159
+ });
2160
+ }
2161
+ return preparedLines;
2162
+ }
2097
2163
  function mapPartyRow(row) {
2098
2164
  return {
2099
2165
  id: row.id,
@@ -2157,23 +2223,18 @@ function listParties(db, kind) {
2157
2223
  return db.query("SELECT * FROM parties ORDER BY legal_name ASC").all().map(mapPartyRow);
2158
2224
  }
2159
2225
  function createInvoice(db, input) {
2226
+ const preparedLines = prepareInvoiceLines(input.lines);
2160
2227
  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);
2228
+ let subtotalCents = 0;
2229
+ let taxCents = 0;
2230
+ for (const line of preparedLines) {
2231
+ subtotalCents += line.lineTotalCents;
2232
+ assertSafeCents(subtotalCents, "subtotalCents");
2233
+ taxCents += line.lineTaxCents;
2234
+ assertSafeCents(taxCents, "taxCents");
2235
+ }
2176
2236
  const totalCents = subtotalCents + taxCents;
2237
+ assertSafeCents(totalCents, "totalCents");
2177
2238
  db.transaction(() => {
2178
2239
  db.query(`
2179
2240
  INSERT INTO invoices (
@@ -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
@@ -210,12 +210,78 @@ function normalizePagination(options, defaults) {
210
210
  const offset = Number.isFinite(options.offset) ? Math.max(Math.trunc(options.offset), 0) : 0;
211
211
  return { limit, offset };
212
212
  }
213
- function toLineTotalCents(line) {
213
+ function assertSafeCents(value, field) {
214
+ if (!Number.isSafeInteger(value) || value < 0) {
215
+ throw new RangeError(`${field} must be a safe nonnegative integer`);
216
+ }
217
+ }
218
+ function normalizeInvoiceLine(line, index) {
219
+ const prefix = `Invoice line ${index + 1}`;
220
+ if (!line || typeof line !== "object") {
221
+ throw new TypeError(`${prefix} must be an object`);
222
+ }
223
+ const id = line.id;
224
+ const description = line.description;
225
+ const quantity = line.quantity;
226
+ const unitPriceCents = line.unitPriceCents;
227
+ const taxRateBasisPoints = line.taxRateBasisPoints;
228
+ if (id !== undefined && typeof id !== "string") {
229
+ throw new TypeError(`${prefix} id must be a string`);
230
+ }
231
+ if (typeof description !== "string" || description.trim().length === 0) {
232
+ throw new TypeError(`${prefix} description must not be blank`);
233
+ }
234
+ if (!Number.isFinite(quantity) || quantity <= 0) {
235
+ throw new RangeError(`${prefix} quantity must be a finite positive number`);
236
+ }
237
+ if (!Number.isSafeInteger(unitPriceCents) || unitPriceCents < 0) {
238
+ throw new RangeError(`${prefix} unitPriceCents must be a safe nonnegative integer`);
239
+ }
240
+ if (taxRateBasisPoints !== undefined && (!Number.isSafeInteger(taxRateBasisPoints) || taxRateBasisPoints < 0)) {
241
+ throw new RangeError(`${prefix} taxRateBasisPoints must be a safe nonnegative integer`);
242
+ }
243
+ return {
244
+ id,
245
+ description,
246
+ quantity,
247
+ unitPriceCents,
248
+ taxRateBasisPoints
249
+ };
250
+ }
251
+ function toLineTotalCents(line, index) {
214
252
  const subtotal = Math.round(line.quantity * line.unitPriceCents);
215
253
  const taxRate = line.taxRateBasisPoints ?? 0;
216
254
  const tax = Math.round(subtotal * taxRate / 1e4);
255
+ const prefix = `Invoice line ${index + 1}`;
256
+ assertSafeCents(subtotal, `${prefix} lineTotalCents`);
257
+ assertSafeCents(tax, `${prefix} lineTaxCents`);
217
258
  return { lineTotalCents: subtotal, lineTaxCents: tax };
218
259
  }
260
+ function prepareInvoiceLines(lines) {
261
+ if (!Array.isArray(lines)) {
262
+ throw new RangeError("Invoice must include at least one line");
263
+ }
264
+ const lineCount = lines.length;
265
+ if (!Number.isSafeInteger(lineCount) || lineCount <= 0) {
266
+ throw new RangeError("Invoice must include at least one line");
267
+ }
268
+ const preparedLines = [];
269
+ for (let index = 0;index < lineCount; index += 1) {
270
+ const line = normalizeInvoiceLine(lines[index], index);
271
+ const { lineTotalCents, lineTaxCents } = toLineTotalCents(line, index);
272
+ preparedLines.push({
273
+ id: line.id ?? randomUUID(),
274
+ position: index,
275
+ description: line.description,
276
+ quantity: line.quantity,
277
+ unitPriceCents: line.unitPriceCents,
278
+ taxRateBasisPoints: line.taxRateBasisPoints ?? 0,
279
+ lineTotalCents,
280
+ lineTaxCents
281
+ });
282
+ }
283
+ return preparedLines;
284
+ }
219
285
  function mapPartyRow(row) {
220
286
  return {
221
287
  id: row.id,
@@ -292,23 +358,18 @@ function listParties(db, kind) {
292
358
  return db.query("SELECT * FROM parties ORDER BY legal_name ASC").all().map(mapPartyRow);
293
359
  }
294
360
  function createInvoice(db, input) {
361
+ const preparedLines = prepareInvoiceLines(input.lines);
295
362
  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);
363
+ let subtotalCents = 0;
364
+ let taxCents = 0;
365
+ for (const line of preparedLines) {
366
+ subtotalCents += line.lineTotalCents;
367
+ assertSafeCents(subtotalCents, "subtotalCents");
368
+ taxCents += line.lineTaxCents;
369
+ assertSafeCents(taxCents, "taxCents");
370
+ }
311
371
  const totalCents = subtotalCents + taxCents;
372
+ assertSafeCents(totalCents, "totalCents");
312
373
  db.transaction(() => {
313
374
  db.query(`
314
375
  INSERT INTO invoices (
package/dist/mcp/index.js CHANGED
@@ -4206,12 +4206,78 @@ function normalizePagination(options, defaults) {
4206
4206
  const offset = Number.isFinite(options.offset) ? Math.max(Math.trunc(options.offset), 0) : 0;
4207
4207
  return { limit, offset };
4208
4208
  }
4209
- function toLineTotalCents(line) {
4209
+ function assertSafeCents(value, field) {
4210
+ if (!Number.isSafeInteger(value) || value < 0) {
4211
+ throw new RangeError(`${field} must be a safe nonnegative integer`);
4212
+ }
4213
+ }
4214
+ function normalizeInvoiceLine(line, index) {
4215
+ const prefix = `Invoice line ${index + 1}`;
4216
+ if (!line || typeof line !== "object") {
4217
+ throw new TypeError(`${prefix} must be an object`);
4218
+ }
4219
+ const id = line.id;
4220
+ const description = line.description;
4221
+ const quantity = line.quantity;
4222
+ const unitPriceCents = line.unitPriceCents;
4223
+ const taxRateBasisPoints = line.taxRateBasisPoints;
4224
+ if (id !== undefined && typeof id !== "string") {
4225
+ throw new TypeError(`${prefix} id must be a string`);
4226
+ }
4227
+ if (typeof description !== "string" || description.trim().length === 0) {
4228
+ throw new TypeError(`${prefix} description must not be blank`);
4229
+ }
4230
+ if (!Number.isFinite(quantity) || quantity <= 0) {
4231
+ throw new RangeError(`${prefix} quantity must be a finite positive number`);
4232
+ }
4233
+ if (!Number.isSafeInteger(unitPriceCents) || unitPriceCents < 0) {
4234
+ throw new RangeError(`${prefix} unitPriceCents must be a safe nonnegative integer`);
4235
+ }
4236
+ if (taxRateBasisPoints !== undefined && (!Number.isSafeInteger(taxRateBasisPoints) || taxRateBasisPoints < 0)) {
4237
+ throw new RangeError(`${prefix} taxRateBasisPoints must be a safe nonnegative integer`);
4238
+ }
4239
+ return {
4240
+ id,
4241
+ description,
4242
+ quantity,
4243
+ unitPriceCents,
4244
+ taxRateBasisPoints
4245
+ };
4246
+ }
4247
+ function toLineTotalCents(line, index) {
4210
4248
  const subtotal = Math.round(line.quantity * line.unitPriceCents);
4211
4249
  const taxRate = line.taxRateBasisPoints ?? 0;
4212
4250
  const tax = Math.round(subtotal * taxRate / 1e4);
4251
+ const prefix = `Invoice line ${index + 1}`;
4252
+ assertSafeCents(subtotal, `${prefix} lineTotalCents`);
4253
+ assertSafeCents(tax, `${prefix} lineTaxCents`);
4213
4254
  return { lineTotalCents: subtotal, lineTaxCents: tax };
4214
4255
  }
4256
+ function prepareInvoiceLines(lines) {
4257
+ if (!Array.isArray(lines)) {
4258
+ throw new RangeError("Invoice must include at least one line");
4259
+ }
4260
+ const lineCount = lines.length;
4261
+ if (!Number.isSafeInteger(lineCount) || lineCount <= 0) {
4262
+ throw new RangeError("Invoice must include at least one line");
4263
+ }
4264
+ const preparedLines = [];
4265
+ for (let index = 0;index < lineCount; index += 1) {
4266
+ const line = normalizeInvoiceLine(lines[index], index);
4267
+ const { lineTotalCents, lineTaxCents } = toLineTotalCents(line, index);
4268
+ preparedLines.push({
4269
+ id: line.id ?? randomUUID(),
4270
+ position: index,
4271
+ description: line.description,
4272
+ quantity: line.quantity,
4273
+ unitPriceCents: line.unitPriceCents,
4274
+ taxRateBasisPoints: line.taxRateBasisPoints ?? 0,
4275
+ lineTotalCents,
4276
+ lineTaxCents
4277
+ });
4278
+ }
4279
+ return preparedLines;
4280
+ }
4215
4281
  function mapPartyRow(row) {
4216
4282
  return {
4217
4283
  id: row.id,
@@ -4275,23 +4341,18 @@ function listParties(db, kind) {
4275
4341
  return db.query("SELECT * FROM parties ORDER BY legal_name ASC").all().map(mapPartyRow);
4276
4342
  }
4277
4343
  function createInvoice(db, input) {
4344
+ const preparedLines = prepareInvoiceLines(input.lines);
4278
4345
  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);
4346
+ let subtotalCents = 0;
4347
+ let taxCents = 0;
4348
+ for (const line of preparedLines) {
4349
+ subtotalCents += line.lineTotalCents;
4350
+ assertSafeCents(subtotalCents, "subtotalCents");
4351
+ taxCents += line.lineTaxCents;
4352
+ assertSafeCents(taxCents, "taxCents");
4353
+ }
4294
4354
  const totalCents = subtotalCents + taxCents;
4355
+ assertSafeCents(totalCents, "totalCents");
4295
4356
  db.transaction(() => {
4296
4357
  db.query(`
4297
4358
  INSERT INTO invoices (
@@ -4014,12 +4014,78 @@ function normalizePagination(options, defaults) {
4014
4014
  const offset = Number.isFinite(options.offset) ? Math.max(Math.trunc(options.offset), 0) : 0;
4015
4015
  return { limit, offset };
4016
4016
  }
4017
- function toLineTotalCents(line) {
4017
+ function assertSafeCents(value, field) {
4018
+ if (!Number.isSafeInteger(value) || value < 0) {
4019
+ throw new RangeError(`${field} must be a safe nonnegative integer`);
4020
+ }
4021
+ }
4022
+ function normalizeInvoiceLine(line, index) {
4023
+ const prefix = `Invoice line ${index + 1}`;
4024
+ if (!line || typeof line !== "object") {
4025
+ throw new TypeError(`${prefix} must be an object`);
4026
+ }
4027
+ const id = line.id;
4028
+ const description = line.description;
4029
+ const quantity = line.quantity;
4030
+ const unitPriceCents = line.unitPriceCents;
4031
+ const taxRateBasisPoints = line.taxRateBasisPoints;
4032
+ if (id !== undefined && typeof id !== "string") {
4033
+ throw new TypeError(`${prefix} id must be a string`);
4034
+ }
4035
+ if (typeof description !== "string" || description.trim().length === 0) {
4036
+ throw new TypeError(`${prefix} description must not be blank`);
4037
+ }
4038
+ if (!Number.isFinite(quantity) || quantity <= 0) {
4039
+ throw new RangeError(`${prefix} quantity must be a finite positive number`);
4040
+ }
4041
+ if (!Number.isSafeInteger(unitPriceCents) || unitPriceCents < 0) {
4042
+ throw new RangeError(`${prefix} unitPriceCents must be a safe nonnegative integer`);
4043
+ }
4044
+ if (taxRateBasisPoints !== undefined && (!Number.isSafeInteger(taxRateBasisPoints) || taxRateBasisPoints < 0)) {
4045
+ throw new RangeError(`${prefix} taxRateBasisPoints must be a safe nonnegative integer`);
4046
+ }
4047
+ return {
4048
+ id,
4049
+ description,
4050
+ quantity,
4051
+ unitPriceCents,
4052
+ taxRateBasisPoints
4053
+ };
4054
+ }
4055
+ function toLineTotalCents(line, index) {
4018
4056
  const subtotal = Math.round(line.quantity * line.unitPriceCents);
4019
4057
  const taxRate = line.taxRateBasisPoints ?? 0;
4020
4058
  const tax = Math.round(subtotal * taxRate / 1e4);
4059
+ const prefix = `Invoice line ${index + 1}`;
4060
+ assertSafeCents(subtotal, `${prefix} lineTotalCents`);
4061
+ assertSafeCents(tax, `${prefix} lineTaxCents`);
4021
4062
  return { lineTotalCents: subtotal, lineTaxCents: tax };
4022
4063
  }
4064
+ function prepareInvoiceLines(lines) {
4065
+ if (!Array.isArray(lines)) {
4066
+ throw new RangeError("Invoice must include at least one line");
4067
+ }
4068
+ const lineCount = lines.length;
4069
+ if (!Number.isSafeInteger(lineCount) || lineCount <= 0) {
4070
+ throw new RangeError("Invoice must include at least one line");
4071
+ }
4072
+ const preparedLines = [];
4073
+ for (let index = 0;index < lineCount; index += 1) {
4074
+ const line = normalizeInvoiceLine(lines[index], index);
4075
+ const { lineTotalCents, lineTaxCents } = toLineTotalCents(line, index);
4076
+ preparedLines.push({
4077
+ id: line.id ?? randomUUID(),
4078
+ position: index,
4079
+ description: line.description,
4080
+ quantity: line.quantity,
4081
+ unitPriceCents: line.unitPriceCents,
4082
+ taxRateBasisPoints: line.taxRateBasisPoints ?? 0,
4083
+ lineTotalCents,
4084
+ lineTaxCents
4085
+ });
4086
+ }
4087
+ return preparedLines;
4088
+ }
4023
4089
  function mapPartyRow(row) {
4024
4090
  return {
4025
4091
  id: row.id,
@@ -4083,23 +4149,18 @@ function listParties(db, kind) {
4083
4149
  return db.query("SELECT * FROM parties ORDER BY legal_name ASC").all().map(mapPartyRow);
4084
4150
  }
4085
4151
  function createInvoice(db, input) {
4152
+ const preparedLines = prepareInvoiceLines(input.lines);
4086
4153
  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);
4154
+ let subtotalCents = 0;
4155
+ let taxCents = 0;
4156
+ for (const line of preparedLines) {
4157
+ subtotalCents += line.lineTotalCents;
4158
+ assertSafeCents(subtotalCents, "subtotalCents");
4159
+ taxCents += line.lineTaxCents;
4160
+ assertSafeCents(taxCents, "taxCents");
4161
+ }
4102
4162
  const totalCents = subtotalCents + taxCents;
4163
+ assertSafeCents(totalCents, "totalCents");
4103
4164
  db.transaction(() => {
4104
4165
  db.query(`
4105
4166
  INSERT INTO invoices (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/invoices",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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",