@aderis-erp/cli 1.0.3 → 1.0.7

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 (111) hide show
  1. package/README.md +6 -0
  2. package/dist/commands/access-groups.d.ts +1 -0
  3. package/dist/commands/access-groups.js +84 -28
  4. package/dist/commands/{async-endpoints-internal.d.ts → account-transaction-allocations.d.ts} +3 -2
  5. package/dist/commands/{async-endpoints-internal.js → account-transaction-allocations.js} +93 -33
  6. package/dist/commands/account-transactions.d.ts +1 -0
  7. package/dist/commands/account-transactions.js +96 -40
  8. package/dist/commands/accounts.d.ts +1 -0
  9. package/dist/commands/accounts.js +89 -33
  10. package/dist/commands/agent-runs.d.ts +1 -0
  11. package/dist/commands/agent-runs.js +85 -27
  12. package/dist/commands/audit-log.d.ts +1 -0
  13. package/dist/commands/audit-log.js +82 -26
  14. package/dist/commands/auth/login.js +1 -2
  15. package/dist/commands/auth/tenant.d.ts +1 -1
  16. package/dist/commands/auth/tenant.js +11 -11
  17. package/dist/commands/auth.js +1 -1
  18. package/dist/commands/bank-accounts.d.ts +1 -0
  19. package/dist/commands/bank-accounts.js +96 -40
  20. package/dist/commands/bank-integrations.d.ts +1 -0
  21. package/dist/commands/bank-integrations.js +97 -41
  22. package/dist/commands/business-partners---contacts.d.ts +1 -0
  23. package/dist/commands/business-partners---contacts.js +89 -33
  24. package/dist/commands/business-partners.d.ts +1 -0
  25. package/dist/commands/business-partners.js +89 -33
  26. package/dist/commands/credit-card-accounts.d.ts +1 -0
  27. package/dist/commands/credit-card-accounts.js +94 -38
  28. package/dist/commands/documents.d.ts +1 -0
  29. package/dist/commands/documents.js +85 -29
  30. package/dist/commands/dunning-configs.d.ts +1 -0
  31. package/dist/commands/dunning-configs.js +84 -28
  32. package/dist/commands/dunning-reminders.d.ts +1 -0
  33. package/dist/commands/dunning-reminders.js +87 -31
  34. package/dist/commands/dunning-runs.d.ts +1 -0
  35. package/dist/commands/dunning-runs.js +80 -24
  36. package/dist/commands/employees.d.ts +1 -0
  37. package/dist/commands/employees.js +111 -55
  38. package/dist/commands/event-processor-mappings.d.ts +1 -0
  39. package/dist/commands/event-processor-mappings.js +84 -28
  40. package/dist/commands/files-internal.d.ts +1 -0
  41. package/dist/commands/files-internal.js +88 -32
  42. package/dist/commands/health.d.ts +1 -0
  43. package/dist/commands/health.js +84 -28
  44. package/dist/commands/inbox.d.ts +1 -0
  45. package/dist/commands/inbox.js +84 -28
  46. package/dist/commands/integrations.d.ts +1 -0
  47. package/dist/commands/integrations.js +82 -26
  48. package/dist/commands/item-categories.d.ts +1 -0
  49. package/dist/commands/item-categories.js +84 -28
  50. package/dist/commands/items.d.ts +1 -0
  51. package/dist/commands/items.js +84 -28
  52. package/dist/commands/journal-lines.d.ts +1 -0
  53. package/dist/commands/journal-lines.js +80 -24
  54. package/dist/commands/journals.d.ts +1 -0
  55. package/dist/commands/journals.js +98 -42
  56. package/dist/commands/ledgers.d.ts +1 -0
  57. package/dist/commands/ledgers.js +95 -39
  58. package/dist/commands/office-locations.d.ts +1 -0
  59. package/dist/commands/office-locations.js +84 -28
  60. package/dist/commands/payment-method-ledger-mappings.d.ts +1 -0
  61. package/dist/commands/payment-method-ledger-mappings.js +84 -28
  62. package/dist/commands/payment-methods.d.ts +1 -0
  63. package/dist/commands/payment-methods.js +84 -28
  64. package/dist/commands/payment-runs.d.ts +1 -0
  65. package/dist/commands/payment-runs.js +97 -41
  66. package/dist/commands/payment-terms.d.ts +1 -0
  67. package/dist/commands/payment-terms.js +85 -29
  68. package/dist/commands/price-lists.d.ts +1 -0
  69. package/dist/commands/price-lists.js +89 -33
  70. package/dist/commands/processor-mappings.d.ts +1 -0
  71. package/dist/commands/processor-mappings.js +83 -27
  72. package/dist/commands/processors.d.ts +1 -0
  73. package/dist/commands/processors.js +88 -32
  74. package/dist/commands/reconciliation.d.ts +1 -0
  75. package/dist/commands/reconciliation.js +85 -29
  76. package/dist/commands/reports.d.ts +1 -0
  77. package/dist/commands/reports.js +85 -26
  78. package/dist/commands/salary-types.d.ts +1 -0
  79. package/dist/commands/salary-types.js +84 -28
  80. package/dist/commands/sales-invoices.d.ts +1 -0
  81. package/dist/commands/sales-invoices.js +109 -53
  82. package/dist/commands/sales-orders.d.ts +1 -0
  83. package/dist/commands/sales-orders.js +107 -51
  84. package/dist/commands/tags.d.ts +1 -0
  85. package/dist/commands/tags.js +84 -28
  86. package/dist/commands/tax-codes.d.ts +1 -0
  87. package/dist/commands/tax-codes.js +87 -31
  88. package/dist/commands/tax-matrix.d.ts +1 -0
  89. package/dist/commands/tax-matrix.js +84 -28
  90. package/dist/commands/tenants.d.ts +1 -0
  91. package/dist/commands/tenants.js +84 -28
  92. package/dist/commands/text-templates.d.ts +1 -0
  93. package/dist/commands/text-templates.js +84 -28
  94. package/dist/commands/tools.d.ts +1 -0
  95. package/dist/commands/tools.js +81 -25
  96. package/dist/commands/vat-declaration.d.ts +41 -0
  97. package/dist/commands/vat-declaration.js +1079 -0
  98. package/dist/commands/vendor-invoices.d.ts +1 -0
  99. package/dist/commands/vendor-invoices.js +113 -57
  100. package/dist/commands/webhooks.d.ts +1 -0
  101. package/dist/commands/webhooks.js +86 -30
  102. package/dist/commands/workflows.d.ts +1 -0
  103. package/dist/commands/workflows.js +82 -26
  104. package/dist/lib/api-client.d.ts +6 -1
  105. package/dist/lib/api-client.js +23 -5
  106. package/dist/lib/auth.js +18 -0
  107. package/dist/lib/config.js +1 -1
  108. package/oclif.manifest.json +229 -52
  109. package/package.json +15 -13
  110. package/dist/index 2.js +0 -1
  111. package/dist/index.d 2.ts +0 -1
