noxctl 0.1.0 → 0.2.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.
Files changed (101) hide show
  1. package/README.md +186 -29
  2. package/dist/auth.d.ts +15 -7
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +123 -22
  5. package/dist/auth.js.map +1 -1
  6. package/dist/cli.d.ts +2 -1
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +948 -16
  9. package/dist/cli.js.map +1 -1
  10. package/dist/config-paths.d.ts +2 -0
  11. package/dist/config-paths.d.ts.map +1 -0
  12. package/dist/config-paths.js +7 -0
  13. package/dist/config-paths.js.map +1 -0
  14. package/dist/credentials-store.d.ts +13 -3
  15. package/dist/credentials-store.d.ts.map +1 -1
  16. package/dist/credentials-store.js +154 -48
  17. package/dist/credentials-store.js.map +1 -1
  18. package/dist/fortnox-client.d.ts.map +1 -1
  19. package/dist/fortnox-client.js +14 -2
  20. package/dist/fortnox-client.js.map +1 -1
  21. package/dist/index.d.ts +15 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +86 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/operations/costcenters.d.ts +20 -0
  26. package/dist/operations/costcenters.d.ts.map +1 -0
  27. package/dist/operations/costcenters.js +38 -0
  28. package/dist/operations/costcenters.js.map +1 -0
  29. package/dist/operations/invoice-payments.d.ts +21 -0
  30. package/dist/operations/invoice-payments.d.ts.map +1 -0
  31. package/dist/operations/invoice-payments.js +40 -0
  32. package/dist/operations/invoice-payments.js.map +1 -0
  33. package/dist/operations/offers.d.ts +25 -0
  34. package/dist/operations/offers.d.ts.map +1 -0
  35. package/dist/operations/offers.js +48 -0
  36. package/dist/operations/offers.js.map +1 -0
  37. package/dist/operations/orders.d.ts +24 -0
  38. package/dist/operations/orders.d.ts.map +1 -0
  39. package/dist/operations/orders.js +44 -0
  40. package/dist/operations/orders.js.map +1 -0
  41. package/dist/operations/pricelists.d.ts +36 -0
  42. package/dist/operations/pricelists.d.ts.map +1 -0
  43. package/dist/operations/pricelists.js +52 -0
  44. package/dist/operations/pricelists.js.map +1 -0
  45. package/dist/operations/projects.d.ts +19 -0
  46. package/dist/operations/projects.d.ts.map +1 -0
  47. package/dist/operations/projects.js +33 -0
  48. package/dist/operations/projects.js.map +1 -0
  49. package/dist/operations/supplier-invoice-payments.d.ts +20 -0
  50. package/dist/operations/supplier-invoice-payments.d.ts.map +1 -0
  51. package/dist/operations/supplier-invoice-payments.js +34 -0
  52. package/dist/operations/supplier-invoice-payments.js.map +1 -0
  53. package/dist/operations/taxreductions.d.ts +19 -0
  54. package/dist/operations/taxreductions.d.ts.map +1 -0
  55. package/dist/operations/taxreductions.js +28 -0
  56. package/dist/operations/taxreductions.js.map +1 -0
  57. package/dist/profile-name.d.ts +9 -0
  58. package/dist/profile-name.d.ts.map +1 -0
  59. package/dist/profile-name.js +58 -0
  60. package/dist/profile-name.js.map +1 -0
  61. package/dist/profiles.d.ts +55 -0
  62. package/dist/profiles.d.ts.map +1 -0
  63. package/dist/profiles.js +210 -0
  64. package/dist/profiles.js.map +1 -0
  65. package/dist/tools/costcenters.d.ts +3 -0
  66. package/dist/tools/costcenters.d.ts.map +1 -0
  67. package/dist/tools/costcenters.js +79 -0
  68. package/dist/tools/costcenters.js.map +1 -0
  69. package/dist/tools/invoice-payments.d.ts +3 -0
  70. package/dist/tools/invoice-payments.d.ts.map +1 -0
  71. package/dist/tools/invoice-payments.js +60 -0
  72. package/dist/tools/invoice-payments.js.map +1 -0
  73. package/dist/tools/offers.d.ts +3 -0
  74. package/dist/tools/offers.d.ts.map +1 -0
  75. package/dist/tools/offers.js +130 -0
  76. package/dist/tools/offers.js.map +1 -0
  77. package/dist/tools/orders.d.ts +3 -0
  78. package/dist/tools/orders.d.ts.map +1 -0
  79. package/dist/tools/orders.js +114 -0
  80. package/dist/tools/orders.js.map +1 -0
  81. package/dist/tools/pricelists.d.ts +3 -0
  82. package/dist/tools/pricelists.d.ts.map +1 -0
  83. package/dist/tools/pricelists.js +104 -0
  84. package/dist/tools/pricelists.js.map +1 -0
  85. package/dist/tools/projects.d.ts +3 -0
  86. package/dist/tools/projects.d.ts.map +1 -0
  87. package/dist/tools/projects.js +80 -0
  88. package/dist/tools/projects.js.map +1 -0
  89. package/dist/tools/supplier-invoice-payments.d.ts +3 -0
  90. package/dist/tools/supplier-invoice-payments.d.ts.map +1 -0
  91. package/dist/tools/supplier-invoice-payments.js +60 -0
  92. package/dist/tools/supplier-invoice-payments.js.map +1 -0
  93. package/dist/tools/taxreductions.d.ts +3 -0
  94. package/dist/tools/taxreductions.d.ts.map +1 -0
  95. package/dist/tools/taxreductions.js +51 -0
  96. package/dist/tools/taxreductions.js.map +1 -0
  97. package/dist/views.d.ts +20 -0
  98. package/dist/views.d.ts.map +1 -1
  99. package/dist/views.js +177 -0
  100. package/dist/views.js.map +1 -1
  101. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -3,18 +3,95 @@ import { Command, Option } from 'commander';
3
3
  import { readFileSync } from 'node:fs';
4
4
  import { createInterface } from 'node:readline/promises';
5
5
  import { isJsonMode, outputList, outputDetail, outputConfirmation, formatTaxReport, formatFinancialReport, } from './formatter.js';
6
- import { invoiceListColumns, invoiceDetailColumns, invoiceConfirmColumns, customerListColumns, customerDetailColumns, voucherListColumns, voucherDetailColumns, voucherRowColumns, accountListColumns, companyDetailColumns, articleListColumns, articleDetailColumns, supplierListColumns, supplierDetailColumns, supplierInvoiceListColumns, supplierInvoiceDetailColumns, supplierInvoiceConfirmColumns, } from './views.js';
6
+ import { readActivePointer, readActivePointerOutcome, writeActivePointer, deleteActivePointer, readProfileIndex, resolveProfile, } from './profiles.js';
7
+ import { setResolvedProfile } from './auth.js';
8
+ import { DEFAULT_PROFILE, InvalidProfileNameError } from './profile-name.js';
9
+ import { invoiceListColumns, invoiceDetailColumns, invoiceConfirmColumns, customerListColumns, customerDetailColumns, voucherListColumns, voucherDetailColumns, voucherRowColumns, accountListColumns, companyDetailColumns, articleListColumns, articleDetailColumns, supplierListColumns, supplierDetailColumns, supplierInvoiceListColumns, supplierInvoiceDetailColumns, supplierInvoiceConfirmColumns, invoicePaymentListColumns, invoicePaymentDetailColumns, supplierInvoicePaymentListColumns, supplierInvoicePaymentDetailColumns, offerListColumns, offerDetailColumns, orderListColumns, orderDetailColumns, projectListColumns, projectDetailColumns, costCenterListColumns, costCenterDetailColumns, taxReductionListColumns, taxReductionDetailColumns, priceListListColumns, priceListDetailColumns, priceListColumns, priceDetailColumns, } from './views.js';
7
10
  const program = new Command();
