@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.
@@ -11,14 +12,14 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = [];
12
13
  const SUBCOMMANDS = [...["list", "create", "get", "update", "delete", "logo", "icon", "update-identity-provider"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list': { method: 'GET', path: '/v1/tenants', summary: 'Get all tenants with pagination (admin only)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'create': { method: 'POST', path: '/v1/tenants', summary: 'Create a new tenant (admin only)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['supportedLocales', 'allowedSenders', 'salesInvoiceRecipientCc', 'salesInvoiceRecipientBcc', 'salesOrderRecipientCc', 'salesOrderRecipientBcc', 'deliveryNoteRecipientCc', 'deliveryNoteRecipientBcc'] },
16
- 'get': { method: 'GET', path: '/v1/tenants/{id}', summary: 'Get a tenant by ID (own tenant or admin)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'update': { method: 'PATCH', path: '/v1/tenants/{id}', summary: 'Update a tenant (own tenant or admin)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['supportedLocales', 'allowedSenders', 'salesInvoiceRecipientCc', 'salesInvoiceRecipientBcc', 'salesOrderRecipientCc', 'salesOrderRecipientBcc', 'deliveryNoteRecipientCc', 'deliveryNoteRecipientBcc'] },
18
- 'delete': { method: 'DELETE', path: '/v1/tenants/{id}', summary: 'Delete a tenant (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
- 'logo': { method: 'POST', path: '/v1/tenants/{id}/logo', summary: 'Upload the tenant logo to the public bucket and set logoUrl (own tenant or admin)', pathParams: ['id'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
- 'icon': { method: 'POST', path: '/v1/tenants/{id}/icon', summary: 'Upload the tenant icon to the public bucket and set iconUrl (own tenant or admin)', pathParams: ['id'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
- 'update-identity-provider': { method: 'PUT', path: '/v1/tenants/{id}/identity-provider', summary: 'Synchronize a tenant with the identity provider (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/tenants', summary: 'Get all tenants with pagination (admin only)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'create': { method: 'POST', path: '/v1/tenants', summary: 'Create a new tenant (admin only)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['supportedLocales', 'allowedSenders', 'salesInvoiceRecipientCc', 'salesInvoiceRecipientBcc', 'salesOrderRecipientCc', 'salesOrderRecipientBcc', 'deliveryNoteRecipientCc', 'deliveryNoteRecipientBcc'], bodyMediaType: '', specParams: [] },
17
+ 'get': { method: 'GET', path: '/v1/tenants/{id}', summary: 'Get a tenant by ID (own tenant or admin)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'update': { method: 'PATCH', path: '/v1/tenants/{id}', summary: 'Update a tenant (own tenant or admin)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['supportedLocales', 'allowedSenders', 'salesInvoiceRecipientCc', 'salesInvoiceRecipientBcc', 'salesOrderRecipientCc', 'salesOrderRecipientBcc', 'deliveryNoteRecipientCc', 'deliveryNoteRecipientBcc'], bodyMediaType: '', specParams: [] },
19
+ 'delete': { method: 'DELETE', path: '/v1/tenants/{id}', summary: 'Delete a tenant (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
20
+ 'logo': { method: 'POST', path: '/v1/tenants/{id}/logo', summary: 'Upload the tenant logo to the public bucket and set logoUrl (own tenant or admin)', pathParams: ['id'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
21
+ 'icon': { method: 'POST', path: '/v1/tenants/{id}/icon', summary: 'Upload the tenant icon to the public bucket and set iconUrl (own tenant or admin)', pathParams: ['id'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
22
+ 'update-identity-provider': { method: 'PUT', path: '/v1/tenants/{id}/identity-provider', summary: 'Synchronize a tenant with the identity provider (admin only)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
22
23
  };
23
24
  export default class Tenants extends Command {
24
25
  static description = 'Manage tenants';
@@ -40,7 +41,7 @@ export default class Tenants extends Command {
40
41
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
41
42
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
42
43
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
43
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
44
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
44
45
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
45
46
  page: Flags.string({ description: 'Page number' }),
46
47
  limit: Flags.string({ description: 'Items per page' }),
@@ -171,6 +172,17 @@ export default class Tenants extends Command {
171
172
  }
172
173
  }
173
174
  }
175
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
176
+ // have them: their body is not JSON, so --set cannot carry a value there.
177
+ for (const param of endpoint.specParams) {
178
+ const value = flags[param.flag];
179
+ if (value === undefined || value === '') {
180
+ if (param.required)
181
+ this.error(`Missing required flag: --${param.flag}`);
182
+ continue;
183
+ }
184
+ queryParams[param.name] = value;
185
+ }
174
186
  let data;
175
187
  if (endpoint.isMultipart) {
176
188
  if (!flags.file)
@@ -191,6 +203,25 @@ export default class Tenants extends Command {
191
203
  }
192
204
  data = form;
193
205
  }
206
+ else if (endpoint.bodyMediaType) {
207
+ // The body is not JSON (today only application/x-ndjson): it is sent
208
+ // verbatim, under the content type the endpoint asks for.
209
+ if (flags.file) {
210
+ const fsModule = await import('node:fs');
211
+ try {
212
+ data = fsModule.readFileSync(flags.file, 'utf-8');
213
+ }
214
+ catch {
215
+ this.error(`Failed to read file: ${flags.file}`);
216
+ }
217
+ }
218
+ else if (flags.data) {
219
+ data = flags.data;
220
+ }
221
+ else {
222
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
223
+ }
224
+ }
194
225
  else if (endpoint.hasBody) {
195
226
  // For binary-response endpoints, --file is the output path, not a JSON
196
227
  // input file — skip the read-as-body branch.
@@ -286,7 +317,7 @@ export default class Tenants extends Command {
286
317
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
287
318
  }
288
319
  else if (data) {
289
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
320
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
290
321
  }
291
322
  process.stderr.write('\n');
292
323
  }
@@ -300,6 +331,7 @@ export default class Tenants extends Command {
300
331
  const binResponse = await apiRequest(endpoint.method, apiPath, {
301
332
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
302
333
  data,
334
+ contentType: endpoint.bodyMediaType || undefined,
303
335
  contextName: flags.context,
304
336
  binary: true,
305
337
  });
@@ -325,6 +357,7 @@ export default class Tenants extends Command {
325
357
  const raw = await apiRequest(endpoint.method, apiPath, {
326
358
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
327
359
  data,
360
+ contentType: endpoint.bodyMediaType || undefined,
328
361
  contextName: flags.context,
329
362
  binary: true,
330
363
  });
@@ -345,6 +378,7 @@ export default class Tenants extends Command {
345
378
  response = await apiRequest(endpoint.method, apiPath, {
346
379
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
347
380
  data,
381
+ contentType: endpoint.bodyMediaType || undefined,
348
382
  contextName: flags.context,
349
383
  acceptAny: true,
350
384
  });
@@ -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,8 +12,8 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = [];
12
13
  const SUBCOMMANDS = [...["list", "find"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list': { method: 'GET', path: '/v1/tools', summary: 'List all available tools', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'find': { method: 'GET', path: '/v1/tools/{name}', summary: 'Get a single tool by canonical name', pathParams: ['name'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/tools', summary: 'List all available tools', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'find': { method: 'GET', path: '/v1/tools/{name}', summary: 'Get a single tool by canonical name', pathParams: ['name'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
17
  };
17
18
  export default class Tools extends Command {
18
19
  static description = 'Manage tools';
@@ -30,7 +31,7 @@ export default class Tools extends Command {
30
31
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
31
32
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
32
33
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
33
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
34
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
34
35
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
35
36
  page: Flags.string({ description: 'Page number' }),
36
37
  limit: Flags.string({ description: 'Items per page' }),
@@ -161,6 +162,17 @@ export default class Tools extends Command {
161
162
  }
162
163
  }
163
164
  }
165
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
166
+ // have them: their body is not JSON, so --set cannot carry a value there.
167
+ for (const param of endpoint.specParams) {
168
+ const value = flags[param.flag];
169
+ if (value === undefined || value === '') {
170
+ if (param.required)
171
+ this.error(`Missing required flag: --${param.flag}`);
172
+ continue;
173
+ }
174
+ queryParams[param.name] = value;
175
+ }
164
176
  let data;
165
177
  if (endpoint.isMultipart) {
166
178
  if (!flags.file)
@@ -181,6 +193,25 @@ export default class Tools extends Command {
181
193
  }
182
194
  data = form;
183
195
  }
196
+ else if (endpoint.bodyMediaType) {
197
+ // The body is not JSON (today only application/x-ndjson): it is sent
198
+ // verbatim, under the content type the endpoint asks for.
199
+ if (flags.file) {
200
+ const fsModule = await import('node:fs');
201
+ try {
202
+ data = fsModule.readFileSync(flags.file, 'utf-8');
203
+ }
204
+ catch {
205
+ this.error(`Failed to read file: ${flags.file}`);
206
+ }
207
+ }
208
+ else if (flags.data) {
209
+ data = flags.data;
210
+ }
211
+ else {
212
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
213
+ }
214
+ }
184
215
  else if (endpoint.hasBody) {
185
216
  // For binary-response endpoints, --file is the output path, not a JSON
186
217
  // input file — skip the read-as-body branch.
@@ -276,7 +307,7 @@ export default class Tools extends Command {
276
307
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
277
308
  }
278
309
  else if (data) {
279
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
310
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
280
311
  }
281
312
  process.stderr.write('\n');
282
313
  }
@@ -290,6 +321,7 @@ export default class Tools extends Command {
290
321
  const binResponse = await apiRequest(endpoint.method, apiPath, {
291
322
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
292
323
  data,
324
+ contentType: endpoint.bodyMediaType || undefined,
293
325
  contextName: flags.context,
294
326
  binary: true,
295
327
  });
@@ -315,6 +347,7 @@ export default class Tools extends Command {
315
347
  const raw = await apiRequest(endpoint.method, apiPath, {
316
348
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
317
349
  data,
350
+ contentType: endpoint.bodyMediaType || undefined,
318
351
  contextName: flags.context,
319
352
  binary: true,
320
353
  });
@@ -335,6 +368,7 @@ export default class Tools extends Command {
335
368
  response = await apiRequest(endpoint.method, apiPath, {
336
369
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
337
370
  data,
371
+ contentType: endpoint.bodyMediaType || undefined,
338
372
  contextName: flags.context,
339
373
  acceptAny: true,
340
374
  });
@@ -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,28 +12,28 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = ["roles"];
12
13
  const SUBCOMMANDS = [...["list-invitations", "create-invitations", "send-invitations-resend", "delete-invitations", "list", "create", "get", "update", "delete", "get-credentials", "password-reset", "delete-credentials-mfa", "delete-credentials-otp", "delete-credentials-passkeys", "delete-identity-providers", "permissions", "list-roles", "create-roles", "update-roles", "delete-roles", "disable", "enable"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list-invitations': { method: 'GET', path: '/v1/users/invitations', summary: 'List the invitations still waiting to be accepted', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'create-invitations': { method: 'POST', path: '/v1/users/invitations', summary: 'Invite a person who has no Aderis login yet', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
- 'send-invitations-resend': { method: 'POST', path: '/v1/users/invitations/{invitationId}/resend', summary: 'Send the invitation mail again', pathParams: ['invitationId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'delete-invitations': { method: 'DELETE', path: '/v1/users/invitations/{invitationId}', summary: 'Withdraw an invitation', pathParams: ['invitationId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
- 'list': { method: 'GET', path: '/v1/users', summary: 'List the people of the current tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
- 'create': { method: 'POST', path: '/v1/users', summary: 'Give an existing login access to this tenant', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['roleIds'] },
20
- 'get': { method: 'GET', path: '/v1/users/{userId}', summary: 'Read one person', pathParams: ['userId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
- 'update': { method: 'PATCH', path: '/v1/users/{userId}', summary: 'Change one person', pathParams: ['userId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
22
- 'delete': { method: 'DELETE', path: '/v1/users/{userId}', summary: 'Remove one person from this tenant', pathParams: ['userId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
23
- 'get-credentials': { method: 'GET', path: '/v1/users/{userId}/credentials', summary: 'Read a tenant member’s sign-in methods', pathParams: ['userId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
24
- 'password-reset': { method: 'POST', path: '/v1/users/{userId}/password-reset', summary: 'Send a password-reset email', pathParams: ['userId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
25
- 'delete-credentials-mfa': { method: 'DELETE', path: '/v1/users/{userId}/credentials/mfa', summary: 'Reset a tenant member’s second factors', pathParams: ['userId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
26
- 'delete-credentials-otp': { method: 'DELETE', path: '/v1/users/{userId}/credentials/otp', summary: 'Reset a tenant member’s OTP factors', pathParams: ['userId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
27
- 'delete-credentials-passkeys': { method: 'DELETE', path: '/v1/users/{userId}/credentials/passkeys/{credentialId}', summary: 'Remove a tenant member’s passwordless passkey', pathParams: ['userId', 'credentialId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
28
- 'delete-identity-providers': { method: 'DELETE', path: '/v1/users/{userId}/identity-providers/{provider}', summary: 'Unlink a tenant member’s external sign-in', pathParams: ['userId', 'provider'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
29
- 'permissions': { method: 'GET', path: '/v1/users/{userId}/permissions', summary: 'Read what one person may do here', pathParams: ['userId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
30
- 'list-roles': { method: 'GET', path: '/v1/users/{userId}/roles', summary: 'Read the roles one person holds here', pathParams: ['userId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
31
- 'create-roles': { method: 'POST', path: '/v1/users/{userId}/roles', summary: 'Give one person one more role', pathParams: ['userId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
32
- 'update-roles': { method: 'PUT', path: '/v1/users/{userId}/roles', summary: 'Replace the roles one person holds here', pathParams: ['userId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['roleIds'] },
33
- 'delete-roles': { method: 'DELETE', path: '/v1/users/{userId}/roles/{roleId}', summary: 'Take one role away from one person', pathParams: ['userId', 'roleId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
34
- 'disable': { method: 'POST', path: '/v1/users/{userId}/disable', summary: 'Disable one person in this tenant', pathParams: ['userId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
35
- 'enable': { method: 'POST', path: '/v1/users/{userId}/enable', summary: 'Enable one person in this tenant', pathParams: ['userId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list-invitations': { method: 'GET', path: '/v1/users/invitations', summary: 'List the invitations still waiting to be accepted', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'create-invitations': { method: 'POST', path: '/v1/users/invitations', summary: 'Invite a person who has no Aderis login yet', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
17
+ 'send-invitations-resend': { method: 'POST', path: '/v1/users/invitations/{invitationId}/resend', summary: 'Send the invitation mail again', pathParams: ['invitationId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'delete-invitations': { method: 'DELETE', path: '/v1/users/invitations/{invitationId}', summary: 'Withdraw an invitation', pathParams: ['invitationId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
+ 'list': { method: 'GET', path: '/v1/users', summary: 'List the people of the current tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
20
+ 'create': { method: 'POST', path: '/v1/users', summary: 'Give an existing login access to this tenant', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['roleIds'], bodyMediaType: '', specParams: [] },
21
+ 'get': { method: 'GET', path: '/v1/users/{userId}', summary: 'Read one person', pathParams: ['userId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
22
+ 'update': { method: 'PATCH', path: '/v1/users/{userId}', summary: 'Change one person', pathParams: ['userId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
23
+ 'delete': { method: 'DELETE', path: '/v1/users/{userId}', summary: 'Remove one person from this tenant', pathParams: ['userId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
24
+ 'get-credentials': { method: 'GET', path: '/v1/users/{userId}/credentials', summary: 'Read a tenant member’s sign-in methods', pathParams: ['userId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
25
+ 'password-reset': { method: 'POST', path: '/v1/users/{userId}/password-reset', summary: 'Send a password-reset email', pathParams: ['userId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
26
+ 'delete-credentials-mfa': { method: 'DELETE', path: '/v1/users/{userId}/credentials/mfa', summary: 'Reset a tenant member’s second factors', pathParams: ['userId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
27
+ 'delete-credentials-otp': { method: 'DELETE', path: '/v1/users/{userId}/credentials/otp', summary: 'Reset a tenant member’s OTP factors', pathParams: ['userId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
28
+ 'delete-credentials-passkeys': { method: 'DELETE', path: '/v1/users/{userId}/credentials/passkeys/{credentialId}', summary: 'Remove a tenant member’s passwordless passkey', pathParams: ['userId', 'credentialId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
29
+ 'delete-identity-providers': { method: 'DELETE', path: '/v1/users/{userId}/identity-providers/{provider}', summary: 'Unlink a tenant member’s external sign-in', pathParams: ['userId', 'provider'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
30
+ 'permissions': { method: 'GET', path: '/v1/users/{userId}/permissions', summary: 'Read what one person may do here', pathParams: ['userId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
31
+ 'list-roles': { method: 'GET', path: '/v1/users/{userId}/roles', summary: 'Read the roles one person holds here', pathParams: ['userId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
32
+ 'create-roles': { method: 'POST', path: '/v1/users/{userId}/roles', summary: 'Give one person one more role', pathParams: ['userId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
33
+ 'update-roles': { method: 'PUT', path: '/v1/users/{userId}/roles', summary: 'Replace the roles one person holds here', pathParams: ['userId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['roleIds'], bodyMediaType: '', specParams: [] },
34
+ 'delete-roles': { method: 'DELETE', path: '/v1/users/{userId}/roles/{roleId}', summary: 'Take one role away from one person', pathParams: ['userId', 'roleId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
35
+ 'disable': { method: 'POST', path: '/v1/users/{userId}/disable', summary: 'Disable one person in this tenant', pathParams: ['userId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
36
+ 'enable': { method: 'POST', path: '/v1/users/{userId}/enable', summary: 'Enable one person in this tenant', pathParams: ['userId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
36
37
  };
37
38
  export default class Users extends Command {
38
39
  static description = 'Manage users';
@@ -54,7 +55,7 @@ export default class Users extends Command {
54
55
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
55
56
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
56
57
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
57
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
58
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
58
59
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
59
60
  'credential-id': Flags.string({ description: 'Path param: credentialId' }),
60
61
  'provider': Flags.string({ description: 'Path param: provider' }),
@@ -188,6 +189,17 @@ export default class Users extends Command {
188
189
  }
189
190
  }
190
191
  }
192
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
193
+ // have them: their body is not JSON, so --set cannot carry a value there.
194
+ for (const param of endpoint.specParams) {
195
+ const value = flags[param.flag];
196
+ if (value === undefined || value === '') {
197
+ if (param.required)
198
+ this.error(`Missing required flag: --${param.flag}`);
199
+ continue;
200
+ }
201
+ queryParams[param.name] = value;
202
+ }
191
203
  let data;
192
204
  if (endpoint.isMultipart) {
193
205
  if (!flags.file)
@@ -208,6 +220,25 @@ export default class Users extends Command {
208
220
  }
209
221
  data = form;
210
222
  }
223
+ else if (endpoint.bodyMediaType) {
224
+ // The body is not JSON (today only application/x-ndjson): it is sent
225
+ // verbatim, under the content type the endpoint asks for.
226
+ if (flags.file) {
227
+ const fsModule = await import('node:fs');
228
+ try {
229
+ data = fsModule.readFileSync(flags.file, 'utf-8');
230
+ }
231
+ catch {
232
+ this.error(`Failed to read file: ${flags.file}`);
233
+ }
234
+ }
235
+ else if (flags.data) {
236
+ data = flags.data;
237
+ }
238
+ else {
239
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
240
+ }
241
+ }
211
242
  else if (endpoint.hasBody) {
212
243
  // For binary-response endpoints, --file is the output path, not a JSON
213
244
  // input file — skip the read-as-body branch.
@@ -303,7 +334,7 @@ export default class Users extends Command {
303
334
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
304
335
  }
305
336
  else if (data) {
306
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
337
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
307
338
  }
308
339
  process.stderr.write('\n');
309
340
  }
@@ -317,6 +348,7 @@ export default class Users extends Command {
317
348
  const binResponse = await apiRequest(endpoint.method, apiPath, {
318
349
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
319
350
  data,
351
+ contentType: endpoint.bodyMediaType || undefined,
320
352
  contextName: flags.context,
321
353
  binary: true,
322
354
  });
@@ -342,6 +374,7 @@ export default class Users extends Command {
342
374
  const raw = await apiRequest(endpoint.method, apiPath, {
343
375
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
344
376
  data,
377
+ contentType: endpoint.bodyMediaType || undefined,
345
378
  contextName: flags.context,
346
379
  binary: true,
347
380
  });
@@ -362,6 +395,7 @@ export default class Users extends Command {
362
395
  response = await apiRequest(endpoint.method, apiPath, {
363
396
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
364
397
  data,
398
+ contentType: endpoint.bodyMediaType || undefined,
365
399
  contextName: flags.context,
366
400
  acceptAny: true,
367
401
  });
@@ -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,31 +12,31 @@ const SERVER_IMPORTS = [];
11
12
  const NESTED_UPSERTS = ["files"];
12
13
  const SUBCOMMANDS = [...["list", "create", "preview", "get", "update", "delete", "regenerate", "replace", "fields-contributions", "fields-contributions-export", "approve", "mark-as-filed", "xml", "notices-dismiss", "notices-restore", "list-files", "create-files", "get-files", "update-files", "delete-files", "files-download", "list-payments", "create-payments", "contributions-exclude", "exclusions-restore"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
13
14
  const ENDPOINTS = {
14
- 'list': { method: 'GET', path: '/v1/vat-declarations', summary: 'List VAT reports (paginated)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
- 'create': { method: 'POST', path: '/v1/vat-declarations', summary: 'Create a VAT declaration report (always async-accepted)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
- 'preview': { method: 'GET', path: '/v1/vat-declarations/preview', summary: 'Preview the in-scope counts and late entries for a candidate period', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
- 'get': { method: 'GET', path: '/v1/vat-declarations/{id}', summary: 'Get a VAT report by id (this is the polling endpoint for async generation)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
- 'update': { method: 'PATCH', path: '/v1/vat-declarations/{id}', summary: 'Update manualAdjustments, submission type, or amount basis on a DRAFT report', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['manualAdjustments'] },
19
- 'delete': { method: 'DELETE', path: '/v1/vat-declarations/{id}', summary: 'Delete a VAT declaration that is still in DRAFT status', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
- 'regenerate': { method: 'POST', path: '/v1/vat-declarations/{id}/regenerate', summary: 'Re-run the full generation pipeline on a DRAFT report', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
- 'replace': { method: 'POST', path: '/v1/vat-declarations/{id}/replace', summary: 'Create a corrective DRAFT (Berichtigungsabrechnung) from a FILED report', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
22
- 'fields-contributions': { method: 'GET', path: '/v1/vat-declarations/{id}/fields/{fieldId}/contributions', summary: 'Drill into the contributions for a single Ziffer', pathParams: ['id', 'fieldId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
23
- 'fields-contributions-export': { method: 'GET', path: '/v1/vat-declarations/{id}/fields/{fieldId}/contributions/export', summary: 'Export the drill-down result set as an Excel workbook', pathParams: ['id', 'fieldId'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [] },
24
- 'approve': { method: 'POST', path: '/v1/vat-declarations/{id}/approve', summary: 'Approve a VAT report and produce the eCH-0217 XML submission file.', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
25
- 'mark-as-filed': { method: 'POST', path: '/v1/vat-declarations/{id}/mark-as-filed', summary: 'Mark a FINALIZED VAT declaration as filed.', pathParams: ['id'], hasBody: true, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
26
- 'xml': { method: 'GET', path: '/v1/vat-declarations/{id}/xml', summary: 'Download the eCH-0217 XML submission file for a FINALIZED or FILED VAT report.', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [] },
27
- 'notices-dismiss': { method: 'POST', path: '/v1/vat-declarations/{id}/notices/{noticeId}/dismiss', summary: 'Dismiss a generation notice with a mandatory reason.', pathParams: ['id', 'noticeId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
28
- 'notices-restore': { method: 'POST', path: '/v1/vat-declarations/{id}/notices/{noticeId}/restore', summary: 'Restore a previously dismissed generation notice.', pathParams: ['id', 'noticeId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
29
- 'list-files': { method: 'GET', path: '/v1/vat-declarations/{parentId}/files', summary: 'List file attachments for a VAT declaration', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
30
- 'create-files': { method: 'POST', path: '/v1/vat-declarations/{parentId}/files', summary: 'Upload a file attachment to a VAT declaration', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
31
- 'get-files': { method: 'GET', path: '/v1/vat-declarations/{parentId}/files/{fileId}', summary: 'Get a file attachment for a VAT declaration', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
32
- 'update-files': { method: 'PATCH', path: '/v1/vat-declarations/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a VAT declaration', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
33
- 'delete-files': { method: 'DELETE', path: '/v1/vat-declarations/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a VAT declaration', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
34
- 'files-download': { method: 'GET', path: '/v1/vat-declarations/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a VAT declaration', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [] },
35
- 'list-payments': { method: 'GET', path: '/v1/vat-declarations/{vatDeclarationId}/payments', summary: 'Get all payments for a VAT declaration', pathParams: ['vatDeclarationId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
36
- 'create-payments': { method: 'POST', path: '/v1/vat-declarations/{vatDeclarationId}/payments', summary: 'Create the payment for a filed VAT declaration', pathParams: ['vatDeclarationId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
37
- 'contributions-exclude': { method: 'POST', path: '/v1/vat-declarations/{id}/contributions/{contributionId}/exclude', summary: 'Take the booking behind one contribution row out of a DRAFT declaration.', pathParams: ['id', 'contributionId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
38
- 'exclusions-restore': { method: 'POST', path: '/v1/vat-declarations/{id}/exclusions/{exclusionId}/restore', summary: 'Put an excluded booking back into a DRAFT declaration.', pathParams: ['id', 'exclusionId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'list': { method: 'GET', path: '/v1/vat-declarations', summary: 'List VAT reports (paginated)', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
16
+ 'create': { method: 'POST', path: '/v1/vat-declarations', summary: 'Create a VAT declaration report (always async-accepted)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
17
+ 'preview': { method: 'GET', path: '/v1/vat-declarations/preview', summary: 'Preview the in-scope counts and late entries for a candidate period', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
18
+ 'get': { method: 'GET', path: '/v1/vat-declarations/{id}', summary: 'Get a VAT report by id (this is the polling endpoint for async generation)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
19
+ 'update': { method: 'PATCH', path: '/v1/vat-declarations/{id}', summary: 'Update manualAdjustments, submission type, or amount basis on a DRAFT report', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['manualAdjustments'], bodyMediaType: '', specParams: [] },
20
+ 'delete': { method: 'DELETE', path: '/v1/vat-declarations/{id}', summary: 'Delete a VAT declaration that is still in DRAFT status', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
21
+ 'regenerate': { method: 'POST', path: '/v1/vat-declarations/{id}/regenerate', summary: 'Re-run the full generation pipeline on a DRAFT report', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
22
+ 'replace': { method: 'POST', path: '/v1/vat-declarations/{id}/replace', summary: 'Create a corrective DRAFT (Berichtigungsabrechnung) from a FILED report', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
23
+ 'fields-contributions': { method: 'GET', path: '/v1/vat-declarations/{id}/fields/{fieldId}/contributions', summary: 'Drill into the contributions for a single Ziffer', pathParams: ['id', 'fieldId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
24
+ 'fields-contributions-export': { method: 'GET', path: '/v1/vat-declarations/{id}/fields/{fieldId}/contributions/export', summary: 'Export the drill-down result set as an Excel workbook', pathParams: ['id', 'fieldId'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
25
+ 'approve': { method: 'POST', path: '/v1/vat-declarations/{id}/approve', summary: 'Approve a VAT report and produce the eCH-0217 XML submission file.', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
26
+ 'mark-as-filed': { method: 'POST', path: '/v1/vat-declarations/{id}/mark-as-filed', summary: 'Mark a FINALIZED VAT declaration as filed.', pathParams: ['id'], hasBody: true, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
27
+ 'xml': { method: 'GET', path: '/v1/vat-declarations/{id}/xml', summary: 'Download the eCH-0217 XML submission file for a FINALIZED or FILED VAT report.', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
28
+ 'notices-dismiss': { method: 'POST', path: '/v1/vat-declarations/{id}/notices/{noticeId}/dismiss', summary: 'Dismiss a generation notice with a mandatory reason.', pathParams: ['id', 'noticeId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
29
+ 'notices-restore': { method: 'POST', path: '/v1/vat-declarations/{id}/notices/{noticeId}/restore', summary: 'Restore a previously dismissed generation notice.', pathParams: ['id', 'noticeId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
30
+ 'list-files': { method: 'GET', path: '/v1/vat-declarations/{parentId}/files', summary: 'List file attachments for a VAT declaration', pathParams: ['parentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
31
+ 'create-files': { method: 'POST', path: '/v1/vat-declarations/{parentId}/files', summary: 'Upload a file attachment to a VAT declaration', pathParams: ['parentId'], hasBody: false, isMultipart: true, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
32
+ 'get-files': { method: 'GET', path: '/v1/vat-declarations/{parentId}/files/{fileId}', summary: 'Get a file attachment for a VAT declaration', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
33
+ 'update-files': { method: 'PATCH', path: '/v1/vat-declarations/{parentId}/files/{fileId}', summary: 'Update file attachment metadata for a VAT declaration', pathParams: ['parentId', 'fileId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
34
+ 'delete-files': { method: 'DELETE', path: '/v1/vat-declarations/{parentId}/files/{fileId}', summary: 'Delete a file attachment from a VAT declaration', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
35
+ 'files-download': { method: 'GET', path: '/v1/vat-declarations/{parentId}/files/{fileId}/download', summary: 'Download a file attachment from a VAT declaration', pathParams: ['parentId', 'fileId'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
36
+ 'list-payments': { method: 'GET', path: '/v1/vat-declarations/{vatDeclarationId}/payments', summary: 'Get all payments for a VAT declaration', pathParams: ['vatDeclarationId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
37
+ 'create-payments': { method: 'POST', path: '/v1/vat-declarations/{vatDeclarationId}/payments', summary: 'Create the payment for a filed VAT declaration', pathParams: ['vatDeclarationId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
38
+ 'contributions-exclude': { method: 'POST', path: '/v1/vat-declarations/{id}/contributions/{contributionId}/exclude', summary: 'Take the booking behind one contribution row out of a DRAFT declaration.', pathParams: ['id', 'contributionId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
39
+ 'exclusions-restore': { method: 'POST', path: '/v1/vat-declarations/{id}/exclusions/{exclusionId}/restore', summary: 'Put an excluded booking back into a DRAFT declaration.', pathParams: ['id', 'exclusionId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [], bodyMediaType: '', specParams: [] },
39
40
  };
40
41
  export default class VatDeclarations extends Command {
41
42
  static description = 'Manage vat declarations';
@@ -57,7 +58,7 @@ export default class VatDeclarations extends Command {
57
58
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
58
59
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
59
60
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
60
- file: Flags.string({ char: 'f', description: 'Read request body from file' }),
61
+ file: Flags.string({ char: 'f', description: 'Read request body from file', parse: async (input) => resolveUserPath(input) }),
61
62
  set: Flags.string({ char: 's', description: 'Set individual field (e.g. --set firstName=John)', multiple: true }),
62
63
  'field-id': Flags.string({ description: 'Path param: fieldId' }),
63
64
  'notice-id': Flags.string({ description: 'Path param: noticeId' }),
@@ -193,6 +194,17 @@ export default class VatDeclarations extends Command {
193
194
  }
194
195
  }
195
196
  }
197
+ // Flags generated from this endpoint's own parameters. Raw-body endpoints
198
+ // have them: their body is not JSON, so --set cannot carry a value there.
199
+ for (const param of endpoint.specParams) {
200
+ const value = flags[param.flag];
201
+ if (value === undefined || value === '') {
202
+ if (param.required)
203
+ this.error(`Missing required flag: --${param.flag}`);
204
+ continue;
205
+ }
206
+ queryParams[param.name] = value;
207
+ }
196
208
  let data;
197
209
  if (endpoint.isMultipart) {
198
210
  if (!flags.file)
@@ -213,6 +225,25 @@ export default class VatDeclarations extends Command {
213
225
  }
214
226
  data = form;
215
227
  }
228
+ else if (endpoint.bodyMediaType) {
229
+ // The body is not JSON (today only application/x-ndjson): it is sent
230
+ // verbatim, under the content type the endpoint asks for.
231
+ if (flags.file) {
232
+ const fsModule = await import('node:fs');
233
+ try {
234
+ data = fsModule.readFileSync(flags.file, 'utf-8');
235
+ }
236
+ catch {
237
+ this.error(`Failed to read file: ${flags.file}`);
238
+ }
239
+ }
240
+ else if (flags.data) {
241
+ data = flags.data;
242
+ }
243
+ else {
244
+ this.error(`This endpoint takes its body as ${endpoint.bodyMediaType}. Pass the file with -f / --file, or the text with --data.`);
245
+ }
246
+ }
216
247
  else if (endpoint.hasBody) {
217
248
  // For binary-response endpoints, --file is the output path, not a JSON
218
249
  // input file — skip the read-as-body branch.
@@ -308,7 +339,7 @@ export default class VatDeclarations extends Command {
308
339
  process.stderr.write(`Body: [multipart/form-data file upload]\n`);
309
340
  }
310
341
  else if (data) {
311
- process.stderr.write(`Body: ${JSON.stringify(data, null, 2)}\n`);
342
+ process.stderr.write(`Body: ${typeof data === 'string' ? data : JSON.stringify(data, null, 2)}\n`);
312
343
  }
313
344
  process.stderr.write('\n');
314
345
  }
@@ -322,6 +353,7 @@ export default class VatDeclarations extends Command {
322
353
  const binResponse = await apiRequest(endpoint.method, apiPath, {
323
354
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
324
355
  data,
356
+ contentType: endpoint.bodyMediaType || undefined,
325
357
  contextName: flags.context,
326
358
  binary: true,
327
359
  });
@@ -347,6 +379,7 @@ export default class VatDeclarations extends Command {
347
379
  const raw = await apiRequest(endpoint.method, apiPath, {
348
380
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
349
381
  data,
382
+ contentType: endpoint.bodyMediaType || undefined,
350
383
  contextName: flags.context,
351
384
  binary: true,
352
385
  });
@@ -367,6 +400,7 @@ export default class VatDeclarations extends Command {
367
400
  response = await apiRequest(endpoint.method, apiPath, {
368
401
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
369
402
  data,
403
+ contentType: endpoint.bodyMediaType || undefined,
370
404
  contextName: flags.context,
371
405
  acceptAny: true,
372
406
  });