@@ -8,15 +8,15 @@ const HAS_CAMT053 = false;
8
8
  const NESTED_IMPORTS = [];
9
9
  const SUBCOMMANDS = [...["list-tools", "list", "create", "resolve", "resolve-event", "get", "update", "delete", "template"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
10
  const ENDPOINTS = {
11
- 'list-tools': { method: 'GET', path: '/v1/processors/tools', summary: 'List all available tools', pathParams: [], hasBody: false, isMultipart: false },
12
- 'list': { method: 'GET', path: '/v1/processors', summary: 'List processors available to the tenant', pathParams: [], hasBody: false, isMultipart: false },
13
- 'create': { method: 'POST', path: '/v1/processors', summary: 'Create a new tenant-scoped processor', pathParams: [], hasBody: true, isMultipart: false },
14
- 'resolve': { method: 'GET', path: '/v1/processors/resolve/{documentType}', summary: 'Resolve processor for a document type', pathParams: ['documentType'], hasBody: false, isMultipart: false },
15
- 'resolve-event': { method: 'GET', path: '/v1/processors/resolve/event/{eventType}', summary: 'Resolve processors for an event type', pathParams: ['eventType'], hasBody: false, isMultipart: false },
16
- 'get': { method: 'GET', path: '/v1/processors/{id}', summary: 'Get a single processor by ID', pathParams: ['id'], hasBody: false, isMultipart: false },
17
- 'update': { method: 'PATCH', path: '/v1/processors/{id}', summary: 'Update a tenant-scoped processor', pathParams: ['id'], hasBody: true, isMultipart: false },
18
- 'delete': { method: 'DELETE', path: '/v1/processors/{id}', summary: 'Delete a tenant-scoped processor', pathParams: ['id'], hasBody: false, isMultipart: false },
19
- 'template': { method: 'POST', path: '/v1/processors/{id}/template', summary: 'Upload a PDF template for a processor', pathParams: ['id'], hasBody: false, isMultipart: false },
11
+ 'list-tools': { method: 'GET', path: '/v1/processors/tools', summary: 'List all available tools', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
12
+ 'list': { method: 'GET', path: '/v1/processors', summary: 'List processors available to the tenant', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
13
+ 'create': { method: 'POST', path: '/v1/processors', summary: 'Create a new tenant-scoped processor', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['processorTools', 'options'] },
14
+ 'resolve': { method: 'GET', path: '/v1/processors/resolve/{documentType}', summary: 'Resolve processor for a document type', pathParams: ['documentType'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
15
+ 'resolve-event': { method: 'GET', path: '/v1/processors/resolve/event/{eventType}', summary: 'Resolve processors for an event type', pathParams: ['eventType'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
16
+ 'get': { method: 'GET', path: '/v1/processors/{id}', summary: 'Get a single processor by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
17
+ 'update': { method: 'PATCH', path: '/v1/processors/{id}', summary: 'Update a tenant-scoped processor', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['processorTools', 'options'] },
18
+ 'delete': { method: 'DELETE', path: '/v1/processors/{id}', summary: 'Delete a tenant-scoped processor', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
19
+ 'template': { method: 'POST', path: '/v1/processors/{id}/template', summary: 'Upload a PDF template for a processor', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
20
20
  };
21
21
  export default class Processors extends Command {
22
22
  static description = 'Manage processors';
@@ -85,11 +85,6 @@ export default class Processors extends Command {
85
85
  const endpoint = ENDPOINTS[args.action];
86
86
  if (!endpoint)
87
87
  this.error(`Unknown action: ${args.action}. Available: ${SUBCOMMANDS.join(', ')}`);
88
- const context = flags.context ? getContext(flags.context) : getCurrentContext();
89
- if (!context)
90
- this.error('No context configured. Run: aderis auth login --new');
91
- if (!context.tokens)
92
- this.error('Not logged in. Run: aderis auth login');
93
88
  let apiPath = endpoint.path;
94
89
  for (let i = 0; i < endpoint.pathParams.length; i++) {
95
90
  const param = endpoint.pathParams[i];
@@ -108,6 +103,11 @@ export default class Processors extends Command {
108
103
  }
109
104
  apiPath = apiPath.replace(`{${param}}`, String(value));
110
105
  }
106
+ const context = flags.context ? getContext(flags.context) : getCurrentContext();
107
+ if (!context)
108
+ this.error('No context configured. Run: aderis auth login --new');
109
+ if (!context.tokens)
110
+ this.error('Not logged in. Run: aderis auth login');
111
111
  const queryParams = {};
112
112
  if (flags.page)
113
113
  queryParams.page = flags.page;
@@ -134,15 +134,7 @@ export default class Processors extends Command {
134
134
  if (eqIndex === -1)
135
135
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
136
136
  const key = f.slice(0, eqIndex);
137
- let value = f.slice(eqIndex + 1);
138
- // Check for operator prefix (with or without $)
139
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
140
- if (opMatch) {
141
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
142
- }
143
- else {
144
- value = `\$eq:${value}`;
145
- }
137
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
146
138
  const filterKey = `filter.${key}`;
147
139
  if (filterKey in queryParams) {
148
140
  // Multiple filters on same field: collect as array
@@ -175,7 +167,9 @@ export default class Processors extends Command {
175
167
  data = form;
176
168
  }
177
169
  else if (endpoint.hasBody) {
178
- if (flags.file) {
170
+ // For binary-response endpoints, --file is the output path, not a JSON
171
+ // input file — skip the read-as-body branch.
172
+ if (flags.file && !endpoint.isBinaryResponse) {
179
173
  const fsModule = await import('node:fs');
180
174
  try {
181
175
  data = JSON.parse(fsModule.readFileSync(flags.file, 'utf-8'));
@@ -197,6 +191,7 @@ export default class Processors extends Command {
197
191
  if (!data)
198
192
  data = {};
199
193
  const body = data;
194
+ const arrayFields = new Set(endpoint.bodyArrayFields);
200
195
  for (const pair of flags.set) {
201
196
  const eqIndex = pair.indexOf('=');
202
197
  if (eqIndex === -1)
@@ -208,6 +203,19 @@ export default class Processors extends Command {
208
203
  value = JSON.parse(value);
209
204
  }
210
205
  catch { /* keep as string */ }
206
+ // For top-level keys typed as array in the API schema, split bare
207
+ // comma-separated input (e.g. "1171,1170" -> ["1171","1170"]) and
208
+ // wrap a lone scalar in a single-element array. Skips when --set
209
+ // already provided a JSON array.
210
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
211
+ if (isArrayField && !Array.isArray(value)) {
212
+ if (typeof value === 'string') {
213
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
214
+ }
215
+ else if (value !== undefined && value !== null) {
216
+ value = [value];
217
+ }
218
+ }
211
219
  // Support nested keys like "address.city"
212
220
  const keys = key.split('.');
213
221
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
@@ -258,6 +266,26 @@ export default class Processors extends Command {
258
266
  process.stderr.write('\n');
259
267
  }
260
268
  try {
269
+ // Binary response: download to file
270
+ if (endpoint.isBinaryResponse) {
271
+ if (!flags.file) {
272
+ this.error(`Endpoint returns a binary file. Specify the output path with -f / --file (e.g. -f report.xlsx).`);
273
+ }
274
+ const binResponse = await apiRequest(endpoint.method, apiPath, {
275
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
276
+ data,
277
+ contextName: flags.context,
278
+ binary: true,
279
+ });
280
+ const fsModule = await import('node:fs');
281
+ const binData = binResponse.data;
282
+ const buffer = Buffer.isBuffer(binData)
283
+ ? binData
284
+ : Buffer.from(binData);
285
+ fsModule.writeFileSync(flags.file, buffer);
286
+ this.log(`Saved ${buffer.length} bytes to ${flags.file}`);
287
+ return;
288
+ }
261
289
  const response = await apiRequest(endpoint.method, apiPath, {
262
290
  params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
263
291
  data,
@@ -348,6 +376,7 @@ export default class Processors extends Command {
348
376
  }
349
377
  else if (flags.set && flags.set.length > 0) {
350
378
  const item = {};
379
+ const arrayFields = new Set(ENDPOINTS['create']?.bodyArrayFields ?? []);
351
380
  for (const pair of flags.set) {
352
381
  const eqIndex = pair.indexOf('=');
353
382
  if (eqIndex === -1)
@@ -358,6 +387,15 @@ export default class Processors extends Command {
358
387
  value = JSON.parse(value);
359
388
  }
360
389
  catch { /* keep as string */ }
390
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
391
+ if (isArrayField && !Array.isArray(value)) {
392
+ if (typeof value === 'string') {
393
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
394
+ }
395
+ else if (value !== undefined && value !== null) {
396
+ value = [value];
397
+ }
398
+ }
361
399
  const keys = key.split('.');
362
400
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
363
401
  this.error(`Invalid --set key: ${key}`);
@@ -507,14 +545,7 @@ export default class Processors extends Command {
507
545
  if (eqIndex === -1)
508
546
  this.error(`Invalid --filter format: ${f}. Use: --filter key=value`);
509
547
  const key = f.slice(0, eqIndex);
510
- let value = f.slice(eqIndex + 1);
511
- const opMatch = value.match(/^\$?(eq|ne|in|like|gt|gte|lt|lte|null):/);
512
- if (opMatch) {
513
- value = `\$${opMatch[1]}:${value.slice(opMatch[0].length)}`;
514
- }
515
- else {
516
- value = `\$eq:${value}`;
517
- }
548
+ const value = this.parseFilterValue(f.slice(eqIndex + 1));
518
549
  const filterKey = `filter.${key}`;
519
550
  if (filterKey in baseParams) {
520
551
  const existing = baseParams[filterKey];
@@ -691,6 +722,7 @@ export default class Processors extends Command {
691
722
  }
692
723
  else if (flags.set && flags.set.length > 0) {
693
724
  const item = {};
725
+ const arrayFields = new Set(ENDPOINTS[`create-${suffix}`]?.bodyArrayFields ?? []);
694
726
  for (const pair of flags.set) {
695
727
  const eqIndex = pair.indexOf('=');
696
728
  if (eqIndex === -1)
@@ -701,6 +733,15 @@ export default class Processors extends Command {
701
733
  value = JSON.parse(value);
702
734
  }
703
735
  catch { /* keep as string */ }
736
+ const isArrayField = !key.includes('.') && arrayFields.has(key);
737
+ if (isArrayField && !Array.isArray(value)) {
738
+ if (typeof value === 'string') {
739
+ value = value.split(',').map(v => v.trim()).filter(v => v.length > 0);
740
+ }
741
+ else if (value !== undefined && value !== null) {
742
+ value = [value];
743
+ }
744
+ }
704
745
  const keys = key.split('.');
705
746
  if (keys.some(k => k === '__proto__' || k === 'constructor' || k === 'prototype')) {
706
747
  this.error(`Invalid --set key: ${key}`);
@@ -820,6 +861,21 @@ export default class Processors extends Command {
820
861
  this.log('');
821
862
  this.log(`Summary: ${created} created, ${updated} updated, ${errors.length} failed`);
822
863
  }
864
+ parseFilterValue(value) {
865
+ // Recognises the nestjs-paginate operator set (with or without the leading $)
866
+ // and recursively unwraps chained operators like 'not:null' -> '$not:$null'
867
+ // or 'not:like:John' -> '$not:$like:John'. Falls back to '$eq:<value>' when
868
+ // no operator prefix is present.
869
+ const opRe = /^\$?(and|or|not|eq|gt|gte|in|null|lt|lte|btw|ne|ilike|like|sw|contains)(?::|$)/;
870
+ const m = value.match(opRe);
871
+ if (!m)
872
+ return `\$eq:${value}`;
873
+ const op = m[1];
874
+ const rest = value.slice(m[0].length);
875
+ if (rest === '')
876
+ return `\$${op}`;
877
+ return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
878
+ }
823
879
  filterFields(data, fields) {
824
880
  const isUnsafeKey = (k) => k === '__proto__' || k === 'constructor' || k === 'prototype';
825
881
  const pick = (obj, keys) => {
@@ -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';
@@ -78,11 +78,6 @@ export default class Reconciliation extends Command {
78
78
  const endpoint = ENDPOINTS[args.action];
79
79
  if (!endpoint)
80
80
  this.error(`Unknown action: ${args.action}. Available: ${SUBCOMMANDS.join(', ')}`);
81
- const context = flags.context ? getContext(flags.context) : getCurrentContext();
82
- if (!context)
83
- this.error('No context configured. Run: aderis auth login --new');
84
- if (!context.tokens)
85
- this.error('Not logged in. Run: aderis auth login');
86
81
  let apiPath = endpoint.path;
87
82
  for (let i = 0; i < endpoint.pathParams.length; i++) {
88
83
  const param = endpoint.pathParams[i];
@@ -101,6 +96,11 @@ export default class Reconciliation extends Command {
101
96
  }
102
97
  apiPath = apiPath.replace(`{${param}}`, String(value));
103
98
  }
99
+ const context = flags.context ? getContext(flags.context) : getCurrentContext();
100
+ if (!context)
101
+ this.error('No context configured. Run: aderis auth login --new');
102
+ if (!context.tokens)
103
+ this.error('Not logged in. Run: aderis auth login');
104
104
  const queryParams = {};
105
105
  if (flags.page)
106
106
  queryParams.page = flags.page;
@@ -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,6 +854,21 @@ 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) => {
@@ -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';
@@ -71,11 +74,6 @@ export default class Reports extends Command {
71
74
  const endpoint = ENDPOINTS[args.action];
72
75
  if (!endpoint)
73
76
  this.error(`Unknown action: ${args.action}. Available: ${SUBCOMMANDS.join(', ')}`);
74
- const context = flags.context ? getContext(flags.context) : getCurrentContext();
75
- if (!context)
76
- this.error('No context configured. Run: aderis auth login --new');
77
- if (!context.tokens)
78
- this.error('Not logged in. Run: aderis auth login');
79
77
  let apiPath = endpoint.path;
80
78
  for (let i = 0; i < endpoint.pathParams.length; i++) {
81
79
  const param = endpoint.pathParams[i];
@@ -94,6 +92,11 @@ export default class Reports extends Command {
94
92
  }
95
93
  apiPath = apiPath.replace(`{${param}}`, String(value));
96
94
  }
95
+ const context = flags.context ? getContext(flags.context) : getCurrentContext();
96
+ if (!context)
97
+ this.error('No context configured. Run: aderis auth login --new');
98
+ if (!context.tokens)
99
+ this.error('Not logged in. Run: aderis auth login');
97
100
  const queryParams = {};
98
101
  if (flags.page)
99
102
  queryParams.page = flags.page;
@@ -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,6 +850,21 @@ 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) => {