8
11
  program
9
12
  .name('noxctl')
10
13
  .description('CLI and MCP server for Fortnox accounting')
11
- .version('0.1.0')
14
+ .version('0.2.0')
12
15
  .addOption(new Option('-o, --output <format>', 'Output format')
13
16
  .choices(['json', 'table'])
14
- .default(undefined));
17
+ .default(undefined))
18
+ .option('--profile <name>', 'Profile to operate on (overrides NOXCTL_PROFILE and active pointer)');
15
19
  function json() {
16
20
  return isJsonMode(program.opts());
17
21
  }
22
+ let resolvedProfileInfo = { name: DEFAULT_PROFILE, source: 'default' };
23
+ export function getResolvedProfileInfo() {
24
+ return resolvedProfileInfo;
25
+ }
26
+ // Commands that don't need (and shouldn't require) a resolved profile.
27
+ const PROFILE_RESOLUTION_SKIP = new Set(['help']);
28
+ program.hook('preAction', async (thisCommand, actionCommand) => {
29
+ const name = actionCommand.name();
30
+ if (PROFILE_RESOLUTION_SKIP.has(name))
31
+ return;
32
+ const flag = program.opts().profile ?? undefined;
33
+ const env = process.env['NOXCTL_PROFILE'] ?? undefined;
34
+ // Typed pointer resolution so we can distinguish "no pointer" (silent
35
+ // default) from "pointer unreadable/corrupt" (must surface). 2s bound
36
+ // catches wedged filesystems; the AbortSignal cancels the underlying read.
37
+ const outcome = await readActivePointerOutcome({ timeoutMs: 2000 });
38
+ let pointer = null;
39
+ if (outcome.kind === 'valid') {
40
+ pointer = outcome.name;
41
+ }
42
+ else if (outcome.kind !== 'missing') {
43
+ const desc = describePointerFault(outcome);
44
+ // Only fail closed when the pointer is the winning source (no flag/env)
45
+ // AND we're about to start the MCP server. For any other command, warn
46
+ // loudly but continue so `doctor` / `profile use` can repair the state.
47
+ const wouldRelyOnPointer = !flag && !env;
48
+ if (wouldRelyOnPointer && name === 'serve') {
49
+ console.error(`Active profile pointer ${desc}. Refusing to start MCP server with ambiguous profile. Run \`noxctl doctor\` or set NOXCTL_PROFILE explicitly.`);
50
+ process.exit(2);
51
+ }
52
+ process.stderr.write(`[warning: active-profile pointer ${desc}; ignoring]\n`);
53
+ }
54
+ try {
55
+ resolvedProfileInfo = resolveProfile({ flag, env, pointer });
56
+ }
57
+ catch (err) {
58
+ if (err instanceof InvalidProfileNameError) {
59
+ console.error(err.message);
60
+ process.exit(2);
61
+ }
62
+ throw err;
63
+ }
64
+ setResolvedProfile(resolvedProfileInfo.name);
65
+ // Banner ownership: MCP `serve` prints its own banner in bindStartupProfile
66
+ // so host logs (Claude Desktop) always see it. Suppress here to avoid a
67
+ // duplicate on TTY invocations like `noxctl --profile X serve`.
68
+ if (resolvedProfileInfo.name.toLowerCase() !== DEFAULT_PROFILE &&
69
+ process.stderr.isTTY &&
70
+ name !== 'current' &&
71
+ name !== 'serve') {
72
+ process.stderr.write(`[profile: ${resolvedProfileInfo.name}]\n`);
73
+ }
74
+ });
75
+ function describePointerFault(outcome) {
76
+ switch (outcome.kind) {
77
+ case 'invalid-content':
78
+ return `contains an invalid profile name: "${outcome.raw}"`;
79
+ case 'read-error':
80
+ return `could not be read (${outcome.error.message})`;
81
+ case 'timeout':
82
+ return 'read timed out';
83
+ }
84
+ }
85
+ async function fetchCompanyHint() {
86
+ try {
87
+ const { loadCredentials } = await import('./auth.js');
88
+ const creds = await loadCredentials();
89
+ return creds?.company_name;
90
+ }
91
+ catch {
92
+ return undefined;
93
+ }
94
+ }
18
95
  async function confirmMutation(action, opts, payload) {
19
96
  if (opts.dryRun) {
20
97
  console.log(`Dry run: ${action}. No request was sent to Fortnox.`);
@@ -29,12 +106,14 @@ async function confirmMutation(action, opts, payload) {
29
106
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
30
107
  throw new Error(`Confirmation required to ${action}. Re-run with --yes, or --dry-run first.`);
31
108
  }
109
+ const company = await fetchCompanyHint();
110
+ const suffix = company ? ` (${company})` : '';
32
111
  const rl = createInterface({
33
112
  input: process.stdin,
34
113
  output: process.stdout,
35
114
  });
36
115
  try {
37
- const answer = await rl.question(`${action}. Continue? [y/N] `);
116
+ const answer = await rl.question(`${action}. Continue?${suffix} [y/N] `);
38
117
  return ['y', 'yes'].includes(answer.trim().toLowerCase());
39
118
  }
40
119
  finally {
@@ -45,10 +124,28 @@ async function confirmMutation(action, opts, payload) {
45
124
  program
46
125
  .command('init')
47
126
  .description('Interactive setup wizard — recommended onboarding path')
48
- .action(async () => {
127
+ .option('--profile <name>', 'Profile to create/re-auth (defaults to resolved profile or "default")')
128
+ .action(async (initOpts) => {
49
129
  const { loadCredentials, runOAuthSetup } = await import('./auth.js');
130
+ const { validateProfileName } = await import('./profile-name.js');
131
+ let targetProfile;
132
+ try {
133
+ targetProfile = validateProfileName(initOpts.profile ?? resolvedProfileInfo.name);
134
+ }
135
+ catch (err) {
136
+ console.error(err instanceof Error ? err.message : String(err));
137
+ process.exit(2);
138
+ }
139
+ // If `init --profile <name>` names a different profile than the preAction
140
+ // hook resolved, rebind the in-process resolved profile so downstream work
141
+ // (verification via getCompanyInfo, runOAuthSetup's internal saveCredentials
142
+ // call chain) targets the profile being initialized — not a stale pointer.
143
+ if (targetProfile.toLowerCase() !== resolvedProfileInfo.name.toLowerCase()) {
144
+ setResolvedProfile(targetProfile);
145
+ resolvedProfileInfo = { name: targetProfile, source: 'flag' };
146
+ }
50
147
  // Step 1: Check if already configured
51
- const existing = await loadCredentials();
148
+ const existing = await loadCredentials(targetProfile);
52
149
  if (existing) {
53
150
  console.log('Existing credentials found.');
54
151
  if (process.stdin.isTTY && process.stdout.isTTY) {
@@ -181,7 +278,20 @@ program
181
278
  }
182
279
  }
183
280
  // Step 6: Run OAuth flow
184
- await runOAuthSetup({ clientId, clientSecret, serviceAccount });
281
+ await runOAuthSetup({ clientId, clientSecret, serviceAccount }, targetProfile);
282
+ // Step 6b: Set the active pointer if this is the first profile or no pointer exists.
283
+ try {
284
+ const idx = await readProfileIndex();
285
+ const existingPointer = await readActivePointer();
286
+ const firstProfile = idx.profiles.length <= 1;
287
+ if (firstProfile || !existingPointer) {
288
+ await writeActivePointer(targetProfile);
289
+ }
290
+ }
291
+ catch (err) {
292
+ const msg = err instanceof Error ? err.message : String(err);
293
+ console.warn(`Warning: could not update active profile pointer: ${msg}`);
294
+ }
185
295
  // Step 7: Verify by fetching company info
186
296
  try {
187
297
  const { getCompanyInfo } = await import('./operations/company.js');
@@ -267,28 +377,139 @@ program
267
377
  // --- logout ---
268
378
  program
269
379
  .command('logout')
270
- .description('Remove stored Fortnox credentials')
380
+ .description('Remove stored Fortnox credentials for the resolved profile')
271
381
  .option('-y, --yes', 'Skip confirmation prompt')
382
+ .option('--all', 'Remove credentials for every profile (and the legacy slot)')
272
383
  .action(async (opts) => {
273
- const { loadCredentials } = await import('./auth.js');
274
384
  const { deleteCredentialBlob } = await import('./credentials-store.js');
275
- const existing = await loadCredentials();
385
+ const { removeProfile } = await import('./profiles.js');
386
+ if (opts.all) {
387
+ if (!(await confirmMutation('Remove stored Fortnox credentials for ALL profiles', opts))) {
388
+ return;
389
+ }
390
+ const idx = await readProfileIndex();
391
+ const names = new Set(idx.profiles.map((p) => p.name));
392
+ names.add(DEFAULT_PROFILE);
393
+ let removedAny = false;
394
+ for (const name of names) {
395
+ const deleted = await deleteCredentialBlob(name);
396
+ if (deleted)
397
+ removedAny = true;
398
+ try {
399
+ await removeProfile(name);
400
+ }
401
+ catch {
402
+ // best-effort — index cleanup must not abort logout
403
+ }
404
+ }
405
+ try {
406
+ await deleteActivePointer();
407
+ }
408
+ catch {
409
+ // ignore
410
+ }
411
+ if (removedAny) {
412
+ console.log('Removed credentials for all profiles.');
413
+ }
414
+ else {
415
+ console.log('No credentials found to remove.');
416
+ }
417
+ return;
418
+ }
419
+ const target = resolvedProfileInfo.name;
420
+ const { loadCredentials } = await import('./auth.js');
421
+ const existing = await loadCredentials(target);
276
422
  if (!existing) {
277
- console.log('No credentials found. Nothing to remove.');
423
+ console.log(`No credentials found for profile "${target}". Nothing to remove.`);
278
424
  return;
279
425
  }
280
- if (!(await confirmMutation('Remove stored Fortnox credentials', opts))) {
426
+ if (!(await confirmMutation(`Remove stored credentials for profile "${target}"`, opts))) {
281
427
  return;
282
428
  }
283
- const deleted = await deleteCredentialBlob();
429
+ const deleted = await deleteCredentialBlob(target);
430
+ try {
431
+ await removeProfile(target);
432
+ }
433
+ catch {
434
+ // best-effort
435
+ }
436
+ try {
437
+ const pointer = await readActivePointer();
438
+ if (pointer && pointer.toLowerCase() === target.toLowerCase()) {
439
+ await deleteActivePointer();
440
+ }
441
+ }
442
+ catch {
443
+ // ignore
444
+ }
284
445
  if (deleted) {
285
- console.log('Credentials removed.');
446
+ console.log(`Credentials for profile "${target}" removed.`);
286
447
  }
287
448
  else {
288
449
  console.log('Could not remove credentials from the system keychain.');
289
450
  console.log('They may have already been removed, or you may need to remove them manually.');
290
451
  }
291
452
  });
453
+ // --- profile ---
454
+ const profile = program.command('profile').description('Manage noxctl profiles');
455
+ profile
456
+ .command('use <name>')
457
+ .description('Set the active profile (writes ~/.fortnox-mcp/active-profile)')
458
+ .action(async (name) => {
459
+ const { validateProfileName } = await import('./profile-name.js');
460
+ let validated;
461
+ try {
462
+ validated = validateProfileName(name);
463
+ }
464
+ catch (err) {
465
+ console.error(err instanceof Error ? err.message : String(err));
466
+ process.exit(2);
467
+ }
468
+ const { loadCredentials } = await import('./auth.js');
469
+ const creds = await loadCredentials(validated);
470
+ if (!creds) {
471
+ console.error(`No credentials found for profile "${validated}". Run \`noxctl init --profile ${validated}\` first.`);
472
+ process.exit(1);
473
+ }
474
+ await writeActivePointer(validated);
475
+ if (json()) {
476
+ console.log(JSON.stringify({ name: validated, source: 'pointer' }));
477
+ }
478
+ else {
479
+ console.log(`Active profile set to "${validated}".`);
480
+ }
481
+ });
482
+ profile
483
+ .command('current')
484
+ .description('Show the currently resolved profile and where it came from')
485
+ .action(() => {
486
+ if (json()) {
487
+ console.log(JSON.stringify(resolvedProfileInfo));
488
+ }
489
+ else {
490
+ console.log(`${resolvedProfileInfo.name} (source: ${resolvedProfileInfo.source})`);
491
+ }
492
+ });
493
+ profile
494
+ .command('list')
495
+ .description('List known profiles from the index')
496
+ .action(async () => {
497
+ const idx = await readProfileIndex();
498
+ if (json()) {
499
+ console.log(JSON.stringify(idx.profiles, null, 2));
500
+ return;
501
+ }
502
+ if (idx.profiles.length === 0) {
503
+ console.log('No profiles registered. Run `noxctl init` to create one.');
504
+ return;
505
+ }
506
+ for (const p of idx.profiles) {
507
+ const marker = p.name.toLowerCase() === resolvedProfileInfo.name.toLowerCase() ? '*' : ' ';
508
+ const company = p.company_name ? ` — ${p.company_name}` : '';
509
+ const tenant = p.tenant_id ? ` [tenant ${p.tenant_id}]` : '';
510
+ console.log(`${marker} ${p.name}${company}${tenant}`);
511
+ }
512
+ });
292
513
  // --- doctor ---
293
514
  program
294
515
  .command('doctor')
@@ -320,14 +541,51 @@ program
320
541
  ? 'Windows DPAPI'
321
542
  : 'Linux Secret Service';
322
543
  pass('Credential store', storeBackend);
544
+ // 2b. Profile resolution
545
+ pass('Profile', `${resolvedProfileInfo.name} (source: ${resolvedProfileInfo.source})`);
546
+ // 2c. Active pointer health — surface a corrupt pointer file even though
547
+ // readActivePointer() degrades silently.
548
+ try {
549
+ const { paths } = await import('./profiles.js');
550
+ const fsp = await import('node:fs/promises');
551
+ const raw = await fsp.readFile(paths.activePointerFile, 'utf-8').catch(() => null);
552
+ if (raw !== null) {
553
+ const trimmed = raw.trim();
554
+ if (trimmed.length > 0) {
555
+ const { validateProfileName } = await import('./profile-name.js');
556
+ try {
557
+ validateProfileName(trimmed);
558
+ const idx = await readProfileIndex();
559
+ const known = idx.profiles.some((p) => p.name.toLowerCase() === trimmed.toLowerCase());
560
+ if (known) {
561
+ pass('Active pointer', trimmed);
562
+ }
563
+ else {
564
+ fail('Active pointer', `points to unknown profile "${trimmed}" — run \`noxctl profile use <name>\` to fix`);
565
+ }
566
+ }
567
+ catch {
568
+ fail('Active pointer', 'contains an invalid name — delete to reset');
569
+ }
570
+ }
571
+ }
572
+ }
573
+ catch {
574
+ // best-effort diagnostic — don't block doctor on filesystem issues
575
+ }
323
576
  // 3. Credentials exist
324
577
  const creds = await loadCredentials();
325
578
  if (!creds) {
326
- fail('Credentials', 'not found — run `noxctl init` to set up');
579
+ fail('Credentials', `not found for profile "${resolvedProfileInfo.name}" — run \`noxctl init${resolvedProfileInfo.name === DEFAULT_PROFILE
580
+ ? ''
581
+ : ` --profile ${resolvedProfileInfo.name}`}\` to set up`);
327
582
  console.log(`\n${ok ? 'All checks passed.' : 'Some checks failed.'}`);
328
583
  return;
329
584
  }
330
585
  pass('Credentials', 'found');
586
+ if (creds.company_name) {
587
+ pass('Company (cached)', creds.company_name);
588
+ }
331
589
  // 4. Client ID present
332
590
  if (creds.client_id) {
333
591
  pass('Client ID', `${creds.client_id.slice(0, 8)}...`);
@@ -372,6 +630,7 @@ program
372
630
  article: 'articles?limit=1',
373
631
  customer: 'customers?limit=1',
374
632
  invoice: 'invoices?limit=1',
633
+ payment: 'invoicepayments?limit=1',
375
634
  supplier: 'suppliers?limit=1',
376
635
  supplierinvoice: 'supplierinvoices?limit=1',
377
636
  bookkeeping: 'vouchers?limit=1',
@@ -408,7 +667,7 @@ program
408
667
  .description('Start the MCP server (stdio transport)')
409
668
  .action(async () => {
410
669
  const { startMcpServer } = await import('./index.js');
411
- await startMcpServer();
670
+ await startMcpServer({ profile: resolvedProfileInfo.name });
412
671
  });
413
672
  // --- invoices ---
414
673
  const invoices = program.command('invoices').description('Invoice operations');
@@ -990,6 +1249,679 @@ Examples:
990
1249
  const data = await createVoucher(params);
991
1250
  outputDetail(data, voucherDetailColumns, json());
992
1251
  });
1252
+ // --- invoice-payments ---
1253
+ const invoicePayments = program
1254
+ .command('invoice-payments')
1255
+ .alias('ip')
1256
+ .description('Invoice payment operations (inbetalningar)');
1257
+ invoicePayments
1258
+ .command('list')
1259
+ .description('List invoice payments')
1260
+ .option('--invoice <number>', 'Filter by invoice number')
1261
+ .option('--page <number>', 'Page number', parseInt)
1262
+ .option('--limit <number>', 'Results per page', parseInt)
1263
+ .option('-a, --all', 'Fetch all pages')
1264
+ .action(async (opts) => {
1265
+ const { listInvoicePayments } = await import('./operations/invoice-payments.js');
1266
+ const data = await listInvoicePayments({
1267
+ invoiceNumber: opts.invoice,
1268
+ page: opts.page,
1269
+ limit: opts.limit,
1270
+ all: opts.all,
1271
+ });
1272
+ const envelope = data;
1273
+ outputList(envelope.InvoicePayments ?? [], invoicePaymentListColumns, json(), data, envelope.MetaInformation);
1274
+ });
1275
+ invoicePayments
1276
+ .command('get <paymentNumber>')
1277
+ .description('Get a single invoice payment')
1278
+ .action(async (paymentNumber) => {
1279
+ const { getInvoicePayment } = await import('./operations/invoice-payments.js');
1280
+ const data = await getInvoicePayment(paymentNumber);
1281
+ outputDetail(data, invoicePaymentDetailColumns, json());
1282
+ });
1283
+ invoicePayments
1284
+ .command('create')
1285
+ .description('Register a payment against an invoice')
1286
+ .requiredOption('--invoice <number>', 'Invoice number')
1287
+ .requiredOption('--amount <amount>', 'Payment amount', parseFloat)
1288
+ .requiredOption('--date <date>', 'Payment date (YYYY-MM-DD)')
1289
+ .option('-y, --yes', 'Skip confirmation prompt')
1290
+ .option('--dry-run', 'Preview the request without sending it')
1291
+ .addHelpText('after', `
1292
+ Examples:
1293
+ noxctl invoice-payments create --invoice 1001 --amount 5000 --date 2026-03-20 --dry-run
1294
+ noxctl ip create --invoice 1001 --amount 5000 --date 2026-03-20 --yes`)
1295
+ .action(async (opts) => {
1296
+ const { createInvoicePayment } = await import('./operations/invoice-payments.js');
1297
+ const params = {
1298
+ InvoiceNumber: parseInt(opts.invoice, 10),
1299
+ Amount: opts.amount,
1300
+ PaymentDate: opts.date,
1301
+ };
1302
+ if (!(await confirmMutation(`Register payment of ${opts.amount} for invoice ${opts.invoice}`, opts, {
1303
+ InvoicePayment: params,
1304
+ }))) {
1305
+ return;
1306
+ }
1307
+ const data = await createInvoicePayment(params);
1308
+ outputDetail(data, invoicePaymentDetailColumns, json());
1309
+ });
1310
+ invoicePayments
1311
+ .command('delete <paymentNumber>')
1312
+ .description('Delete an invoice payment')
1313
+ .option('-y, --yes', 'Skip confirmation prompt')
1314
+ .option('--dry-run', 'Preview the action without sending it')
1315
+ .action(async (paymentNumber, opts) => {
1316
+ const { deleteInvoicePayment } = await import('./operations/invoice-payments.js');
1317
+ if (!(await confirmMutation(`Delete invoice payment ${paymentNumber}`, opts))) {
1318
+ return;
1319
+ }
1320
+ await deleteInvoicePayment(paymentNumber);
1321
+ outputConfirmation(`Invoice payment ${paymentNumber} deleted.`, json(), {});
1322
+ });
1323
+ invoicePayments
1324
+ .command('bookkeep <paymentNumber>')
1325
+ .description('Bookkeep an invoice payment')
1326
+ .option('-y, --yes', 'Skip confirmation prompt')
1327
+ .option('--dry-run', 'Preview the action without sending it')
1328
+ .action(async (paymentNumber, opts) => {
1329
+ const { bookkeepInvoicePayment } = await import('./operations/invoice-payments.js');
1330
+ if (!(await confirmMutation(`Bookkeep invoice payment ${paymentNumber}`, opts))) {
1331
+ return;
1332
+ }
1333
+ const result = await bookkeepInvoicePayment(paymentNumber);
1334
+ outputConfirmation(`Invoice payment ${paymentNumber} bookkeept.`, json(), result);
1335
+ });
1336
+ // --- supplier-invoice-payments ---
1337
+ const supplierInvoicePayments = program
1338
+ .command('supplier-invoice-payments')
1339
+ .alias('sip')
1340
+ .description('Supplier invoice payment operations (utbetalningar)');
1341
+ supplierInvoicePayments
1342
+ .command('list')
1343
+ .description('List supplier invoice payments')
1344
+ .option('--invoice <number>', 'Filter by invoice number')
1345
+ .option('--page <number>', 'Page number', parseInt)
1346
+ .option('--limit <number>', 'Results per page', parseInt)
1347
+ .option('-a, --all', 'Fetch all pages')
1348
+ .action(async (opts) => {
1349
+ const { listSupplierInvoicePayments } = await import('./operations/supplier-invoice-payments.js');
1350
+ const data = await listSupplierInvoicePayments({
1351
+ invoiceNumber: opts.invoice,
1352
+ page: opts.page,
1353
+ limit: opts.limit,
1354
+ all: opts.all,
1355
+ });
1356
+ const envelope = data;
1357
+ outputList(envelope.SupplierInvoicePayments ?? [], supplierInvoicePaymentListColumns, json(), data, envelope.MetaInformation);
1358
+ });
1359
+ supplierInvoicePayments
1360
+ .command('get <paymentNumber>')
1361
+ .description('Get a single supplier invoice payment')
1362
+ .action(async (paymentNumber) => {
1363
+ const { getSupplierInvoicePayment } = await import('./operations/supplier-invoice-payments.js');
1364
+ const data = await getSupplierInvoicePayment(paymentNumber);
1365
+ outputDetail(data, supplierInvoicePaymentDetailColumns, json());
1366
+ });
1367
+ supplierInvoicePayments
1368
+ .command('create')
1369
+ .description('Register a payment against a supplier invoice')
1370
+ .requiredOption('--invoice <number>', 'Supplier invoice number')
1371
+ .requiredOption('--amount <amount>', 'Payment amount', parseFloat)
1372
+ .requiredOption('--date <date>', 'Payment date (YYYY-MM-DD)')
1373
+ .option('-y, --yes', 'Skip confirmation prompt')
1374
+ .option('--dry-run', 'Preview the request without sending it')
1375
+ .addHelpText('after', `
1376
+ Examples:
1377
+ noxctl supplier-invoice-payments create --invoice 501 --amount 3000 --date 2026-03-20 --dry-run
1378
+ noxctl sip create --invoice 501 --amount 3000 --date 2026-03-20 --yes`)
1379
+ .action(async (opts) => {
1380
+ const { createSupplierInvoicePayment } = await import('./operations/supplier-invoice-payments.js');
1381
+ const params = {
1382
+ InvoiceNumber: opts.invoice,
1383
+ Amount: opts.amount,
1384
+ PaymentDate: opts.date,
1385
+ };
1386
+ if (!(await confirmMutation(`Register payment of ${opts.amount} for supplier invoice ${opts.invoice}`, opts, { SupplierInvoicePayment: params }))) {
1387
+ return;
1388
+ }
1389
+ const data = await createSupplierInvoicePayment(params);
1390
+ outputDetail(data, supplierInvoicePaymentDetailColumns, json());
1391
+ });
1392
+ supplierInvoicePayments
1393
+ .command('delete <paymentNumber>')
1394
+ .description('Delete a supplier invoice payment')
1395
+ .option('-y, --yes', 'Skip confirmation prompt')
1396
+ .option('--dry-run', 'Preview the action without sending it')
1397
+ .action(async (paymentNumber, opts) => {
1398
+ const { deleteSupplierInvoicePayment } = await import('./operations/supplier-invoice-payments.js');
1399
+ if (!(await confirmMutation(`Delete supplier invoice payment ${paymentNumber}`, opts))) {
1400
+ return;
1401
+ }
1402
+ await deleteSupplierInvoicePayment(paymentNumber);
1403
+ outputConfirmation(`Supplier invoice payment ${paymentNumber} deleted.`, json(), {});
1404
+ });
1405
+ // --- offers ---
1406
+ const offers = program.command('offers').description('Offer/quote operations (offerter)');
1407
+ offers
1408
+ .command('list')
1409
+ .description('List/filter offers')
1410
+ .option('--filter <filter>', 'Filter: cancelled, expired, ordercreated, invoicecreated')
1411
+ .option('--customer <number>', 'Filter by customer number')
1412
+ .option('--from <date>', 'From date (YYYY-MM-DD)')
1413
+ .option('--to <date>', 'To date (YYYY-MM-DD)')
1414
+ .option('--page <number>', 'Page number', parseInt)
1415
+ .option('--limit <number>', 'Results per page', parseInt)
1416
+ .option('-a, --all', 'Fetch all pages')
1417
+ .action(async (opts) => {
1418
+ const { listOffers } = await import('./operations/offers.js');
1419
+ const data = await listOffers({
1420
+ filter: opts.filter,
1421
+ customerNumber: opts.customer,
1422
+ fromDate: opts.from,
1423
+ toDate: opts.to,
1424
+ page: opts.page,
1425
+ limit: opts.limit,
1426
+ all: opts.all,
1427
+ });
1428
+ const envelope = data;
1429
+ outputList(envelope.Offers ?? [], offerListColumns, json(), data, envelope.MetaInformation);
1430
+ });
1431
+ offers
1432
+ .command('get <documentNumber>')
1433
+ .description('Get a single offer')
1434
+ .action(async (documentNumber) => {
1435
+ const { getOffer } = await import('./operations/offers.js');
1436
+ const data = await getOffer(documentNumber);
1437
+ outputDetail(data, offerDetailColumns, json());
1438
+ });
1439
+ offers
1440
+ .command('create')
1441
+ .description('Create an offer')
1442
+ .requiredOption('--customer <number>', 'Customer number')
1443
+ .requiredOption('--input <file>', 'Offer data as JSON file (or - for stdin)')
1444
+ .option('-y, --yes', 'Skip confirmation prompt')
1445
+ .option('--dry-run', 'Preview the request without sending it')
1446
+ .addHelpText('after', `
1447
+ Examples:
1448
+ echo '{"OfferRows":[{"Description":"Consulting","DeliveredQuantity":10,"Price":1200}]}' | noxctl offers create --customer 25 --input - --dry-run`)
1449
+ .action(async (opts) => {
1450
+ const { createOffer } = await import('./operations/offers.js');
1451
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
1452
+ const input = JSON.parse(raw);
1453
+ const params = { CustomerNumber: opts.customer, ...input };
1454
+ if (!(await confirmMutation(`Create offer for customer ${opts.customer}`, opts, {
1455
+ Offer: params,
1456
+ }))) {
1457
+ return;
1458
+ }
1459
+ const data = await createOffer(params);
1460
+ outputDetail(data, offerDetailColumns, json());
1461
+ });
1462
+ offers
1463
+ .command('update <documentNumber>')
1464
+ .description('Update an offer')
1465
+ .requiredOption('--input <file>', 'Offer data as JSON file (or - for stdin)')
1466
+ .option('-y, --yes', 'Skip confirmation prompt')
1467
+ .option('--dry-run', 'Preview the request without sending it')
1468
+ .action(async (documentNumber, opts) => {
1469
+ const { updateOffer } = await import('./operations/offers.js');
1470
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
1471
+ const fields = JSON.parse(raw);
1472
+ if (!(await confirmMutation(`Update offer ${documentNumber}`, opts, { Offer: fields }))) {
1473
+ return;
1474
+ }
1475
+ const data = await updateOffer(documentNumber, fields);
1476
+ outputDetail(data, offerDetailColumns, json());
1477
+ });
1478
+ offers
1479
+ .command('create-invoice <documentNumber>')
1480
+ .description('Create an invoice from an offer')
1481
+ .option('-y, --yes', 'Skip confirmation prompt')
1482
+ .option('--dry-run', 'Preview the action without sending it')
1483
+ .action(async (documentNumber, opts) => {
1484
+ const { createInvoiceFromOffer } = await import('./operations/offers.js');
1485
+ if (!(await confirmMutation(`Create invoice from offer ${documentNumber}`, opts))) {
1486
+ return;
1487
+ }
1488
+ const data = await createInvoiceFromOffer(documentNumber);
1489
+ outputConfirmation(`Invoice created from offer ${documentNumber}.`, json(), data, invoiceConfirmColumns);
1490
+ });
1491
+ offers
1492
+ .command('create-order <documentNumber>')
1493
+ .description('Create an order from an offer')
1494
+ .option('-y, --yes', 'Skip confirmation prompt')
1495
+ .option('--dry-run', 'Preview the action without sending it')
1496
+ .action(async (documentNumber, opts) => {
1497
+ const { createOrderFromOffer } = await import('./operations/offers.js');
1498
+ if (!(await confirmMutation(`Create order from offer ${documentNumber}`, opts))) {
1499
+ return;
1500
+ }
1501
+ const data = await createOrderFromOffer(documentNumber);
1502
+ outputConfirmation(`Order created from offer ${documentNumber}.`, json(), data);
1503
+ });
1504
+ // --- orders ---
1505
+ const orders = program.command('orders').description('Order operations (ordrar)');
1506
+ orders
1507
+ .command('list')
1508
+ .description('List/filter orders')
1509
+ .option('--filter <filter>', 'Filter: cancelled, invoicecreated, invoicenotcreated')
1510
+ .option('--customer <number>', 'Filter by customer number')
1511
+ .option('--from <date>', 'From date (YYYY-MM-DD)')
1512
+ .option('--to <date>', 'To date (YYYY-MM-DD)')
1513
+ .option('--page <number>', 'Page number', parseInt)
1514
+ .option('--limit <number>', 'Results per page', parseInt)
1515
+ .option('-a, --all', 'Fetch all pages')
1516
+ .action(async (opts) => {
1517
+ const { listOrders } = await import('./operations/orders.js');
1518
+ const data = await listOrders({
1519
+ filter: opts.filter,
1520
+ customerNumber: opts.customer,
1521
+ fromDate: opts.from,
1522
+ toDate: opts.to,
1523
+ page: opts.page,
1524
+ limit: opts.limit,
1525
+ all: opts.all,
1526
+ });
1527
+ const envelope = data;
1528
+ outputList(envelope.Orders ?? [], orderListColumns, json(), data, envelope.MetaInformation);
1529
+ });
1530
+ orders
1531
+ .command('get <documentNumber>')
1532
+ .description('Get a single order')
1533
+ .action(async (documentNumber) => {
1534
+ const { getOrder } = await import('./operations/orders.js');
1535
+ const data = await getOrder(documentNumber);
1536
+ outputDetail(data, orderDetailColumns, json());
1537
+ });
1538
+ orders
1539
+ .command('create')
1540
+ .description('Create an order')
1541
+ .requiredOption('--customer <number>', 'Customer number')
1542
+ .requiredOption('--input <file>', 'Order data as JSON file (or - for stdin)')
1543
+ .option('-y, --yes', 'Skip confirmation prompt')
1544
+ .option('--dry-run', 'Preview the request without sending it')
1545
+ .addHelpText('after', `
1546
+ Examples:
1547
+ echo '{"OrderRows":[{"Description":"Consulting","DeliveredQuantity":10,"Price":1200}]}' | noxctl orders create --customer 25 --input - --dry-run`)
1548
+ .action(async (opts) => {
1549
+ const { createOrder } = await import('./operations/orders.js');
1550
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
1551
+ const input = JSON.parse(raw);
1552
+ const params = { CustomerNumber: opts.customer, ...input };
1553
+ if (!(await confirmMutation(`Create order for customer ${opts.customer}`, opts, {
1554
+ Order: params,
1555
+ }))) {
1556
+ return;
1557
+ }
1558
+ const data = await createOrder(params);
1559
+ outputDetail(data, orderDetailColumns, json());
1560
+ });
1561
+ orders
1562
+ .command('update <documentNumber>')
1563
+ .description('Update an order')
1564
+ .requiredOption('--input <file>', 'Order data as JSON file (or - for stdin)')
1565
+ .option('-y, --yes', 'Skip confirmation prompt')
1566
+ .option('--dry-run', 'Preview the request without sending it')
1567
+ .action(async (documentNumber, opts) => {
1568
+ const { updateOrder } = await import('./operations/orders.js');
1569
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
1570
+ const fields = JSON.parse(raw);
1571
+ if (!(await confirmMutation(`Update order ${documentNumber}`, opts, { Order: fields }))) {
1572
+ return;
1573
+ }
1574
+ const data = await updateOrder(documentNumber, fields);
1575
+ outputDetail(data, orderDetailColumns, json());
1576
+ });
1577
+ orders
1578
+ .command('create-invoice <documentNumber>')
1579
+ .description('Create an invoice from an order')
1580
+ .option('-y, --yes', 'Skip confirmation prompt')
1581
+ .option('--dry-run', 'Preview the action without sending it')
1582
+ .action(async (documentNumber, opts) => {
1583
+ const { createInvoiceFromOrder } = await import('./operations/orders.js');
1584
+ if (!(await confirmMutation(`Create invoice from order ${documentNumber}`, opts))) {
1585
+ return;
1586
+ }
1587
+ const data = await createInvoiceFromOrder(documentNumber);
1588
+ outputConfirmation(`Invoice created from order ${documentNumber}.`, json(), data, invoiceConfirmColumns);
1589
+ });
1590
+ // --- projects ---
1591
+ const projects = program.command('projects').description('Project operations');
1592
+ projects
1593
+ .command('list')
1594
+ .description('List projects')
1595
+ .option('--page <number>', 'Page number', parseInt)
1596
+ .option('--limit <number>', 'Results per page', parseInt)
1597
+ .option('-a, --all', 'Fetch all pages')
1598
+ .action(async (opts) => {
1599
+ const { listProjects } = await import('./operations/projects.js');
1600
+ const data = await listProjects({
1601
+ page: opts.page,
1602
+ limit: opts.limit,
1603
+ all: opts.all,
1604
+ });
1605
+ const envelope = data;
1606
+ outputList(envelope.Projects ?? [], projectListColumns, json(), data, envelope.MetaInformation);
1607
+ });
1608
+ projects
1609
+ .command('get <projectNumber>')
1610
+ .description('Get a single project')
1611
+ .action(async (projectNumber) => {
1612
+ const { getProject } = await import('./operations/projects.js');
1613
+ const data = await getProject(projectNumber);
1614
+ outputDetail(data, projectDetailColumns, json());
1615
+ });
1616
+ projects
1617
+ .command('create')
1618
+ .description('Create a project')
1619
+ .requiredOption('--description <text>', 'Project description')
1620
+ .option('--project-number <number>', 'Project number (auto-generated if omitted)')
1621
+ .option('--status <status>', 'Status (ONGOING or COMPLETED)')
1622
+ .option('--input <file>', 'Project data as JSON file (or - for stdin)')
1623
+ .option('-y, --yes', 'Skip confirmation prompt')
1624
+ .option('--dry-run', 'Preview the request without sending it')
1625
+ .action(async (opts) => {
1626
+ const { createProject } = await import('./operations/projects.js');
1627
+ let input = {};
1628
+ if (opts.input) {
1629
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
1630
+ input = JSON.parse(raw);
1631
+ }
1632
+ const params = { ...input, Description: opts.description };
1633
+ if (opts.projectNumber)
1634
+ params.ProjectNumber = opts.projectNumber;
1635
+ if (opts.status)
1636
+ params.Status = opts.status;
1637
+ if (!(await confirmMutation(`Create project "${opts.description}"`, opts, { Project: params }))) {
1638
+ return;
1639
+ }
1640
+ const data = await createProject(params);
1641
+ outputDetail(data, projectDetailColumns, json());
1642
+ });
1643
+ projects
1644
+ .command('update <projectNumber>')
1645
+ .description('Update a project')
1646
+ .requiredOption('--input <file>', 'Project data as JSON file (or - for stdin)')
1647
+ .option('-y, --yes', 'Skip confirmation prompt')
1648
+ .option('--dry-run', 'Preview the request without sending it')
1649
+ .action(async (projectNumber, opts) => {
1650
+ const { updateProject } = await import('./operations/projects.js');
1651
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
1652
+ const fields = JSON.parse(raw);
1653
+ if (!(await confirmMutation(`Update project ${projectNumber}`, opts, { Project: fields }))) {
1654
+ return;
1655
+ }
1656
+ const data = await updateProject(projectNumber, fields);
1657
+ outputDetail(data, projectDetailColumns, json());
1658
+ });
1659
+ // --- cost centers ---
1660
+ const costcenters = program.command('costcenters').description('Cost center operations');
1661
+ costcenters
1662
+ .command('list')
1663
+ .description('List cost centers')
1664
+ .option('--page <number>', 'Page number', parseInt)
1665
+ .option('--limit <number>', 'Results per page', parseInt)
1666
+ .option('-a, --all', 'Fetch all pages')
1667
+ .action(async (opts) => {
1668
+ const { listCostCenters } = await import('./operations/costcenters.js');
1669
+ const data = await listCostCenters({
1670
+ page: opts.page,
1671
+ limit: opts.limit,
1672
+ all: opts.all,
1673
+ });
1674
+ const envelope = data;
1675
+ outputList(envelope.CostCenters ?? [], costCenterListColumns, json(), data, envelope.MetaInformation);
1676
+ });
1677
+ costcenters
1678
+ .command('get <code>')
1679
+ .description('Get a single cost center')
1680
+ .action(async (code) => {
1681
+ const { getCostCenter } = await import('./operations/costcenters.js');
1682
+ const data = await getCostCenter(code);
1683
+ outputDetail(data, costCenterDetailColumns, json());
1684
+ });
1685
+ costcenters
1686
+ .command('create')
1687
+ .description('Create a cost center')
1688
+ .requiredOption('--code <code>', 'Cost center code')
1689
+ .requiredOption('--description <text>', 'Description')
1690
+ .option('--input <file>', 'Cost center data as JSON file (or - for stdin)')
1691
+ .option('-y, --yes', 'Skip confirmation prompt')
1692
+ .option('--dry-run', 'Preview the request without sending it')
1693
+ .action(async (opts) => {
1694
+ const { createCostCenter } = await import('./operations/costcenters.js');
1695
+ let input = {};
1696
+ if (opts.input) {
1697
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
1698
+ input = JSON.parse(raw);
1699
+ }
1700
+ const params = {
1701
+ ...input,
1702
+ Code: opts.code,
1703
+ Description: opts.description,
1704
+ };
1705
+ if (!(await confirmMutation(`Create cost center "${opts.code}"`, opts, { CostCenter: params }))) {
1706
+ return;
1707
+ }
1708
+ const data = await createCostCenter(params);
1709
+ outputDetail(data, costCenterDetailColumns, json());
1710
+ });
1711
+ costcenters
1712
+ .command('update <code>')
1713
+ .description('Update a cost center')
1714
+ .requiredOption('--input <file>', 'Cost center data as JSON file (or - for stdin)')
1715
+ .option('-y, --yes', 'Skip confirmation prompt')
1716
+ .option('--dry-run', 'Preview the request without sending it')
1717
+ .action(async (code, opts) => {
1718
+ const { updateCostCenter } = await import('./operations/costcenters.js');
1719
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
1720
+ const fields = JSON.parse(raw);
1721
+ if (!(await confirmMutation(`Update cost center ${code}`, opts, { CostCenter: fields }))) {
1722
+ return;
1723
+ }
1724
+ const data = await updateCostCenter(code, fields);
1725
+ outputDetail(data, costCenterDetailColumns, json());
1726
+ });
1727
+ costcenters
1728
+ .command('delete <code>')
1729
+ .description('Delete a cost center')
1730
+ .option('-y, --yes', 'Skip confirmation prompt')
1731
+ .option('--dry-run', 'Preview the request without sending it')
1732
+ .action(async (code, opts) => {
1733
+ const { deleteCostCenter } = await import('./operations/costcenters.js');
1734
+ if (!(await confirmMutation(`Delete cost center ${code}`, opts))) {
1735
+ return;
1736
+ }
1737
+ await deleteCostCenter(code);
1738
+ console.log(`Cost center ${code} deleted.`);
1739
+ });
1740
+ // --- tax reductions (ROT/RUT) ---
1741
+ const taxreductions = program
1742
+ .command('tax-reductions')
1743
+ .description('Tax reduction (ROT/RUT) operations');
1744
+ taxreductions
1745
+ .command('list')
1746
+ .description('List tax reductions')
1747
+ .option('--filter <type>', 'Filter by document type (invoices, offers, orders)')
1748
+ .option('--page <number>', 'Page number', parseInt)
1749
+ .option('--limit <number>', 'Results per page', parseInt)
1750
+ .option('-a, --all', 'Fetch all pages')
1751
+ .action(async (opts) => {
1752
+ const { listTaxReductions } = await import('./operations/taxreductions.js');
1753
+ const data = await listTaxReductions({
1754
+ filter: opts.filter,
1755
+ page: opts.page,
1756
+ limit: opts.limit,
1757
+ all: opts.all,
1758
+ });
1759
+ const envelope = data;
1760
+ outputList(envelope.TaxReductions ?? [], taxReductionListColumns, json(), data, envelope.MetaInformation);
1761
+ });
1762
+ taxreductions
1763
+ .command('get <id>')
1764
+ .description('Get a single tax reduction')
1765
+ .action(async (id) => {
1766
+ const { getTaxReduction } = await import('./operations/taxreductions.js');
1767
+ const data = await getTaxReduction(parseInt(id, 10));
1768
+ outputDetail(data, taxReductionDetailColumns, json());
1769
+ });
1770
+ taxreductions
1771
+ .command('create')
1772
+ .description('Create a tax reduction (ROT/RUT)')
1773
+ .requiredOption('--reference <number>', 'Reference number (e.g. invoice number)')
1774
+ .requiredOption('--type <type>', 'Type of reduction (rot or rut)')
1775
+ .requiredOption('--document-type <type>', 'Document type (INVOICE, OFFER, ORDER)')
1776
+ .requiredOption('--customer-name <name>', 'Customer name')
1777
+ .requiredOption('--amount <amount>', 'Asked amount in öre', parseInt)
1778
+ .option('--property <designation>', 'Property designation (required for ROT)')
1779
+ .option('--input <file>', 'Tax reduction data as JSON file (or - for stdin)')
1780
+ .option('-y, --yes', 'Skip confirmation prompt')
1781
+ .option('--dry-run', 'Preview the request without sending it')
1782
+ .action(async (opts) => {
1783
+ const { createTaxReduction } = await import('./operations/taxreductions.js');
1784
+ let input = {};
1785
+ if (opts.input) {
1786
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
1787
+ input = JSON.parse(raw);
1788
+ }
1789
+ const params = {
1790
+ ...input,
1791
+ ReferenceNumber: opts.reference,
1792
+ ReferenceDocumentType: opts.documentType,
1793
+ TypeOfReduction: opts.type,
1794
+ CustomerName: opts.customerName,
1795
+ AskedAmount: opts.amount,
1796
+ };
1797
+ if (opts.property)
1798
+ params.PropertyDesignation = opts.property;
1799
+ if (!(await confirmMutation(`Create ${opts.type.toUpperCase()} tax reduction for ref ${opts.reference}`, opts, { TaxReduction: params }))) {
1800
+ return;
1801
+ }
1802
+ const data = await createTaxReduction(params);
1803
+ outputDetail(data, taxReductionDetailColumns, json());
1804
+ });
1805
+ // --- price lists ---
1806
+ const pricelists = program.command('pricelists').description('Price list operations');
1807
+ pricelists
1808
+ .command('list')
1809
+ .description('List price lists')
1810
+ .option('--page <number>', 'Page number', parseInt)
1811
+ .option('--limit <number>', 'Results per page', parseInt)
1812
+ .option('-a, --all', 'Fetch all pages')
1813
+ .action(async (opts) => {
1814
+ const { listPriceLists } = await import('./operations/pricelists.js');
1815
+ const data = await listPriceLists({
1816
+ page: opts.page,
1817
+ limit: opts.limit,
1818
+ all: opts.all,
1819
+ });
1820
+ const envelope = data;
1821
+ outputList(envelope.PriceLists ?? [], priceListListColumns, json(), data, envelope.MetaInformation);
1822
+ });
1823
+ pricelists
1824
+ .command('get <code>')
1825
+ .description('Get a single price list')
1826
+ .action(async (code) => {
1827
+ const { getPriceList } = await import('./operations/pricelists.js');
1828
+ const data = await getPriceList(code);
1829
+ outputDetail(data, priceListDetailColumns, json());
1830
+ });
1831
+ pricelists
1832
+ .command('create')
1833
+ .description('Create a price list')
1834
+ .requiredOption('--code <code>', 'Price list code')
1835
+ .requiredOption('--description <text>', 'Description')
1836
+ .option('--input <file>', 'Price list data as JSON file (or - for stdin)')
1837
+ .option('-y, --yes', 'Skip confirmation prompt')
1838
+ .option('--dry-run', 'Preview the request without sending it')
1839
+ .action(async (opts) => {
1840
+ const { createPriceList } = await import('./operations/pricelists.js');
1841
+ let input = {};
1842
+ if (opts.input) {
1843
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
1844
+ input = JSON.parse(raw);
1845
+ }
1846
+ const params = {
1847
+ ...input,
1848
+ Code: opts.code,
1849
+ Description: opts.description,
1850
+ };
1851
+ if (!(await confirmMutation(`Create price list "${opts.code}"`, opts, { PriceList: params }))) {
1852
+ return;
1853
+ }
1854
+ const data = await createPriceList(params);
1855
+ outputDetail(data, priceListDetailColumns, json());
1856
+ });
1857
+ pricelists
1858
+ .command('update <code>')
1859
+ .description('Update a price list')
1860
+ .requiredOption('--input <file>', 'Price list data as JSON file (or - for stdin)')
1861
+ .option('-y, --yes', 'Skip confirmation prompt')
1862
+ .option('--dry-run', 'Preview the request without sending it')
1863
+ .action(async (code, opts) => {
1864
+ const { updatePriceList } = await import('./operations/pricelists.js');
1865
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
1866
+ const fields = JSON.parse(raw);
1867
+ if (!(await confirmMutation(`Update price list ${code}`, opts, { PriceList: fields }))) {
1868
+ return;
1869
+ }
1870
+ const data = await updatePriceList(code, fields);
1871
+ outputDetail(data, priceListDetailColumns, json());
1872
+ });
1873
+ // --- prices ---
1874
+ const prices = program.command('prices').description('Price operations within price lists');
1875
+ prices
1876
+ .command('list')
1877
+ .description('List prices in a price list')
1878
+ .requiredOption('--pricelist <code>', 'Price list code')
1879
+ .option('--article <number>', 'Filter by article number')
1880
+ .option('--page <number>', 'Page number', parseInt)
1881
+ .option('--limit <number>', 'Results per page', parseInt)
1882
+ .action(async (opts) => {
1883
+ const { listPrices } = await import('./operations/pricelists.js');
1884
+ const data = await listPrices({
1885
+ priceListCode: opts.pricelist,
1886
+ articleNumber: opts.article,
1887
+ page: opts.page,
1888
+ limit: opts.limit,
1889
+ });
1890
+ const envelope = data;
1891
+ outputList(envelope.Prices ?? [], priceListColumns, json(), data, envelope.MetaInformation);
1892
+ });
1893
+ prices
1894
+ .command('get')
1895
+ .description('Get a specific price')
1896
+ .requiredOption('--pricelist <code>', 'Price list code')
1897
+ .requiredOption('--article <number>', 'Article number')
1898
+ .option('--from-quantity <number>', 'From quantity (default 0)', parseInt)
1899
+ .action(async (opts) => {
1900
+ const { getPrice } = await import('./operations/pricelists.js');
1901
+ const data = await getPrice(opts.pricelist, opts.article, opts.fromQuantity);
1902
+ outputDetail(data, priceDetailColumns, json());
1903
+ });
1904
+ prices
1905
+ .command('update')
1906
+ .description('Update a price')
1907
+ .requiredOption('--pricelist <code>', 'Price list code')
1908
+ .requiredOption('--article <number>', 'Article number')
1909
+ .option('--from-quantity <number>', 'From quantity (default 0)', parseInt)
1910
+ .requiredOption('--input <file>', 'Price data as JSON file (or - for stdin)')
1911
+ .option('-y, --yes', 'Skip confirmation prompt')
1912
+ .option('--dry-run', 'Preview the request without sending it')
1913
+ .action(async (opts) => {
1914
+ const { updatePrice } = await import('./operations/pricelists.js');
1915
+ const raw = opts.input === '-' ? readFileSync(0, 'utf-8') : readFileSync(opts.input, 'utf-8');
1916
+ const fields = JSON.parse(raw);
1917
+ if (!(await confirmMutation(`Update price ${opts.pricelist}/${opts.article}`, opts, {
1918
+ Price: fields,
1919
+ }))) {
1920
+ return;
1921
+ }
1922
+ const data = await updatePrice(opts.pricelist, opts.article, fields, opts.fromQuantity);
1923
+ outputDetail(data, priceDetailColumns, json());
1924
+ });
993
1925
  // Error handling
994
1926
  program.exitOverride();
995
1927
  try {