@profullstack/coinpay 0.16.0 → 0.17.0

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/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to `@profullstack/coinpay` will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.17.0] - 2026-10-17
9
+
10
+ ### Added
11
+ - Itemised invoices: `createInvoice` / `updateInvoice` take `lineItems`
12
+ (description, quantity, unit price, optional tax rate) and a `discount`
13
+ (`percent` or `fixed`, applied before tax). The server computes the total;
14
+ `amount` is only for single-amount invoices.
15
+ - `coinpay invoice create --item "Desc|qty|price[|tax %[|tax name]]"`
16
+ (repeatable) and `--discount 10%` / `--discount 25`.
17
+ - Saved products/services and tax rates: `coinpay item list|create|delete`,
18
+ `coinpay tax-rate list|create|delete`; SDK `listInvoiceItems`,
19
+ `createInvoiceItem`, `deleteInvoiceItem`, `listTaxRates`, `createTaxRate`,
20
+ `deleteTaxRate`.
21
+
8
22
  ## [0.16.0] - 2026-10-12
9
23
 
10
24
  ### Added
package/bin/coinpay.js CHANGED
@@ -24,6 +24,12 @@ import {
24
24
  sendInvoice,
25
25
  updateInvoice,
26
26
  InvoiceStatus,
27
+ listTaxRates,
28
+ createTaxRate,
29
+ deleteTaxRate,
30
+ listInvoiceItems,
31
+ createInvoiceItem,
32
+ deleteInvoiceItem,
27
33
  } from '../src/invoices.js';
28
34
  import { readFileSync, writeFileSync, existsSync, unlinkSync, chmodSync } from 'fs';
29
35
  import { execSync, spawn } from 'child_process';
