@aderis-erp/cli 1.1.1 → 1.1.3

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 (138) hide show
  1. package/dist/commands/access-groups.d.ts +19 -2
  2. package/dist/commands/access-groups.js +211 -29
  3. package/dist/commands/account-transaction-allocations.d.ts +19 -2
  4. package/dist/commands/account-transaction-allocations.js +209 -28
  5. package/dist/commands/account-transactions.d.ts +23 -2
  6. package/dist/commands/account-transactions.js +216 -30
  7. package/dist/commands/accounts.d.ts +19 -2
  8. package/dist/commands/accounts.js +210 -29
  9. package/dist/commands/agent-platform.d.ts +19 -3
  10. package/dist/commands/agent-platform.js +209 -37
  11. package/dist/commands/agent-runs.d.ts +19 -2
  12. package/dist/commands/agent-runs.js +209 -28
  13. package/dist/commands/analytics.d.ts +19 -2
  14. package/dist/commands/analytics.js +209 -28
  15. package/dist/commands/api-keys.d.ts +19 -2
  16. package/dist/commands/api-keys.js +209 -28
  17. package/dist/commands/async-jobs.d.ts +19 -2
  18. package/dist/commands/async-jobs.js +210 -29
  19. package/dist/commands/audit-log.d.ts +19 -2
  20. package/dist/commands/audit-log.js +209 -28
  21. package/dist/commands/bank-accounts.d.ts +19 -2
  22. package/dist/commands/bank-accounts.js +210 -29
  23. package/dist/commands/bank-integrations.d.ts +19 -2
  24. package/dist/commands/bank-integrations.js +210 -29
  25. package/dist/commands/business-partners.d.ts +20 -2
  26. package/dist/commands/business-partners.js +220 -29
  27. package/dist/commands/chat.d.ts +19 -2
  28. package/dist/commands/chat.js +210 -29
  29. package/dist/commands/credit-card-accounts.d.ts +19 -2
  30. package/dist/commands/credit-card-accounts.js +210 -29
  31. package/dist/commands/delivery-notes.d.ts +19 -2
  32. package/dist/commands/delivery-notes.js +210 -29
  33. package/dist/commands/dunning-configs.d.ts +19 -2
  34. package/dist/commands/dunning-configs.js +210 -29
  35. package/dist/commands/dunning-reminders.d.ts +19 -2
  36. package/dist/commands/dunning-reminders.js +209 -28
  37. package/dist/commands/dunning-runs.d.ts +19 -2
  38. package/dist/commands/dunning-runs.js +209 -28
  39. package/dist/commands/email-transactions.d.ts +19 -2
  40. package/dist/commands/email-transactions.js +209 -28
  41. package/dist/commands/employees.d.ts +21 -2
  42. package/dist/commands/employees.js +221 -29
  43. package/dist/commands/entity-configurations.d.ts +19 -2
  44. package/dist/commands/entity-configurations.js +209 -28
  45. package/dist/commands/event-mapping-rules.d.ts +56 -0
  46. package/dist/commands/event-mapping-rules.js +1324 -0
  47. package/dist/commands/event-processor-mappings.d.ts +19 -2
  48. package/dist/commands/event-processor-mappings.js +210 -29
  49. package/dist/commands/files.d.ts +19 -2
  50. package/dist/commands/files.js +210 -29
  51. package/dist/commands/health.d.ts +19 -2
  52. package/dist/commands/health.js +209 -28
  53. package/dist/commands/imports.d.ts +19 -2
  54. package/dist/commands/imports.js +209 -30
  55. package/dist/commands/inbox.d.ts +19 -2
  56. package/dist/commands/inbox.js +209 -29
  57. package/dist/commands/integration-identities.d.ts +19 -2
  58. package/dist/commands/integration-identities.js +209 -28
  59. package/dist/commands/integrations.d.ts +19 -2
  60. package/dist/commands/integrations.js +209 -28
  61. package/dist/commands/item-categories.d.ts +19 -2
  62. package/dist/commands/item-categories.js +210 -29
  63. package/dist/commands/items.d.ts +23 -2
  64. package/dist/commands/items.js +215 -29
  65. package/dist/commands/journal-lines.d.ts +19 -2
  66. package/dist/commands/journal-lines.js +209 -28
  67. package/dist/commands/journals.d.ts +23 -2
  68. package/dist/commands/journals.js +215 -29
  69. package/dist/commands/ledgers.d.ts +19 -2
  70. package/dist/commands/ledgers.js +210 -29
  71. package/dist/commands/my-account.d.ts +19 -2
  72. package/dist/commands/my-account.js +209 -28
  73. package/dist/commands/office-locations.d.ts +19 -2
  74. package/dist/commands/office-locations.js +210 -29
  75. package/dist/commands/payment-methods.d.ts +19 -2
  76. package/dist/commands/payment-methods.js +210 -29
  77. package/dist/commands/payment-runs.d.ts +19 -2
  78. package/dist/commands/payment-runs.js +211 -29
  79. package/dist/commands/payment-terms.d.ts +19 -2
  80. package/dist/commands/payment-terms.js +211 -30
  81. package/dist/commands/payments.d.ts +19 -2
  82. package/dist/commands/payments.js +210 -29
  83. package/dist/commands/payroll-runs.d.ts +19 -2
  84. package/dist/commands/payroll-runs.js +210 -29
  85. package/dist/commands/permissions.d.ts +19 -2
  86. package/dist/commands/permissions.js +209 -28
  87. package/dist/commands/price-lists.d.ts +23 -2
  88. package/dist/commands/price-lists.js +216 -29
  89. package/dist/commands/processor-mappings.d.ts +19 -2
  90. package/dist/commands/processor-mappings.js +210 -29
  91. package/dist/commands/processors.d.ts +19 -2
  92. package/dist/commands/processors.js +210 -29
  93. package/dist/commands/purchase-order-types.d.ts +19 -2
  94. package/dist/commands/purchase-order-types.js +210 -29
  95. package/dist/commands/purchase-orders.d.ts +19 -2
  96. package/dist/commands/purchase-orders.js +210 -29
  97. package/dist/commands/reports.d.ts +19 -2
  98. package/dist/commands/reports.js +209 -28
  99. package/dist/commands/roles.d.ts +19 -2
  100. package/dist/commands/roles.js +210 -29
  101. package/dist/commands/salary-types.d.ts +19 -2
  102. package/dist/commands/salary-types.js +210 -29
  103. package/dist/commands/sales-invoices.d.ts +21 -2
  104. package/dist/commands/sales-invoices.js +222 -29
  105. package/dist/commands/sales-orders.d.ts +19 -2
  106. package/dist/commands/sales-orders.js +210 -29
  107. package/dist/commands/saved-views.d.ts +19 -2
  108. package/dist/commands/saved-views.js +209 -28
  109. package/dist/commands/scheduled-processor-mappings.d.ts +19 -2
  110. package/dist/commands/scheduled-processor-mappings.js +210 -29
  111. package/dist/commands/schedules.d.ts +19 -2
  112. package/dist/commands/schedules.js +210 -29
  113. package/dist/commands/tags.d.ts +19 -2
  114. package/dist/commands/tags.js +210 -29
  115. package/dist/commands/tax-codes.d.ts +19 -2
  116. package/dist/commands/tax-codes.js +210 -29
  117. package/dist/commands/tax-matrix.d.ts +19 -2
  118. package/dist/commands/tax-matrix.js +210 -29
  119. package/dist/commands/templates.d.ts +19 -2
  120. package/dist/commands/templates.js +214 -29
  121. package/dist/commands/tenants.d.ts +19 -2
  122. package/dist/commands/tenants.js +210 -29
  123. package/dist/commands/tools.d.ts +19 -2
  124. package/dist/commands/tools.js +209 -28
  125. package/dist/commands/users.d.ts +19 -2
  126. package/dist/commands/users.js +210 -29
  127. package/dist/commands/vat-declarations.d.ts +19 -2
  128. package/dist/commands/vat-declarations.js +210 -29
  129. package/dist/commands/vendor-invoices.d.ts +20 -2
  130. package/dist/commands/vendor-invoices.js +220 -31
  131. package/dist/commands/webhooks.d.ts +19 -2
  132. package/dist/commands/webhooks.js +210 -29
  133. package/dist/commands/workflows.d.ts +19 -2
  134. package/dist/commands/workflows.js +209 -28
  135. package/dist/lib/api-client.d.ts +1 -0
  136. package/dist/lib/api-client.js +3 -0
  137. package/oclif.manifest.json +896 -169
  138. package/package.json +1 -1
