@aderis-erp/cli 1.1.4 → 1.1.5

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 (85) hide show
  1. package/dist/commands/access-groups.js +42 -8
  2. package/dist/commands/account-transaction-allocations.js +38 -4
  3. package/dist/commands/account-transactions.d.ts +2 -0
  4. package/dist/commands/account-transactions.js +68 -32
  5. package/dist/commands/accounts.js +46 -12
  6. package/dist/commands/agent-platform.js +37 -3
  7. package/dist/commands/agent-runs.js +49 -15
  8. package/dist/commands/analytics.js +39 -5
  9. package/dist/commands/api-clients.js +47 -13
  10. package/dist/commands/api-keys.js +40 -6
  11. package/dist/commands/async-jobs.js +45 -11
  12. package/dist/commands/audit-log.js +38 -4
  13. package/dist/commands/bank-accounts.js +54 -20
  14. package/dist/commands/bank-integrations.js +56 -22
  15. package/dist/commands/business-partners.js +77 -43
  16. package/dist/commands/chat.js +43 -9
  17. package/dist/commands/credit-card-accounts.js +57 -23
  18. package/dist/commands/currencies.js +37 -3
  19. package/dist/commands/custody-accounts.js +41 -7
  20. package/dist/commands/dashboards.js +52 -18
  21. package/dist/commands/delivery-notes.js +69 -35
  22. package/dist/commands/document-checksums.d.ts +1 -0
  23. package/dist/commands/document-checksums.js +38 -3
  24. package/dist/commands/dunning-configs.js +41 -7
  25. package/dist/commands/dunning-reminders.js +44 -10
  26. package/dist/commands/dunning-runs.js +37 -3
  27. package/dist/commands/email-transactions.js +39 -5
  28. package/dist/commands/employees.js +77 -43
  29. package/dist/commands/entity-configurations.js +39 -5
  30. package/dist/commands/event-mapping-rules.js +37 -3
  31. package/dist/commands/event-processor-mappings.js +41 -7
  32. package/dist/commands/files.js +48 -14
  33. package/dist/commands/financial-instrument-transactions.d.ts +56 -0
  34. package/dist/commands/financial-instrument-transactions.js +1369 -0
  35. package/dist/commands/financial-instrument-types.js +41 -7
  36. package/dist/commands/financial-instruments.js +41 -7
  37. package/dist/commands/health.js +41 -7
  38. package/dist/commands/imports.js +39 -5
  39. package/dist/commands/inbox.js +42 -8
  40. package/dist/commands/integrations.js +39 -5
  41. package/dist/commands/item-categories.js +41 -7
  42. package/dist/commands/items.js +42 -8
  43. package/dist/commands/journal-lines.js +37 -3
  44. package/dist/commands/journals.js +61 -27
  45. package/dist/commands/kpis.js +44 -10
  46. package/dist/commands/ledgers.js +83 -49
  47. package/dist/commands/my-account.js +54 -20
  48. package/dist/commands/notifications.js +37 -3
  49. package/dist/commands/office-locations.js +41 -7
  50. package/dist/commands/payment-methods.js +46 -12
  51. package/dist/commands/payment-runs.js +54 -20
  52. package/dist/commands/payment-terms.js +42 -8
  53. package/dist/commands/payments.js +43 -9
  54. package/dist/commands/payroll-runs.js +71 -37
  55. package/dist/commands/permissions.js +37 -3
  56. package/dist/commands/price-lists.js +48 -14
  57. package/dist/commands/processor-mappings.js +40 -6
  58. package/dist/commands/processors.js +46 -12
  59. package/dist/commands/purchase-order-types.js +41 -7
  60. package/dist/commands/purchase-orders.js +60 -26
  61. package/dist/commands/reports.js +43 -8
  62. package/dist/commands/roles.js +40 -6
  63. package/dist/commands/salary-types.js +41 -7
  64. package/dist/commands/sales-invoices.js +85 -51
  65. package/dist/commands/sales-orders.js +69 -35
  66. package/dist/commands/saved-views.js +39 -5
  67. package/dist/commands/scheduled-processor-mappings.js +41 -7
  68. package/dist/commands/schedules.js +45 -11
  69. package/dist/commands/settings.js +38 -4
  70. package/dist/commands/tags.js +42 -8
  71. package/dist/commands/tax-codes.js +44 -10
  72. package/dist/commands/tax-matrix.js +41 -7
  73. package/dist/commands/templates.js +49 -15
  74. package/dist/commands/tenants.js +44 -10
  75. package/dist/commands/tools.js +38 -4
  76. package/dist/commands/users.js +58 -24
  77. package/dist/commands/vat-declarations.js +61 -27
  78. package/dist/commands/vendor-invoices.js +81 -47
  79. package/dist/commands/webhooks.js +43 -9
  80. package/dist/commands/work-summary.js +37 -3
  81. package/dist/lib/paths.d.ts +8 -0
  82. package/dist/lib/paths.js +16 -0
  83. package/dist/lib/production-guard.js +2 -0
  84. package/oclif.manifest.json +199 -2
  85. package/package.json +9 -3
@@ -1,6 +1,7 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
+ import { resolveUserPath } from '../lib/paths.js';
4
5
  // 'upsert' loops create/update from the CLI, one request per record. It exists
5
6
  // only where the API has no import endpoint of its own; those keep the name
6
7
  // 'import' and are listed in SERVER_IMPORTS.
@@ -9,13 +10,14 @@ const HAS_EXPORT = false;
9
10
  const HAS_CAMT053 = false;
10
11
  const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = [];
