@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
@@ -30,6 +30,7 @@ export default class Reconciliation 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,12 +8,12 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["list", "transactions-matches", "transactions-summary", "matches-confirm", "matches-reject", "process-transaction"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list': { method: 'GET', path: '/v1/reconciliation/pending', summary: 'Get pending reconciliation matches', pathParams: [], hasBody: false, isMultipart: false },
12
- 'transactions-matches': { method: 'GET', path: '/v1/reconciliation/transactions/{transactionId}/matches', summary: 'Get matches for a transaction', pathParams: ['transactionId'], hasBody: false, isMultipart: false },
13
- 'transactions-summary': { method: 'GET', path: '/v1/reconciliation/transactions/{transactionId}/summary', summary: 'Get transaction allocation summary', pathParams: ['transactionId'], hasBody: false, isMultipart: false },
14
- 'matches-confirm': { method: 'POST', path: '/v1/reconciliation/matches/{matchId}/confirm', summary: 'Confirm a reconciliation match', pathParams: ['matchId'], hasBody: false, isMultipart: false },
15
- 'matches-reject': { method: 'POST', path: '/v1/reconciliation/matches/{matchId}/reject', summary: 'Reject a reconciliation match', pathParams: ['matchId'], hasBody: true, isMultipart: false },
16
- 'process-transaction': { method: 'POST', path: '/v1/reconciliation/transactions/{transactionId}/process', summary: 'Trigger reconciliation for a transaction', pathParams: ['transactionId'], hasBody: false, isMultipart: false },
11
+ 'list': { method: 'GET', path: '/v1/reconciliation/pending', summary: 'Get pending reconciliation matches', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
+ 'transactions-matches': { method: 'GET', path: '/v1/reconciliation/transactions/{transactionId}/matches', summary: 'Get matches for a transaction', pathParams: ['transactionId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
+ 'transactions-summary': { method: 'GET', path: '/v1/reconciliation/transactions/{transactionId}/summary', summary: 'Get transaction allocation summary', pathParams: ['transactionId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
14
+ 'matches-confirm': { method: 'POST', path: '/v1/reconciliation/matches/{matchId}/confirm', summary: 'Confirm a reconciliation match', pathParams: ['matchId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
15
+ 'matches-reject': { method: 'POST', path: '/v1/reconciliation/matches/{matchId}/reject', summary: 'Reject a reconciliation match', pathParams: ['matchId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
+ 'process-transaction': { method: 'POST', path: '/v1/reconciliation/transactions/{transactionId}/process', summary: 'Trigger reconciliation for a transaction', pathParams: ['transactionId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
17
17
  };
18
18
  export default class Reconciliation extends Command {
19
19
  static description = 'Manage reconciliation';
@@ -127,15 +127,7 @@ export default class Reconciliation extends Command {
127
127
  if (eqIndex === -1)
128
128
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
129
129
  const key = f.slice(0, eqIndex);
130
- let value = f.slice(eqIndex + 1);
131
- // Check for operator prefix (with or without $)
132
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
133
- if (opMatch) {
134
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
135
- }
136
- else {
137
- value = `\$eq:${value}`;
138
- }
130
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
139
131
  const filterKey = `filter.${key}`;
140
132
  if (filterKey in queryParams) {
141
133
  // Multiple filters on same field: collect as array
@@ -168,7 +160,9 @@ export default class Reconciliation extends Command {
168
160
  data = form;
169
161
  }
170
162
  else if (endpoint.hasBody) {
171
- if (flags.file) {
163
+ // For binary-response endpoints, --file is the output path, not a JSON
164
+ // input file — skip the read-as-body branch.
165
+ if (flags.file && !endpoint.isBinaryResponse) {
172
166
  const fsModule = await import('node:fs');
173
167
  try {
174
168
  data = JSON.parse(fsModule.readFileSync(flags.file, 'utf-8'));
@@ -190,6 +184,7 @@ export default class Reconciliation extends Command {
190
184
  if (!data)
191
185
  data = {};
192
186
  const body = data;
187
+ const arrayFields = new Set(endpoint.bodyArrayFields);
193
188
  for (const pair of flags.set) {
194
189
  const eqIndex = pair.indexOf('=');
195
190
  if (eqIndex === -1)
@@ -201,6 +196,19 @@ export default class Reconciliation extends Command {
201
196
  value = JSON.parse(value);
202
197
  }
203
198
  catch { /* keep as string */ }
199
+ // For top-level keys typed as array in the API schema, split bare
200
+ // comma-separated input (e.g. "1171,1170" -> ["1171","1170"]) and
201
+ // wrap a lone scalar in a single-element array. Skips when --set
202
+ // already provided a JSON array.
203
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
204
+ if (isArrayField && !Array.isArray(value)) {
205
+ if (typeof value === 'string') {
206
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
207
+ }
208
+ else if (value !== undefined && value !== null) {
209
+ value = [value];
210
+ }
211
+ }
204
212
  // Support nested keys like "address.city"
205
213
  const keys = key.split('.');
206
214
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
@@ -251,6 +259,26 @@ export default class Reconciliation extends Command {
251
259
  process.stderr.write('\n');
252
260
  }
253
261
  try {
262
+ // Binary response: download to file
263
+ if (endpoint.isBinaryResponse) {
264
+ if (!flags.file) {
265
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
266
+ }
267
+ const binResponse = await apiRequest(endpoint.method, apiPath, {
268
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
269
+ data,
270
+ contextName: flags.context,
271
+ binary: true,
272
+ });
273
+ const fsModule = await import('node:fs');
274
+ const binData = binResponse.data;
275
+ const buffer = Buffer.isBuffer(binData)
276
+ ? binData
277
+ : Buffer.from(binData);
278
+ fsModule.writeFileSync(flags.file, buffer);
279
+ this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
280
+ return;
281
+ }
254
282
  const response = await apiRequest(endpoint.method, apiPath, {
255
283
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
256
284
  data,
@@ -341,6 +369,7 @@ export default class Reconciliation extends Command {
341
369
  }
342
370
  else if (flags.set && flags.set.length > 0) {
343
371
  const item = {};
372
+ const arrayFields = new Set(ENDPOINTS['create']?.bodyArrayFields ?? []);
344
373
  for (const pair of flags.set) {
345
374
  const eqIndex = pair.indexOf('=');
346
375
  if (eqIndex === -1)
@@ -351,6 +380,15 @@ export default class Reconciliation extends Command {
351
380
  value = JSON.parse(value);
352
381
  }
353
382
  catch { /* keep as string */ }
383
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
384
+ if (isArrayField && !Array.isArray(value)) {
385
+ if (typeof value === 'string') {
386
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
387
+ }
388
+ else if (value !== undefined && value !== null) {
389
+ value = [value];
390
+ }
391
+ }
354
392
  const keys = key.split('.');
355
393
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
356
394
  this.error(`Invalid --set key: ${key}`);
@@ -500,14 +538,7 @@ export default class Reconciliation extends Command {
500
538
  if (eqIndex === -1)
501
539
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
502
540
  const key = f.slice(0, eqIndex);
503
- let value = f.slice(eqIndex + 1);
504
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
505
- if (opMatch) {
506
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
507
- }
508
- else {
509
- value = `\$eq:${value}`;
510
- }
541
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
511
542
  const filterKey = `filter.${key}`;
512
543
  if (filterKey in baseParams) {
513
544
  const existing = baseParams[filterKey];
@@ -684,6 +715,7 @@ export default class Reconciliation extends Command {
684
715
  }
685
716
  else if (flags.set && flags.set.length > 0) {
686
717
  const item = {};
718
+ const arrayFields = new Set(ENDPOINTS[`create-${suffix}`]?.bodyArrayFields ?? []);
687
719
  for (const pair of flags.set) {
688
720
  const eqIndex = pair.indexOf('=');
689
721
  if (eqIndex === -1)
@@ -694,6 +726,15 @@ export default class Reconciliation extends Command {
694
726
  value = JSON.parse(value);
695
727
  }
696
728
  catch { /* keep as string */ }
729
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
730
+ if (isArrayField && !Array.isArray(value)) {
731
+ if (typeof value === 'string') {
732
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
733
+ }
734
+ else if (value !== undefined && value !== null) {
735
+ value = [value];
736
+ }
737
+ }
697
738
  const keys = key.split('.');
698
739
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
699
740
  this.error(`Invalid --set key: ${key}`);
@@ -813,35 +854,62 @@ export default class Reconciliation extends Command {
813
854
  this.log('');
814
855
  this.log(`Summary: ${created} created, ${updated} updated, ${errors.length} failed`);
815
856
  }
857
+ parseFilterValue(value) {
858
+ // Recognises the nestjs-paginate operator set (with or without the leading $)
859
+ // and recursively unwraps chained operators like 'not:null' -> '$not:$null'
860
+ // or 'not:like:John' -> '$not:$like:John'. Falls back to '$eq:<value>' when
861
+ // no operator prefix is present.
862
+ const opRe = /^\$?(and|or|not|eq|gt|gte|in|null|lt|lte|btw|ne|ilike|like|sw|contains)(?::|$)/;
863
+ const m = value.match(opRe);
864
+ if (!m)
865
+ return `\$eq:${value}`;
866
+ const op = m[1];
867
+ const rest = value.slice(m[0].length);
868
+ if (rest === '')
869
+ return `\$${op}`;
870
+ return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
871
+ }
816
872
  filterFields(data, fields) {
817
873
  const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
818
874
  const pick = (obj, keys) => {
819
- const result = {};
875
+ // Group keys by their first segment so sibling sub-paths
876
+ // (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
877
+ // and don't clobber each other on merge.
878
+ const directKeys = new Set();
879
+ const nestedKeys = new Map();
820
880
  for (const key of keys) {
821
881
  if (key.split('.').some(isUnsafeKey))
822
882
  continue;
823
883
  if (key.includes('.')) {
824
- // Handle nested fields like 'customer.name' or 'allocations.salesInvoiceId'
825
884
  const [first, ...rest] = key.split('.');
826
- const val = obj[first];
827
- if (val && typeof val === 'object') {
828
- const restKey = rest.join('.');
829
- if (Array.isArray(val)) {
830
- // Map over array elements (e.g., allocations[].salesInvoiceId)
831
- const existing = Array.isArray(result[first]) ? result[first] : [];
832
- result[first] = val.map((item, i) => ({
833
- ...(existing[i] || {}),
834
- ...pick(item, [restKey]),
835
- }));
836
- }
837
- else {
838
- const nested = pick(val, [restKey]);
839
- result[first] = { ...(result[first] || {}), ...nested };
840
- }
885
+ if (isUnsafeKey(first))
886
+ continue;
887
+ const list = nestedKeys.get(first);
888
+ if (list) {
889
+ list.push(rest.join('.'));
890
+ }
891
+ else {
892
+ nestedKeys.set(first, [rest.join('.')]);
841
893
  }
842
894
  }
843
- else if (key in obj) {
895
+ else {
896
+ directKeys.add(key);
897
+ }
898
+ }
899
+ const result = {};
900
+ for (const key of directKeys) {
901
+ if (key in obj)
844
902
  result[key] = obj[key];
903
+ }
904
+ for (const [first, restKeys] of nestedKeys) {
905
+ const val = obj[first];
906
+ if (!val || typeof val !== 'object')
907
+ continue;
908
+ if (Array.isArray(val)) {
909
+ result[first] = val.map(item => pick(item, restKeys));
910
+ }
911
+ else {
912
+ result[first] = pick(val, restKeys);
845
913
  }
846
914
  }
847
915
  return result;
@@ -30,6 +30,7 @@ export default class Reports 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;
@@ -6,10 +6,13 @@ const HAS_IMPORT = false;
6
6
  const HAS_EXPORT = false;
7
7
  const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
- const SUBCOMMANDS = [...["profit-and-loss", "balance-sheet"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
9
+ const SUBCOMMANDS = [...["profit-and-loss", "balance-sheet", "gl-detail", "gl-detail-export-xlsx", "gl-detail-export-pdf"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'profit-and-loss': { method: 'GET', path: '/v1/reports/profit-and-loss', summary: 'Get Profit & Loss report', pathParams: [], hasBody: false, isMultipart: false },
12
- 'balance-sheet': { method: 'GET', path: '/v1/reports/balance-sheet', summary: 'Generate Balance Sheet report', pathParams: [], hasBody: false, isMultipart: false },
11
+ 'profit-and-loss': { method: 'GET', path: '/v1/reports/profit-and-loss', summary: 'Get Profit & Loss report', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
+ 'balance-sheet': { method: 'GET', path: '/v1/reports/balance-sheet', summary: 'Generate Balance Sheet report', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
+ 'gl-detail': { method: 'POST', path: '/v1/reports/gl-detail', summary: 'Get GL Detail report', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['accountIds', 'accountCodes'] },
14
+ 'gl-detail-export-xlsx': { method: 'POST', path: '/v1/reports/gl-detail/export/xlsx', summary: 'Export GL Detail report as Excel (one sheet per account)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: true, bodyArrayFields: ['accountIds', 'accountCodes'] },
15
+ 'gl-detail-export-pdf': { method: 'POST', path: '/v1/reports/gl-detail/export/pdf', summary: 'Export GL Detail report as PDF (one page per account)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: true, bodyArrayFields: ['accountIds', 'accountCodes'] },
13
16
  };
14
17
  export default class Reports extends Command {
15
18
  static description = 'Manage reports';
@@ -120,15 +123,7 @@ export default class Reports extends Command {
120
123
  if (eqIndex === -1)
121
124
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
122
125
  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
- }
126
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
132
127
  const filterKey = `filter.${key}`;
133
128
  if (filterKey in queryParams) {
134
129
  // Multiple filters on same field: collect as array
@@ -161,7 +156,9 @@ export default class Reports extends Command {
161
156
  data = form;
162
157
  }
163
158
  else if (endpoint.hasBody) {
164
- if (flags.file) {
159
+ // For binary-response endpoints, --file is the output path, not a JSON
160
+ // input file — skip the read-as-body branch.
161
+ if (flags.file && !endpoint.isBinaryResponse) {
165
162
  const fsModule = await import('node:fs');
166
163
  try {
167
164
  data = JSON.parse(fsModule.readFileSync(flags.file, 'utf-8'));
@@ -183,6 +180,7 @@ export default class Reports extends Command {
183
180
  if (!data)
184
181
  data = {};
185
182
  const body = data;
183
+ const arrayFields = new Set(endpoint.bodyArrayFields);
186
184
  for (const pair of flags.set) {
187
185
  const eqIndex = pair.indexOf('=');
188
186
  if (eqIndex === -1)
@@ -194,6 +192,19 @@ export default class Reports extends Command {
194
192
  value = JSON.parse(value);
195
193
  }
196
194
  catch { /* keep as string */ }
195
+ // For top-level keys typed as array in the API schema, split bare
196
+ // comma-separated input (e.g. "1171,1170" -> ["1171","1170"]) and
197
+ // wrap a lone scalar in a single-element array. Skips when --set
198
+ // already provided a JSON array.
199
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
200
+ if (isArrayField && !Array.isArray(value)) {
201
+ if (typeof value === 'string') {
202
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
203
+ }
204
+ else if (value !== undefined && value !== null) {
205
+ value = [value];
206
+ }
207
+ }
197
208
  // Support nested keys like "address.city"
198
209
  const keys = key.split('.');
199
210
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
@@ -244,6 +255,26 @@ export default class Reports extends Command {
244
255
  process.stderr.write('\n');
245
256
  }
246
257
  try {
258
+ // Binary response: download to file
259
+ if (endpoint.isBinaryResponse) {
260
+ if (!flags.file) {
261
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
262
+ }
263
+ const binResponse = await apiRequest(endpoint.method, apiPath, {
264
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
265
+ data,
266
+ contextName: flags.context,
267
+ binary: true,
268
+ });
269
+ const fsModule = await import('node:fs');
270
+ const binData = binResponse.data;
271
+ const buffer = Buffer.isBuffer(binData)
272
+ ? binData
273
+ : Buffer.from(binData);
274
+ fsModule.writeFileSync(flags.file, buffer);
275
+ this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
276
+ return;
277
+ }
247
278
  const response = await apiRequest(endpoint.method, apiPath, {
248
279
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
249
280
  data,
@@ -334,6 +365,7 @@ export default class Reports extends Command {
334
365
  }
335
366
  else if (flags.set && flags.set.length > 0) {
336
367
  const item = {};
368
+ const arrayFields = new Set(ENDPOINTS['create']?.bodyArrayFields ?? []);
337
369
  for (const pair of flags.set) {
338
370
  const eqIndex = pair.indexOf('=');
339
371
  if (eqIndex === -1)
@@ -344,6 +376,15 @@ export default class Reports extends Command {
344
376
  value = JSON.parse(value);
345
377
  }
346
378
  catch { /* keep as string */ }
379
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
380
+ if (isArrayField && !Array.isArray(value)) {
381
+ if (typeof value === 'string') {
382
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
383
+ }
384
+ else if (value !== undefined && value !== null) {
385
+ value = [value];
386
+ }
387
+ }
347
388
  const keys = key.split('.');
348
389
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
349
390
  this.error(`Invalid --set key: ${key}`);
@@ -493,14 +534,7 @@ export default class Reports extends Command {
493
534
  if (eqIndex === -1)
494
535
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
495
536
  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
- }
537
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
504
538
  const filterKey = `filter.${key}`;
505
539
  if (filterKey in baseParams) {
506
540
  const existing = baseParams[filterKey];
@@ -677,6 +711,7 @@ export default class Reports extends Command {
677
711
  }
678
712
  else if (flags.set && flags.set.length > 0) {
679
713
  const item = {};
714
+ const arrayFields = new Set(ENDPOINTS[`create-${suffix}`]?.bodyArrayFields ?? []);
680
715
  for (const pair of flags.set) {
681
716
  const eqIndex = pair.indexOf('=');
682
717
  if (eqIndex === -1)
@@ -687,6 +722,15 @@ export default class Reports extends Command {
687
722
  value = JSON.parse(value);
688
723
  }
689
724
  catch { /* keep as string */ }
725
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
726
+ if (isArrayField && !Array.isArray(value)) {
727
+ if (typeof value === 'string') {
728
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
729
+ }
730
+ else if (value !== undefined && value !== null) {
731
+ value = [value];
732
+ }
733
+ }
690
734
  const keys = key.split('.');
691
735
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
692
736
  this.error(`Invalid --set key: ${key}`);
@@ -806,35 +850,62 @@ export default class Reports extends Command {
806
850
  this.log('');
807
851
  this.log(`Summary: ${created} created, ${updated} updated, ${errors.length} failed`);
808
852
  }
853
+ parseFilterValue(value) {
854
+ // Recognises the nestjs-paginate operator set (with or without the leading $)
855
+ // and recursively unwraps chained operators like 'not:null' -> '$not:$null'
856
+ // or 'not:like:John' -> '$not:$like:John'. Falls back to '$eq:<value>' when
857
+ // no operator prefix is present.
858
+ const opRe = /^\$?(and|or|not|eq|gt|gte|in|null|lt|lte|btw|ne|ilike|like|sw|contains)(?::|$)/;
859
+ const m = value.match(opRe);
860
+ if (!m)
861
+ return `\$eq:${value}`;
862
+ const op = m[1];
863
+ const rest = value.slice(m[0].length);
864
+ if (rest === '')
865
+ return `\$${op}`;
866
+ return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
867
+ }
809
868
  filterFields(data, fields) {
810
869
  const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
811
870
  const pick = (obj, keys) => {
812
- const result = {};
871
+ // Group keys by their first segment so sibling sub-paths
872
+ // (e.g. 'items.amount' + 'items._type') resolve in one recursive pass
873
+ // and don't clobber each other on merge.
874
+ const directKeys = new Set();
875
+ const nestedKeys = new Map();
813
876
  for (const key of keys) {
814
877
  if (key.split('.').some(isUnsafeKey))
815
878
  continue;
816
879
  if (key.includes('.')) {
817
- // Handle nested fields like 'customer.name' or 'allocations.salesInvoiceId'
818
880
  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
- }
881
+ if (isUnsafeKey(first))
882
+ continue;
883
+ const list = nestedKeys.get(first);
884
+ if (list) {
885
+ list.push(rest.join('.'));
886
+ }
887
+ else {
888
+ nestedKeys.set(first, [rest.join('.')]);
834
889
  }
835
890
  }
836
- else if (key in obj) {
891
+ else {
892
+ directKeys.add(key);
893
+ }
894
+ }
895
+ const result = {};
896
+ for (const key of directKeys) {
897
+ if (key in obj)
837
898
  result[key] = obj[key];
899
+ }
900
+ for (const [first, restKeys] of nestedKeys) {
901
+ const val = obj[first];
902
+ if (!val || typeof val !== 'object')
903
+ continue;
904
+ if (Array.isArray(val)) {
905
+ result[first] = val.map(item => pick(item, restKeys));
906
+ }
907
+ else {
908
+ result[first] = pick(val, restKeys);
838
909
  }
839
910
  }
840
911
  return result;
@@ -0,0 +1,40 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class SalaryTypes extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ action: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ id: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
8
+ };
9
+ static flags: {
10
+ context: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ table: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
+ data: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ file: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ set: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ page: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
+ limit: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
19
+ include: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
20
+ search: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
21
+ 'sort-by': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
22
+ filter: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
23
+ query: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
24
+ fields: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
25
+ sanitize: import("@oclif/core/interfaces").BooleanFlag<boolean>;
26
+ 'show-http-request': import("@oclif/core/interfaces").BooleanFlag<boolean>;
27
+ };
28
+ run(): Promise<void>;
29
+ private runImport;
30
+ private runExport;
31
+ private runCamt053Import;
32
+ private runNestedImport;
33
+ private parseFilterValue;
34
+ private filterFields;
35
+ private sanitizeOutput;
36
+ private getColumns;
37
+ private getValue;
38
+ private printTable;
39
+ private printCsv;
40
+ }