@@ -1,12 +1,15 @@
1
1
  import { Command, Args, Flags } from '@oclif/core';
2
2
  import { apiRequest, ApiError, isTextualMediaType, normalizeSortBy } from '../lib/api-client.js';
3
3
  import { getCurrentContext, getContext, getBaseUrls, hasCredentials, missingCredentialsMessage } from '../lib/config.js';
4
- // Add 'import' action if both create and update exist
5
- const HAS_IMPORT = true;
4
+ // 'upsert' loops create/update from the CLI, one request per record. It exists
5
+ // only where the API has no import endpoint of its own; those keep the name
6
+ // 'import' and are listed in SERVER_IMPORTS.
7
+ const HAS_UPSERT = true;
6
8
  const HAS_EXPORT = true;
7
9
  const HAS_CAMT053 = false;
8
- const NESTED_IMPORTS = ["notes", "files", "contacts", "contacts-notes"];
9
- const SUBCOMMANDS = [...["list", "create", "match-candidates", "get", "update", "delete", "merge", "activate", "get-favicon", "update-favicon", "trigger-favicon-backfill", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-files", "create-files", "get-files", "update-files", "delete-files", "files-download", "list-contacts", "create-contacts", "get-contacts", "update-contacts", "delete-contacts", "list-contacts-notes", "create-contacts-notes", "get-contacts-notes", "update-contacts-notes", "delete-contacts-notes"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
10
+ const SERVER_IMPORTS = [];
11
+ const NESTED_UPSERTS = ["notes", "files", "contacts", "contacts-notes", "beneficiary-accounts"];
12
+ const SUBCOMMANDS = [...["list", "create", "match-candidates", "get", "update", "delete", "merge", "activate", "get-favicon", "update-favicon", "trigger-favicon-backfill", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-files", "create-files", "get-files", "update-files", "delete-files", "files-download", "list-contacts", "create-contacts", "get-contacts", "update-contacts", "delete-contacts", "list-contacts-notes", "create-contacts-notes", "get-contacts-notes", "update-contacts-notes", "delete-contacts-notes", "list-beneficiary-accounts", "create-beneficiary-accounts", "get-beneficiary-accounts", "update-beneficiary-accounts", "delete-beneficiary-accounts", "beneficiary-accounts-approve", "beneficiary-accounts-retire", "beneficiary-accounts-reactivate", "beneficiary-accounts-default"], ...(HAS_UPSERT ? ['upsert'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_UPSERTS.map(n => `upsert-${n}`)];
10
13
  const ENDPOINTS = {
11
14
  'list': { method: 'GET', path: '/v1/business-partners', summary: 'Get all business partners with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
12
15
  'create': { method: 'POST', path: '/v1/business-partners', summary: 'Create a new business partner', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: ['salesOrderRecipientCc', 'salesOrderRecipientBcc', 'salesInvoiceRecipientCc', 'salesInvoiceRecipientBcc', 'deliveryNoteRecipientCc', 'deliveryNoteRecipientBcc', 'dunningRecipientCc', 'dunningRecipientBcc', 'contacts', 'tagIds', 'tagCodes', 'salesEarlyPaymentDiscounts', 'purchaseEarlyPaymentDiscounts', 'notes'] },
@@ -40,6 +43,15 @@ const ENDPOINTS = {
40
43
  'get-contacts-notes': { method: 'GET', path: '/v1/business-partners/contacts/{contactId}/notes/{noteId}', summary: 'Get a specific note for a contact', pathParams: ['contactId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
41
44
  'update-contacts-notes': { method: 'PATCH', path: '/v1/business-partners/contacts/{contactId}/notes/{noteId}', summary: 'Update a note for a contact', pathParams: ['contactId', 'noteId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
42
45
  'delete-contacts-notes': { method: 'DELETE', path: '/v1/business-partners/contacts/{contactId}/notes/{noteId}', summary: 'Delete a note from a contact', pathParams: ['contactId', 'noteId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
46
+ 'list-beneficiary-accounts': { method: 'GET', path: '/v1/business-partners/{businessPartnerId}/beneficiary-accounts', summary: 'List the beneficiary bank accounts of a business partner', pathParams: ['businessPartnerId'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
47
+ 'create-beneficiary-accounts': { method: 'POST', path: '/v1/business-partners/{businessPartnerId}/beneficiary-accounts', summary: 'Store a new beneficiary bank account (status NEW)', pathParams: ['businessPartnerId'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
48
+ 'get-beneficiary-accounts': { method: 'GET', path: '/v1/business-partners/{businessPartnerId}/beneficiary-accounts/{id}', summary: 'Get one beneficiary bank account of a business partner', pathParams: ['businessPartnerId', 'id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
49
+ 'update-beneficiary-accounts': { method: 'PATCH', path: '/v1/business-partners/{businessPartnerId}/beneficiary-accounts/{id}', summary: 'Edit a beneficiary bank account', pathParams: ['businessPartnerId', 'id'], hasBody: true, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
50
+ 'delete-beneficiary-accounts': { method: 'DELETE', path: '/v1/business-partners/{businessPartnerId}/beneficiary-accounts/{id}', summary: 'Delete a beneficiary bank account', pathParams: ['businessPartnerId', 'id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
51
+ 'beneficiary-accounts-approve': { method: 'POST', path: '/v1/business-partners/{businessPartnerId}/beneficiary-accounts/{id}/approve', summary: 'Approve a beneficiary bank account', pathParams: ['businessPartnerId', 'id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
52
+ 'beneficiary-accounts-retire': { method: 'POST', path: '/v1/business-partners/{businessPartnerId}/beneficiary-accounts/{id}/retire', summary: 'Retire a beneficiary bank account; clears its default flag', pathParams: ['businessPartnerId', 'id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
53
+ 'beneficiary-accounts-reactivate': { method: 'POST', path: '/v1/business-partners/{businessPartnerId}/beneficiary-accounts/{id}/reactivate', summary: 'Reactivate a retired account: NEW again, both approvals cleared', pathParams: ['businessPartnerId', 'id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
54
+ 'beneficiary-accounts-default': { method: 'POST', path: '/v1/business-partners/{businessPartnerId}/beneficiary-accounts/{id}/default', summary: 'Make this account the partner default; the previous one is unset', pathParams: ['businessPartnerId', 'id'], hasBody: false, isMultipart: false, isBinaryResponse: false, hasBinaryVariant: false, bodyArrayFields: [] },
43
55
  };
44
56
  export default class BusinessPartners extends Command {
45
57
  static description = 'Manage business partners';
@@ -48,7 +60,7 @@ export default class BusinessPartners extends Command {
48
60
  '<%= config.bin %> business-partners get <id>',
49
61
  '<%= config.bin %> business-partners create --data \'{...}\'',
50
62
  '<%= config.bin %> business-partners update <id> --set field=value',
51
- '<%= config.bin %> business-partners import --file data.json',
63
+ '<%= config.bin %> business-partners upsert --file data.json',
52
64
  '<%= config.bin %> business-partners export --csv > business-partners.csv',
53
65
  ];
54
66
  static args = {
@@ -57,7 +69,7 @@ export default class BusinessPartners extends Command {
57
69
  };
58
70
  static flags = {
59
71
  context: Flags.string({ char: 'c', aliases: ['config'], env: 'ADERIS_CONTEXT', description: 'Context to use for this command, overriding the current context (alias: --config)' }),
60
- output: Flags.string({ char: 'o', description: 'Output format (default: table for import, json for other commands)', options: ['json', 'table', 'csv'] }),
72
+ output: Flags.string({ char: 'o', description: 'Output format (default: table for upsert, json for other commands)', options: ['json', 'table', 'csv'] }),
61
73
  table: Flags.boolean({ char: 't', description: 'Output as table (shortcut for --output table)' }),
62
74
  csv: Flags.boolean({ description: 'Output as CSV (shortcut for --output csv)' }),
63
75
  data: Flags.string({ char: 'd', description: 'JSON data for request body' }),
@@ -66,6 +78,7 @@ export default class BusinessPartners extends Command {
66
78
  'note-id': Flags.string({ description: 'Path param: noteId' }),
67
79
  'file-id': Flags.string({ description: 'Path param: fileId' }),
68
80
  'business-partner-id': Flags.string({ description: 'Path param: businessPartnerId' }),
81
+ 'id': Flags.string({ description: 'Path param: id' }),
69
82
  page: Flags.string({ description: 'Page number' }),
70
83
  limit: Flags.string({ description: 'Items per page' }),
71
84
  include: Flags.string({ description: 'Relations to include' }),
@@ -75,15 +88,31 @@ export default class BusinessPartners extends Command {
75
88
  query: Flags.string({ description: 'Additional query params as JSON' }),
76
89
  fields: Flags.string({ description: 'Comma-separated list of fields to return, applied by the API (e.g. --fields=id,name,status)' }),
77
90
  sanitize: Flags.boolean({ description: 'Remove null values, audit fields, and duplicate UUIDs from output' }),
91
+ meta: Flags.boolean({ description: 'Keep the API envelope in JSON output: data plus page, totals and sums' }),
78
92
  'show-http-request': Flags.boolean({ description: 'Show HTTP request details (method, URL, body)' }),
79
93
  };
80
94
  async run() {
81
95
  const { args, flags } = await this.parse(BusinessPartners);
82
- // Handle import action specially
83
- if (args.action === 'import') {
84
- if (!HAS_IMPORT)
85
- this.error('Import not available for this resource');
86
- await this.runImport(flags);
96
+ // Bulk actions. 'import' is the API's own endpoint: one request, answered
97
+ // by a run that records what happened to every record. 'upsert' is the CLI
98
+ // looping create/update. A resource has one or the other, never both.
99
+ if (args.action === 'upsert') {
100
+ if (!HAS_UPSERT) {
101
+ this.error(SERVER_IMPORTS.includes('import')
102
+ ? 'This resource is imported by the API. Use: aderis business-partners import --file data.json'
103
+ : 'Upsert needs both a create and an update endpoint, which this resource does not have');
104
+ }
105
+ await this.runUpsert(flags);
106
+ return;
107
+ }
108
+ if (args.action.startsWith('upsert-')) {
109
+ const suffix = args.action.replace('upsert-', '');
110
+ if (!NESTED_UPSERTS.includes(suffix)) {
111
+ this.error(SERVER_IMPORTS.includes('import-' + suffix)
112
+ ? `That resource is imported by the API. Use: aderis business-partners import-${suffix} <id> --file data.json`
113
+ : `Unknown action: ${args.action}. Available: ${SUBCOMMANDS.join(', ')}`);
114
+ }
115
+ await this.runNestedUpsert(suffix, args.id, flags);
87
116
  return;
88
117
  }
89
118
  // Handle export action specially
@@ -100,12 +129,17 @@ export default class BusinessPartners extends Command {
100
129
  await this.runCamt053Import(flags);
101
130
  return;
102
131
  }
103
- // Handle nested resource imports (e.g., import-prices)
104
- if (args.action.startsWith('import-')) {
105
- const suffix = args.action.replace('import-', '');
106
- if (NESTED_IMPORTS.includes(suffix)) {
107
- await this.runNestedImport(suffix, args.id, flags);
108
- return;
132
+ if (SERVER_IMPORTS.includes(args.action)) {
133
+ await this.runServerImport(args.action, args.id, flags);
134
+ return;
135
+ }
136
+ // 'import' used to name the CLI's create/update loop on every resource. It
137
+ // now names the API endpoint, which most resources do not have.
138
+ if (args.action === 'import' || args.action.startsWith('import-')) {
139
+ const suffix = args.action === 'import' ? '' : '-' + args.action.replace('import-', '');
140
+ const loops = args.action === 'import' ? HAS_UPSERT : NESTED_UPSERTS.includes(args.action.replace('import-', ''));
141
+ if (loops) {
142
+ this.error(`This resource has no import endpoint. Use: aderis business-partners upsert${suffix}${suffix ? ' <id>' : ''} --file data.json`);
109
143
  }
110
144
  }
111
145
  const endpoint = ENDPOINTS[args.action];
@@ -367,9 +401,15 @@ export default class BusinessPartners extends Command {
367
401
  }
368
402
  catch { /* keep as string */ }
369
403
  }
370
- // Extract data array from paginated wrapper (e.g., { data: [...], total: 3 })
404
+ // A paginated answer is { data, meta, links, totals }. The records are
405
+ // unwrapped for output, but what the API said about the page is kept:
406
+ // dropping it silently let a script read one page as the whole result
407
+ // (AderisERP/product#61).
408
+ let envelope;
371
409
  if (output && typeof output === 'object' && !Array.isArray(output) && 'data' in output && Array.isArray(output.data)) {
372
- output = output.data;
410
+ const page = output;
411
+ envelope = page;
412
+ output = page.data;
373
413
  }
374
414
  // For table/csv on non-array responses, auto-extract the primary array
375
415
  // e.g., { payments: [...], total: 3 } → extract payments array
@@ -386,12 +426,24 @@ export default class BusinessPartners extends Command {
386
426
  output = this.sanitizeOutput(output);
387
427
  }
388
428
  const fieldList = flags.fields?.split(',').map(f => f.trim());
389
- if (flags.csv || flags.output === 'csv')
429
+ if (envelope)
430
+ this.warnShortPage(envelope, flags.limit);
431
+ if (flags.csv || flags.output === 'csv') {
390
432
  this.printCsv(output, fieldList);
391
- else if (flags.table || flags.output === 'table')
433
+ }
434
+ else if (flags.table || flags.output === 'table') {
392
435
  this.printTable(output, fieldList);
393
- else
436
+ if (envelope)
437
+ this.printPageSummary(envelope);
438
+ }
439
+ else if (envelope && flags.meta) {
440
+ // The whole envelope, for a caller that asked for it; the default stays
441
+ // a bare array so existing scripts are unaffected.
442
+ this.log(JSON.stringify({ ...envelope, data: output }, null, 2));
443
+ }
444
+ else {
394
445
  this.log(JSON.stringify(output, null, 2));
446
+ }
395
447
  }
396
448
  catch (error) {
397
449
  if (error instanceof ApiError) {
@@ -402,7 +454,7 @@ export default class BusinessPartners extends Command {
402
454
  throw error;
403
455
  }
404
456
  }
405
- async runImport(flags) {
457
+ async runUpsert(flags) {
406
458
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
407
459
  if (!context)
408
460
  this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
@@ -466,12 +518,12 @@ export default class BusinessPartners extends Command {
466
518
  items = [item];
467
519
  }
468
520
  else {
469
- this.error('Import requires --file, --data, or --set flags');
521
+ this.error('Upsert requires --file, --data, or --set flags');
470
522
  }
471
523
  const createEndpoint = ENDPOINTS['create'];
472
524
  const updateEndpoint = ENDPOINTS['update'];
473
525
  if (!createEndpoint || !updateEndpoint) {
474
- this.error('Import requires both create and update endpoints');
526
+ this.error('Upsert requires both create and update endpoints');
475
527
  }
476
528
  // Helper to extract identifier from item (id, code, or xxxCode)
477
529
  const getItemIdentifier = (item) => {
@@ -614,6 +666,117 @@ export default class BusinessPartners extends Command {
614
666
  this.log('');
615
667
  this.log(`Summary: ${created} created, ${updated} updated, ${errors.length} failed`);
616
668
  }
669
+ /**
670
+ * The API's own import: the whole array goes up in one request and comes
671
+ * back as an import run, which records what happened to every record. A bad
672
+ * record lands in errorLog and the rest are still applied, and the run stays
673
+ * readable afterwards with: aderis imports get <id>.
674
+ */
675
+ async runServerImport(action, parentId, flags) {
676
+ const endpoint = ENDPOINTS[action];
677
+ if (!endpoint)
678
+ this.error(`Unknown action: ${action}. Available: ${SUBCOMMANDS.join(', ')}`);
679
+ const context = flags.context ? getContext(flags.context) : getCurrentContext();
680
+ if (!context)
681
+ this.error(flags.context ? `Context "${flags.context}" not found. List contexts with: aderis config list` : 'No context configured. Run: aderis auth login --new');
682
+ if (!hasCredentials(context))
683
+ this.error(missingCredentialsMessage(context));
684
+ let apiPath = endpoint.path;
685
+ if (endpoint.pathParams.length > 0) {
686
+ if (!parentId)
687
+ this.error(`Missing required argument: id. Usage: aderis business-partners ${action} <parent-id> --file data.json`);
688
+ apiPath = apiPath.replace(`{${endpoint.pathParams[0]}}`, parentId);
689
+ }
690
+ // The endpoint takes an array. A single object is wrapped, so importing
691
+ // one record reads the same as importing many.
692
+ let records = [];
693
+ if (flags.file) {
694
+ const fsModule = await import('node:fs');
695
+ try {
696
+ const content = JSON.parse(fsModule.readFileSync(flags.file, 'utf-8'));
697
+ records = Array.isArray(content) ? content : [content];
698
+ }
699
+ catch {
700
+ this.error(`Failed to read file: ${flags.file}`);
701
+ }
702
+ }
703
+ else if (flags.data) {
704
+ try {
705
+ const content = JSON.parse(flags.data);
706
+ records = Array.isArray(content) ? content : [content];
707
+ }
708
+ catch {
709
+ this.error('Invalid JSON in --data');
710
+ }
711
+ }
712
+ else {
713
+ this.error('Import requires --file or --data holding a JSON array of records');
714
+ }
715
+ if (records.length === 0)
716
+ this.error('Nothing to import: the input is an empty array');
717
+ const params = { format: 'json' };
718
+ if (flags.mode)
719
+ params.mode = flags.mode;
720
+ if (flags.reference)
721
+ params.reference = flags.reference;
722
+ if (flags.async)
723
+ params.async = true;
724
+ const headers = {};
725
+ if (flags['idempotency-key'])
726
+ headers['Idempotency-Key'] = flags['idempotency-key'];
727
+ if (flags['show-http-request']) {
728
+ const baseUrls = getBaseUrls(context.environment);
729
+ const queryString = '?' + Object.entries(params).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join('&');
730
+ process.stderr.write(`\n${endpoint.method.toUpperCase()} ${baseUrls.api}${apiPath}${queryString}\n`);
731
+ process.stderr.write(`Body: [${records.length} record${records.length === 1 ? '' : 's'}]\n\n`);
732
+ }
733
+ try {
734
+ const response = await apiRequest(endpoint.method, apiPath, {
735
+ contextName: flags.context,
736
+ data: records,
737
+ headers,
738
+ params,
739
+ });
740
+ this.printImportRun(response.data, flags);
741
+ }
742
+ catch (error) {
743
+ if (error instanceof ApiError) {
744
+ this.error(`API Error (${error.status}): ${error.message}`);
745
+ }
746
+ throw error;
747
+ }
748
+ }
749
+ /** Renders an import run: its counts, then whatever the server refused. */
750
+ printImportRun(run, flags) {
751
+ if (flags.output === 'json') {
752
+ this.log(JSON.stringify(run, null, 2));
753
+ return;
754
+ }
755
+ const record = (run ?? {});
756
+ const count = (key) => (typeof record[key] === 'number' ? record[key] : 0);
757
+ const id = record.id ? String(record.id) : '';
758
+ this.log(`Import ${id} - ${String(record.status ?? 'UNKNOWN')}${record.async ? ' (background)' : ''}`);
759
+ this.log(` ${count('inputSize')} submitted, ${count('createdCount')} created, ${count('updatedCount')} updated, ${count('unchangedCount')} unchanged`);
760
+ if (count('errorCount') > 0)
761
+ this.log(` ${count('errorCount')} not applied: ${count('refusedCount')} refused, ${count('failedCount')} failed`);
762
+ if (count('warningCount') > 0)
763
+ this.log(` ${count('warningCount')} applied with a warning`);
764
+ // errorLog is keyed by the record's identifier, then by field name.
765
+ const errorLog = record.errorLog;
766
+ if (errorLog && Object.keys(errorLog).length > 0) {
767
+ this.log('');
768
+ for (const [key, fields] of Object.entries(errorLog)) {
769
+ for (const [field, messages] of Object.entries(fields ?? {})) {
770
+ for (const message of messages ?? [])
771
+ this.log(` ${key} ${field}: ${message}`);
772
+ }
773
+ }
774
+ }
775
+ if (record.async && id) {
776
+ this.log('');
777
+ this.log(`Follow it with: aderis imports get ${id}`);
778
+ }
779
+ }
617
780
  async runExport(flags) {
618
781
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
619
782
  if (!context)
@@ -792,9 +955,9 @@ export default class BusinessPartners extends Command {
792
955
  throw error;
793
956
  }
794
957
  }
795
- async runNestedImport(suffix, parentId, flags) {
958
+ async runNestedUpsert(suffix, parentId, flags) {
796
959
  if (!parentId) {
797
- this.error(`Missing required argument: id. Usage: aderis business-partners import-${suffix} <parent-id> --file data.json`);
960
+ this.error(`Missing required argument: id. Usage: aderis business-partners upsert-${suffix} <parent-id> --file data.json`);
798
961
  }
799
962
  const context = flags.context ? getContext(flags.context) : getCurrentContext();
800
963
  if (!context)
@@ -859,12 +1022,12 @@ export default class BusinessPartners extends Command {
859
1022
  items = [item];
860
1023
  }
861
1024
  else {
862
- this.error(`Import requires --file, --data, or --set flags`);
1025
+ this.error('Upsert requires --file, --data, or --set flags');
863
1026
  }
864
1027
  const createEndpoint = ENDPOINTS[`create-${suffix}`];
865
1028
  const updateEndpoint = ENDPOINTS[`update-${suffix}`];
866
1029
  if (!createEndpoint || !updateEndpoint) {
867
- this.error(`Import requires both create-${suffix} and update-${suffix} endpoints`);
1030
+ this.error(`Upsert requires both create-${suffix} and update-${suffix} endpoints`);
868
1031
  }
869
1032
  // Build the base path with parent ID (first path param)
870
1033
  const firstParam = createEndpoint.pathParams[0];
@@ -1020,6 +1183,34 @@ export default class BusinessPartners extends Command {
1020
1183
  return `\$${op}`;
1021
1184
  return opRe.test(rest) ? `\$${op}:${this.parseFilterValue(rest)}` : `\$${op}:${rest}`;
1022
1185
  }
1186
+ /** The page line under a table: which page, how many, and any sums the API returned. */
1187
+ printPageSummary(envelope) {
1188
+ const meta = envelope.meta;
1189
+ if (meta && typeof meta.currentPage === 'number' && typeof meta.totalPages === 'number') {
1190
+ const shown = Array.isArray(envelope.data) ? envelope.data.length : 0;
1191
+ this.log(`
1192
+ Page ${meta.currentPage}/${meta.totalPages} — ${shown} of ${meta.totalItems ?? '?'} records`);
1193
+ }
1194
+ for (const total of envelope.totals ?? []) {
1195
+ const { currency, ...sums } = total;
1196
+ const parts = Object.entries(sums).map(([column, value]) => `${column} ${value}`).join(', ');
1197
+ this.log(`Total ${currency ?? ''}: ${parts}`.trim());
1198
+ }
1199
+ }
1200
+ /**
1201
+ * The API caps how many records a page may hold. Asking for more and getting
1202
+ * fewer looks like "that is everything" unless it is said out loud.
1203
+ */
1204
+ warnShortPage(envelope, requestedLimit) {
1205
+ const asked = requestedLimit === undefined ? undefined : Number(requestedLimit);
1206
+ const perPage = envelope.meta?.itemsPerPage;
1207
+ if (asked === undefined || Number.isNaN(asked) || typeof perPage !== 'number' || perPage >= asked)
1208
+ return;
1209
+ const pages = envelope.meta?.totalPages;
1210
+ const more = typeof pages === 'number' && pages > 1 ? ` Use --page, or the export action for every page.` : '';
1211
+ process.stderr.write(`! Asked for ${asked} records per page; the API returned at most ${perPage}.${more}
1212
+ `);
1213
+ }
1023
1214
  sanitizeOutput(data) {
1024
1215
  const auditFields = ['_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy'];
1025
1216
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -23,14 +23,31 @@ export default class Chat extends Command {
23
23
  query: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
24
24
  fields: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
25
25
  sanitize: import("@oclif/core/interfaces").BooleanFlag<boolean>;
26
+ meta: import("@oclif/core/interfaces").BooleanFlag<boolean>;
26
27
  'show-http-request': import("@oclif/core/interfaces").BooleanFlag<boolean>;
27
28
  };
28
29
  run(): Promise<void>;
29
- private runImport;
30
+ private runUpsert;
31
+ /**
32
+ * The API's own import: the whole array goes up in one request and comes
33
+ * back as an import run, which records what happened to every record. A bad
34
+ * record lands in errorLog and the rest are still applied, and the run stays
35
+ * readable afterwards with: aderis imports get <id>.
36
+ */
37
+ private runServerImport;
38
+ /** Renders an import run: its counts, then whatever the server refused. */
39
+ private printImportRun;
30
40
  private runExport;
31
41
  private runCamt053Import;
32
- private runNestedImport;
42
+ private runNestedUpsert;
33
43
  private parseFilterValue;
44
+ /** The page line under a table: which page, how many, and any sums the API returned. */
45
+ private printPageSummary;
46
+ /**
47
+ * The API caps how many records a page may hold. Asking for more and getting
48
+ * fewer looks like "that is everything" unless it is said out loud.
49
+ */
50
+ private warnShortPage;
34
51
  private sanitizeOutput;
35
52
  private getColumns;
36
53
  private getValue;