@@ -206,8 +212,18 @@ const BOOLEAN_FLAGS = new Set([
206
212
  /**
207
213
  * Parse command line arguments
208
214
  */
215
+ // Flags that may be given more than once collect into an array.
216
+ const REPEATABLE_FLAGS = new Set(['item']);
217
+
209
218
  function parseArgs(args) {
210
219
  const result = { command: null, subcommand: null, args: [], flags: {} };
220
+ const setFlag = (name, value) => {
221
+ if (REPEATABLE_FLAGS.has(name) && value !== true) {
222
+ result.flags[name] = [...(Array.isArray(result.flags[name]) ? result.flags[name] : []), value];
223
+ } else {
224
+ result.flags[name] = value;
225
+ }
226
+ };
211
227
 
212
228
  for (let i = 0; i < args.length; i++) {
213
229
  const arg = args[i];
@@ -215,14 +231,14 @@ function parseArgs(args) {
215
231
  if (arg.startsWith('--')) {
216
232
  const eqIdx = arg.indexOf('=');
217
233
  if (eqIdx !== -1) {
218
- result.flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
234
+ setFlag(arg.slice(2, eqIdx), arg.slice(eqIdx + 1));
219
235
  } else {
220
236
  const name = arg.slice(2);
221
237
  const next = args[i + 1];
222
238
  if (BOOLEAN_FLAGS.has(name)) {
223
239
  result.flags[name] = true;
224
240
  } else if (next && !next.startsWith('-')) {
225
- result.flags[name] = next;
241
+ setFlag(name, next);
226
242
  i++;
227
243
  } else {
228
244
  result.flags[name] = true;
@@ -289,6 +305,40 @@ function isValidDate(value) {
289
305
  );
290
306
  }
291
307
 
308
+ function parseItemFlags(values) {
309
+ return values.map((value) => {
310
+ const [description, qty, price, tax, taxName] = String(value).split('|').map((p) => p.trim());
311
+ const quantity = Number(qty);
312
+ const unitPrice = parseNonNegativeAmount(price);
313
+ const taxRate = tax ? Number(tax) : null;
314
+ if (!description || !(quantity > 0) || unitPrice === null || (tax && !(taxRate >= 0 && taxRate <= 100))) {
315
+ print.error('--item must look like "Description|qty|unit price[|tax %[|tax name]]", got: ' + value);
316
+ process.exit(1);
317
+ }
318
+ return {
319
+ description,
320
+ quantity,
321
+ unit_price: unitPrice,
322
+ ...(taxRate ? { tax_rate: taxRate, tax_name: taxName || null } : {}),
323
+ };
324
+ });
325
+ }
326
+
327
+ function parseNonNegativeAmount(value) {
328
+ if (typeof value !== 'string' || !/^\d+(\.\d{1,2})?$/.test(value)) return null;
329
+ return Number(value);
330
+ }
331
+
332
+ /** "10%" is a percent discount, "25" a fixed one in the invoice currency. */
333
+ function parseDiscountFlag(value) {
334
+ const match = /^(\d+(?:\.\d+)?)(%?)$/.exec(String(value).trim());
335
+ if (!match || !(Number(match[1]) > 0)) {
336
+ print.error('--discount must be a percent like 10% or a fixed amount like 25');
337
+ process.exit(1);
338
+ }
339
+ return { type: match[2] ? 'percent' : 'fixed', value: Number(match[1]) };
340
+ }
341
+
292
342
  function requireFlagValues(flags, names) {
293
343
  for (const name of names) {
294
344
  if (flags[name] === true) {
@@ -325,6 +375,10 @@ function invoiceForOutput(data, fallbackId) {
325
375
  clientId: invoice.clientId ?? invoice.client_id,
326
376
  currency: invoice.currency,
327
377
  amount: invoice.amount,
378
+ lineItems: invoice.lineItems ?? invoice.line_items ?? null,
379
+ subtotal: invoice.subtotal ?? null,
380
+ discountTotal: invoice.discountTotal ?? invoice.discount_total ?? null,
381
+ taxTotal: invoice.taxTotal ?? invoice.tax_total ?? null,
328
382
  cryptoCurrency: invoice.cryptoCurrency ?? invoice.crypto_currency,
329
383
  cryptoAmount: invoice.cryptoAmount ?? invoice.crypto_amount,
330
384
  status: invoice.status,
@@ -403,7 +457,9 @@ ${colors.cyan}Commands:${colors.reset}
403
457
  qr <id> Get payment QR code
404
458
 
405
459
  ${colors.bright}invoice${colors.reset}
406
- create Create a draft invoice (--amount required)
460
+ create Create a draft invoice (--amount, or one or more
461
+ --item "Desc|qty|price[|tax %[|tax name]]" and
462
+ an optional --discount 10% / --discount 25)
407
463
  --wallet-id <id> Route settlement through a saved wallet
408
464
  --merchant-wallet-address <address>
409
465
  Route settlement to a direct address
@@ -415,6 +471,17 @@ ${colors.cyan}Commands:${colors.reset}
415
471
  delete <id> Permanently delete a draft invoice
416
472
  --yes Skip confirmation for publish/send/delete
417
473
 
474
+ ${colors.bright}item${colors.reset}
475
+ list Saved products/services (--business-id)
476
+ create Save one: --name, --price [--unit hour] [--description]
477
+ [--tax-rate-id <id>]
478
+ delete <id> Delete a saved item (issued invoices keep their copy)
479
+
480
+ ${colors.bright}tax-rate${colors.reset}
481
+ list Saved tax rates (--business-id)
482
+ create Save one: --name "CA Sales Tax" --rate 8.25 [--default]
483
+ delete <id> Delete a saved tax rate
484
+
418
485
  ${colors.bright}tokens${colors.reset}
419
486
  list List checkout tokens (--business-id, --active-only)
420
487
 
@@ -1068,6 +1135,15 @@ function printInvoiceDetails(invoice, { shareUrl } = {}) {
1068
1135
  console.log('\n' + colors.bright + label + colors.reset);
1069
1136
  print.info('ID: ' + invoice.id);
1070
1137
  print.info('Status: ' + (invoice.status || 'unknown'));
1138
+ if (Array.isArray(invoice.lineItems) && invoice.lineItems.length > 0) {
1139
+ for (const line of invoice.lineItems) {
1140
+ const tax = line.tax_rate ? ' +' + line.tax_rate + '% ' + (line.tax_name || 'tax') : '';
1141
+ print.info(' ' + line.description.split('\n')[0] + ': ' + line.quantity + ' x ' + line.unit_price + ' = ' + line.amount + tax);
1142
+ }
1143
+ print.info('Subtotal: ' + invoice.subtotal);
1144
+ if (Number(invoice.discountTotal) > 0) print.info('Discount: -' + invoice.discountTotal);
1145
+ if (Number(invoice.taxTotal) > 0) print.info('Tax: ' + invoice.taxTotal);
1146
+ }
1071
1147
  print.info('Amount: ' + invoice.amount + ' ' + (invoice.currency || ''));
1072
1148
  if (invoice.businessId) print.info('Business: ' + invoice.businessId);
1073
1149
  if (invoice.clientId) print.info('Client: ' + invoice.clientId);
@@ -1088,6 +1164,92 @@ function printInvoiceDetails(invoice, { shareUrl } = {}) {
1088
1164
  /**
1089
1165
  * Invoice commands
1090
1166
  */
1167
+ async function handleCatalog(kind, subcommand, args, flags) {
1168
+ const client = createClient();
1169
+ const isItem = kind === 'item';
1170
+ requireFlagValues(flags, ['business-id', 'name', 'price', 'unit', 'description', 'tax-rate-id', 'rate']);
1171
+
1172
+ switch (subcommand) {
1173
+ case 'list': {
1174
+ const rows = isItem
1175
+ ? await listInvoiceItems(client, { businessId: flags['business-id'] })
1176
+ : await listTaxRates(client, { businessId: flags['business-id'] });
1177
+ if (flags.json) {
1178
+ print.json(rows);
1179
+ break;
1180
+ }
1181
+ if (rows.length === 0) {
1182
+ print.info(isItem ? 'No saved items' : 'No saved tax rates');
1183
+ break;
1184
+ }
1185
+ for (const row of rows) {
1186
+ print.info(
1187
+ isItem
1188
+ ? row.id + ' ' + row.name + ' ' + row.unit_price + (row.unit ? ' / ' + row.unit : '')
1189
+ : row.id + ' ' + row.name + ' ' + row.rate + '%' + (row.is_default ? ' (default)' : '')
1190
+ );
1191
+ }
1192
+ break;
1193
+ }
1194
+
1195
+ case 'create': {
1196
+ if (!flags.name) {
1197
+ print.error('--name is required');
1198
+ process.exit(1);
1199
+ }
1200
+ let row;
1201
+ if (isItem) {
1202
+ const unitPrice = parseNonNegativeAmount(flags.price);
1203
+ if (unitPrice === null) {
1204
+ print.error('--price must be a decimal with at most two decimal places');
1205
+ process.exit(1);
1206
+ }
1207
+ row = await createInvoiceItem(client, {
1208
+ businessId: flags['business-id'],
1209
+ name: flags.name,
1210
+ unitPrice,
1211
+ description: flags.description,
1212
+ unit: flags.unit,
1213
+ taxRateId: flags['tax-rate-id'],
1214
+ });
1215
+ } else {
1216
+ const rate = Number(flags.rate);
1217
+ if (flags.rate === undefined || !(rate >= 0 && rate <= 100)) {
1218
+ print.error('--rate must be a percent between 0 and 100');
1219
+ process.exit(1);
1220
+ }
1221
+ row = await createTaxRate(client, {
1222
+ businessId: flags['business-id'],
1223
+ name: flags.name,
1224
+ rate,
1225
+ isDefault: flags.default === true || flags.default === 'true',
1226
+ });
1227
+ }
1228
+ if (flags.json) {
1229
+ print.json(row);
1230
+ break;
1231
+ }
1232
+ print.success((isItem ? 'Item' : 'Tax rate') + ' saved: ' + row.id);
1233
+ break;
1234
+ }
1235
+
1236
+ case 'delete': {
1237
+ if (args.length !== 1) {
1238
+ print.error('Usage: coinpay ' + kind + ' delete <id>');
1239
+ process.exit(1);
1240
+ }
1241
+ if (isItem) await deleteInvoiceItem(client, args[0]);
1242
+ else await deleteTaxRate(client, args[0]);
1243
+ print.success('Deleted ' + args[0]);
1244
+ break;
1245
+ }
1246
+
1247
+ default:
1248
+ print.error('Usage: coinpay ' + kind + ' <list|create|delete>');
1249
+ process.exit(1);
1250
+ }
1251
+ }
1252
+
1091
1253
  async function handleInvoice(subcommand, args, flags) {
1092
1254
  const client = createClient();
1093
1255
 
@@ -1103,12 +1265,23 @@ async function handleInvoice(subcommand, args, flags) {
1103
1265
  'notes',
1104
1266
  'wallet-id',
1105
1267
  'merchant-wallet-address',
1268
+ 'item',
1269
+ 'discount',
1106
1270
  ]);
1107
1271
 
1108
- const amount = parsePositiveAmount(flags.amount);
1109
- if (amount === null) {
1272
+ // --item "Description|qty|unit price[|tax %[|tax name]]", repeatable.
1273
+ const lineItems = flags.item === undefined ? undefined : parseItemFlags(flags.item);
1274
+ const discount = flags.discount === undefined ? undefined : parseDiscountFlag(flags.discount);
1275
+ if (discount && !lineItems) {
1276
+ print.error('--discount applies to itemised invoices; add one or more --item');
1277
+ process.exit(1);
1278
+ }
1279
+
1280
+ const amount = lineItems ? undefined : parsePositiveAmount(flags.amount);
1281
+ if (!lineItems && amount === null) {
1110
1282
  print.error('--amount must be a positive decimal with at most two decimal places');
1111
1283
  print.info('Example: coinpay invoice create --amount 125 --currency USD --notes "Consulting"');
1284
+ print.info('Or itemise: coinpay invoice create --item "Design|3|100" --item "Hosting|1|19.99|8.25|CA tax"');
1112
1285
  process.exit(1);
1113
1286
  }
1114
1287
 
@@ -1130,6 +1303,8 @@ async function handleInvoice(subcommand, args, flags) {
1130
1303
  clientId: flags['client-id'],
1131
1304
  currency: currency.toUpperCase(),
1132
1305
  amount,
1306
+ lineItems,
1307
+ discount,
1133
1308
  cryptoCurrency: cryptoCurrency?.toUpperCase(),
1134
1309
  dueDate,
1135
1310
  notes: flags.notes,
@@ -4589,6 +4764,11 @@ async function handleLightning(subcommand, args, flags) {
4589
4764
  await handleInvoice(subcommand, args, flags);
4590
4765
  break;
4591
4766
 
4767
+ case 'item':
4768
+ case 'tax-rate':
4769
+ await handleCatalog(command, subcommand, args, flags);
4770
+ break;
4771
+
4592
4772
  case 'tokens':
4593
4773
  await handleTokens(subcommand, args, flags);
4594
4774
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profullstack/coinpay",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "CoinPay SDK & CLI \u2014 Accept cryptocurrency and card payments (BTC, ETH, SOL, POL, BCH, USDC) with wallet, swap and a live finances dashboard",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/index.d.ts CHANGED
@@ -55,9 +55,20 @@ export {
55
55
  sendInvoice,
56
56
  getInvoicePaymentData,
57
57
  InvoiceStatus,
58
+ listTaxRates,
59
+ createTaxRate,
60
+ deleteTaxRate,
61
+ listInvoiceItems,
62
+ createInvoiceItem,
63
+ deleteInvoiceItem,
58
64
  } from './invoices.js';
59
65
  export type {
60
66
  Invoice,
67
+ InvoiceLine,
68
+ InvoiceLineInput,
69
+ InvoiceDiscount,
70
+ TaxRate,
71
+ InvoiceItem,
61
72
  InvoiceStatusType,
62
73
  CreateInvoiceParams,
63
74
  ListInvoicesParams,
package/src/index.js CHANGED
@@ -82,6 +82,12 @@ import {
82
82
  sendInvoice,
83
83
  getInvoicePaymentData,
84
84
  InvoiceStatus,
85
+ listTaxRates,
86
+ createTaxRate,
87
+ deleteTaxRate,
88
+ listInvoiceItems,
89
+ createInvoiceItem,
90
+ deleteInvoiceItem,
85
91
  } from './invoices.js';
86
92
 
87
93
  // Wallet exports
@@ -231,6 +237,12 @@ export {
231
237
  sendInvoice,
232
238
  getInvoicePaymentData,
233
239
  InvoiceStatus,
240
+ listTaxRates,
241
+ createTaxRate,
242
+ deleteTaxRate,
243
+ listInvoiceItems,
244
+ createInvoiceItem,
245
+ deleteInvoiceItem,
234
246
 
235
247
  // Wallet
236
248
  WalletClient,
package/src/invoices.d.ts CHANGED
@@ -10,6 +10,27 @@ export const InvoiceStatus: {
10
10
 
11
11
  export type InvoiceStatusType = (typeof InvoiceStatus)[keyof typeof InvoiceStatus];
12
12
 
13
+ export interface InvoiceLineInput {
14
+ description: string;
15
+ quantity: number;
16
+ unit_price: number;
17
+ tax_rate?: number | null;
18
+ tax_name?: string | null;
19
+ item_id?: string | null;
20
+ }
21
+
22
+ export interface InvoiceLine extends Required<InvoiceLineInput> {
23
+ /** quantity x unit_price, before discount and tax */
24
+ amount: number;
25
+ discount: number;
26
+ tax: number;
27
+ }
28
+
29
+ export interface InvoiceDiscount {
30
+ type: 'percent' | 'fixed';
31
+ value: number;
32
+ }
33
+
13
34
  export interface Invoice {
14
35
  id: string;
15
36
  invoiceNumber?: string;
@@ -17,6 +38,11 @@ export interface Invoice {
17
38
  clientId?: string;
18
39
  currency?: string;
19
40
  amount?: number;
41
+ lineItems?: InvoiceLine[] | null;
42
+ subtotal?: number | null;
43
+ discount?: InvoiceDiscount | null;
44
+ discountTotal?: number | null;
45
+ taxTotal?: number | null;
20
46
  cryptoCurrency?: string;
21
47
  cryptoAmount?: string | number;
22
48
  status?: InvoiceStatusType;
@@ -36,7 +62,10 @@ export interface CreateInvoiceParams {
36
62
  businessId?: string;
37
63
  clientId?: string;
38
64
  currency: string;
39
- amount: number;
65
+ /** Required unless lineItems is given; the server computes itemised totals. */
66
+ amount?: number;
67
+ lineItems?: InvoiceLineInput[];
68
+ discount?: InvoiceDiscount;
40
69
  cryptoCurrency?: string;
41
70
  dueDate?: string;
42
71
  notes?: string;
@@ -54,6 +83,9 @@ export interface ListInvoicesParams {
54
83
 
55
84
  export interface UpdateInvoiceParams {
56
85
  amount?: number;
86
+ /** Replace the lines; null turns itemising off. */
87
+ lineItems?: InvoiceLineInput[] | null;
88
+ discount?: InvoiceDiscount | null;
57
89
  currency?: string;
58
90
  cryptoCurrency?: string;
59
91
  dueDate?: string;
@@ -76,3 +108,42 @@ export function deleteInvoice(client: CoinPayClient, id: string): Promise<Record
76
108
  export function publishInvoice(client: CoinPayClient, id: string): Promise<Record<string, unknown>>;
77
109
  export function sendInvoice(client: CoinPayClient, id: string): Promise<Record<string, unknown>>;
78
110
  export function getInvoicePaymentData(client: CoinPayClient, id: string): Promise<Record<string, unknown>>;
111
+
112
+ export interface TaxRate {
113
+ id: string;
114
+ business_id: string;
115
+ name: string;
116
+ rate: number;
117
+ is_default: boolean;
118
+ }
119
+
120
+ export interface InvoiceItem {
121
+ id: string;
122
+ business_id: string;
123
+ name: string;
124
+ description: string | null;
125
+ unit_price: number;
126
+ unit: string | null;
127
+ tax_rate_id: string | null;
128
+ archived: boolean;
129
+ }
130
+
131
+ export function listTaxRates(client: CoinPayClient, options?: { businessId?: string }): Promise<TaxRate[]>;
132
+ export function createTaxRate(
133
+ client: CoinPayClient,
134
+ options: { businessId?: string; name: string; rate: number; isDefault?: boolean }
135
+ ): Promise<TaxRate>;
136
+ export function deleteTaxRate(client: CoinPayClient, id: string): Promise<Record<string, unknown>>;
137
+ export function listInvoiceItems(client: CoinPayClient, options?: { businessId?: string }): Promise<InvoiceItem[]>;
138
+ export function createInvoiceItem(
139
+ client: CoinPayClient,
140
+ options: {
141
+ businessId?: string;
142
+ name: string;
143
+ unitPrice: number;
144
+ description?: string;
145
+ unit?: string;
146
+ taxRateId?: string;
147
+ }
148
+ ): Promise<InvoiceItem>;
149
+ export function deleteInvoiceItem(client: CoinPayClient, id: string): Promise<Record<string, unknown>>;
package/src/invoices.js CHANGED
@@ -35,7 +35,10 @@
35
35
  * @param {string} [options.businessId] - Business ID (optional for business-scoped API keys)
36
36
  * @param {string} [options.clientId] - Client ID (optional)
37
37
  * @param {string} options.currency - Fiat currency code (USD, EUR, etc.)
38
- * @param {number} options.amount - Invoice amount
38
+ * @param {number} [options.amount] - Invoice amount (omit when lineItems is given)
39
+ * @param {Array<{description: string, quantity: number, unit_price: number, tax_rate?: number, tax_name?: string, item_id?: string}>} [options.lineItems]
40
+ * Itemised lines; the server computes the total from them
41
+ * @param {{type: 'percent'|'fixed', value: number}} [options.discount] - Invoice-level discount, applied before tax
39
42
  * @param {string} [options.cryptoCurrency] - Preferred crypto for payment
40
43
  * @param {string} [options.dueDate] - Due date (ISO string or YYYY-MM-DD)
41
44
  * @param {string} [options.notes] - Notes / description
@@ -48,6 +51,8 @@ export async function createInvoice(client, {
48
51
  clientId,
49
52
  currency,
50
53
  amount,
54
+ lineItems,
55
+ discount,
51
56
  cryptoCurrency,
52
57
  dueDate,
53
58
  notes,
@@ -57,8 +62,13 @@ export async function createInvoice(client, {
57
62
  const body = {
58
63
  business_id: businessId,
59
64
  currency,
60
- amount,
61
65
  };
66
+ if (lineItems) {
67
+ body.line_items = lineItems;
68
+ if (discount) body.discount = discount;
69
+ } else {
70
+ body.amount = amount;
71
+ }
62
72
  if (clientId) body.client_id = clientId;
63
73
  if (cryptoCurrency) body.crypto_currency = cryptoCurrency;
64
74
  if (dueDate) body.due_date = dueDate;
@@ -124,7 +134,9 @@ export async function getInvoice(client, id) {
124
134
  * @param {CoinPayClient} client
125
135
  * @param {string} id - Invoice ID
126
136
  * @param {Object} updates - Partial invoice fields to update
127
- * @param {number} [updates.amount] - Updated amount
137
+ * @param {number} [updates.amount] - Updated amount (single-amount invoices only)
138
+ * @param {Array|null} [updates.lineItems] - Replace the lines (null turns itemising off)
139
+ * @param {Object|null} [updates.discount] - Replace the discount
128
140
  * @param {string} [updates.currency] - Updated currency
129
141
  * @param {string} [updates.cryptoCurrency] - Updated crypto currency
130
142
  * @param {string} [updates.dueDate] - Updated due date
@@ -137,6 +149,8 @@ export async function getInvoice(client, id) {
137
149
  export async function updateInvoice(client, id, updates) {
138
150
  const body = {};
139
151
  if (updates.amount !== undefined) body.amount = updates.amount;
152
+ if (updates.lineItems !== undefined) body.line_items = updates.lineItems;
153
+ if (updates.discount !== undefined) body.discount = updates.discount;
140
154
  if (updates.currency !== undefined) body.currency = updates.currency;
141
155
  if (updates.cryptoCurrency !== undefined) body.crypto_currency = updates.cryptoCurrency;
142
156
  if (updates.dueDate !== undefined) body.due_date = updates.dueDate;
@@ -233,6 +247,11 @@ function normalizeInvoice(data) {
233
247
  clientId: invoice.client_id,
234
248
  currency: invoice.currency,
235
249
  amount: invoice.amount,
250
+ lineItems: invoice.line_items,
251
+ subtotal: invoice.subtotal,
252
+ discount: invoice.discount,
253
+ discountTotal: invoice.discount_total,
254
+ taxTotal: invoice.tax_total,
236
255
  cryptoCurrency: invoice.crypto_currency,
237
256
  cryptoAmount: invoice.crypto_amount,
238
257
  status: invoice.status,
@@ -248,3 +267,52 @@ function normalizeInvoice(data) {
248
267
  updatedAt: invoice.updated_at,
249
268
  };
250
269
  }
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // Catalog: saved tax rates and products/services for itemised invoices.
273
+ // An API key is locked to its own business, so businessId is optional there.
274
+ // ---------------------------------------------------------------------------
275
+
276
+ function businessQuery(businessId) {
277
+ return businessId ? '?business_id=' + encodeURIComponent(businessId) : '';
278
+ }
279
+
280
+ /** List a business's saved tax rates. */
281
+ export async function listTaxRates(client, { businessId } = {}) {
282
+ const data = await client.request('/tax-rates' + businessQuery(businessId));
283
+ return data.tax_rates ?? [];
284
+ }
285
+
286
+ /** Save a tax rate (percent, up to three decimals). */
287
+ export async function createTaxRate(client, { businessId, name, rate, isDefault }) {
288
+ const body = { name, rate };
289
+ if (businessId) body.business_id = businessId;
290
+ if (isDefault !== undefined) body.is_default = isDefault;
291
+ const data = await client.request('/tax-rates', { method: 'POST', body: JSON.stringify(body) });
292
+ return data.tax_rate;
293
+ }
294
+
295
+ export async function deleteTaxRate(client, id) {
296
+ return client.request('/tax-rates/' + encodeURIComponent(id), { method: 'DELETE' });
297
+ }
298
+
299
+ /** List a business's saved products and services. */
300
+ export async function listInvoiceItems(client, { businessId } = {}) {
301
+ const data = await client.request('/invoice-items' + businessQuery(businessId));
302
+ return data.invoice_items ?? [];
303
+ }
304
+
305
+ /** Save a product or service to bill again later. */
306
+ export async function createInvoiceItem(client, { businessId, name, unitPrice, description, unit, taxRateId }) {
307
+ const body = { name, unit_price: unitPrice };
308
+ if (businessId) body.business_id = businessId;
309
+ if (description) body.description = description;
310
+ if (unit) body.unit = unit;
311
+ if (taxRateId) body.tax_rate_id = taxRateId;
312
+ const data = await client.request('/invoice-items', { method: 'POST', body: JSON.stringify(body) });
313
+ return data.invoice_item;
314
+ }
315
+
316
+ export async function deleteInvoiceItem(client, id) {
317
+ return client.request('/invoice-items/' + encodeURIComponent(id), { method: 'DELETE' });
318
+ }
package/src/x402.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * x402 Payment Protocol Support
3
3
  *
4
4
  * CoinPayPortal as the first multi-chain, multi-asset x402 facilitator.
5
- * Supports native crypto (BTC, ETH, SOL, POL), USDC stablecoins,
5
+ * Supports native crypto (BTC, ETH, SOL, POL, XNO), USDC stablecoins,
6
6
  * Lightning (BOLT12), and Stripe fiat — all via HTTP 402.
7
7
  *
8
8
  * Note: Bitcoin Cash is intentionally not listed. The x402 settle path has no
@@ -92,6 +92,18 @@ export const PAYMENT_METHODS = {
92
92
  label: 'USDC on Base',
93
93
  },
94
94
 
95
+ // ── Nano ───────────────────────────────────────────────────────
96
+ // Opt-in: only advertised when `payTo` names a Nano address (`payTo.nano`
97
+ // or a `nano_` string) and an XNO amount can be quoted (`rates.XNO`, or a
98
+ // raw `amount`). The payer sends a block; the proof carries its `blockHash`.
99
+ xno: {
100
+ network: 'nano',
101
+ asset: 'XNO',
102
+ scheme: 'exact',
103
+ decimals: 30,
104
+ label: 'Nano (XNO)',
105
+ },
106
+
95
107
  // ── Lightning ──────────────────────────────────────────────────
96
108
  lightning: {
97
109
  network: 'lightning',
@@ -175,6 +187,14 @@ export function convertUsdToAssetAmount(amountUsd, methodKey, rates = {}) {
175
187
  if (!rate) throw new Error(`No exchange rate for ${method.asset}`);
176
188
 
177
189
  const assetAmount = amountUsd / rate;
190
+
191
+ if (method.network === 'nano') {
192
+ // 30 decimals is past float64's exact range, and String() of a number that
193
+ // large is exponent notation ("1e+27"), which no verifier parses. Quote to
194
+ // the nearest 10^-9 XNO and scale the rest in BigInt.
195
+ return (BigInt(Math.round(assetAmount * 1e9)) * 10n ** 21n).toString();
196
+ }
197
+
178
198
  const smallest = Math.round(assetAmount * Math.pow(10, method.decimals));
179
199
  return String(smallest);
180
200
  }
@@ -288,6 +308,8 @@ export function buildPaymentRequired(options) {
288
308
  addr = payTo;
289
309
  }
290
310
  if (!addr) continue; // skip methods where merchant has no address
311
+ // A single non-Nano address must not be advertised as a Nano payee.
312
+ if (method.network === 'nano' && !/^(nano|xrb)_/.test(addr)) continue;
291
313
 
292
314
  // Convert USD to asset amount
293
315
  let assetAmount;
@@ -329,6 +351,7 @@ function _networkToMethodKey(network) {
329
351
  polygon: 'pol',
330
352
  solana: 'sol',
331
353
  bitcoin: 'btc',
354
+ nano: 'xno',
332
355
  lightning: 'lightning',
333
356
  stripe: 'stripe',
334
357
  };