@aderis-erp/cli 1.0.15 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/README.md +30 -8
  2. package/dist/commands/access-groups.d.ts +0 -1
  3. package/dist/commands/access-groups.js +54 -78
  4. package/dist/commands/account-transaction-allocations.d.ts +0 -1
  5. package/dist/commands/account-transaction-allocations.js +51 -75
  6. package/dist/commands/account-transactions.d.ts +0 -1
  7. package/dist/commands/account-transactions.js +80 -94
  8. package/dist/commands/accounts.d.ts +0 -1
  9. package/dist/commands/accounts.js +59 -83
  10. package/dist/commands/agent-platform.d.ts +0 -1
  11. package/dist/commands/agent-platform.js +58 -82
  12. package/dist/commands/agent-runs.d.ts +39 -0
  13. package/dist/commands/agent-runs.js +1155 -0
  14. package/dist/commands/analytics.d.ts +0 -1
  15. package/dist/commands/analytics.js +52 -76
  16. package/dist/commands/{text-templates.d.ts → api-keys.d.ts} +1 -2
  17. package/dist/commands/api-keys.js +1150 -0
  18. package/dist/commands/async-jobs.d.ts +39 -0
  19. package/dist/commands/async-jobs.js +1158 -0
  20. package/dist/commands/audit-log.d.ts +0 -1
  21. package/dist/commands/audit-log.js +51 -75
  22. package/dist/commands/auth/login.d.ts +9 -0
  23. package/dist/commands/auth/login.js +73 -20
  24. package/dist/commands/auth/logout.d.ts +4 -0
  25. package/dist/commands/auth/logout.js +4 -0
  26. package/dist/commands/auth/status.js +26 -17
  27. package/dist/commands/auth/tenant.js +3 -6
  28. package/dist/commands/auth.d.ts +0 -1
  29. package/dist/commands/auth.js +5 -34
  30. package/dist/commands/bank-accounts.d.ts +0 -1
  31. package/dist/commands/bank-accounts.js +68 -91
  32. package/dist/commands/bank-integrations.d.ts +1 -1
  33. package/dist/commands/bank-integrations.js +71 -92
  34. package/dist/commands/business-partners.d.ts +1 -1
  35. package/dist/commands/business-partners.js +84 -100
  36. package/dist/commands/chat.d.ts +0 -1
  37. package/dist/commands/chat.js +57 -80
  38. package/dist/commands/config/add.d.ts +3 -0
  39. package/dist/commands/config/add.js +34 -16
  40. package/dist/commands/config/current.js +3 -0
  41. package/dist/commands/config/list.js +3 -0
  42. package/dist/commands/credit-card-accounts.d.ts +1 -1
  43. package/dist/commands/credit-card-accounts.js +72 -90
  44. package/dist/commands/delivery-notes.d.ts +0 -1
  45. package/dist/commands/delivery-notes.js +83 -105
  46. package/dist/commands/dunning-configs.d.ts +0 -1
  47. package/dist/commands/dunning-configs.js +54 -78
  48. package/dist/commands/dunning-reminders.d.ts +0 -1
  49. package/dist/commands/dunning-reminders.js +57 -81
  50. package/dist/commands/dunning-runs.d.ts +0 -1
  51. package/dist/commands/dunning-runs.js +50 -74
  52. package/dist/commands/email-transactions.d.ts +0 -1
  53. package/dist/commands/email-transactions.js +52 -76
  54. package/dist/commands/employees.d.ts +0 -1
  55. package/dist/commands/employees.js +81 -105
  56. package/dist/commands/entity-configurations.d.ts +0 -1
  57. package/dist/commands/entity-configurations.js +52 -76
  58. package/dist/commands/event-processor-mappings.d.ts +0 -1
  59. package/dist/commands/event-processor-mappings.js +54 -78
  60. package/dist/commands/files.d.ts +0 -1
  61. package/dist/commands/files.js +61 -85
  62. package/dist/commands/health.d.ts +0 -1
  63. package/dist/commands/health.js +54 -78
  64. package/dist/commands/imports.d.ts +39 -0
  65. package/dist/commands/imports.js +1151 -0
  66. package/dist/commands/inbox.d.ts +0 -1
  67. package/dist/commands/inbox.js +57 -80
  68. package/dist/commands/integration-identities.d.ts +40 -0
  69. package/dist/commands/integration-identities.js +1151 -0
  70. package/dist/commands/integrations.d.ts +0 -1
  71. package/dist/commands/integrations.js +52 -76
  72. package/dist/commands/item-categories.d.ts +0 -1
  73. package/dist/commands/item-categories.js +54 -78
  74. package/dist/commands/items.d.ts +0 -1
  75. package/dist/commands/items.js +54 -78
  76. package/dist/commands/journal-lines.d.ts +0 -1
  77. package/dist/commands/journal-lines.js +50 -74
  78. package/dist/commands/journals.d.ts +1 -1
  79. package/dist/commands/journals.js +75 -96
  80. package/dist/commands/ledgers.d.ts +3 -2
  81. package/dist/commands/ledgers.js +100 -95
  82. package/dist/commands/my-account.d.ts +39 -0
  83. package/dist/commands/my-account.js +1160 -0
  84. package/dist/commands/office-locations.d.ts +0 -1
  85. package/dist/commands/office-locations.js +54 -78
  86. package/dist/commands/payment-methods.d.ts +0 -1
  87. package/dist/commands/payment-methods.js +59 -83
  88. package/dist/commands/payment-runs.d.ts +0 -1
  89. package/dist/commands/payment-runs.js +66 -90
  90. package/dist/commands/payment-terms.d.ts +0 -1
  91. package/dist/commands/payment-terms.js +55 -79
  92. package/dist/commands/payments.d.ts +0 -1
  93. package/dist/commands/payments.js +56 -80
  94. package/dist/commands/payroll-runs.d.ts +43 -0
  95. package/dist/commands/payroll-runs.js +1188 -0
  96. package/dist/commands/permissions.d.ts +39 -0
  97. package/dist/commands/permissions.js +1146 -0
  98. package/dist/commands/price-lists.d.ts +0 -1
  99. package/dist/commands/price-lists.js +59 -83
  100. package/dist/commands/processor-mappings.d.ts +0 -1
  101. package/dist/commands/processor-mappings.js +53 -77
  102. package/dist/commands/processors.d.ts +0 -1
  103. package/dist/commands/processors.js +60 -85
  104. package/dist/commands/purchase-order-types.d.ts +0 -1
  105. package/dist/commands/purchase-order-types.js +54 -78
  106. package/dist/commands/purchase-orders.d.ts +0 -1
  107. package/dist/commands/purchase-orders.js +74 -97
  108. package/dist/commands/reports.d.ts +0 -1
  109. package/dist/commands/reports.js +54 -78
  110. package/dist/commands/roles.d.ts +39 -0
  111. package/dist/commands/{text-templates.js → roles.js} +64 -90
  112. package/dist/commands/salary-types.d.ts +0 -1
  113. package/dist/commands/salary-types.js +54 -78
  114. package/dist/commands/sales-invoices.d.ts +0 -1
  115. package/dist/commands/sales-invoices.js +90 -110
  116. package/dist/commands/sales-orders.d.ts +0 -1
  117. package/dist/commands/sales-orders.js +83 -105
  118. package/dist/commands/saved-views.d.ts +0 -1
  119. package/dist/commands/saved-views.js +52 -76
  120. package/dist/commands/scheduled-processor-mappings.d.ts +0 -1
  121. package/dist/commands/scheduled-processor-mappings.js +54 -78
  122. package/dist/commands/schedules.d.ts +0 -1
  123. package/dist/commands/schedules.js +58 -82
  124. package/dist/commands/switch.js +4 -4
  125. package/dist/commands/tag-configurations.d.ts +0 -1
  126. package/dist/commands/tag-configurations.js +56 -79
  127. package/dist/commands/tags.d.ts +0 -1
  128. package/dist/commands/tags.js +54 -78
  129. package/dist/commands/tax-codes.d.ts +0 -1
  130. package/dist/commands/tax-codes.js +57 -81
  131. package/dist/commands/tax-matrix.d.ts +0 -1
  132. package/dist/commands/tax-matrix.js +54 -78
  133. package/dist/commands/templates.d.ts +39 -0
  134. package/dist/commands/templates.js +1157 -0
  135. package/dist/commands/tenants.d.ts +0 -1
  136. package/dist/commands/tenants.js +58 -81
  137. package/dist/commands/tools.d.ts +0 -1
  138. package/dist/commands/tools.js +51 -75
  139. package/dist/commands/users.d.ts +42 -0
  140. package/dist/commands/users.js +1174 -0
  141. package/dist/commands/vat-declarations.d.ts +3 -1
  142. package/dist/commands/vat-declarations.js +78 -93
  143. package/dist/commands/vendor-invoices.d.ts +0 -1
  144. package/dist/commands/vendor-invoices.js +87 -110
  145. package/dist/commands/webhooks.d.ts +0 -1
  146. package/dist/commands/webhooks.js +56 -80
  147. package/dist/commands/workflows.d.ts +0 -1
  148. package/dist/commands/workflows.js +52 -76
  149. package/dist/lib/api-client.d.ts +6 -0
  150. package/dist/lib/api-client.js +16 -6
  151. package/dist/lib/auth/constants.d.ts +11 -0
  152. package/dist/lib/auth/constants.js +11 -0
  153. package/dist/lib/auth/errors.d.ts +17 -0
  154. package/dist/lib/auth/errors.js +22 -0
  155. package/dist/lib/auth/flow.d.ts +27 -0
  156. package/dist/lib/auth/flow.js +163 -0
  157. package/dist/lib/auth/guards.d.ts +5 -0
  158. package/dist/lib/auth/guards.js +14 -0
  159. package/dist/lib/auth/http.d.ts +16 -0
  160. package/dist/lib/auth/http.js +49 -0
  161. package/dist/lib/auth/index.d.ts +49 -0
  162. package/dist/lib/auth/index.js +228 -0
  163. package/dist/lib/auth/jwks.d.ts +19 -0
  164. package/dist/lib/auth/jwks.js +112 -0
  165. package/dist/lib/auth/keycloak-provider.d.ts +91 -0
  166. package/dist/lib/auth/keycloak-provider.js +244 -0
  167. package/dist/lib/auth/pages.d.ts +25 -0
  168. package/dist/lib/auth/pages.js +123 -0
  169. package/dist/lib/auth/pkce.d.ts +11 -0
  170. package/dist/lib/auth/pkce.js +17 -0
  171. package/dist/lib/auth/tokens.d.ts +59 -0
  172. package/dist/lib/auth/tokens.js +105 -0
  173. package/dist/lib/auth/types.d.ts +61 -0
  174. package/dist/lib/auth/types.js +1 -0
  175. package/dist/lib/auth/validated-url.d.ts +6 -0
  176. package/dist/lib/auth/validated-url.js +53 -0
  177. package/dist/lib/config.d.ts +13 -3
  178. package/dist/lib/config.js +24 -19
  179. package/oclif.manifest.json +2447 -641
  180. package/package.json +5 -4
  181. package/dist/lib/auth.d.ts +0 -43
  182. package/dist/lib/auth.js +0 -610
