@aderis-erp/cli 1.0.2 → 1.0.4

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 (191) hide show
  1. package/README.md +6 -0
  2. package/dist/commands/access-groups 3.d.ts +40 -0
  3. package/dist/commands/{access-groups 2.js → access-groups 3.js } +106 -40
  4. package/dist/commands/access-groups.d.ts +40 -0
  5. package/dist/commands/access-groups.js +1074 -0
  6. package/dist/commands/account-transactions.d.ts +42 -0
  7. package/dist/commands/account-transactions.js +1088 -0
  8. package/dist/commands/accounts 2.d.ts +41 -0
  9. package/dist/commands/accounts 2.js +1078 -0
  10. package/dist/commands/accounts.d.ts +41 -0
  11. package/dist/commands/accounts.js +1080 -0
  12. package/dist/commands/agent-runs.d.ts +40 -0
  13. package/dist/commands/agent-runs.js +1071 -0
  14. package/dist/commands/async-endpoints-internal 3.d.ts +40 -0
  15. package/dist/commands/{processing-logs.js → async-endpoints-internal 3.js } +110 -46
  16. package/dist/commands/async-endpoints-internal.d.ts +40 -0
  17. package/dist/commands/async-endpoints-internal.js +1064 -0
  18. package/dist/commands/audit-log 3.d.ts +40 -0
  19. package/dist/commands/audit-log 3.js +1066 -0
  20. package/dist/commands/audit-log.d.ts +40 -0
  21. package/dist/commands/audit-log.js +1068 -0
  22. package/dist/commands/auth/login.d.ts +14 -0
  23. package/dist/commands/auth/login.js +116 -0
  24. package/dist/commands/auth/logout.d.ts +10 -0
  25. package/dist/commands/auth/logout.js +58 -0
  26. package/dist/commands/auth/status.d.ts +12 -0
  27. package/dist/commands/auth/status.js +101 -0
  28. package/dist/commands/auth/tenant.d.ts +19 -0
  29. package/dist/commands/auth/tenant.js +184 -0
  30. package/dist/commands/auth.d.ts +37 -0
  31. package/dist/commands/auth.js +871 -0
  32. package/dist/commands/bank-accounts.d.ts +42 -0
  33. package/dist/commands/bank-accounts.js +1088 -0
  34. package/dist/commands/bank-integrations.d.ts +1 -0
  35. package/dist/commands/bank-integrations.js +122 -54
  36. package/dist/commands/business-partners 3.d.ts +41 -0
  37. package/dist/commands/business-partners 3.js +1078 -0
  38. package/dist/commands/business-partners---contacts 3.d.ts +42 -0
  39. package/dist/commands/business-partners---contacts 3.js +1072 -0
  40. package/dist/commands/business-partners---contacts.d.ts +1 -0
  41. package/dist/commands/business-partners---contacts.js +114 -46
  42. package/dist/commands/business-partners.d.ts +1 -0
  43. package/dist/commands/business-partners.js +114 -46
  44. package/dist/commands/config/add.d.ts +13 -0
  45. package/dist/commands/config/add.js +120 -0
  46. package/dist/commands/config/current.d.ts +9 -0
  47. package/dist/commands/config/current.js +43 -0
  48. package/dist/commands/config/delete.d.ts +12 -0
  49. package/dist/commands/config/delete.js +67 -0
  50. package/dist/commands/config/list.d.ts +9 -0
  51. package/dist/commands/config/list.js +50 -0
  52. package/dist/commands/config/use.d.ts +9 -0
  53. package/dist/commands/config/use.js +40 -0
  54. package/dist/commands/credit-card-accounts 3.d.ts +42 -0
  55. package/dist/commands/credit-card-accounts 3.js +1084 -0
  56. package/dist/commands/credit-card-accounts.d.ts +1 -0
  57. package/dist/commands/credit-card-accounts.js +119 -51
  58. package/dist/commands/documents.d.ts +1 -0
  59. package/dist/commands/documents.js +110 -42
  60. package/dist/commands/dunning-configs 3.d.ts +40 -0
  61. package/dist/commands/dunning-configs 3.js +1072 -0
  62. package/dist/commands/dunning-configs.d.ts +1 -0
  63. package/dist/commands/dunning-configs.js +109 -41
  64. package/dist/commands/dunning-reminders.d.ts +1 -0
  65. package/dist/commands/dunning-reminders.js +112 -44
  66. package/dist/commands/dunning-runs 3.d.ts +40 -0
  67. package/dist/commands/{pubsub.js → dunning-runs 3.js } +108 -43
  68. package/dist/commands/dunning-runs.d.ts +1 -0
  69. package/dist/commands/dunning-runs.js +105 -37
  70. package/dist/commands/employees.d.ts +1 -0
  71. package/dist/commands/employees.js +136 -68
  72. package/dist/commands/event-processor-mappings 3.d.ts +40 -0
  73. package/dist/commands/event-processor-mappings 3.js +1071 -0
  74. package/dist/commands/event-processor-mappings.d.ts +1 -0
  75. package/dist/commands/event-processor-mappings.js +109 -41
  76. package/dist/commands/files-internal 3.d.ts +40 -0
  77. package/dist/commands/files-internal 3.js +1073 -0
  78. package/dist/commands/files-internal.d.ts +1 -0
  79. package/dist/commands/files-internal.js +113 -45
  80. package/dist/commands/health 3.d.ts +40 -0
  81. package/dist/commands/health 3.js +1067 -0
  82. package/dist/commands/health.d.ts +1 -0
  83. package/dist/commands/health.js +109 -41
  84. package/dist/commands/{pubsub.d.ts → inbox 3.d.ts } +2 -1
  85. package/dist/commands/inbox 3.js +1065 -0
  86. package/dist/commands/inbox.d.ts +1 -0
  87. package/dist/commands/inbox.js +110 -40
  88. package/dist/commands/integrations 3.d.ts +41 -0
  89. package/dist/commands/integrations 3.js +1067 -0
  90. package/dist/commands/integrations.d.ts +1 -0
  91. package/dist/commands/integrations.js +107 -39
  92. package/dist/commands/item-categories 3.d.ts +40 -0
  93. package/dist/commands/item-categories 3.js +1072 -0
  94. package/dist/commands/item-categories.d.ts +1 -0
  95. package/dist/commands/item-categories.js +109 -41
  96. package/dist/commands/items 3.d.ts +40 -0
  97. package/dist/commands/items 3.js +1072 -0
  98. package/dist/commands/items.d.ts +1 -0
  99. package/dist/commands/items.js +109 -41
  100. package/dist/commands/journal-lines.d.ts +1 -0
  101. package/dist/commands/journal-lines.js +105 -37
  102. package/dist/commands/journals 3.d.ts +42 -0
  103. package/dist/commands/journals 3.js +1088 -0
  104. package/dist/commands/journals.d.ts +1 -0
  105. package/dist/commands/journals.js +123 -55
  106. package/dist/commands/ledgers.d.ts +1 -0
  107. package/dist/commands/ledgers.js +120 -52
  108. package/dist/commands/office-locations 3.d.ts +40 -0
  109. package/dist/commands/office-locations 3.js +1072 -0
  110. package/dist/commands/office-locations.d.ts +1 -0
  111. package/dist/commands/office-locations.js +109 -41
  112. package/dist/commands/payment-method-ledger-mappings 3.d.ts +41 -0
  113. package/dist/commands/payment-method-ledger-mappings 3.js +1066 -0
  114. package/dist/commands/payment-method-ledger-mappings.d.ts +1 -0
  115. package/dist/commands/payment-method-ledger-mappings.js +109 -41
  116. package/dist/commands/payment-methods 3.d.ts +40 -0
  117. package/dist/commands/payment-methods 3.js +1072 -0
  118. package/dist/commands/payment-methods.d.ts +1 -0
  119. package/dist/commands/payment-methods.js +109 -41
  120. package/dist/commands/payment-runs 3.d.ts +42 -0
  121. package/dist/commands/payment-runs 3.js +1087 -0
  122. package/dist/commands/payment-runs.d.ts +1 -0
  123. package/dist/commands/payment-runs.js +122 -54
  124. package/dist/commands/payment-terms 3.d.ts +40 -0
  125. package/dist/commands/payment-terms 3.js +1073 -0
  126. package/dist/commands/payment-terms.d.ts +1 -0
  127. package/dist/commands/payment-terms.js +110 -42
  128. package/dist/commands/price-lists 3.d.ts +41 -0
  129. package/dist/commands/price-lists 3.js +1078 -0
  130. package/dist/commands/price-lists.d.ts +1 -0
  131. package/dist/commands/price-lists.js +114 -46
  132. package/dist/commands/processor-mappings 2.d.ts +40 -0
  133. package/dist/commands/processor-mappings 2.js +1070 -0
  134. package/dist/commands/processor-mappings.d.ts +1 -0
  135. package/dist/commands/processor-mappings.js +108 -40
  136. package/dist/commands/processors.d.ts +1 -0
  137. package/dist/commands/processors.js +113 -45
  138. package/dist/commands/reconciliation 3.d.ts +40 -0
  139. package/dist/commands/reconciliation 3.js +1069 -0
  140. package/dist/commands/reconciliation.d.ts +1 -0
  141. package/dist/commands/reconciliation.js +110 -42
  142. package/dist/commands/reports.d.ts +1 -0
  143. package/dist/commands/reports.js +110 -39
  144. package/dist/commands/salary-types 3.d.ts +40 -0
  145. package/dist/commands/salary-types 3.js +1072 -0
  146. package/dist/commands/salary-types.d.ts +1 -0
  147. package/dist/commands/salary-types.js +1074 -0
  148. package/dist/commands/sales-invoices 3.d.ts +43 -0
  149. package/dist/commands/sales-invoices 3.js +1100 -0
  150. package/dist/commands/sales-invoices.d.ts +1 -0
  151. package/dist/commands/sales-invoices.js +134 -66
  152. package/dist/commands/sales-orders 3.d.ts +43 -0
  153. package/dist/commands/sales-orders 3.js +1098 -0
  154. package/dist/commands/sales-orders.d.ts +1 -0
  155. package/dist/commands/sales-orders.js +133 -64
  156. package/dist/commands/switch.d.ts +1 -0
  157. package/dist/commands/switch.js +58 -3
  158. package/dist/commands/{processing-logs.d.ts → tags 3.d.ts } +2 -1
  159. package/dist/commands/tags 3.js +1072 -0
  160. package/dist/commands/tags.d.ts +1 -0
  161. package/dist/commands/tags.js +109 -41
  162. package/dist/commands/tax-codes 3.d.ts +40 -0
  163. package/dist/commands/tax-codes 3.js +1075 -0
  164. package/dist/commands/tax-codes.d.ts +1 -0
  165. package/dist/commands/tax-codes.js +112 -44
  166. package/dist/commands/tax-matrix.d.ts +1 -0
  167. package/dist/commands/tax-matrix.js +109 -41
  168. package/dist/commands/tenants.d.ts +1 -0
  169. package/dist/commands/tenants.js +109 -41
  170. package/dist/commands/text-templates 3.d.ts +40 -0
  171. package/dist/commands/text-templates 3.js +1072 -0
  172. package/dist/commands/text-templates.d.ts +1 -0
  173. package/dist/commands/text-templates.js +109 -41
  174. package/dist/commands/tools.d.ts +1 -0
  175. package/dist/commands/tools.js +106 -38
  176. package/dist/commands/vendor-invoices.d.ts +1 -0
  177. package/dist/commands/vendor-invoices.js +139 -69
  178. package/dist/commands/webhooks 3.d.ts +40 -0
  179. package/dist/commands/webhooks 3.js +1074 -0
  180. package/dist/commands/webhooks.d.ts +1 -0
  181. package/dist/commands/webhooks.js +111 -43
  182. package/dist/commands/workflows.d.ts +1 -0
  183. package/dist/commands/workflows.js +107 -39
  184. package/dist/lib/api-client.d.ts +5 -1
  185. package/dist/lib/api-client.js +19 -5
  186. package/dist/lib/auth.d.ts +16 -1
  187. package/dist/lib/auth.js +170 -45
  188. package/dist/lib/config.d.ts +7 -0
  189. package/dist/lib/config.js +7 -1
  190. package/oclif.manifest.json +7464 -653
  191. package/package.json +2 -2
