@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 +77 -16
- package/dist/db/invoices.d.ts.map +1 -1
- package/dist/index.js +77 -16
- package/dist/mcp/index.js +77 -16
- package/dist/server/index.js +77 -16
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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);
|
|
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
|
|
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
|
-
|
|
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);
|
|
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
|
|
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
|
-
|
|
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);
|
|
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 (
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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);
|
|
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 (
|