@@ -1,5 +1,5 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
- import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
2
+ import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
4
  // Add 'import' action if both create and update exist
5
5
  const HAS_IMPORT = true;
@@ -8,23 +8,23 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["open-submissions", "eligible-payments", "list", "create", "preview", "schedule", "status", "get", "update", "delete", "create-payments", "delete-payments", "send", "painfile", "sync-status", "sync-status-xml", "sync-all"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'open-submissions': { method: 'GET', path: '/v1/payment-runs/bank-accounts/{bankAccountId}/open-submissions', summary: 'Get open submission IDs for a bank account', pathParams: ['bankAccountId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'eligible-payments': { method: 'GET', path: '/v1/payment-runs/eligible-payments/{bankAccountId}', summary: 'Show what a payment run from this bank account would pay', pathParams: ['bankAccountId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
- 'list': { method: 'GET', path: '/v1/payment-runs', summary: 'List all payment runs with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'create': { method: 'POST', path: '/v1/payment-runs', summary: 'Create a DRAFT payment run from payment IDs', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['paymentIds'] },
15
- 'preview': { method: 'POST', path: '/v1/payment-runs/preview/{bankAccountId}', summary: 'Preview eligible and filtered invoices for a payment run', pathParams: ['bankAccountId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['invoiceIds'] },
16
- 'schedule': { method: 'POST', path: '/v1/payment-runs/schedule/{bankAccountId}', summary: 'Auto-create DRAFT from eligible payments for a bank account', pathParams: ['bankAccountId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
17
- 'status': { method: 'GET', path: '/v1/payment-runs/status/{providerId}/{submissionId}', summary: 'Fetch live payment status from provider (read-only)', pathParams: ['providerId', 'submissionId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
18
- 'get': { method: 'GET', path: '/v1/payment-runs/{id}', summary: 'Get a payment run details by ID and show the currently persisted status.', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
19
- 'update': { method: 'PATCH', path: '/v1/payment-runs/{id}', summary: 'Update a DRAFT payment run (mode)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
20
- 'delete': { method: 'DELETE', path: '/v1/payment-runs/{id}', summary: 'Delete a DRAFT payment run (releases payments)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
21
- 'create-payments': { method: 'POST', path: '/v1/payment-runs/{id}/payments', summary: 'Add payments to a DRAFT payment run', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['paymentIds'] },
22
- 'delete-payments': { method: 'DELETE', path: '/v1/payment-runs/{id}/payments/{paymentId}', summary: 'Remove an item from a DRAFT payment run', pathParams: ['id', 'paymentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
23
- 'send': { method: 'POST', path: '/v1/payment-runs/{id}/send', summary: 'Execute a DRAFT payment run (with dryRun support)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
24
- 'painfile': { method: 'GET', path: '/v1/payment-runs/{id}/painfile', summary: 'Download or preview PAIN XML for a payment run', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: true, bodyArrayFields: [] },
25
- 'sync-status': { method: 'POST', path: '/v1/payment-runs/update/status/{submissionId}', summary: 'Sync payment status for a specific submission', pathParams: ['submissionId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
26
- 'sync-status-xml': { method: 'POST', path: '/v1/payment-runs/update/status/{submissionId}/xml', summary: 'Sync payment status from pain.002 XML for a specific submission', pathParams: ['submissionId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
27
- 'sync-all': { method: 'POST', path: '/v1/payment-runs/status', summary: 'Sync payment status for all pending submissions', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'open-submissions': { method: 'GET', path: '/v1/payment-runs/bank-accounts/{bankAccountId}/open-submissions', summary: 'Get open submission IDs for a bank account', pathParams: ['bankAccountId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'eligible-payments': { method: 'GET', path: '/v1/payment-runs/eligible-payments/{bankAccountId}', summary: 'Show what a payment run from this bank account would pay', pathParams: ['bankAccountId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
13
+ 'list': { method: 'GET', path: '/v1/payment-runs', summary: 'List all payment runs with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
14
+ 'create': { method: 'POST', path: '/v1/payment-runs', summary: 'Create a DRAFT payment run from payment IDs', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['paymentIds'] },
15
+ 'preview': { method: 'POST', path: '/v1/payment-runs/preview/{bankAccountId}', summary: 'Preview eligible and filtered invoices for a payment run', pathParams: ['bankAccountId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['invoiceIds'] },
16
+ 'schedule': { method: 'POST', path: '/v1/payment-runs/schedule/{bankAccountId}', summary: 'Auto-create DRAFT from eligible payments for a bank account', pathParams: ['bankAccountId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
+ 'status': { method: 'GET', path: '/v1/payment-runs/status/{providerId}/{submissionId}', summary: 'Fetch live payment status from provider (read-only)', pathParams: ['providerId', 'submissionId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
18
+ 'get': { method: 'GET', path: '/v1/payment-runs/{id}', summary: 'Get a payment run details by ID and show the currently persisted status.', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
19
+ 'update': { method: 'PATCH', path: '/v1/payment-runs/{id}', summary: 'Update a DRAFT payment run (mode, execution date)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
20
+ 'delete': { method: 'DELETE', path: '/v1/payment-runs/{id}', summary: 'Delete a DRAFT payment run (releases payments)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
21
+ 'create-payments': { method: 'POST', path: '/v1/payment-runs/{id}/payments', summary: 'Add payments to a DRAFT payment run', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['paymentIds'] },
22
+ 'delete-payments': { method: 'DELETE', path: '/v1/payment-runs/{id}/payments/{paymentId}', summary: 'Remove an item from a DRAFT payment run', pathParams: ['id', 'paymentId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
23
+ 'send': { method: 'POST', path: '/v1/payment-runs/{id}/send', summary: 'Execute a DRAFT payment run (with dryRun support)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
24
+ 'painfile': { method: 'GET', path: '/v1/payment-runs/{id}/painfile', summary: 'Download or preview PAIN XML for a payment run', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: true, hasBinaryVariant: false, bodyArrayFields: [] },
25
+ 'sync-status': { method: 'POST', path: '/v1/payment-runs/update/status/{submissionId}', summary: 'Sync payment status for a specific submission', pathParams: ['submissionId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
26
+ 'sync-status-xml': { method: 'POST', path: '/v1/payment-runs/update/status/{submissionId}/xml', summary: 'Sync payment status from pain.002 XML for a specific submission', pathParams: ['submissionId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
27
+ 'sync-all': { method: 'POST', path: '/v1/payment-runs/status', summary: 'Sync payment status for all pending submissions', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
28
28
  };
29
29
  export default class PaymentRuns extends Command {
30
30
  static description = 'Manage payment runs';
@@ -57,7 +57,7 @@ export default class PaymentRuns extends Command {
57
57
  'sort-by': Flags.string({ description: 'Sort field(s), comma-separated. Direction (:ASC/:DESC) is optional; date-like fields default to DESC, others to ASC.' }),
58
58
  filter: Flags.string({ description: 'Filter (e.g. --filter status=ACTIVE). Operators: in:A,B, like:%, gt:, lt:, null:', multiple: true }),
59
59
  query: Flags.string({ description: 'Additional query params as JSON' }),
60
- fields: Flags.string({ description: 'Comma-separated list of fields to show (e.g. --fields=id,name,status)' }),
60
+ fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
61
61
  sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
62
62
  'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
63
63
  };
@@ -129,6 +129,8 @@ export default class PaymentRuns extends Command {
129
129
  queryParams.search = flags.search;
130
130
  if (flags['sort-by'])
131
131
  queryParams.sortBy = normalizeSortBy(flags['sort-by']);
132
+ if (flags.fields)
133
+ queryParams.fields = flags.fields;
132
134
  if (flags.query) {
133
135
  try {
134
136
  Object.assign(queryParams, JSON.parse(flags.query));
@@ -276,10 +278,11 @@ export default class PaymentRuns extends Command {
276
278
  process.stderr.write('\n');
277
279
  }
278
280
  try {
279
- // Binary response: download to file
281
+ // Binary response: to -f / --file, or to stdout when it is redirected.
280
282
  if (endpoint.isBinaryResponse) {
281
- if (!flags.file) {
282
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
283
+ const toStdout = !flags.file;
284
+ if (toStdout && process.stdout.isTTY) {
285
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
283
286
  }
284
287
  const binResponse = await apiRequest(endpoint.method, apiPath, {
285
288
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -287,24 +290,55 @@ export default class PaymentRuns extends Command {
287
290
  contextName: flags.context,
288
291
  binary: true,
289
292
  });
290
- const fsModule = await import('node:fs');
291
293
  const binData = binResponse.data;
292
294
  const buffer = Buffer.isBuffer(binData)
293
295
  ? binData
294
296
  : Buffer.from(binData);
297
+ if (toStdout) {
298
+ // Nothing else may reach stdout here — it is the file.
299
+ process.stdout.write(buffer);
300
+ return;
301
+ }
302
+ const fsModule = await import('node:fs');
295
303
  fsModule.writeFileSync(flags.file, buffer);
296
304
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
297
305
  return;
298
306
  }
299
- const response = await apiRequest(endpoint.method, apiPath, {
300
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
301
- data,
302
- contextName: flags.context,
303
- acceptAny: true,
304
- });
307
+ let response;
308
+ if (endpoint.hasBinaryVariant) {
309
+ // This route answers JSON or a file depending on the request body, so the
310
+ // bytes are read first and the response's own content-type decides. Asking
311
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
312
+ const raw = await apiRequest(endpoint.method, apiPath, {
313
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
314
+ data,
315
+ contextName: flags.context,
316
+ binary: true,
317
+ });
318
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
319
+ const rawData = raw.data;
320
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
321
+ if (!isTextualMediaType(rawType)) {
322
+ if (process.stdout.isTTY) {
323
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
324
+ }
325
+ // Nothing else may reach stdout here — it is the file.
326
+ process.stdout.write(buffer);
327
+ return;
328
+ }
329
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
330
+ }
331
+ else {
332
+ response = await apiRequest(endpoint.method, apiPath, {
333
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
334
+ data,
335
+ contextName: flags.context,
336
+ acceptAny: true,
337
+ });
338
+ }
305
339
  // Check content-type for non-JSON responses (XML, text, etc.)
306
340
  const contentType = (response.headers['content-type'] || '').toLowerCase();
307
- if (contentType.includes('xml') || contentType.includes('text/')) {
341
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
308
342
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
309
343
  this.log(String(response.data));
310
344
  return;
@@ -331,11 +365,6 @@ export default class PaymentRuns extends Command {
331
365
  output = obj[arrayKeys[0]];
332
366
  }
333
367
  }
334
- // Filter fields if --fields flag is provided
335
- if (flags.fields) {
336
- const fieldList = flags.fields.split(',').map(f => f.trim());
337
- output = this.filterFields(output, fieldList);
338
- }
339
368
  // Sanitize output if requested
340
369
  if (flags.sanitize) {
341
370
  output = this.sanitizeOutput(output);
@@ -590,6 +619,8 @@ export default class PaymentRuns extends Command {
590
619
  baseParams.sortBy = normalizeSortBy(flags['sort-by']);
591
620
  if (flags.include)
592
621
  baseParams.include = flags.include;
622
+ if (flags.fields)
623
+ baseParams.fields = flags.fields;
593
624
  if (flags.query) {
594
625
  try {
595
626
  Object.assign(baseParams, JSON.parse(flags.query));
@@ -644,12 +675,7 @@ export default class PaymentRuns extends Command {
644
675
  }
645
676
  }
646
677
  process.stderr.write(' done\n');
647
- // Filter fields if specified
648
678
  let output = allItems;
649
- if (flags.fields) {
650
- const fieldList = flags.fields.split(',').map(f => f.trim());
651
- output = this.filterFields(allItems, fieldList);
652
- }
653
679
  // Sanitize output if requested
654
680
  if (flags.sanitize) {
655
681
  output = this.sanitizeOutput(output);
@@ -978,56 +1004,6 @@ export default class PaymentRuns extends Command {
978
1004
  return `\$${op}`;
979
1005
  return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
980
1006
  }
981
- filterFields(data, fields) {
982
- const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
983
- const pick = (obj, keys) => {
984
- // Group keys by their first segment so sibling sub-paths
985
- // (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
986
- // and don't clobber each other on merge.
987
- const directKeys = new Set();
988
- const nestedKeys = new Map();
989
- for (const key of keys) {
990
- if (key.split('.').some(isUnsafeKey))
991
- continue;
992
- if (key.includes('.')) {
993
- const [first, ...rest] = key.split('.');
994
- if (isUnsafeKey(first))
995
- continue;
996
- const list = nestedKeys.get(first);
997
- if (list) {
998
- list.push(rest.join('.'));
999
- }
1000
- else {
1001
- nestedKeys.set(first, [rest.join('.')]);
1002
- }
1003
- }
1004
- else {
1005
- directKeys.add(key);
1006
- }
1007
- }
1008
- const result = {};
1009
- for (const key of directKeys) {
1010
- if (key in obj)
1011
- result[key] = obj[key];
1012
- }
1013
- for (const [first, restKeys] of nestedKeys) {
1014
- const val = obj[first];
1015
- if (!val || typeof val !== 'object')
1016
- continue;
1017
- if (Array.isArray(val)) {
1018
- result[first] = val.map(item => pick(item, restKeys));
1019
- }
1020
- else {
1021
- result[first] = pick(val, restKeys);
1022
- }
1023
- }
1024
- return result;
1025
- };
1026
- if (Array.isArray(data)) {
1027
- return data.map(item => pick(item, fields));
1028
- }
1029
- return pick(data, fields);
1030
- }
1031
1007
  sanitizeOutput(data) {
1032
1008
  const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
1033
1009
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -31,7 +31,6 @@ export default class PaymentTerms extends Command {
31
31
  private runCamt053Import;
32
32
  private runNestedImport;
33
33
  private parseFilterValue;
34
- private filterFields;
35
34
  private sanitizeOutput;
36
35
  private getColumns;
37
36
  private getValue;
@@ -1,5 +1,5 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
- import { apiRequest, ApiError, normalizeSortBy } from '../lib/api-client.js';
2
+ import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
4
  // Add 'import' action if both create and update exist
5
5
  const HAS_IMPORT = true;
@@ -8,12 +8,12 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["list", "create", "default", "get", "update", "delete"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/payment-terms', summary: 'Get all payment terms with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
- 'create': { method: 'POST', path: '/v1/payment-terms', summary: 'Create a new payment term', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['earlyPaymentDiscounts'] },
13
- 'default': { method: 'GET', path: '/v1/payment-terms/default', summary: 'Get the default payment term for the tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
- 'get': { method: 'GET', path: '/v1/payment-terms/{id}', summary: 'Get a payment term by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
15
- 'update': { method: 'PATCH', path: '/v1/payment-terms/{id}', summary: 'Update a payment term', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
- 'delete': { method: 'DELETE', path: '/v1/payment-terms/{id}', summary: 'Delete a payment term', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
11
+ 'list': { method: 'GET', path: '/v1/payment-terms', summary: 'Get all payment terms with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
+ 'create': { method: 'POST', path: '/v1/payment-terms', summary: 'Create a new payment term', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['earlyPaymentDiscounts'] },
13
+ 'default': { method: 'GET', path: '/v1/payment-terms/default', summary: 'Get the default payment term for the tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
14
+ 'get': { method: 'GET', path: '/v1/payment-terms/{id}', summary: 'Get a payment term by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
15
+ 'update': { method: 'PATCH', path: '/v1/payment-terms/{id}', summary: 'Update a payment term', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
16
+ 'delete': { method: 'DELETE', path: '/v1/payment-terms/{id}', summary: 'Delete a payment term', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
17
17
  };
18
18
  export default class PaymentTerms extends Command {
19
19
  static description = 'Manage payment terms';
@@ -44,7 +44,7 @@ export default class PaymentTerms extends Command {
44
44
  'sort-by': Flags.string({ description: 'Sort field(s), comma-separated. Direction (:ASC/:DESC) is optional; date-like fields default to DESC, others to ASC.' }),
45
45
  filter: Flags.string({ description: 'Filter (e.g. --filter status=ACTIVE). Operators: in:A,B, like:%, gt:, lt:, null:', multiple: true }),
46
46
  query: Flags.string({ description: 'Additional query params as JSON' }),
47
- fields: Flags.string({ description: 'Comma-separated list of fields to show (e.g. --fields=id,name,status)' }),
47
+ fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
48
48
  sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
49
49
  'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
50
50
  };
@@ -116,6 +116,8 @@ export default class PaymentTerms extends Command {
116
116
  queryParams.search = flags.search;
117
117
  if (flags['sort-by'])
118
118
  queryParams.sortBy = normalizeSortBy(flags['sort-by']);
119
+ if (flags.fields)
120
+ queryParams.fields = flags.fields;
119
121
  if (flags.query) {
120
122
  try {
121
123
  Object.assign(queryParams, JSON.parse(flags.query));
@@ -263,10 +265,11 @@ export default class PaymentTerms extends Command {
263
265
  process.stderr.write('\n');
264
266
  }
265
267
  try {
266
- // Binary response: download to file
268
+ // Binary response: to -f / --file, or to stdout when it is redirected.
267
269
  if (endpoint.isBinaryResponse) {
268
- if (!flags.file) {
269
- this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
270
+ const toStdout = !flags.file;
271
+ if (toStdout && process.stdout.isTTY) {
272
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx), or redirect stdout to a file.`);
270
273
  }
271
274
  const binResponse = await apiRequest(endpoint.method, apiPath, {
272
275
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
@@ -274,24 +277,55 @@ export default class PaymentTerms extends Command {
274
277
  contextName: flags.context,
275
278
  binary: true,
276
279
  });
277
- const fsModule = await import('node:fs');
278
280
  const binData = binResponse.data;
279
281
  const buffer = Buffer.isBuffer(binData)
280
282
  ? binData
281
283
  : Buffer.from(binData);
284
+ if (toStdout) {
285
+ // Nothing else may reach stdout here — it is the file.
286
+ process.stdout.write(buffer);
287
+ return;
288
+ }
289
+ const fsModule = await import('node:fs');
282
290
  fsModule.writeFileSync(flags.file, buffer);
283
291
  this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
284
292
  return;
285
293
  }
286
- const response = await apiRequest(endpoint.method, apiPath, {
287
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
288
- data,
289
- contextName: flags.context,
290
- acceptAny: true,
291
- });
294
+ let response;
295
+ if (endpoint.hasBinaryVariant) {
296
+ // This route answers JSON or a file depending on the request body, so the
297
+ // bytes are read first and the response's own content-type decides. Asking
298
+ // for text here would corrupt an xlsx/pdf before anything could inspect it.
299
+ const raw = await apiRequest(endpoint.method, apiPath, {
300
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
301
+ data,
302
+ contextName: flags.context,
303
+ binary: true,
304
+ });
305
+ const rawType = String(raw.headers['content-type'] || '').toLowerCase();
306
+ const rawData = raw.data;
307
+ const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);
308
+ if (!isTextualMediaType(rawType)) {
309
+ if (process.stdout.isTTY) {
310
+ this.error(`The API returned ${rawType || 'binary data'}. Redirect stdout to a file, e.g. ... > report.xlsx`);
311
+ }
312
+ // Nothing else may reach stdout here — it is the file.
313
+ process.stdout.write(buffer);
314
+ return;
315
+ }
316
+ response = { data: buffer.toString('utf8'), headers: raw.headers, status: raw.status };
317
+ }
318
+ else {
319
+ response = await apiRequest(endpoint.method, apiPath, {
320
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
321
+ data,
322
+ contextName: flags.context,
323
+ acceptAny: true,
324
+ });
325
+ }
292
326
  // Check content-type for non-JSON responses (XML, text, etc.)
293
327
  const contentType = (response.headers['content-type'] || '').toLowerCase();
294
- if (contentType.includes('xml') || contentType.includes('text/')) {
328
+ if (isTextualMediaType(contentType) && !contentType.startsWith('application/json')) {
295
329
  // Output raw content for XML/text responses (CSV, plain, HTML, etc.)
296
330
  this.log(String(response.data));
297
331
  return;
@@ -318,11 +352,6 @@ export default class PaymentTerms extends Command {
318
352
  output = obj[arrayKeys[0]];
319
353
  }
320
354
  }
321
- // Filter fields if --fields flag is provided
322
- if (flags.fields) {
323
- const fieldList = flags.fields.split(',').map(f => f.trim());
324
- output = this.filterFields(output, fieldList);
325
- }
326
355
  // Sanitize output if requested
327
356
  if (flags.sanitize) {
328
357
  output = this.sanitizeOutput(output);
@@ -577,6 +606,8 @@ export default class PaymentTerms extends Command {
577
606
  baseParams.sortBy = normalizeSortBy(flags['sort-by']);
578
607
  if (flags.include)
579
608
  baseParams.include = flags.include;
609
+ if (flags.fields)
610
+ baseParams.fields = flags.fields;
580
611
  if (flags.query) {
581
612
  try {
582
613
  Object.assign(baseParams, JSON.parse(flags.query));
@@ -631,12 +662,7 @@ export default class PaymentTerms extends Command {
631
662
  }
632
663
  }
633
664
  process.stderr.write(' done\n');
634
- // Filter fields if specified
635
665
  let output = allItems;
636
- if (flags.fields) {
637
- const fieldList = flags.fields.split(',').map(f => f.trim());
638
- output = this.filterFields(allItems, fieldList);
639
- }
640
666
  // Sanitize output if requested
641
667
  if (flags.sanitize) {
642
668
  output = this.sanitizeOutput(output);
@@ -965,56 +991,6 @@ export default class PaymentTerms extends Command {
965
991
  return `\$${op}`;
966
992
  return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
967
993
  }
968
- filterFields(data, fields) {
969
- const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
970
- const pick = (obj, keys) => {
971
- // Group keys by their first segment so sibling sub-paths
972
- // (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
973
- // and don't clobber each other on merge.
974
- const directKeys = new Set();
975
- const nestedKeys = new Map();
976
- for (const key of keys) {
977
- if (key.split('.').some(isUnsafeKey))
978
- continue;
979
- if (key.includes('.')) {
980
- const [first, ...rest] = key.split('.');
981
- if (isUnsafeKey(first))
982
- continue;
983
- const list = nestedKeys.get(first);
984
- if (list) {
985
- list.push(rest.join('.'));
986
- }
987
- else {
988
- nestedKeys.set(first, [rest.join('.')]);
989
- }
990
- }
991
- else {
992
- directKeys.add(key);
993
- }
994
- }
995
- const result = {};
996
- for (const key of directKeys) {
997
- if (key in obj)
998
- result[key] = obj[key];
999
- }
1000
- for (const [first, restKeys] of nestedKeys) {
1001
- const val = obj[first];
1002
- if (!val || typeof val !== 'object')
1003
- continue;
1004
- if (Array.isArray(val)) {
1005
- result[first] = val.map(item => pick(item, restKeys));
1006
- }
1007
- else {
1008
- result[first] = pick(val, restKeys);
1009
- }
1010
- }
1011
- return result;
1012
- };
1013
- if (Array.isArray(data)) {
1014
- return data.map(item => pick(item, fields));
1015
- }
1016
- return pick(data, fields);
1017
- }
1018
994
  sanitizeOutput(data) {
1019
995
  const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
1020
996
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -31,7 +31,6 @@ export default class Payments extends Command {
31
31
  private runCamt053Import;
32
32
  private runNestedImport;
33
33
  private parseFilterValue;
34
- private filterFields;
35
34
  private sanitizeOutput;
36
35
  private getColumns;
37
36
  private getValue;