12
- const SUBCOMMANDS = [...["profit-and-loss", "balance-sheet", "gl-detail", "gl-detail-export-xlsx", "gl-detail-export-pdf"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
+ const SUBCOMMANDS = [...["profit-and-loss", "balance-sheet", "gl-detail", "gl-detail-export-xlsx", "gl-detail-export-pdf", "financial-instrument-holdings"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'profit-and-loss': { method: 'POST', path: '/v1/reports/profit-and-loss', summary: 'Get Profit & Loss report', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: true, bodyArrayFields: ['ledgerIds', 'periods'] },
15
- 'balance-sheet': { method: 'GET', path: '/v1/reports/balance-sheet', summary: 'Generate Balance Sheet report', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
- 'gl-detail': { method: 'POST', path: '/v1/reports/gl-detail', summary: 'Get GL Detail report', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['accountIds', 'accountCodes'] },
17
- 'gl-detail-export-xlsx': { method: 'POST', path: '/v1/reports/gl-detail/export/xlsx', summary: 'Export GL Detail report as Excel (one sheet per account)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: ['accountIds', 'accountCodes'] },
18
- 'gl-detail-export-pdf': { method: 'POST', path: '/v1/reports/gl-detail/export/pdf', summary: 'Export GL Detail report as PDF (one page per account)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: ['accountIds', 'accountCodes'] },
15
+ 'profit-and-loss': { method: 'POST', path: '/v1/reports/profit-and-loss', summary: 'Get Profit & Loss report', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: true, bodyArrayFields: ['ledgerIds', 'periods'], bodyMediaType: '', specParams: [] },
16
+ 'balance-sheet': { method: 'GET', path: '/v1/reports/balance-sheet', summary: 'Generate Balance Sheet report', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
17
+ 'gl-detail': { method: 'POST', path: '/v1/reports/gl-detail', summary: 'Get GL Detail report', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['accountIds', 'accountCodes'], bodyMediaType: '', specParams: [] },
18
+ 'gl-detail-export-xlsx': { method: 'POST', path: '/v1/reports/gl-detail/export/xlsx', summary: 'Export GL Detail report as Excel (one sheet per account)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: ['accountIds', 'accountCodes'], bodyMediaType: '', specParams: [] },
19
+ 'gl-detail-export-pdf': { method: 'POST', path: '/v1/reports/gl-detail/export/pdf', summary: 'Export GL Detail report as PDF (one page per account)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: ['accountIds', 'accountCodes'], bodyMediaType: '', specParams: [] },
20
+ 'financial-instrument-holdings': { method: 'GET', path: '/v1/reports/financial-instrument-holdings', summary: 'Get financial-instrument holdings report', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
21
  };
20
22
  export default class Reports extends Command {
21
23
  static description = 'Manage reports';
@@ -30,7 +32,7 @@ export default class Reports extends Command {
30
32
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
31
33
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
32
34
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
33
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
35
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
34
36
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
35
37
  page: Flags.string({ description: 'Page number' }),
36
38
  limit: Flags.string({ description: 'Items per page' }),
@@ -161,6 +163,17 @@ export default class Reports extends Command {
161
163
  }
162
164
  }
163
165
  }
166
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
167
+ // have them: their body is not JSON, so --set cannot carry a value there.
168
+ for (const param of endpoint.specParams) {
169
+ const value = flags[param.flag];
170
+ if (value === undefined || value === '') {
171
+ if (param.required)
172
+ this.error(`Missing required flag: --${param.flag}`);
173
+ continue;
174
+ }
175
+ queryParams[param.name] = value;
176
+ }
164
177
  let data;
165
178
  if (endpoint.isMultipart) {
166
179
  if (!flags.file)
@@ -181,6 +194,25 @@ export default class Reports extends Command {
181
194
  }
182
195
  data = form;
183
196
  }
197
+ else if (endpoint.bodyMediaType) {
198
+ // The body is not JSON (today only application/x-ndjson): it is sent
199
+ // verbatim, under the content type the endpoint asks for.
200
+ if (flags.file) {
201
+ const fsModule = await import('node:fs');
202
+ try {
203
+ data = fsModule.readFileSync(flags.file, 'utf-8');
204
+ }
205
+ catch {
206
+ this.error(`Failed to read file: ${flags.file}`);
207
+ }
208
+ }
209
+ else if (flags.data) {
210
+ data = flags.data;
211
+ }
212
+ else {
213
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
214
+ }
215
+ }
184
216
  else if (endpoint.hasBody) {
185
217
  // For binary-response endpoints, --file is the output path, not a JSON
186
218
  // input file — skip the read-as-body branch.
@@ -276,7 +308,7 @@ export default class Reports extends Command {
276
308
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
277
309
  }
278
310
  else if (data) {
279
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
311
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
280
312
  }
281
313
  process.stderr.write('\n');
282
314
  }
@@ -290,6 +322,7 @@ export default class Reports extends Command {
290
322
  const binResponse = await apiRequest(endpoint.method, apiPath, {
291
323
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
292
324
  data,
325
+ contentType: endpoint.bodyMediaType || undefined,
293
326
  contextName: flags.context,
294
327
  binary: true,
295
328
  });
@@ -315,6 +348,7 @@ export default class Reports extends Command {
315
348
  const raw = await apiRequest(endpoint.method, apiPath, {
316
349
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
317
350
  data,
351
+ contentType: endpoint.bodyMediaType || undefined,
318
352
  contextName: flags.context,
319
353
  binary: true,
320
354
  });
@@ -335,6 +369,7 @@ export default class Reports extends Command {
335
369
  response = await apiRequest(endpoint.method, apiPath, {
336
370
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
337
371
  data,
372
+ contentType: endpoint.bodyMediaType || undefined,
338
373
  contextName: flags.context,
339
374
  acceptAny: true,
340
375
  });
@@ -1,6 +1,7 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
+ import { resolveUserPath } from '../lib/paths.js';
4
5
  // 'upsert' loops create/update from the CLI, one request per record. It exists
5
6
  // only where the API has no import endpoint of its own; those keep the name
6
7
  // 'import' and are listed in SERVER_IMPORTS.
@@ -11,10 +12,10 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = [];
12
13
  const SUBCOMMANDS = [...["list", "create", "update", "delete"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list': { method: 'GET', path: '/v1/roles', summary: 'List the roles of the current tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'create': { method: 'POST', path: '/v1/roles', summary: 'Create a role for the current tenant', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['permissionIds'] },
16
- 'update': { method: 'PATCH', path: '/v1/roles/{roleId}', summary: 'Change a tenant-created role', pathParams: ['roleId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['permissionIds'] },
17
- 'delete': { method: 'DELETE', path: '/v1/roles/{roleId}', summary: 'Delete a tenant-created role', pathParams: ['roleId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/roles', summary: 'List the roles of the current tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'create': { method: 'POST', path: '/v1/roles', summary: 'Create a role for the current tenant', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['permissionIds'], bodyMediaType: '', specParams: [] },
17
+ 'update': { method: 'PATCH', path: '/v1/roles/{roleId}', summary: 'Change a tenant-created role', pathParams: ['roleId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['permissionIds'], bodyMediaType: '', specParams: [] },
18
+ 'delete': { method: 'DELETE', path: '/v1/roles/{roleId}', summary: 'Delete a tenant-created role', pathParams: ['roleId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
19
  };
19
20
  export default class Roles extends Command {
20
21
  static description = 'Manage roles';
@@ -35,7 +36,7 @@ export default class Roles extends Command {
35
36
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
36
37
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
37
38
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
38
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
39
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
39
40
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
40
41
  page: Flags.string({ description: 'Page number' }),
41
42
  limit: Flags.string({ description: 'Items per page' }),
@@ -166,6 +167,17 @@ export default class Roles extends Command {
166
167
  }
167
168
  }
168
169
  }
170
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
171
+ // have them: their body is not JSON, so --set cannot carry a value there.
172
+ for (const param of endpoint.specParams) {
173
+ const value = flags[param.flag];
174
+ if (value === undefined || value === '') {
175
+ if (param.required)
176
+ this.error(`Missing required flag: --${param.flag}`);
177
+ continue;
178
+ }
179
+ queryParams[param.name] = value;
180
+ }
169
181
  let data;
170
182
  if (endpoint.isMultipart) {
171
183
  if (!flags.file)
@@ -186,6 +198,25 @@ export default class Roles extends Command {
186
198
  }
187
199
  data = form;
188
200
  }
201
+ else if (endpoint.bodyMediaType) {
202
+ // The body is not JSON (today only application/x-ndjson): it is sent
203
+ // verbatim, under the content type the endpoint asks for.
204
+ if (flags.file) {
205
+ const fsModule = await import('node:fs');
206
+ try {
207
+ data = fsModule.readFileSync(flags.file, 'utf-8');
208
+ }
209
+ catch {
210
+ this.error(`Failed to read file: ${flags.file}`);
211
+ }
212
+ }
213
+ else if (flags.data) {
214
+ data = flags.data;
215
+ }
216
+ else {
217
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
218
+ }
219
+ }
189
220
  else if (endpoint.hasBody) {
190
221
  // For binary-response endpoints, --file is the output path, not a JSON
191
222
  // input file — skip the read-as-body branch.
@@ -281,7 +312,7 @@ export default class Roles extends Command {
281
312
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
282
313
  }
283
314
  else if (data) {
284
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
315
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
285
316
  }
286
317
  process.stderr.write('\n');
287
318
  }
@@ -295,6 +326,7 @@ export default class Roles extends Command {
295
326
  const binResponse = await apiRequest(endpoint.method, apiPath, {
296
327
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
297
328
  data,
329
+ contentType: endpoint.bodyMediaType || undefined,
298
330
  contextName: flags.context,
299
331
  binary: true,
300
332
  });
@@ -320,6 +352,7 @@ export default class Roles extends Command {
320
352
  const raw = await apiRequest(endpoint.method, apiPath, {
321
353
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
322
354
  data,
355
+ contentType: endpoint.bodyMediaType || undefined,
323
356
  contextName: flags.context,
324
357
  binary: true,
325
358
  });
@@ -340,6 +373,7 @@ export default class Roles extends Command {
340
373
  response = await apiRequest(endpoint.method, apiPath, {
341
374
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
342
375
  data,
376
+ contentType: endpoint.bodyMediaType || undefined,
343
377
  contextName: flags.context,
344
378
  acceptAny: true,
345
379
  });
@@ -1,6 +1,7 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
+ import { resolveUserPath } from '../lib/paths.js';
4
5
  // 'upsert' loops create/update from the CLI, one request per record. It exists
5
6
  // only where the API has no import endpoint of its own; those keep the name
6
7
  // 'import' and are listed in SERVER_IMPORTS.
@@ -11,11 +12,11 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = [];
12
13
  const SUBCOMMANDS = [...["list", "create", "get", "update", "delete"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list': { method: 'GET', path: '/v1/salary-types', summary: 'Get all salary types with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'create': { method: 'POST', path: '/v1/salary-types', summary: 'Create a new salary type', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
- 'get': { method: 'GET', path: '/v1/salary-types/{id}', summary: 'Get a salary type by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'update': { method: 'PATCH', path: '/v1/salary-types/{id}', summary: 'Update a salary type', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
- 'delete': { method: 'DELETE', path: '/v1/salary-types/{id}', summary: 'Delete a salary type', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/salary-types', summary: 'Get all salary types with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'create': { method: 'POST', path: '/v1/salary-types', summary: 'Create a new salary type', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
17
+ 'get': { method: 'GET', path: '/v1/salary-types/{id}', summary: 'Get a salary type by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'update': { method: 'PATCH', path: '/v1/salary-types/{id}', summary: 'Update a salary type', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
+ 'delete': { method: 'DELETE', path: '/v1/salary-types/{id}', summary: 'Delete a salary type', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
20
  };
20
21
  export default class SalaryTypes extends Command {
21
22
  static description = 'Manage salary types';
@@ -37,7 +38,7 @@ export default class SalaryTypes extends Command {
37
38
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
38
39
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
39
40
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
40
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
41
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
41
42
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
42
43
  page: Flags.string({ description: 'Page number' }),
43
44
  limit: Flags.string({ description: 'Items per page' }),
@@ -168,6 +169,17 @@ export default class SalaryTypes extends Command {
168
169
  }
169
170
  }
170
171
  }
172
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
173
+ // have them: their body is not JSON, so --set cannot carry a value there.
174
+ for (const param of endpoint.specParams) {
175
+ const value = flags[param.flag];
176
+ if (value === undefined || value === '') {
177
+ if (param.required)
178
+ this.error(`Missing required flag: --${param.flag}`);
179
+ continue;
180
+ }
181
+ queryParams[param.name] = value;
182
+ }
171
183
  let data;
172
184
  if (endpoint.isMultipart) {
173
185
  if (!flags.file)
@@ -188,6 +200,25 @@ export default class SalaryTypes extends Command {
188
200
  }
189
201
  data = form;
190
202
  }
203
+ else if (endpoint.bodyMediaType) {
204
+ // The body is not JSON (today only application/x-ndjson): it is sent
205
+ // verbatim, under the content type the endpoint asks for.
206
+ if (flags.file) {
207
+ const fsModule = await import('node:fs');
208
+ try {
209
+ data = fsModule.readFileSync(flags.file, 'utf-8');
210
+ }
211
+ catch {
212
+ this.error(`Failed to read file: ${flags.file}`);
213
+ }
214
+ }
215
+ else if (flags.data) {
216
+ data = flags.data;
217
+ }
218
+ else {
219
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
220
+ }
221
+ }
191
222
  else if (endpoint.hasBody) {
192
223
  // For binary-response endpoints, --file is the output path, not a JSON
193
224
  // input file — skip the read-as-body branch.
@@ -283,7 +314,7 @@ export default class SalaryTypes extends Command {
283
314
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
284
315
  }
285
316
  else if (data) {
286
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
317
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
287
318
  }
288
319
  process.stderr.write('\n');
289
320
  }
@@ -297,6 +328,7 @@ export default class SalaryTypes extends Command {
297
328
  const binResponse = await apiRequest(endpoint.method, apiPath, {
298
329
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
299
330
  data,
331
+ contentType: endpoint.bodyMediaType || undefined,
300
332
  contextName: flags.context,
301
333
  binary: true,
302
334
  });
@@ -322,6 +354,7 @@ export default class SalaryTypes extends Command {
322
354
  const raw = await apiRequest(endpoint.method, apiPath, {
323
355
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
324
356
  data,
357
+ contentType: endpoint.bodyMediaType || undefined,
325
358
  contextName: flags.context,
326
359
  binary: true,
327
360
  });
@@ -342,6 +375,7 @@ export default class SalaryTypes extends Command {
342
375
  response = await apiRequest(endpoint.method, apiPath, {
343
376
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
344
377
  data,
378
+ contentType: endpoint.bodyMediaType || undefined,
345
379
  contextName: flags.context,
346
380
  acceptAny: true,
347
381
  });
@@ -1,6 +1,7 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
+ import { resolveUserPath } from '../lib/paths.js';
4
5
  // 'upsert' loops create/update from the CLI, one request per record. It exists
5
6
  // only where the API has no import endpoint of its own; those keep the name
6
7
  // 'import' and are listed in SERVER_IMPORTS.
@@ -11,55 +12,55 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = ["migrated-invoice", "line-items", "notes", "files"];
12
13
  const SUBCOMMANDS = [...["create-offsets", "delete-offsets", "offset-candidates", "list", "create", "create-migrated-invoice", "get-pdf", "create-pdf", "get", "update", "delete", "payment-derivation", "update-migrated-invoice", "approve", "match", "validate", "request-approval", "send", "send-resend", "cancel", "copy", "replace", "rebuild-journal", "recalculate-payment-status", "merge", "create-line-items", "line-items-from-sales-order", "line-items-from-delivery-note", "update-line-items-reorder", "update-line-items", "delete-line-items", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-files", "create-files", "get-files", "update-files", "delete-files", "files-download", "create-nettings", "delete-nettings", "netting-candidates", "line-items-from-sales-invoice", "credit-notes", "list-refund-account-options", "update-refund-payments-beneficiary-account"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'create-offsets': { method: 'POST', path: '/v1/sales-invoices/{salesInvoiceId}/offsets', summary: 'Deduct a prepayment or credit note from a sales invoice', pathParams: ['salesInvoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'delete-offsets': { method: 'DELETE', path: '/v1/sales-invoices/{salesInvoiceId}/offsets/{offsetId}', summary: 'Remove a deduction of a prepayment or credit note from a sales invoice', pathParams: ['salesInvoiceId', 'offsetId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
- 'offset-candidates': { method: 'GET', path: '/v1/sales-invoices/{salesInvoiceId}/offset-candidates', summary: 'List the prepayments and credit notes that can be deducted from a sales invoice', pathParams: ['salesInvoiceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'list': { method: 'GET', path: '/v1/sales-invoices', summary: 'Get all sales invoices with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
- 'create': { method: 'POST', path: '/v1/sales-invoices', summary: 'Create a new sales invoice', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['earlyPaymentDiscounts', 'lineItems', 'discounts', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'tagCodes', 'invoiceEmailCc'] },
19
- 'create-migrated-invoice': { method: 'POST', path: '/v1/sales-invoices/create-migrated-invoice', summary: 'Create a sales invoice with explicit tax codes on line items (for data migration)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lineItems', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'tagCodes', 'invoiceEmailCc'] },
20
- 'get-pdf': { method: 'GET', path: '/v1/sales-invoices/{id}/pdf', summary: 'Render a sales invoice PDF as the mapped event would, or preview a stored template', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
- 'create-pdf': { method: 'POST', path: '/v1/sales-invoices/{id}/pdf', summary: 'Render a sales invoice PDF from unsaved HTML posted in the body', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
22
- 'get': { method: 'GET', path: '/v1/sales-invoices/{id}', summary: 'Get a sales invoice by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
23
- 'update': { method: 'PATCH', path: '/v1/sales-invoices/{id}', summary: 'Update a sales invoice', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['earlyPaymentDiscounts', 'lineItems', 'discounts', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'tagCodes', 'invoiceEmailCc', 'removeFileIds'] },
24
- 'delete': { method: 'DELETE', path: '/v1/sales-invoices/{id}', summary: 'Delete a sales invoice', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
25
- 'payment-derivation': { method: 'GET', path: '/v1/sales-invoices/{id}/payment-derivation', summary: 'Explain how a sales invoice\'s paid amount is derived', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
26
- 'update-migrated-invoice': { method: 'PATCH', path: '/v1/sales-invoices/{id}/update-migrated-invoice', summary: 'Update a migrated sales invoice during migration (before go-live)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lineItems', 'files', 'notes', 'tagIds', 'tagCodes', 'invoiceEmailCc', 'totalAmountByTaxRate'] },
27
- 'approve': { method: 'POST', path: '/v1/sales-invoices/{id}/approve', summary: 'Approve a sales invoice from DRAFT or PENDING_APPROVAL status (generates invoice number and finalizes)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['earlyPaymentDiscounts', 'lineItems', 'discounts', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'tagCodes', 'invoiceEmailCc', 'removeFileIds'] },
28
- 'match': { method: 'POST', path: '/v1/sales-invoices/{id}/match', summary: 'Match this invoice\'s lines to sales-order lines and to delivery-note lines', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
29
- 'validate': { method: 'POST', path: '/v1/sales-invoices/{id}/validate', summary: 'Validate a sales invoice update without persisting changes', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['earlyPaymentDiscounts', 'lineItems', 'discounts', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'tagCodes', 'invoiceEmailCc', 'removeFileIds'] },
30
- 'request-approval': { method: 'POST', path: '/v1/sales-invoices/{id}/request-approval', summary: 'Request approval for sales invoice (DRAFT → PENDING_APPROVAL)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
31
- 'send': { method: 'POST', path: '/v1/sales-invoices/{id}/send', summary: 'Mark invoice as sent to customer (APPROVED → SENT)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
32
- 'send-resend': { method: 'POST', path: '/v1/sales-invoices/{id}/resend', summary: 'Resend a sales invoice to the original delivery recipient or an alternative one', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['emailCc', 'emailBcc'] },
33
- 'cancel': { method: 'POST', path: '/v1/sales-invoices/{id}/cancel', summary: 'Cancel a sales invoice', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
34
- 'copy': { method: 'POST', path: '/v1/sales-invoices/{id}/copy', summary: 'Create a copy of an invoice as a new draft', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
35
- 'replace': { method: 'POST', path: '/v1/sales-invoices/{id}/replace', summary: 'Replace invoice (creates a new CORRECTED_INVOICE; a SELF_BILLED_INVOICE is replaced by a new SELF_BILLED_INVOICE)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
36
- 'rebuild-journal': { method: 'POST', path: '/v1/sales-invoices/{id}/rebuild-journal', summary: 'Re-create the booking journal of a sales invoice', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
37
- 'recalculate-payment-status': { method: 'POST', path: '/v1/sales-invoices/{id}/recalculate-payment-status', summary: 'Recalculate a sales invoice\'s open amount and payment status', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
38
- 'merge': { method: 'POST', path: '/v1/sales-invoices/{id}/merge', summary: 'Merge draft invoices into this master', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['sourceIds'] },
39
- 'create-line-items': { method: 'POST', path: '/v1/sales-invoices/{invoiceId}/line-items', summary: 'Add a line item to a sales invoice', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['discounts', 'salesOrderLineItems', 'deliveryNoteLineItems', 'tagIds', 'tagCodes'] },
40
- 'line-items-from-sales-order': { method: 'POST', path: '/v1/sales-invoices/{invoiceId}/line-items/from-sales-order', summary: 'Add line items to a sales invoice from a sales order', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lines'] },
41
- 'line-items-from-delivery-note': { method: 'POST', path: '/v1/sales-invoices/{invoiceId}/line-items/from-delivery-note', summary: 'Add line items to a sales invoice from a delivery note', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lines'] },
42
- 'update-line-items-reorder': { method: 'PATCH', path: '/v1/sales-invoices/{invoiceId}/line-items/reorder', summary: 'Bulk reorder line items (drag-and-drop)', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['items'] },
43
- 'update-line-items': { method: 'PATCH', path: '/v1/sales-invoices/{invoiceId}/line-items/{lineItemId}', summary: 'Update a line item on a sales invoice', pathParams: ['invoiceId', 'lineItemId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['discounts', 'salesOrderLineItems', 'deliveryNoteLineItems', 'tagIds', 'tagCodes'] },
44
- 'delete-line-items': { method: 'DELETE', path: '/v1/sales-invoices/{invoiceId}/line-items/{lineItemId}', summary: 'Remove a line item from a sales invoice', pathParams: ['invoiceId', 'lineItemId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
45
- 'list-notes': { method: 'GET', path: '/v1/sales-invoices/{salesInvoiceId}/notes', summary: 'Get all notes for a sales invoice', pathParams: ['salesInvoiceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
46
- 'create-notes': { method: 'POST', path: '/v1/sales-invoices/{salesInvoiceId}/notes', summary: 'Create a note for a sales invoice', pathParams: ['salesInvoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
47
- 'get-notes': { method: 'GET', path: '/v1/sales-invoices/{salesInvoiceId}/notes/{noteId}', summary: 'Get a specific note for a sales invoice', pathParams: ['salesInvoiceId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
48
- 'update-notes': { method: 'PATCH', path: '/v1/sales-invoices/{salesInvoiceId}/notes/{noteId}', summary: 'Update a note for a sales invoice', pathParams: ['salesInvoiceId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
49
- 'delete-notes': { method: 'DELETE', path: '/v1/sales-invoices/{salesInvoiceId}/notes/{noteId}', summary: 'Delete a note from a sales invoice', pathParams: ['salesInvoiceId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
50
- 'list-files': { method: 'GET', path: '/v1/sales-invoices/{parentId}/files', summary: 'List file attachments for a sales invoice', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
51
- 'create-files': { method: 'POST', path: '/v1/sales-invoices/{parentId}/files', summary: 'Upload a file attachment to a sales invoice', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
52
- 'get-files': { method: 'GET', path: '/v1/sales-invoices/{parentId}/files/{fileId}', summary: 'Get a file attachment for a sales invoice', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
53
- 'update-files': { method: 'PATCH', path: '/v1/sales-invoices/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a sales invoice', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
54
- 'delete-files': { method: 'DELETE', path: '/v1/sales-invoices/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a sales invoice', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
55
- 'files-download': { method: 'GET', path: '/v1/sales-invoices/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a sales invoice', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [] },
56
- 'create-nettings': { method: 'POST', path: '/v1/sales-invoices/{salesInvoiceId}/nettings', summary: 'Net a vendor invoice of the same business partner against a sales invoice', pathParams: ['salesInvoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
57
- 'delete-nettings': { method: 'DELETE', path: '/v1/sales-invoices/{salesInvoiceId}/nettings/{offsetId}', summary: 'Remove a netting of a sales invoice against a vendor invoice', pathParams: ['salesInvoiceId', 'offsetId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
58
- 'netting-candidates': { method: 'GET', path: '/v1/sales-invoices/{salesInvoiceId}/netting-candidates', summary: 'List the vendor invoices that can be netted against a sales invoice', pathParams: ['salesInvoiceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
59
- 'line-items-from-sales-invoice': { method: 'POST', path: '/v1/sales-invoices/{invoiceId}/line-items/from-sales-invoice', summary: 'Add line items to a sales credit note from a sales invoice', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lines'] },
60
- 'credit-notes': { method: 'POST', path: '/v1/sales-invoices/{invoiceId}/credit-notes', summary: 'Create a sales credit note from a sales invoice', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lines'] },
61
- 'list-refund-account-options': { method: 'GET', path: '/v1/sales-invoices/{salesInvoiceId}/refund-account-options', summary: 'List the accounts the refund of a sales credit note can be paid to', pathParams: ['salesInvoiceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
62
- 'update-refund-payments-beneficiary-account': { method: 'PUT', path: '/v1/sales-invoices/{salesInvoiceId}/refund-payments/{paymentId}/beneficiary-account', summary: 'Choose the account a refund payment pays to', pathParams: ['salesInvoiceId', 'paymentId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'create-offsets': { method: 'POST', path: '/v1/sales-invoices/{salesInvoiceId}/offsets', summary: 'Deduct a prepayment or credit note from a sales invoice', pathParams: ['salesInvoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'delete-offsets': { method: 'DELETE', path: '/v1/sales-invoices/{salesInvoiceId}/offsets/{offsetId}', summary: 'Remove a deduction of a prepayment or credit note from a sales invoice', pathParams: ['salesInvoiceId', 'offsetId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
17
+ 'offset-candidates': { method: 'GET', path: '/v1/sales-invoices/{salesInvoiceId}/offset-candidates', summary: 'List the prepayments and credit notes that can be deducted from a sales invoice', pathParams: ['salesInvoiceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'list': { method: 'GET', path: '/v1/sales-invoices', summary: 'Get all sales invoices with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
+ 'create': { method: 'POST', path: '/v1/sales-invoices', summary: 'Create a new sales invoice', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['earlyPaymentDiscounts', 'lineItems', 'discounts', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'tagCodes', 'invoiceEmailCc'], bodyMediaType: '', specParams: [] },
20
+ 'create-migrated-invoice': { method: 'POST', path: '/v1/sales-invoices/create-migrated-invoice', summary: 'Create a sales invoice with explicit tax codes on line items (for data migration)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lineItems', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'tagCodes', 'invoiceEmailCc'], bodyMediaType: '', specParams: [] },
21
+ 'get-pdf': { method: 'GET', path: '/v1/sales-invoices/{id}/pdf', summary: 'Render a sales invoice PDF as the mapped event would, or preview a stored template', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
22
+ 'create-pdf': { method: 'POST', path: '/v1/sales-invoices/{id}/pdf', summary: 'Render a sales invoice PDF from unsaved HTML posted in the body', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
23
+ 'get': { method: 'GET', path: '/v1/sales-invoices/{id}', summary: 'Get a sales invoice by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
24
+ 'update': { method: 'PATCH', path: '/v1/sales-invoices/{id}', summary: 'Update a sales invoice', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['earlyPaymentDiscounts', 'lineItems', 'discounts', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'tagCodes', 'invoiceEmailCc', 'removeFileIds'], bodyMediaType: '', specParams: [] },
25
+ 'delete': { method: 'DELETE', path: '/v1/sales-invoices/{id}', summary: 'Delete a sales invoice', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
26
+ 'payment-derivation': { method: 'GET', path: '/v1/sales-invoices/{id}/payment-derivation', summary: 'Explain how a sales invoice\'s paid amount is derived', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
27
+ 'update-migrated-invoice': { method: 'PATCH', path: '/v1/sales-invoices/{id}/update-migrated-invoice', summary: 'Update a migrated sales invoice during migration (before go-live)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lineItems', 'files', 'notes', 'tagIds', 'tagCodes', 'invoiceEmailCc', 'totalAmountByTaxRate'], bodyMediaType: '', specParams: [] },
28
+ 'approve': { method: 'POST', path: '/v1/sales-invoices/{id}/approve', summary: 'Approve a sales invoice from DRAFT or PENDING_APPROVAL status (generates invoice number and finalizes)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['earlyPaymentDiscounts', 'lineItems', 'discounts', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'tagCodes', 'invoiceEmailCc', 'removeFileIds'], bodyMediaType: '', specParams: [] },
29
+ 'match': { method: 'POST', path: '/v1/sales-invoices/{id}/match', summary: 'Match this invoice\'s lines to sales-order lines and to delivery-note lines', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
30
+ 'validate': { method: 'POST', path: '/v1/sales-invoices/{id}/validate', summary: 'Validate a sales invoice update without persisting changes', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['earlyPaymentDiscounts', 'lineItems', 'discounts', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'tagCodes', 'invoiceEmailCc', 'removeFileIds'], bodyMediaType: '', specParams: [] },
31
+ 'request-approval': { method: 'POST', path: '/v1/sales-invoices/{id}/request-approval', summary: 'Request approval for sales invoice (DRAFT → PENDING_APPROVAL)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
32
+ 'send': { method: 'POST', path: '/v1/sales-invoices/{id}/send', summary: 'Mark invoice as sent to customer (APPROVED → SENT)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
33
+ 'send-resend': { method: 'POST', path: '/v1/sales-invoices/{id}/resend', summary: 'Resend a sales invoice to the original delivery recipient or an alternative one', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['emailCc', 'emailBcc'], bodyMediaType: '', specParams: [] },
34
+ 'cancel': { method: 'POST', path: '/v1/sales-invoices/{id}/cancel', summary: 'Cancel a sales invoice', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
35
+ 'copy': { method: 'POST', path: '/v1/sales-invoices/{id}/copy', summary: 'Create a copy of an invoice as a new draft', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
36
+ 'replace': { method: 'POST', path: '/v1/sales-invoices/{id}/replace', summary: 'Replace invoice (creates a new CORRECTED_INVOICE; a SELF_BILLED_INVOICE is replaced by a new SELF_BILLED_INVOICE)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
37
+ 'rebuild-journal': { method: 'POST', path: '/v1/sales-invoices/{id}/rebuild-journal', summary: 'Re-create the booking journal of a sales invoice', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
38
+ 'recalculate-payment-status': { method: 'POST', path: '/v1/sales-invoices/{id}/recalculate-payment-status', summary: 'Recalculate a sales invoice\'s open amount and payment status', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
39
+ 'merge': { method: 'POST', path: '/v1/sales-invoices/{id}/merge', summary: 'Merge draft invoices into this master', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['sourceIds'], bodyMediaType: '', specParams: [] },
40
+ 'create-line-items': { method: 'POST', path: '/v1/sales-invoices/{invoiceId}/line-items', summary: 'Add a line item to a sales invoice', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['discounts', 'salesOrderLineItems', 'deliveryNoteLineItems', 'tagIds', 'tagCodes'], bodyMediaType: '', specParams: [] },
41
+ 'line-items-from-sales-order': { method: 'POST', path: '/v1/sales-invoices/{invoiceId}/line-items/from-sales-order', summary: 'Add line items to a sales invoice from a sales order', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lines'], bodyMediaType: '', specParams: [] },
42
+ 'line-items-from-delivery-note': { method: 'POST', path: '/v1/sales-invoices/{invoiceId}/line-items/from-delivery-note', summary: 'Add line items to a sales invoice from a delivery note', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lines'], bodyMediaType: '', specParams: [] },
43
+ 'update-line-items-reorder': { method: 'PATCH', path: '/v1/sales-invoices/{invoiceId}/line-items/reorder', summary: 'Bulk reorder line items (drag-and-drop)', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['items'], bodyMediaType: '', specParams: [] },
44
+ 'update-line-items': { method: 'PATCH', path: '/v1/sales-invoices/{invoiceId}/line-items/{lineItemId}', summary: 'Update a line item on a sales invoice', pathParams: ['invoiceId', 'lineItemId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['discounts', 'salesOrderLineItems', 'deliveryNoteLineItems', 'tagIds', 'tagCodes'], bodyMediaType: '', specParams: [] },
45
+ 'delete-line-items': { method: 'DELETE', path: '/v1/sales-invoices/{invoiceId}/line-items/{lineItemId}', summary: 'Remove a line item from a sales invoice', pathParams: ['invoiceId', 'lineItemId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
46
+ 'list-notes': { method: 'GET', path: '/v1/sales-invoices/{salesInvoiceId}/notes', summary: 'Get all notes for a sales invoice', pathParams: ['salesInvoiceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
47
+ 'create-notes': { method: 'POST', path: '/v1/sales-invoices/{salesInvoiceId}/notes', summary: 'Create a note for a sales invoice', pathParams: ['salesInvoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
48
+ 'get-notes': { method: 'GET', path: '/v1/sales-invoices/{salesInvoiceId}/notes/{noteId}', summary: 'Get a specific note for a sales invoice', pathParams: ['salesInvoiceId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
49
+ 'update-notes': { method: 'PATCH', path: '/v1/sales-invoices/{salesInvoiceId}/notes/{noteId}', summary: 'Update a note for a sales invoice', pathParams: ['salesInvoiceId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
50
+ 'delete-notes': { method: 'DELETE', path: '/v1/sales-invoices/{salesInvoiceId}/notes/{noteId}', summary: 'Delete a note from a sales invoice', pathParams: ['salesInvoiceId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
51
+ 'list-files': { method: 'GET', path: '/v1/sales-invoices/{parentId}/files', summary: 'List file attachments for a sales invoice', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
52
+ 'create-files': { method: 'POST', path: '/v1/sales-invoices/{parentId}/files', summary: 'Upload a file attachment to a sales invoice', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
53
+ 'get-files': { method: 'GET', path: '/v1/sales-invoices/{parentId}/files/{fileId}', summary: 'Get a file attachment for a sales invoice', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
54
+ 'update-files': { method: 'PATCH', path: '/v1/sales-invoices/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a sales invoice', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
55
+ 'delete-files': { method: 'DELETE', path: '/v1/sales-invoices/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a sales invoice', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
56
+ 'files-download': { method: 'GET', path: '/v1/sales-invoices/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a sales invoice', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
57
+ 'create-nettings': { method: 'POST', path: '/v1/sales-invoices/{salesInvoiceId}/nettings', summary: 'Net a vendor invoice of the same business partner against a sales invoice', pathParams: ['salesInvoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
58
+ 'delete-nettings': { method: 'DELETE', path: '/v1/sales-invoices/{salesInvoiceId}/nettings/{offsetId}', summary: 'Remove a netting of a sales invoice against a vendor invoice', pathParams: ['salesInvoiceId', 'offsetId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
59
+ 'netting-candidates': { method: 'GET', path: '/v1/sales-invoices/{salesInvoiceId}/netting-candidates', summary: 'List the vendor invoices that can be netted against a sales invoice', pathParams: ['salesInvoiceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
60
+ 'line-items-from-sales-invoice': { method: 'POST', path: '/v1/sales-invoices/{invoiceId}/line-items/from-sales-invoice', summary: 'Add line items to a sales credit note from a sales invoice', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lines'], bodyMediaType: '', specParams: [] },
61
+ 'credit-notes': { method: 'POST', path: '/v1/sales-invoices/{invoiceId}/credit-notes', summary: 'Create a sales credit note from a sales invoice', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['lines'], bodyMediaType: '', specParams: [] },
62
+ 'list-refund-account-options': { method: 'GET', path: '/v1/sales-invoices/{salesInvoiceId}/refund-account-options', summary: 'List the accounts the refund of a sales credit note can be paid to', pathParams: ['salesInvoiceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
63
+ 'update-refund-payments-beneficiary-account': { method: 'PUT', path: '/v1/sales-invoices/{salesInvoiceId}/refund-payments/{paymentId}/beneficiary-account', summary: 'Choose the account a refund payment pays to', pathParams: ['salesInvoiceId', 'paymentId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
63
64
  };
64
65
  export default class SalesInvoices extends Command {
65
66
  static description = 'Manage sales invoices';
@@ -81,7 +82,7 @@ export default class SalesInvoices extends Command {
81
82
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
82
83
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
83
84
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
84
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
85
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
85
86
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
86
87
  'offset-id': Flags.string({ description: 'Path param: offsetId' }),
87
88
  'line-item-id': Flags.string({ description: 'Path param: lineItemId' }),
@@ -217,6 +218,17 @@ export default class SalesInvoices extends Command {
217
218
  }
218
219
  }
219
220
  }
221
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
222
+ // have them: their body is not JSON, so --set cannot carry a value there.
223
+ for (const param of endpoint.specParams) {
224
+ const value = flags[param.flag];
225
+ if (value === undefined || value === '') {
226
+ if (param.required)
227
+ this.error(`Missing required flag: --${param.flag}`);
228
+ continue;
229
+ }
230
+ queryParams[param.name] = value;
231
+ }
220
232
  let data;
221
233
  if (endpoint.isMultipart) {
222
234
  if (!flags.file)
@@ -237,6 +249,25 @@ export default class SalesInvoices extends Command {
237
249
  }
238
250
  data = form;
239
251
  }
252
+ else if (endpoint.bodyMediaType) {
253
+ // The body is not JSON (today only application/x-ndjson): it is sent
254
+ // verbatim, under the content type the endpoint asks for.
255
+ if (flags.file) {
256
+ const fsModule = await import('node:fs');
257
+ try {
258
+ data = fsModule.readFileSync(flags.file, 'utf-8');
259
+ }
260
+ catch {
261
+ this.error(`Failed to read file: ${flags.file}`);
262
+ }
263
+ }
264
+ else if (flags.data) {
265
+ data = flags.data;
266
+ }
267
+ else {
268
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
269
+ }
270
+ }
240
271
  else if (endpoint.hasBody) {
241
272
  // For binary-response endpoints, --file is the output path, not a JSON
242
273
  // input file — skip the read-as-body branch.
@@ -332,7 +363,7 @@ export default class SalesInvoices extends Command {
332
363
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
333
364
  }
334
365
  else if (data) {
335
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
366
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
336
367
  }
337
368
  process.stderr.write('\n');
338
369
  }
@@ -346,6 +377,7 @@ export default class SalesInvoices extends Command {
346
377
  const binResponse = await apiRequest(endpoint.method, apiPath, {
347
378
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
348
379
  data,
380
+ contentType: endpoint.bodyMediaType || undefined,
349
381
  contextName: flags.context,
350
382
  binary: true,
351
383
  });
@@ -371,6 +403,7 @@ export default class SalesInvoices extends Command {
371
403
  const raw = await apiRequest(endpoint.method, apiPath, {
372
404
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
373
405
  data,
406
+ contentType: endpoint.bodyMediaType || undefined,
374
407
  contextName: flags.context,
375
408
  binary: true,
376
409
  });
@@ -391,6 +424,7 @@ export default class SalesInvoices extends Command {
391
424
  response = await apiRequest(endpoint.method, apiPath, {
392
425
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
393
426
  data,
427
+ contentType: endpoint.bodyMediaType || undefined,
394
428
  contextName: flags.context,
395
429
  acceptAny: true,
396
430
  });