@ktmcp-cli/billingo 1.0.0 → 1.0.1
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/AGENT.md +101 -371
- package/README.md +100 -279
- package/bin/billingo.js +2 -67
- package/package.json +14 -10
- package/src/api.js +199 -0
- package/src/config.js +34 -0
- package/src/index.js +808 -0
- package/.env.example +0 -8
- package/CLI_SUMMARY.md +0 -377
- package/INDEX.md +0 -364
- package/INSTALL.sh +0 -62
- package/OPENCLAW.md +0 -503
- package/PROJECT_REPORT.md +0 -462
- package/QUICKSTART.md +0 -212
- package/STRUCTURE.txt +0 -266
- package/TESTING.md +0 -513
- package/banner.png +0 -0
- package/examples/bank-account.json +0 -8
- package/examples/invoice.json +0 -32
- package/examples/partner.json +0 -20
- package/examples/product.json +0 -10
- package/logo.png +0 -0
- package/src/commands/bank-accounts.js +0 -131
- package/src/commands/config.js +0 -73
- package/src/commands/currencies.js +0 -40
- package/src/commands/document-blocks.js +0 -40
- package/src/commands/documents.js +0 -248
- package/src/commands/organization.js +0 -35
- package/src/commands/partners.js +0 -130
- package/src/commands/products.js +0 -130
- package/src/commands/utilities.js +0 -34
- package/src/lib/api.js +0 -160
- package/src/lib/auth.js +0 -32
- package/src/lib/config.js +0 -87
package/src/index.js
ADDED
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { getConfig, setConfig, isConfigured } from './config.js';
|
|
5
|
+
import {
|
|
6
|
+
listDocuments,
|
|
7
|
+
getDocument,
|
|
8
|
+
createDocument,
|
|
9
|
+
downloadDocument,
|
|
10
|
+
sendDocument,
|
|
11
|
+
listPartners,
|
|
12
|
+
getPartner,
|
|
13
|
+
createPartner,
|
|
14
|
+
updatePartner,
|
|
15
|
+
deletePartner,
|
|
16
|
+
listProducts,
|
|
17
|
+
getProduct,
|
|
18
|
+
createProduct,
|
|
19
|
+
updateProduct,
|
|
20
|
+
deleteProduct,
|
|
21
|
+
listBankAccounts,
|
|
22
|
+
getBankAccount,
|
|
23
|
+
createBankAccount,
|
|
24
|
+
updateBankAccount,
|
|
25
|
+
deleteBankAccount,
|
|
26
|
+
listDocumentBlocks,
|
|
27
|
+
getDocumentBlock
|
|
28
|
+
} from './api.js';
|
|
29
|
+
|
|
30
|
+
const program = new Command();
|
|
31
|
+
|
|
32
|
+
// ============================================================
|
|
33
|
+
// Helpers
|
|
34
|
+
// ============================================================
|
|
35
|
+
|
|
36
|
+
function printSuccess(message) {
|
|
37
|
+
console.log(chalk.green('✓') + ' ' + message);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function printError(message) {
|
|
41
|
+
console.error(chalk.red('✗') + ' ' + message);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function printTable(data, columns) {
|
|
45
|
+
if (!data || data.length === 0) {
|
|
46
|
+
console.log(chalk.yellow('No results found.'));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const widths = {};
|
|
51
|
+
columns.forEach(col => {
|
|
52
|
+
widths[col.key] = col.label.length;
|
|
53
|
+
data.forEach(row => {
|
|
54
|
+
const val = String(col.format ? col.format(row[col.key], row) : (row[col.key] ?? ''));
|
|
55
|
+
if (val.length > widths[col.key]) widths[col.key] = val.length;
|
|
56
|
+
});
|
|
57
|
+
widths[col.key] = Math.min(widths[col.key], 40);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const header = columns.map(col => col.label.padEnd(widths[col.key])).join(' ');
|
|
61
|
+
console.log(chalk.bold(chalk.cyan(header)));
|
|
62
|
+
console.log(chalk.dim('─'.repeat(header.length)));
|
|
63
|
+
|
|
64
|
+
data.forEach(row => {
|
|
65
|
+
const line = columns.map(col => {
|
|
66
|
+
const val = String(col.format ? col.format(row[col.key], row) : (row[col.key] ?? ''));
|
|
67
|
+
return val.substring(0, widths[col.key]).padEnd(widths[col.key]);
|
|
68
|
+
}).join(' ');
|
|
69
|
+
console.log(line);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
console.log(chalk.dim(`\n${data.length} result(s)`));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function printJson(data) {
|
|
76
|
+
console.log(JSON.stringify(data, null, 2));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function withSpinner(message, fn) {
|
|
80
|
+
const spinner = ora(message).start();
|
|
81
|
+
try {
|
|
82
|
+
const result = await fn();
|
|
83
|
+
spinner.stop();
|
|
84
|
+
return result;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
spinner.stop();
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function requireAuth() {
|
|
92
|
+
if (!isConfigured()) {
|
|
93
|
+
printError('Billingo API key not configured.');
|
|
94
|
+
console.log('\nRun the following to configure:');
|
|
95
|
+
console.log(chalk.cyan(' billingohu config set --api-key <key>'));
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ============================================================
|
|
101
|
+
// Program metadata
|
|
102
|
+
// ============================================================
|
|
103
|
+
|
|
104
|
+
program
|
|
105
|
+
.name('billingohu')
|
|
106
|
+
.description(chalk.bold('Billingo CLI') + ' - Hungarian invoicing from your terminal')
|
|
107
|
+
.version('1.0.0');
|
|
108
|
+
|
|
109
|
+
// ============================================================
|
|
110
|
+
// CONFIG
|
|
111
|
+
// ============================================================
|
|
112
|
+
|
|
113
|
+
const configCmd = program.command('config').description('Manage CLI configuration');
|
|
114
|
+
|
|
115
|
+
configCmd
|
|
116
|
+
.command('set')
|
|
117
|
+
.description('Set API key')
|
|
118
|
+
.option('--api-key <key>', 'Billingo API key')
|
|
119
|
+
.action((options) => {
|
|
120
|
+
if (options.apiKey) {
|
|
121
|
+
setConfig('apiKey', options.apiKey);
|
|
122
|
+
printSuccess(`API key set`);
|
|
123
|
+
} else {
|
|
124
|
+
printError('No API key provided. Use --api-key <key>');
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
configCmd
|
|
129
|
+
.command('show')
|
|
130
|
+
.description('Show current configuration')
|
|
131
|
+
.action(() => {
|
|
132
|
+
const apiKey = getConfig('apiKey');
|
|
133
|
+
console.log(chalk.bold('\nBillingo CLI Configuration\n'));
|
|
134
|
+
console.log('API Key: ', apiKey ? chalk.green('*'.repeat(16)) : chalk.red('not set'));
|
|
135
|
+
console.log('');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ============================================================
|
|
139
|
+
// DOCUMENTS (Invoices)
|
|
140
|
+
// ============================================================
|
|
141
|
+
|
|
142
|
+
const documentsCmd = program.command('documents').description('Manage documents (invoices)');
|
|
143
|
+
|
|
144
|
+
documentsCmd
|
|
145
|
+
.command('list')
|
|
146
|
+
.description('List documents')
|
|
147
|
+
.option('--type <type>', 'Filter by type (invoice, receipt, proforma, etc.)')
|
|
148
|
+
.option('--status <status>', 'Filter by status')
|
|
149
|
+
.option('--page <n>', 'Page number', '1')
|
|
150
|
+
.option('--per-page <n>', 'Results per page', '25')
|
|
151
|
+
.option('--json', 'Output as JSON')
|
|
152
|
+
.action(async (options) => {
|
|
153
|
+
requireAuth();
|
|
154
|
+
try {
|
|
155
|
+
const documents = await withSpinner('Fetching documents...', () =>
|
|
156
|
+
listDocuments({
|
|
157
|
+
page: parseInt(options.page),
|
|
158
|
+
perPage: parseInt(options.perPage),
|
|
159
|
+
type: options.type,
|
|
160
|
+
status: options.status
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
if (options.json) {
|
|
165
|
+
printJson(documents);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
printTable(documents, [
|
|
170
|
+
{ key: 'id', label: 'ID' },
|
|
171
|
+
{ key: 'invoice_number', label: 'Number' },
|
|
172
|
+
{ key: 'type', label: 'Type' },
|
|
173
|
+
{ key: 'partner', label: 'Partner', format: (v) => v?.name || 'N/A' },
|
|
174
|
+
{ key: 'gross_total', label: 'Total' },
|
|
175
|
+
{ key: 'currency', label: 'Currency' },
|
|
176
|
+
{ key: 'fulfillment_date', label: 'Date' }
|
|
177
|
+
]);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
printError(error.message);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
documentsCmd
|
|
185
|
+
.command('get <id>')
|
|
186
|
+
.description('Get a specific document')
|
|
187
|
+
.option('--json', 'Output as JSON')
|
|
188
|
+
.action(async (id, options) => {
|
|
189
|
+
requireAuth();
|
|
190
|
+
try {
|
|
191
|
+
const document = await withSpinner('Fetching document...', () => getDocument(id));
|
|
192
|
+
|
|
193
|
+
if (!document) {
|
|
194
|
+
printError('Document not found');
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (options.json) {
|
|
199
|
+
printJson(document);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
console.log(chalk.bold('\nDocument Details\n'));
|
|
204
|
+
console.log('ID: ', chalk.cyan(document.id));
|
|
205
|
+
console.log('Number: ', document.invoice_number || 'N/A');
|
|
206
|
+
console.log('Type: ', document.type);
|
|
207
|
+
console.log('Partner: ', document.partner?.name || 'N/A');
|
|
208
|
+
console.log('Currency: ', document.currency);
|
|
209
|
+
console.log('Net Total: ', document.net_total);
|
|
210
|
+
console.log('Gross Total: ', chalk.bold(document.gross_total));
|
|
211
|
+
console.log('Date: ', document.fulfillment_date);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
printError(error.message);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
documentsCmd
|
|
219
|
+
.command('create')
|
|
220
|
+
.description('Create a new document')
|
|
221
|
+
.requiredOption('--data <json>', 'Document data as JSON')
|
|
222
|
+
.option('--json', 'Output as JSON')
|
|
223
|
+
.action(async (options) => {
|
|
224
|
+
requireAuth();
|
|
225
|
+
let documentData;
|
|
226
|
+
try {
|
|
227
|
+
documentData = JSON.parse(options.data);
|
|
228
|
+
} catch {
|
|
229
|
+
printError('Invalid JSON for --data');
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const document = await withSpinner('Creating document...', () =>
|
|
235
|
+
createDocument(documentData)
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
if (options.json) {
|
|
239
|
+
printJson(document);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
printSuccess(`Document created: ${chalk.bold(document.id)}`);
|
|
244
|
+
console.log('Number: ', document.invoice_number || 'N/A');
|
|
245
|
+
console.log('Total: ', document.gross_total, document.currency);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
printError(error.message);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
documentsCmd
|
|
253
|
+
.command('download <id>')
|
|
254
|
+
.description('Download document PDF')
|
|
255
|
+
.action(async (id) => {
|
|
256
|
+
requireAuth();
|
|
257
|
+
try {
|
|
258
|
+
const data = await withSpinner('Downloading document...', () => downloadDocument(id));
|
|
259
|
+
printSuccess('Document downloaded (data returned)');
|
|
260
|
+
printJson(data);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
printError(error.message);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
documentsCmd
|
|
268
|
+
.command('send <id>')
|
|
269
|
+
.description('Send document via email')
|
|
270
|
+
.requiredOption('--emails <emails>', 'Comma-separated email addresses')
|
|
271
|
+
.action(async (id, options) => {
|
|
272
|
+
requireAuth();
|
|
273
|
+
const emails = options.emails.split(',').map(e => e.trim());
|
|
274
|
+
try {
|
|
275
|
+
await withSpinner('Sending document...', () => sendDocument(id, emails));
|
|
276
|
+
printSuccess(`Document sent to: ${emails.join(', ')}`);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
printError(error.message);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// ============================================================
|
|
284
|
+
// PARTNERS (Clients)
|
|
285
|
+
// ============================================================
|
|
286
|
+
|
|
287
|
+
const partnersCmd = program.command('partners').description('Manage partners (clients)');
|
|
288
|
+
|
|
289
|
+
partnersCmd
|
|
290
|
+
.command('list')
|
|
291
|
+
.description('List partners')
|
|
292
|
+
.option('--query <q>', 'Search query')
|
|
293
|
+
.option('--page <n>', 'Page number', '1')
|
|
294
|
+
.option('--per-page <n>', 'Results per page', '25')
|
|
295
|
+
.option('--json', 'Output as JSON')
|
|
296
|
+
.action(async (options) => {
|
|
297
|
+
requireAuth();
|
|
298
|
+
try {
|
|
299
|
+
const partners = await withSpinner('Fetching partners...', () =>
|
|
300
|
+
listPartners({
|
|
301
|
+
page: parseInt(options.page),
|
|
302
|
+
perPage: parseInt(options.perPage),
|
|
303
|
+
query: options.query
|
|
304
|
+
})
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
if (options.json) {
|
|
308
|
+
printJson(partners);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
printTable(partners, [
|
|
313
|
+
{ key: 'id', label: 'ID' },
|
|
314
|
+
{ key: 'name', label: 'Name' },
|
|
315
|
+
{ key: 'email', label: 'Email' },
|
|
316
|
+
{ key: 'taxcode', label: 'Tax Code' },
|
|
317
|
+
{ key: 'iban', label: 'IBAN' }
|
|
318
|
+
]);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
printError(error.message);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
partnersCmd
|
|
326
|
+
.command('get <id>')
|
|
327
|
+
.description('Get a specific partner')
|
|
328
|
+
.option('--json', 'Output as JSON')
|
|
329
|
+
.action(async (id, options) => {
|
|
330
|
+
requireAuth();
|
|
331
|
+
try {
|
|
332
|
+
const partner = await withSpinner('Fetching partner...', () => getPartner(id));
|
|
333
|
+
|
|
334
|
+
if (!partner) {
|
|
335
|
+
printError('Partner not found');
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (options.json) {
|
|
340
|
+
printJson(partner);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
console.log(chalk.bold('\nPartner Details\n'));
|
|
345
|
+
console.log('ID: ', chalk.cyan(partner.id));
|
|
346
|
+
console.log('Name: ', chalk.bold(partner.name));
|
|
347
|
+
console.log('Email: ', partner.email || 'N/A');
|
|
348
|
+
console.log('Tax Code: ', partner.taxcode || 'N/A');
|
|
349
|
+
console.log('IBAN: ', partner.iban || 'N/A');
|
|
350
|
+
console.log('Address: ', partner.address?.address || 'N/A');
|
|
351
|
+
} catch (error) {
|
|
352
|
+
printError(error.message);
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
partnersCmd
|
|
358
|
+
.command('create')
|
|
359
|
+
.description('Create a new partner')
|
|
360
|
+
.requiredOption('--data <json>', 'Partner data as JSON')
|
|
361
|
+
.option('--json', 'Output as JSON')
|
|
362
|
+
.action(async (options) => {
|
|
363
|
+
requireAuth();
|
|
364
|
+
let partnerData;
|
|
365
|
+
try {
|
|
366
|
+
partnerData = JSON.parse(options.data);
|
|
367
|
+
} catch {
|
|
368
|
+
printError('Invalid JSON for --data');
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
const partner = await withSpinner('Creating partner...', () =>
|
|
374
|
+
createPartner(partnerData)
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
if (options.json) {
|
|
378
|
+
printJson(partner);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
printSuccess(`Partner created: ${chalk.bold(partner.name)}`);
|
|
383
|
+
console.log('ID: ', partner.id);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
printError(error.message);
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
partnersCmd
|
|
391
|
+
.command('update <id>')
|
|
392
|
+
.description('Update a partner')
|
|
393
|
+
.requiredOption('--data <json>', 'Partner data as JSON')
|
|
394
|
+
.option('--json', 'Output as JSON')
|
|
395
|
+
.action(async (id, options) => {
|
|
396
|
+
requireAuth();
|
|
397
|
+
let partnerData;
|
|
398
|
+
try {
|
|
399
|
+
partnerData = JSON.parse(options.data);
|
|
400
|
+
} catch {
|
|
401
|
+
printError('Invalid JSON for --data');
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
const partner = await withSpinner('Updating partner...', () =>
|
|
407
|
+
updatePartner(id, partnerData)
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
if (options.json) {
|
|
411
|
+
printJson(partner);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
printSuccess(`Partner updated: ${chalk.bold(partner.name)}`);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
printError(error.message);
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
partnersCmd
|
|
423
|
+
.command('delete <id>')
|
|
424
|
+
.description('Delete a partner')
|
|
425
|
+
.action(async (id) => {
|
|
426
|
+
requireAuth();
|
|
427
|
+
try {
|
|
428
|
+
await withSpinner('Deleting partner...', () => deletePartner(id));
|
|
429
|
+
printSuccess('Partner deleted');
|
|
430
|
+
} catch (error) {
|
|
431
|
+
printError(error.message);
|
|
432
|
+
process.exit(1);
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// ============================================================
|
|
437
|
+
// PRODUCTS
|
|
438
|
+
// ============================================================
|
|
439
|
+
|
|
440
|
+
const productsCmd = program.command('products').description('Manage products');
|
|
441
|
+
|
|
442
|
+
productsCmd
|
|
443
|
+
.command('list')
|
|
444
|
+
.description('List products')
|
|
445
|
+
.option('--page <n>', 'Page number', '1')
|
|
446
|
+
.option('--per-page <n>', 'Results per page', '25')
|
|
447
|
+
.option('--json', 'Output as JSON')
|
|
448
|
+
.action(async (options) => {
|
|
449
|
+
requireAuth();
|
|
450
|
+
try {
|
|
451
|
+
const products = await withSpinner('Fetching products...', () =>
|
|
452
|
+
listProducts({ page: parseInt(options.page), perPage: parseInt(options.perPage) })
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
if (options.json) {
|
|
456
|
+
printJson(products);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
printTable(products, [
|
|
461
|
+
{ key: 'id', label: 'ID' },
|
|
462
|
+
{ key: 'name', label: 'Name' },
|
|
463
|
+
{ key: 'net_unit_price', label: 'Net Price' },
|
|
464
|
+
{ key: 'gross_unit_price', label: 'Gross Price' },
|
|
465
|
+
{ key: 'currency', label: 'Currency' },
|
|
466
|
+
{ key: 'vat', label: 'VAT' }
|
|
467
|
+
]);
|
|
468
|
+
} catch (error) {
|
|
469
|
+
printError(error.message);
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
productsCmd
|
|
475
|
+
.command('get <id>')
|
|
476
|
+
.description('Get a specific product')
|
|
477
|
+
.option('--json', 'Output as JSON')
|
|
478
|
+
.action(async (id, options) => {
|
|
479
|
+
requireAuth();
|
|
480
|
+
try {
|
|
481
|
+
const product = await withSpinner('Fetching product...', () => getProduct(id));
|
|
482
|
+
|
|
483
|
+
if (!product) {
|
|
484
|
+
printError('Product not found');
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (options.json) {
|
|
489
|
+
printJson(product);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
console.log(chalk.bold('\nProduct Details\n'));
|
|
494
|
+
console.log('ID: ', chalk.cyan(product.id));
|
|
495
|
+
console.log('Name: ', chalk.bold(product.name));
|
|
496
|
+
console.log('Net Price: ', product.net_unit_price);
|
|
497
|
+
console.log('Gross Price: ', product.gross_unit_price);
|
|
498
|
+
console.log('Currency: ', product.currency);
|
|
499
|
+
console.log('VAT: ', product.vat);
|
|
500
|
+
} catch (error) {
|
|
501
|
+
printError(error.message);
|
|
502
|
+
process.exit(1);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
productsCmd
|
|
507
|
+
.command('create')
|
|
508
|
+
.description('Create a new product')
|
|
509
|
+
.requiredOption('--data <json>', 'Product data as JSON')
|
|
510
|
+
.option('--json', 'Output as JSON')
|
|
511
|
+
.action(async (options) => {
|
|
512
|
+
requireAuth();
|
|
513
|
+
let productData;
|
|
514
|
+
try {
|
|
515
|
+
productData = JSON.parse(options.data);
|
|
516
|
+
} catch {
|
|
517
|
+
printError('Invalid JSON for --data');
|
|
518
|
+
process.exit(1);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
try {
|
|
522
|
+
const product = await withSpinner('Creating product...', () =>
|
|
523
|
+
createProduct(productData)
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
if (options.json) {
|
|
527
|
+
printJson(product);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
printSuccess(`Product created: ${chalk.bold(product.name)}`);
|
|
532
|
+
console.log('ID: ', product.id);
|
|
533
|
+
} catch (error) {
|
|
534
|
+
printError(error.message);
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
productsCmd
|
|
540
|
+
.command('update <id>')
|
|
541
|
+
.description('Update a product')
|
|
542
|
+
.requiredOption('--data <json>', 'Product data as JSON')
|
|
543
|
+
.option('--json', 'Output as JSON')
|
|
544
|
+
.action(async (id, options) => {
|
|
545
|
+
requireAuth();
|
|
546
|
+
let productData;
|
|
547
|
+
try {
|
|
548
|
+
productData = JSON.parse(options.data);
|
|
549
|
+
} catch {
|
|
550
|
+
printError('Invalid JSON for --data');
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
try {
|
|
555
|
+
const product = await withSpinner('Updating product...', () =>
|
|
556
|
+
updateProduct(id, productData)
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
if (options.json) {
|
|
560
|
+
printJson(product);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
printSuccess(`Product updated: ${chalk.bold(product.name)}`);
|
|
565
|
+
} catch (error) {
|
|
566
|
+
printError(error.message);
|
|
567
|
+
process.exit(1);
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
productsCmd
|
|
572
|
+
.command('delete <id>')
|
|
573
|
+
.description('Delete a product')
|
|
574
|
+
.action(async (id) => {
|
|
575
|
+
requireAuth();
|
|
576
|
+
try {
|
|
577
|
+
await withSpinner('Deleting product...', () => deleteProduct(id));
|
|
578
|
+
printSuccess('Product deleted');
|
|
579
|
+
} catch (error) {
|
|
580
|
+
printError(error.message);
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// ============================================================
|
|
586
|
+
// BANK ACCOUNTS
|
|
587
|
+
// ============================================================
|
|
588
|
+
|
|
589
|
+
const bankAccountsCmd = program.command('bank-accounts').description('Manage bank accounts');
|
|
590
|
+
|
|
591
|
+
bankAccountsCmd
|
|
592
|
+
.command('list')
|
|
593
|
+
.description('List bank accounts')
|
|
594
|
+
.option('--page <n>', 'Page number', '1')
|
|
595
|
+
.option('--per-page <n>', 'Results per page', '25')
|
|
596
|
+
.option('--json', 'Output as JSON')
|
|
597
|
+
.action(async (options) => {
|
|
598
|
+
requireAuth();
|
|
599
|
+
try {
|
|
600
|
+
const accounts = await withSpinner('Fetching bank accounts...', () =>
|
|
601
|
+
listBankAccounts({ page: parseInt(options.page), perPage: parseInt(options.perPage) })
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
if (options.json) {
|
|
605
|
+
printJson(accounts);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
printTable(accounts, [
|
|
610
|
+
{ key: 'id', label: 'ID' },
|
|
611
|
+
{ key: 'name', label: 'Name' },
|
|
612
|
+
{ key: 'account_number', label: 'Account Number' },
|
|
613
|
+
{ key: 'iban', label: 'IBAN' },
|
|
614
|
+
{ key: 'swift', label: 'SWIFT' },
|
|
615
|
+
{ key: 'currency', label: 'Currency' }
|
|
616
|
+
]);
|
|
617
|
+
} catch (error) {
|
|
618
|
+
printError(error.message);
|
|
619
|
+
process.exit(1);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
bankAccountsCmd
|
|
624
|
+
.command('get <id>')
|
|
625
|
+
.description('Get a specific bank account')
|
|
626
|
+
.option('--json', 'Output as JSON')
|
|
627
|
+
.action(async (id, options) => {
|
|
628
|
+
requireAuth();
|
|
629
|
+
try {
|
|
630
|
+
const account = await withSpinner('Fetching bank account...', () => getBankAccount(id));
|
|
631
|
+
|
|
632
|
+
if (!account) {
|
|
633
|
+
printError('Bank account not found');
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (options.json) {
|
|
638
|
+
printJson(account);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
console.log(chalk.bold('\nBank Account Details\n'));
|
|
643
|
+
console.log('ID: ', chalk.cyan(account.id));
|
|
644
|
+
console.log('Name: ', chalk.bold(account.name));
|
|
645
|
+
console.log('Account Number: ', account.account_number);
|
|
646
|
+
console.log('IBAN: ', account.iban);
|
|
647
|
+
console.log('SWIFT: ', account.swift);
|
|
648
|
+
console.log('Currency: ', account.currency);
|
|
649
|
+
} catch (error) {
|
|
650
|
+
printError(error.message);
|
|
651
|
+
process.exit(1);
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
bankAccountsCmd
|
|
656
|
+
.command('create')
|
|
657
|
+
.description('Create a new bank account')
|
|
658
|
+
.requiredOption('--data <json>', 'Bank account data as JSON')
|
|
659
|
+
.option('--json', 'Output as JSON')
|
|
660
|
+
.action(async (options) => {
|
|
661
|
+
requireAuth();
|
|
662
|
+
let accountData;
|
|
663
|
+
try {
|
|
664
|
+
accountData = JSON.parse(options.data);
|
|
665
|
+
} catch {
|
|
666
|
+
printError('Invalid JSON for --data');
|
|
667
|
+
process.exit(1);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
try {
|
|
671
|
+
const account = await withSpinner('Creating bank account...', () =>
|
|
672
|
+
createBankAccount(accountData)
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
if (options.json) {
|
|
676
|
+
printJson(account);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
printSuccess(`Bank account created: ${chalk.bold(account.name)}`);
|
|
681
|
+
console.log('ID: ', account.id);
|
|
682
|
+
} catch (error) {
|
|
683
|
+
printError(error.message);
|
|
684
|
+
process.exit(1);
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
bankAccountsCmd
|
|
689
|
+
.command('update <id>')
|
|
690
|
+
.description('Update a bank account')
|
|
691
|
+
.requiredOption('--data <json>', 'Bank account data as JSON')
|
|
692
|
+
.option('--json', 'Output as JSON')
|
|
693
|
+
.action(async (id, options) => {
|
|
694
|
+
requireAuth();
|
|
695
|
+
let accountData;
|
|
696
|
+
try {
|
|
697
|
+
accountData = JSON.parse(options.data);
|
|
698
|
+
} catch {
|
|
699
|
+
printError('Invalid JSON for --data');
|
|
700
|
+
process.exit(1);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
try {
|
|
704
|
+
const account = await withSpinner('Updating bank account...', () =>
|
|
705
|
+
updateBankAccount(id, accountData)
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
if (options.json) {
|
|
709
|
+
printJson(account);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
printSuccess(`Bank account updated: ${chalk.bold(account.name)}`);
|
|
714
|
+
} catch (error) {
|
|
715
|
+
printError(error.message);
|
|
716
|
+
process.exit(1);
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
bankAccountsCmd
|
|
721
|
+
.command('delete <id>')
|
|
722
|
+
.description('Delete a bank account')
|
|
723
|
+
.action(async (id) => {
|
|
724
|
+
requireAuth();
|
|
725
|
+
try {
|
|
726
|
+
await withSpinner('Deleting bank account...', () => deleteBankAccount(id));
|
|
727
|
+
printSuccess('Bank account deleted');
|
|
728
|
+
} catch (error) {
|
|
729
|
+
printError(error.message);
|
|
730
|
+
process.exit(1);
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
// ============================================================
|
|
735
|
+
// DOCUMENT BLOCKS (Invoice pads)
|
|
736
|
+
// ============================================================
|
|
737
|
+
|
|
738
|
+
const blocksCmd = program.command('document-blocks').description('Manage document blocks (invoice pads)');
|
|
739
|
+
|
|
740
|
+
blocksCmd
|
|
741
|
+
.command('list')
|
|
742
|
+
.description('List document blocks')
|
|
743
|
+
.option('--page <n>', 'Page number', '1')
|
|
744
|
+
.option('--per-page <n>', 'Results per page', '25')
|
|
745
|
+
.option('--json', 'Output as JSON')
|
|
746
|
+
.action(async (options) => {
|
|
747
|
+
requireAuth();
|
|
748
|
+
try {
|
|
749
|
+
const blocks = await withSpinner('Fetching document blocks...', () =>
|
|
750
|
+
listDocumentBlocks({ page: parseInt(options.page), perPage: parseInt(options.perPage) })
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
if (options.json) {
|
|
754
|
+
printJson(blocks);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
printTable(blocks, [
|
|
759
|
+
{ key: 'id', label: 'ID' },
|
|
760
|
+
{ key: 'name', label: 'Name' },
|
|
761
|
+
{ key: 'prefix', label: 'Prefix' },
|
|
762
|
+
{ key: 'type', label: 'Type' }
|
|
763
|
+
]);
|
|
764
|
+
} catch (error) {
|
|
765
|
+
printError(error.message);
|
|
766
|
+
process.exit(1);
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
blocksCmd
|
|
771
|
+
.command('get <id>')
|
|
772
|
+
.description('Get a specific document block')
|
|
773
|
+
.option('--json', 'Output as JSON')
|
|
774
|
+
.action(async (id, options) => {
|
|
775
|
+
requireAuth();
|
|
776
|
+
try {
|
|
777
|
+
const block = await withSpinner('Fetching document block...', () => getDocumentBlock(id));
|
|
778
|
+
|
|
779
|
+
if (!block) {
|
|
780
|
+
printError('Document block not found');
|
|
781
|
+
process.exit(1);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (options.json) {
|
|
785
|
+
printJson(block);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
console.log(chalk.bold('\nDocument Block Details\n'));
|
|
790
|
+
console.log('ID: ', chalk.cyan(block.id));
|
|
791
|
+
console.log('Name: ', chalk.bold(block.name));
|
|
792
|
+
console.log('Prefix: ', block.prefix);
|
|
793
|
+
console.log('Type: ', block.type);
|
|
794
|
+
} catch (error) {
|
|
795
|
+
printError(error.message);
|
|
796
|
+
process.exit(1);
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
// ============================================================
|
|
801
|
+
// Parse
|
|
802
|
+
// ============================================================
|
|
803
|
+
|
|
804
|
+
program.parse(process.argv);
|
|
805
|
+
|
|
806
|
+
if (process.argv.length <= 2) {
|
|
807
|
+
program.help();
|
|
808
|
+
}
|