@@ -6,13 +6,12 @@ const HAS_IMPORT = false;
6
6
  const HAS_EXPORT = false;
7
7
  const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
- const SUBCOMMANDS = [...["webhook-events", "processor-jobs"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
9
+ const SUBCOMMANDS = [...["trigger"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'webhook-events': { method: 'POST', path: '/v1/pubsub/webhook-events', summary: 'GCP Pub/Sub push endpoint for webhook event delivery', pathParams: [], hasBody: false, isMultipart: false },
12
- 'processor-jobs': { method: 'POST', path: '/v1/pubsub/processor-jobs', summary: 'GCP Pub/Sub push endpoint for processor-job.v1 envelopes (PDF rendering)', pathParams: [], hasBody: false, isMultipart: false },
11
+ 'trigger': { method: 'POST', path: '/v1/dunning-runs/trigger', summary: 'Manually trigger a dunning run', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
12
  };
14
- export default class Pubsub extends Command {
15
- static description = 'Manage pubsub';
13
+ export default class DunningRuns extends Command {
14
+ static description = 'Manage dunning runs';
16
15
  static examples = [];
17
16
  static args = {
18
17
  action: Args.string({ description: 'Action: ' + SUBCOMMANDS.join(', '), required: true }),
@@ -38,7 +37,7 @@ export default class Pubsub extends Command {
38
37
  'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
39
38
  };
40
39
  async run() {
41
- const { args, flags } = await this.parse(Pubsub);
40
+ const { args, flags } = await this.parse(DunningRuns);
42
41
  // Handle import action specially
43
42
  if (args.action === 'import') {
44
43
  if (!HAS_IMPORT)
@@ -84,7 +83,7 @@ export default class Pubsub extends Command {
84
83
  // First path param always comes from positional argument
85
84
  value = args.id;
86
85
  if (!value)
87
- this.error('Missing required argument: id. Usage: aderis pubsub <action> <id>');
86
+ this.error('Missing required argument: id. Usage: aderis dunning-runs <action> <id>');
88
87
  }
89
88
  else {
90
89
  const flagName = param.replace(/\./g, '-').replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '');
@@ -120,15 +119,7 @@ export default class Pubsub extends Command {
120
119
  if (eqIndex === -1)
121
120
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
122
121
  const key = f.slice(0, eqIndex);
123
- let value = f.slice(eqIndex + 1);
124
- // Check for operator prefix (with or without $)
125
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
126
- if (opMatch) {
127
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
128
- }
129
- else {
130
- value = `\$eq:${value}`;
131
- }
122
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
132
123
  const filterKey = `filter.${key}`;
133
124
  if (filterKey in queryParams) {
134
125
  // Multiple filters on same field: collect as array
@@ -183,6 +174,7 @@ export default class Pubsub extends Command {
183
174
  if (!data)
184
175
  data = {};
185
176
  const body = data;
177
+ const arrayFields = new Set(endpoint.bodyArrayFields);
186
178
  for (const pair of flags.set) {
187
179
  const eqIndex = pair.indexOf('=');
188
180
  if (eqIndex === -1)
@@ -194,6 +186,19 @@ export default class Pubsub extends Command {
194
186
  value = JSON.parse(value);
195
187
  }
196
188
  catch { /* keep as string */ }
189
+ // For top-level keys typed as array in the API schema, split bare
190
+ // comma-separated input (e.g. "1171,1170" -> ["1171","1170"]) and
191
+ // wrap a lone scalar in a single-element array. Skips when --set
192
+ // already provided a JSON array.
193
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
194
+ if (isArrayField && !Array.isArray(value)) {
195
+ if (typeof value === 'string') {
196
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
197
+ }
198
+ else if (value !== undefined && value !== null) {
199
+ value = [value];
200
+ }
201
+ }
197
202
  // Support nested keys like "address.city"
198
203
  const keys = key.split('.');
199
204
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
@@ -244,6 +249,26 @@ export default class Pubsub extends Command {
244
249
  process.stderr.write('\n');
245
250
  }
246
251
  try {
252
+ // Binary response: download to file
253
+ if (endpoint.isBinaryResponse) {
254
+ if (!flags.file) {
255
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
256
+ }
257
+ const binResponse = await apiRequest(endpoint.method, apiPath, {
258
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
259
+ data,
260
+ contextName: flags.context,
261
+ binary: true,
262
+ });
263
+ const fsModule = await import('node:fs');
264
+ const binData = binResponse.data;
265
+ const buffer = Buffer.isBuffer(binData)
266
+ ? binData
267
+ : Buffer.from(binData);
268
+ fsModule.writeFileSync(flags.file, buffer);
269
+ this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
270
+ return;
271
+ }
247
272
  const response = await apiRequest(endpoint.method, apiPath, {
248
273
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
249
274
  data,
@@ -334,6 +359,7 @@ export default class Pubsub extends Command {
334
359
  }
335
360
  else if (flags.set && flags.set.length > 0) {
336
361
  const item = {};
362
+ const arrayFields = new Set(ENDPOINTS['create']?.bodyArrayFields ?? []);
337
363
  for (const pair of flags.set) {
338
364
  const eqIndex = pair.indexOf('=');
339
365
  if (eqIndex === -1)
@@ -344,6 +370,15 @@ export default class Pubsub extends Command {
344
370
  value = JSON.parse(value);
345
371
  }
346
372
  catch { /* keep as string */ }
373
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
374
+ if (isArrayField && !Array.isArray(value)) {
375
+ if (typeof value === 'string') {
376
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
377
+ }
378
+ else if (value !== undefined && value !== null) {
379
+ value = [value];
380
+ }
381
+ }
347
382
  const keys = key.split('.');
348
383
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
349
384
  this.error(`Invalid --set key: ${key}`);
@@ -493,14 +528,7 @@ export default class Pubsub extends Command {
493
528
  if (eqIndex === -1)
494
529
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
495
530
  const key = f.slice(0, eqIndex);
496
- let value = f.slice(eqIndex + 1);
497
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
498
- if (opMatch) {
499
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
500
- }
501
- else {
502
- value = `\$eq:${value}`;
503
- }
531
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
504
532
  const filterKey = `filter.${key}`;
505
533
  if (filterKey in baseParams) {
506
534
  const existing = baseParams[filterKey];
@@ -647,7 +675,7 @@ export default class Pubsub extends Command {
647
675
  }
648
676
  async runNestedImport(suffix, parentId, flags) {
649
677
  if (!parentId) {
650
- this.error(`Missing required argument: id. Usage: aderis pubsub import-${suffix} <parent-id> --file data.json`);
678
+ this.error(`Missing required argument: id. Usage: aderis dunning-runs import-${suffix} <parent-id> --file data.json`);
651
679
  }
652
680
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
653
681
  if (!context)
@@ -677,6 +705,7 @@ export default class Pubsub extends Command {
677
705
  }
678
706
  else if (flags.set && flags.set.length > 0) {
679
707
  const item = {};
708
+ const arrayFields = new Set(ENDPOINTS[`create-${suffix}`]?.bodyArrayFields ?? []);
680
709
  for (const pair of flags.set) {
681
710
  const eqIndex = pair.indexOf('=');
682
711
  if (eqIndex === -1)
@@ -687,6 +716,15 @@ export default class Pubsub extends Command {
687
716
  value = JSON.parse(value);
688
717
  }
689
718
  catch { /* keep as string */ }
719
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
720
+ if (isArrayField && !Array.isArray(value)) {
721
+ if (typeof value === 'string') {
722
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
723
+ }
724
+ else if (value !== undefined && value !== null) {
725
+ value = [value];
726
+ }
727
+ }
690
728
  const keys = key.split('.');
691
729
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
692
730
  this.error(`Invalid --set key: ${key}`);
@@ -806,35 +844,62 @@ export default class Pubsub extends Command {
806
844
  this.log('');
807
845
  this.log(`Summary: ${created} created, ${updated} updated, ${errors.length} failed`);
808
846
  }
847
+ parseFilterValue(value) {
848
+ // Recognises the nestjs-paginate operator set (with or without the leading $)
849
+ // and recursively unwraps chained operators like 'not:null' -> '$not:$null'
850
+ // or 'not:like:John' -> '$not:$like:John'. Falls back to '$eq:<value>' when
851
+ // no operator prefix is present.
852
+ const opRe = /^\$?(and|or|not|eq|gt|gte|in|null|lt|lte|btw|ne|ilike|like|sw|contains)(?::|$)/;
853
+ const m = value.match(opRe);
854
+ if (!m)
855
+ return `\$eq:${value}`;
856
+ const op = m[1];
857
+ const rest = value.slice(m[0].length);
858
+ if (rest === '')
859
+ return `\$${op}`;
860
+ return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
861
+ }
809
862
  filterFields(data, fields) {
810
863
  const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
811
864
  const pick = (obj, keys) => {
812
- const result = {};
865
+ // Group keys by their first segment so sibling sub-paths
866
+ // (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
867
+ // and don't clobber each other on merge.
868
+ const directKeys = new Set();
869
+ const nestedKeys = new Map();
813
870
  for (const key of keys) {
814
871
  if (key.split('.').some(isUnsafeKey))
815
872
  continue;
816
873
  if (key.includes('.')) {
817
- // Handle nested fields like 'customer.name' or 'allocations.salesInvoiceId'
818
874
  const [first, ...rest] = key.split('.');
819
- const val = obj[first];
820
- if (val && typeof val === 'object') {
821
- const restKey = rest.join('.');
822
- if (Array.isArray(val)) {
823
- // Map over array elements (e.g., allocations[].salesInvoiceId)
824
- const existing = Array.isArray(result[first]) ? result[first] : [];
825
- result[first] = val.map((item, i) => ({
826
- ...(existing[i] || {}),
827
- ...pick(item, [restKey]),
828
- }));
829
- }
830
- else {
831
- const nested = pick(val, [restKey]);
832
- result[first] = { ...(result[first] || {}), ...nested };
833
- }
875
+ if (isUnsafeKey(first))
876
+ continue;
877
+ const list = nestedKeys.get(first);
878
+ if (list) {
879
+ list.push(rest.join('.'));
880
+ }
881
+ else {
882
+ nestedKeys.set(first, [rest.join('.')]);
834
883
  }
835
884
  }
836
- else if (key in obj) {
885
+ else {
886
+ directKeys.add(key);
887
+ }
888
+ }
889
+ const result = {};
890
+ for (const key of directKeys) {
891
+ if (key in obj)
837
892
  result[key] = obj[key];
893
+ }
894
+ for (const [first, restKeys] of nestedKeys) {
895
+ const val = obj[first];
896
+ if (!val || typeof val !== 'object')
897
+ continue;
898
+ if (Array.isArray(val)) {
899
+ result[first] = val.map(item => pick(item, restKeys));
900
+ }
901
+ else {
902
+ result[first] = pick(val, restKeys);
838
903
  }
839
904
  }
840
905
  return result;
@@ -30,6 +30,7 @@ export default class DunningRuns extends Command {
30
30
  private runExport;
31
31
  private runCamt053Import;
32
32
  private runNestedImport;
33
+ private parseFilterValue;
33
34
  private filterFields;
34
35
  private sanitizeOutput;
35
36
  private getColumns;
@@ -8,7 +8,7 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["trigger"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'trigger': { method: 'POST', path: '/v1/dunning-runs/trigger', summary: 'Manually trigger a dunning run', pathParams: [], hasBody: true, isMultipart: false },
11
+ 'trigger': { method: 'POST', path: '/v1/dunning-runs/trigger', summary: 'Manually trigger a dunning run', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
12
  };
13
13
  export default class DunningRuns extends Command {
14
14
  static description = 'Manage dunning runs';
@@ -119,15 +119,7 @@ export default class DunningRuns extends Command {
119
119
  if (eqIndex === -1)
120
120
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
121
121
  const key = f.slice(0, eqIndex);
122
- let value = f.slice(eqIndex + 1);
123
- // Check for operator prefix (with or without $)
124
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
125
- if (opMatch) {
126
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
127
- }
128
- else {
129
- value = `\$eq:${value}`;
130
- }
122
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
131
123
  const filterKey = `filter.${key}`;
132
124
  if (filterKey in queryParams) {
133
125
  // Multiple filters on same field: collect as array
@@ -160,7 +152,9 @@ export default class DunningRuns extends Command {
160
152
  data = form;
161
153
  }
162
154
  else if (endpoint.hasBody) {
163
- if (flags.file) {
155
+ // For binary-response endpoints, --file is the output path, not a JSON
156
+ // input file — skip the read-as-body branch.
157
+ if (flags.file && !endpoint.isBinaryResponse) {
164
158
  const fsModule = await import('node:fs');
165
159
  try {
166
160
  data = JSON.parse(fsModule.readFileSync(flags.file, 'utf-8'));
@@ -182,6 +176,7 @@ export default class DunningRuns extends Command {
182
176
  if (!data)
183
177
  data = {};
184
178
  const body = data;
179
+ const arrayFields = new Set(endpoint.bodyArrayFields);
185
180
  for (const pair of flags.set) {
186
181
  const eqIndex = pair.indexOf('=');
187
182
  if (eqIndex === -1)
@@ -193,6 +188,19 @@ export default class DunningRuns extends Command {
193
188
  value = JSON.parse(value);
194
189
  }
195
190
  catch { /* keep as string */ }
191
+ // For top-level keys typed as array in the API schema, split bare
192
+ // comma-separated input (e.g. "1171,1170" -> ["1171","1170"]) and
193
+ // wrap a lone scalar in a single-element array. Skips when --set
194
+ // already provided a JSON array.
195
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
196
+ if (isArrayField && !Array.isArray(value)) {
197
+ if (typeof value === 'string') {
198
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
199
+ }
200
+ else if (value !== undefined && value !== null) {
201
+ value = [value];
202
+ }
203
+ }
196
204
  // Support nested keys like "address.city"
197
205
  const keys = key.split('.');
198
206
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
@@ -243,6 +251,26 @@ export default class DunningRuns extends Command {
243
251
  process.stderr.write('\n');
244
252
  }
245
253
  try {
254
+ // Binary response: download to file
255
+ if (endpoint.isBinaryResponse) {
256
+ if (!flags.file) {
257
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
258
+ }
259
+ const binResponse = await apiRequest(endpoint.method, apiPath, {
260
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
261
+ data,
262
+ contextName: flags.context,
263
+ binary: true,
264
+ });
265
+ const fsModule = await import('node:fs');
266
+ const binData = binResponse.data;
267
+ const buffer = Buffer.isBuffer(binData)
268
+ ? binData
269
+ : Buffer.from(binData);
270
+ fsModule.writeFileSync(flags.file, buffer);
271
+ this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
272
+ return;
273
+ }
246
274
  const response = await apiRequest(endpoint.method, apiPath, {
247
275
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
248
276
  data,
@@ -333,6 +361,7 @@ export default class DunningRuns extends Command {
333
361
  }
334
362
  else if (flags.set && flags.set.length > 0) {
335
363
  const item = {};
364
+ const arrayFields = new Set(ENDPOINTS['create']?.bodyArrayFields ?? []);
336
365
  for (const pair of flags.set) {
337
366
  const eqIndex = pair.indexOf('=');
338
367
  if (eqIndex === -1)
@@ -343,6 +372,15 @@ export default class DunningRuns extends Command {
343
372
  value = JSON.parse(value);
344
373
  }
345
374
  catch { /* keep as string */ }
375
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
376
+ if (isArrayField && !Array.isArray(value)) {
377
+ if (typeof value === 'string') {
378
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
379
+ }
380
+ else if (value !== undefined && value !== null) {
381
+ value = [value];
382
+ }
383
+ }
346
384
  const keys = key.split('.');
347
385
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
348
386
  this.error(`Invalid --set key: ${key}`);
@@ -492,14 +530,7 @@ export default class DunningRuns extends Command {
492
530
  if (eqIndex === -1)
493
531
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
494
532
  const key = f.slice(0, eqIndex);
495
- let value = f.slice(eqIndex + 1);
496
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
497
- if (opMatch) {
498
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
499
- }
500
- else {
501
- value = `\$eq:${value}`;
502
- }
533
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
503
534
  const filterKey = `filter.${key}`;
504
535
  if (filterKey in baseParams) {
505
536
  const existing = baseParams[filterKey];
@@ -676,6 +707,7 @@ export default class DunningRuns extends Command {
676
707
  }
677
708
  else if (flags.set && flags.set.length > 0) {
678
709
  const item = {};
710
+ const arrayFields = new Set(ENDPOINTS[`create-${suffix}`]?.bodyArrayFields ?? []);
679
711
  for (const pair of flags.set) {
680
712
  const eqIndex = pair.indexOf('=');
681
713
  if (eqIndex === -1)
@@ -686,6 +718,15 @@ export default class DunningRuns extends Command {
686
718
  value = JSON.parse(value);
687
719
  }
688
720
  catch { /* keep as string */ }
721
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
722
+ if (isArrayField && !Array.isArray(value)) {
723
+ if (typeof value === 'string') {
724
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
725
+ }
726
+ else if (value !== undefined && value !== null) {
727
+ value = [value];
728
+ }
729
+ }
689
730
  const keys = key.split('.');
690
731
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
691
732
  this.error(`Invalid --set key: ${key}`);
@@ -805,35 +846,62 @@ export default class DunningRuns extends Command {
805
846
  this.log('');
806
847
  this.log(`Summary: ${created} created, ${updated} updated, ${errors.length} failed`);
807
848
  }
849
+ parseFilterValue(value) {
850
+ // Recognises the nestjs-paginate operator set (with or without the leading $)
851
+ // and recursively unwraps chained operators like 'not:null' -> '$not:$null'
852
+ // or 'not:like:John' -> '$not:$like:John'. Falls back to '$eq:<value>' when
853
+ // no operator prefix is present.
854
+ const opRe = /^\$?(and|or|not|eq|gt|gte|in|null|lt|lte|btw|ne|ilike|like|sw|contains)(?::|$)/;
855
+ const m = value.match(opRe);
856
+ if (!m)
857
+ return `\$eq:${value}`;
858
+ const op = m[1];
859
+ const rest = value.slice(m[0].length);
860
+ if (rest === '')
861
+ return `\$${op}`;
862
+ return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
863
+ }
808
864
  filterFields(data, fields) {
809
865
  const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
810
866
  const pick = (obj, keys) => {
811
- const result = {};
867
+ // Group keys by their first segment so sibling sub-paths
868
+ // (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
869
+ // and don't clobber each other on merge.
870
+ const directKeys = new Set();
871
+ const nestedKeys = new Map();
812
872
  for (const key of keys) {
813
873
  if (key.split('.').some(isUnsafeKey))
814
874
  continue;
815
875
  if (key.includes('.')) {
816
- // Handle nested fields like 'customer.name' or 'allocations.salesInvoiceId'
817
876
  const [first, ...rest] = key.split('.');
818
- const val = obj[first];
819
- if (val && typeof val === 'object') {
820
- const restKey = rest.join('.');
821
- if (Array.isArray(val)) {
822
- // Map over array elements (e.g., allocations[].salesInvoiceId)
823
- const existing = Array.isArray(result[first]) ? result[first] : [];
824
- result[first] = val.map((item, i) => ({
825
- ...(existing[i] || {}),
826
- ...pick(item, [restKey]),
827
- }));
828
- }
829
- else {
830
- const nested = pick(val, [restKey]);
831
- result[first] = { ...(result[first] || {}), ...nested };
832
- }
877
+ if (isUnsafeKey(first))
878
+ continue;
879
+ const list = nestedKeys.get(first);
880
+ if (list) {
881
+ list.push(rest.join('.'));
882
+ }
883
+ else {
884
+ nestedKeys.set(first, [rest.join('.')]);
833
885
  }
834
886
  }
835
- else if (key in obj) {
887
+ else {
888
+ directKeys.add(key);
889
+ }
890
+ }
891
+ const result = {};
892
+ for (const key of directKeys) {
893
+ if (key in obj)
836
894
  result[key] = obj[key];
895
+ }
896
+ for (const [first, restKeys] of nestedKeys) {
897
+ const val = obj[first];
898
+ if (!val || typeof val !== 'object')
899
+ continue;
900
+ if (Array.isArray(val)) {
901
+ result[first] = val.map(item => pick(item, restKeys));
902
+ }
903
+ else {
904
+ result[first] = pick(val, restKeys);
837
905
  }
838
906
  }
839
907
  return result;
@@ -34,6 +34,7 @@ export default class Employees extends Command {
34
34
  private runExport;
35
35
  private runCamt053Import;
36
36
  private runNestedImport;
37
+ private parseFilterValue;
37
38
  private filterFields;
38
39
  private sanitizeOutput;
39
40
  private getColumns;