@aderis-erp/cli 1.0.10 → 1.0.12
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.
- package/README.md +1 -0
- package/dist/commands/access-groups.js +178 -86
- package/dist/commands/account-transaction-allocations.js +178 -86
- package/dist/commands/account-transactions.js +181 -87
- package/dist/commands/accounts.js +178 -86
- package/dist/commands/agent-runs.d.ts +1 -0
- package/dist/commands/agent-runs.js +183 -87
- package/dist/commands/audit-log.js +178 -86
- package/dist/commands/auth/login.js +4 -3
- package/dist/commands/bank-accounts.js +178 -86
- package/dist/commands/bank-integrations.js +178 -86
- package/dist/commands/business-partners.js +178 -86
- package/dist/commands/chat.d.ts +40 -0
- package/dist/commands/chat.js +1173 -0
- package/dist/commands/config/add.js +4 -3
- package/dist/commands/credit-card-accounts.js +178 -86
- package/dist/commands/documents.js +178 -86
- package/dist/commands/dunning-configs.js +180 -88
- package/dist/commands/dunning-reminders.js +178 -86
- package/dist/commands/dunning-runs.js +178 -86
- package/dist/commands/email-transactions.d.ts +40 -0
- package/dist/commands/email-transactions.js +1173 -0
- package/dist/commands/employees.js +180 -88
- package/dist/commands/entity-configurations.d.ts +40 -0
- package/dist/commands/entity-configurations.js +1169 -0
- package/dist/commands/event-processor-mappings.js +178 -86
- package/dist/commands/files-internal.js +178 -86
- package/dist/commands/health.js +178 -86
- package/dist/commands/inbox.js +181 -89
- package/dist/commands/integrations.js +178 -86
- package/dist/commands/item-categories.js +178 -86
- package/dist/commands/items.js +178 -86
- package/dist/commands/journal-lines.js +178 -86
- package/dist/commands/journals.js +178 -86
- package/dist/commands/ledgers.js +178 -86
- package/dist/commands/office-locations.js +178 -86
- package/dist/commands/payment-methods.js +178 -86
- package/dist/commands/payment-runs.js +178 -86
- package/dist/commands/payment-terms.js +178 -86
- package/dist/commands/price-lists.js +178 -86
- package/dist/commands/processor-mappings.js +178 -86
- package/dist/commands/processors.js +181 -87
- package/dist/commands/purchase-order-types.d.ts +40 -0
- package/dist/commands/purchase-order-types.js +1178 -0
- package/dist/commands/purchase-orders.d.ts +42 -0
- package/dist/commands/purchase-orders.js +1192 -0
- package/dist/commands/reports.js +178 -86
- package/dist/commands/salary-types.js +178 -86
- package/dist/commands/sales-invoices.js +187 -93
- package/dist/commands/sales-orders.js +186 -93
- package/dist/commands/saved-views.js +178 -86
- package/dist/commands/tags.js +178 -86
- package/dist/commands/tax-codes.js +180 -88
- package/dist/commands/tax-matrix.js +178 -86
- package/dist/commands/tenants.js +180 -88
- package/dist/commands/text-templates.js +178 -86
- package/dist/commands/tools.js +178 -86
- package/dist/commands/vat-declarations.d.ts +1 -0
- package/dist/commands/vat-declarations.js +188 -89
- package/dist/commands/vendor-invoices.js +181 -87
- package/dist/commands/webhooks.js +178 -86
- package/dist/commands/workflows.js +178 -86
- package/dist/lib/auth.js +2 -2
- package/dist/lib/config.d.ts +1 -1
- package/dist/lib/config.js +5 -0
- package/oclif.manifest.json +1174 -328
- package/package.json +2 -1
|
@@ -6,26 +6,28 @@ const HAS_IMPORT = true;
|
|
|
6
6
|
const HAS_EXPORT = true;
|
|
7
7
|
const HAS_CAMT053 = false;
|
|
8
8
|
const NESTED_IMPORTS = ["line-items", "notes", "files"];
|
|
9
|
-
const SUBCOMMANDS = [...["list", "create", "create-migrated-invoice", "pdf", "get", "update", "delete", "approve", "validate", "request-approval", "send", "send-resend", "cancel", "copy", "replace", "create-line-items", "update-line-items-reorder", "update-line-items", "delete-line-items", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-files", "create-files", "get-files", "update-files", "delete-files", "files-download"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
|
|
9
|
+
const SUBCOMMANDS = [...["list", "create", "create-migrated-invoice", "pdf", "get", "update", "delete", "approve", "match-sales-orders", "validate", "request-approval", "send", "send-resend", "cancel", "copy", "replace", "create-line-items", "line-items-from-sales-order", "update-line-items-reorder", "update-line-items", "delete-line-items", "list-notes", "create-notes", "get-notes", "update-notes", "delete-notes", "list-files", "create-files", "get-files", "update-files", "delete-files", "files-download"], ...(HAS_IMPORT ? ['import'] : []), ...(HAS_EXPORT ? ['export'] : []), ...NESTED_IMPORTS.map(n => `import-${n}`)];
|
|
10
10
|
const ENDPOINTS = {
|
|
11
11
|
'list': { method: 'GET', path: '/v1/sales-invoices', summary: 'Get all sales invoices with pagination', pathParams: [], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
12
|
-
'create': { method: 'POST', path: '/v1/sales-invoices', summary: 'Create a new sales invoice', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['lineItems', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'invoiceEmailCc'] },
|
|
12
|
+
'create': { method: 'POST', path: '/v1/sales-invoices', summary: 'Create a new sales invoice', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['lineItems', 'discounts', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'invoiceEmailCc'] },
|
|
13
13
|
'create-migrated-invoice': { method: 'POST', path: '/v1/sales-invoices/create-migrated-invoice', summary: 'Create a sales invoice with explicit tax codes on line items (for data migration)', pathParams: [], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['lineItems', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'invoiceEmailCc'] },
|
|
14
14
|
'pdf': { method: 'GET', path: '/v1/sales-invoices/{id}/pdf', summary: 'Generate a PDF preview of a sales invoice', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
15
15
|
'get': { method: 'GET', path: '/v1/sales-invoices/{id}', summary: 'Get a sales invoice by ID', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
16
|
-
'update': { method: 'PATCH', path: '/v1/sales-invoices/{id}', summary: 'Update a sales invoice', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['lineItems', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'invoiceEmailCc', 'removeFileIds'] },
|
|
16
|
+
'update': { method: 'PATCH', path: '/v1/sales-invoices/{id}', summary: 'Update a sales invoice', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['lineItems', 'discounts', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'invoiceEmailCc', 'removeFileIds'] },
|
|
17
17
|
'delete': { method: 'DELETE', path: '/v1/sales-invoices/{id}', summary: 'Delete a sales invoice', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
18
|
-
'approve': { method: 'POST', path: '/v1/sales-invoices/{id}/approve', summary: 'Approve a sales invoice from DRAFT or PENDING_APPROVAL status (generates invoice number and finalizes)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['lineItems', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'invoiceEmailCc', 'removeFileIds'] },
|
|
19
|
-
'
|
|
18
|
+
'approve': { method: 'POST', path: '/v1/sales-invoices/{id}/approve', summary: 'Approve a sales invoice from DRAFT or PENDING_APPROVAL status (generates invoice number and finalizes)', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['lineItems', 'discounts', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'invoiceEmailCc', 'removeFileIds'] },
|
|
19
|
+
'match-sales-orders': { method: 'POST', path: '/v1/sales-invoices/{id}/match-sales-orders', summary: 'Match this invoice\'s lines to sales-order lines', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
20
|
+
'validate': { method: 'POST', path: '/v1/sales-invoices/{id}/validate', summary: 'Validate a sales invoice update without persisting changes', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['lineItems', 'discounts', 'files', 'notes', 'totalAmountByTaxRate', 'tagIds', 'invoiceEmailCc', 'removeFileIds'] },
|
|
20
21
|
'request-approval': { method: 'POST', path: '/v1/sales-invoices/{id}/request-approval', summary: 'Request approval for sales invoice (DRAFT → PENDING_APPROVAL)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
21
22
|
'send': { method: 'POST', path: '/v1/sales-invoices/{id}/send', summary: 'Mark invoice as sent to customer (APPROVED → SENT)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
22
23
|
'send-resend': { method: 'POST', path: '/v1/sales-invoices/{id}/resend', summary: 'Resend a sales invoice to the original delivery recipient or an alternative one', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['emailCc'] },
|
|
23
24
|
'cancel': { method: 'POST', path: '/v1/sales-invoices/{id}/cancel', summary: 'Cancel a sales invoice', pathParams: ['id'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
24
25
|
'copy': { method: 'POST', path: '/v1/sales-invoices/{id}/copy', summary: 'Create a copy of a cancelled invoice', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
25
26
|
'replace': { method: 'POST', path: '/v1/sales-invoices/{id}/replace', summary: 'Replace invoice with corrected version (creates new CORRECTED_INVOICE)', pathParams: ['id'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
26
|
-
'create-line-items': { method: 'POST', path: '/v1/sales-invoices/{invoiceId}/line-items', summary: 'Add a line item to a sales invoice', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['salesOrderLineItems'] },
|
|
27
|
+
'create-line-items': { method: 'POST', path: '/v1/sales-invoices/{invoiceId}/line-items', summary: 'Add a line item to a sales invoice', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['discounts', 'salesOrderLineItems'] },
|
|
28
|
+
'line-items-from-sales-order': { method: 'POST', path: '/v1/sales-invoices/{invoiceId}/line-items/from-sales-order', summary: 'Add line items to a sales invoice from a sales order', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['lines'] },
|
|
27
29
|
'update-line-items-reorder': { method: 'PATCH', path: '/v1/sales-invoices/{invoiceId}/line-items/reorder', summary: 'Bulk reorder line items (drag-and-drop)', pathParams: ['invoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['items'] },
|
|
28
|
-
'update-line-items': { method: 'PATCH', path: '/v1/sales-invoices/{invoiceId}/line-items/{lineItemId}', summary: 'Update a line item on a sales invoice', pathParams: ['invoiceId', 'lineItemId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['salesOrderLineItems'] },
|
|
30
|
+
'update-line-items': { method: 'PATCH', path: '/v1/sales-invoices/{invoiceId}/line-items/{lineItemId}', summary: 'Update a line item on a sales invoice', pathParams: ['invoiceId', 'lineItemId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: ['discounts', 'salesOrderLineItems'] },
|
|
29
31
|
'delete-line-items': { method: 'DELETE', path: '/v1/sales-invoices/{invoiceId}/line-items/{lineItemId}', summary: 'Remove a line item from a sales invoice', pathParams: ['invoiceId', 'lineItemId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
30
32
|
'list-notes': { method: 'GET', path: '/v1/sales-invoices/{salesInvoiceId}/notes', summary: 'Get all notes for a sales invoice', pathParams: ['salesInvoiceId'], hasBody: false, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
31
33
|
'create-notes': { method: 'POST', path: '/v1/sales-invoices/{salesInvoiceId}/notes', summary: 'Create a note for a sales invoice', pathParams: ['salesInvoiceId'], hasBody: true, isMultipart: false, isBinaryResponse: false, bodyArrayFields: [] },
|
|
@@ -459,76 +461,126 @@ export default class SalesInvoices extends Command {
|
|
|
459
461
|
return '';
|
|
460
462
|
};
|
|
461
463
|
const results = [];
|
|
462
|
-
|
|
464
|
+
// Stream output row-by-row so progress is visible during long imports.
|
|
465
|
+
// Pre-compute the identifier column width from the input items so the
|
|
466
|
+
// table stays aligned without buffering all rows.
|
|
467
|
+
// When stdout is piped (IDE terminals, tee, CI), Node switches to async-buffered
|
|
468
|
+
// mode and rows accumulate in the pipe buffer until it fills. Force blocking
|
|
469
|
+
// mode so each \n-terminated write flushes immediately.
|
|
470
|
+
const stdoutHandle = process.stdout._handle;
|
|
471
|
+
if (stdoutHandle?.setBlocking)
|
|
472
|
+
stdoutHandle.setBlocking(true);
|
|
473
|
+
const outputFormat = flags.output || 'table';
|
|
474
|
+
const useCsv = outputFormat === 'csv' || Boolean(flags.csv);
|
|
475
|
+
const useJson = outputFormat === 'json';
|
|
476
|
+
const statusWidth = 8;
|
|
477
|
+
const identifierWidth = Math.max(10, ...items.map(i => getItemIdentifier(i).length));
|
|
478
|
+
if (useCsv) {
|
|
479
|
+
this.log('status,identifier,id,error');
|
|
480
|
+
}
|
|
481
|
+
else if (!useJson) {
|
|
482
|
+
this.log(`${'status'.padEnd(statusWidth)} ${'identifier'.padEnd(identifierWidth)} error`);
|
|
483
|
+
this.log(`${'-'.repeat(statusWidth)} ${'-'.repeat(identifierWidth)} ${'-'.repeat(50)}`);
|
|
484
|
+
}
|
|
485
|
+
const emitRow = (r) => {
|
|
486
|
+
if (useJson)
|
|
487
|
+
return;
|
|
488
|
+
if (useCsv) {
|
|
489
|
+
const err = (r.error || '').replace(/"/g, '""');
|
|
490
|
+
this.log(`${r.status},${r.identifier || ''},${r.id || ''},"${err}"`);
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
this.log(`${r.status.padEnd(statusWidth)} ${(r.identifier || '').padEnd(identifierWidth)} ${r.error || ''}`);
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
// Server-managed fields the API rejects in create/update bodies. Stripped
|
|
497
|
+
// before every request so re-importing a previously-exported payload Just Works.
|
|
498
|
+
const STRIP_FIELDS = new Set(['id', '_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy']);
|
|
499
|
+
const stripServerFields = (item) => {
|
|
500
|
+
const out = {};
|
|
501
|
+
for (const [k, v] of Object.entries(item)) {
|
|
502
|
+
if (!STRIP_FIELDS.has(k))
|
|
503
|
+
out[k] = v;
|
|
504
|
+
}
|
|
505
|
+
return out;
|
|
506
|
+
};
|
|
507
|
+
const processItem = async (item) => {
|
|
463
508
|
const identifier = getItemIdentifier(item);
|
|
509
|
+
const body = stripServerFields(item);
|
|
510
|
+
// If the payload carries an id (e.g. re-importing an export), PATCH directly.
|
|
511
|
+
// Fall through to POST only on 404 (so a missing record still gets created).
|
|
512
|
+
const itemId = typeof item.id === 'string' ? item.id : undefined;
|
|
513
|
+
if (itemId) {
|
|
514
|
+
try {
|
|
515
|
+
const updatePath = updateEndpoint.path.replace('{id}', itemId);
|
|
516
|
+
await apiRequest('PATCH', updatePath, {
|
|
517
|
+
data: body,
|
|
518
|
+
contextName: flags.context,
|
|
519
|
+
});
|
|
520
|
+
return { status: 'updated', id: itemId, identifier };
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
if (!(error instanceof ApiError && error.status === 404)) {
|
|
524
|
+
const msg = error instanceof ApiError ? `(${error.status}) ${error.message}` : String(error);
|
|
525
|
+
return { status: 'error', id: itemId, identifier, error: msg };
|
|
526
|
+
}
|
|
527
|
+
// 404 → record gone, try create below
|
|
528
|
+
}
|
|
529
|
+
}
|
|
464
530
|
try {
|
|
465
|
-
// Try to create
|
|
466
531
|
const response = await apiRequest('POST', createEndpoint.path, {
|
|
467
|
-
data:
|
|
532
|
+
data: body,
|
|
468
533
|
contextName: flags.context,
|
|
469
534
|
});
|
|
470
535
|
const id = response.data?.id;
|
|
471
|
-
|
|
536
|
+
return { status: 'created', id: id ? String(id) : undefined, identifier };
|
|
472
537
|
}
|
|
473
538
|
catch (error) {
|
|
474
539
|
if (error instanceof ApiError && error.status === 409) {
|
|
475
|
-
// Conflict - try to update existing
|
|
476
540
|
const errorData = error.data;
|
|
477
541
|
const existingId = errorData?.existingId;
|
|
478
542
|
if (existingId) {
|
|
479
543
|
try {
|
|
480
544
|
const updatePath = updateEndpoint.path.replace('{id}', existingId);
|
|
481
545
|
await apiRequest('PATCH', updatePath, {
|
|
482
|
-
data:
|
|
546
|
+
data: body,
|
|
483
547
|
contextName: flags.context,
|
|
484
548
|
});
|
|
485
|
-
|
|
549
|
+
return { status: 'updated', id: existingId, identifier };
|
|
486
550
|
}
|
|
487
551
|
catch (updateError) {
|
|
488
552
|
const msg = updateError instanceof ApiError ? updateError.message : String(updateError);
|
|
489
|
-
|
|
553
|
+
return { status: 'error', id: existingId, identifier, error: `Update failed: ${msg}` };
|
|
490
554
|
}
|
|
491
555
|
}
|
|
492
|
-
|
|
493
|
-
results.push({ status: 'error', identifier, error: 'Conflict but no existingId returned' });
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
else {
|
|
497
|
-
const msg = error instanceof ApiError ? `(${error.status}) ${error.message}` : String(error);
|
|
498
|
-
results.push({ status: 'error', identifier, error: msg });
|
|
556
|
+
return { status: 'error', identifier, error: 'Conflict but no existingId returned' };
|
|
499
557
|
}
|
|
558
|
+
const msg = error instanceof ApiError ? `(${error.status}) ${error.message}` : String(error);
|
|
559
|
+
return { status: 'error', identifier, error: msg };
|
|
500
560
|
}
|
|
501
|
-
}
|
|
561
|
+
};
|
|
562
|
+
// Worker pool: keep N requests in flight at all times. Rows are emitted as
|
|
563
|
+
// each request resolves (so output order may differ from input order). The
|
|
564
|
+
// summary at the end remains accurate regardless.
|
|
565
|
+
const concurrency = 4;
|
|
566
|
+
const queue = [...items];
|
|
567
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
|
568
|
+
for (;;) {
|
|
569
|
+
const item = queue.shift();
|
|
570
|
+
if (!item)
|
|
571
|
+
return;
|
|
572
|
+
const r = await processItem(item);
|
|
573
|
+
results.push(r);
|
|
574
|
+
emitRow(r);
|
|
575
|
+
}
|
|
576
|
+
}));
|
|
502
577
|
// Summary counts
|
|
503
578
|
const created = results.filter(r => r.status === 'created').length;
|
|
504
579
|
const updated = results.filter(r => r.status === 'updated').length;
|
|
505
580
|
const errors = results.filter(r => r.status === 'error');
|
|
506
|
-
|
|
507
|
-
const outputFormat = flags.output || 'table';
|
|
508
|
-
if (outputFormat === 'json') {
|
|
581
|
+
if (useJson) {
|
|
509
582
|
this.log(JSON.stringify(results, null, 2));
|
|
510
583
|
}
|
|
511
|
-
else if (outputFormat === 'csv' || flags.csv) {
|
|
512
|
-
// CSV output
|
|
513
|
-
this.log('status,identifier,id,error');
|
|
514
|
-
for (const r of results) {
|
|
515
|
-
const err = (r.error || '').replace(/"/g, '""');
|
|
516
|
-
this.log(`${r.status},${r.identifier || ''},${r.id || ''},"${err}"`);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
else {
|
|
520
|
-
// Table output (default)
|
|
521
|
-
if (results.length > 0) {
|
|
522
|
-
const statusWidth = 8;
|
|
523
|
-
const identifierWidth = Math.max(10, ...results.map(r => (r.identifier || '').length));
|
|
524
|
-
this.log(`${'status'.padEnd(statusWidth)} ${'identifier'.padEnd(identifierWidth)} error`);
|
|
525
|
-
this.log(`${'-'.repeat(statusWidth)} ${'-'.repeat(identifierWidth)} ${'-'.repeat(50)}`);
|
|
526
|
-
for (const r of results) {
|
|
527
|
-
const status = r.status === 'created' ? 'created' : r.status === 'updated' ? 'updated' : 'error';
|
|
528
|
-
this.log(`${status.padEnd(statusWidth)} ${(r.identifier || '').padEnd(identifierWidth)} ${r.error || ''}`);
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
584
|
// Always show summary at the end
|
|
533
585
|
this.log('');
|
|
534
586
|
this.log(`Summary: ${created} created, ${updated} updated, ${errors.length} failed`);
|
|
@@ -808,80 +860,122 @@ export default class SalesInvoices extends Command {
|
|
|
808
860
|
return '';
|
|
809
861
|
};
|
|
810
862
|
const results = [];
|
|
811
|
-
|
|
863
|
+
const stdoutHandle = process.stdout._handle;
|
|
864
|
+
if (stdoutHandle?.setBlocking)
|
|
865
|
+
stdoutHandle.setBlocking(true);
|
|
866
|
+
const outputFormat = flags.output || 'table';
|
|
867
|
+
const useCsv = outputFormat === 'csv' || Boolean(flags.csv);
|
|
868
|
+
const useJson = outputFormat === 'json';
|
|
869
|
+
const statusWidth = 8;
|
|
870
|
+
const identifierWidth = Math.max(10, ...items.map(i => getItemIdentifier(i).length));
|
|
871
|
+
if (useCsv) {
|
|
872
|
+
this.log('status,identifier,id,error');
|
|
873
|
+
}
|
|
874
|
+
else if (!useJson) {
|
|
875
|
+
this.log(`${'status'.padEnd(statusWidth)} ${'identifier'.padEnd(identifierWidth)} error`);
|
|
876
|
+
this.log(`${'-'.repeat(statusWidth)} ${'-'.repeat(identifierWidth)} ${'-'.repeat(50)}`);
|
|
877
|
+
}
|
|
878
|
+
const emitRow = (r) => {
|
|
879
|
+
if (useJson)
|
|
880
|
+
return;
|
|
881
|
+
if (useCsv) {
|
|
882
|
+
const err = (r.error || '').replace(/"/g, '""');
|
|
883
|
+
this.log(`${r.status},${r.identifier || ''},${r.id || ''},"${err}"`);
|
|
884
|
+
}
|
|
885
|
+
else {
|
|
886
|
+
this.log(`${r.status.padEnd(statusWidth)} ${(r.identifier || '').padEnd(identifierWidth)} ${r.error || ''}`);
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
const STRIP_FIELDS = new Set(['id', '_type', 'tenantId', 'createdAt', 'updatedAt', 'createdBy', 'updatedBy']);
|
|
890
|
+
const stripServerFields = (item) => {
|
|
891
|
+
const out = {};
|
|
892
|
+
for (const [k, v] of Object.entries(item)) {
|
|
893
|
+
if (!STRIP_FIELDS.has(k))
|
|
894
|
+
out[k] = v;
|
|
895
|
+
}
|
|
896
|
+
return out;
|
|
897
|
+
};
|
|
898
|
+
const buildUpdatePath = (existingId) => {
|
|
899
|
+
const firstUpdateParam = updateEndpoint.pathParams[0];
|
|
900
|
+
const nestedIdParam = updateEndpoint.pathParams.find((p, i) => i > 0);
|
|
901
|
+
let updatePath = updateEndpoint.path.replace(`{${firstUpdateParam}}`, parentId);
|
|
902
|
+
if (nestedIdParam) {
|
|
903
|
+
updatePath = updatePath.replace(`{${nestedIdParam}}`, existingId);
|
|
904
|
+
}
|
|
905
|
+
return updatePath;
|
|
906
|
+
};
|
|
907
|
+
const processItem = async (item) => {
|
|
812
908
|
const identifier = getItemIdentifier(item);
|
|
909
|
+
const body = stripServerFields(item);
|
|
910
|
+
const itemId = typeof item.id === 'string' ? item.id : undefined;
|
|
911
|
+
if (itemId) {
|
|
912
|
+
try {
|
|
913
|
+
await apiRequest('PATCH', buildUpdatePath(itemId), {
|
|
914
|
+
data: body,
|
|
915
|
+
contextName: flags.context,
|
|
916
|
+
});
|
|
917
|
+
return { status: 'updated', id: itemId, identifier };
|
|
918
|
+
}
|
|
919
|
+
catch (error) {
|
|
920
|
+
if (!(error instanceof ApiError && error.status === 404)) {
|
|
921
|
+
const msg = error instanceof ApiError ? `(${error.status}) ${error.message}` : String(error);
|
|
922
|
+
return { status: 'error', id: itemId, identifier, error: msg };
|
|
923
|
+
}
|
|
924
|
+
// 404 → fall through and try create
|
|
925
|
+
}
|
|
926
|
+
}
|
|
813
927
|
try {
|
|
814
|
-
// Try to create
|
|
815
928
|
const response = await apiRequest('POST', basePath, {
|
|
816
|
-
data:
|
|
929
|
+
data: body,
|
|
817
930
|
contextName: flags.context,
|
|
818
931
|
});
|
|
819
932
|
const id = response.data?.id;
|
|
820
|
-
|
|
933
|
+
return { status: 'created', id: id ? String(id) : undefined, identifier };
|
|
821
934
|
}
|
|
822
935
|
catch (error) {
|
|
823
936
|
if (error instanceof ApiError && error.status === 409) {
|
|
824
|
-
// Conflict - try to update existing
|
|
825
937
|
const errorData = error.data;
|
|
826
938
|
const existingId = errorData?.existingId || item.id;
|
|
827
939
|
if (existingId) {
|
|
828
940
|
try {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const nestedIdParam = updateEndpoint.pathParams.find((p, i) => i > 0);
|
|
832
|
-
let updatePath = updateEndpoint.path.replace(`{${firstUpdateParam}}`, parentId);
|
|
833
|
-
if (nestedIdParam) {
|
|
834
|
-
updatePath = updatePath.replace(`{${nestedIdParam}}`, existingId);
|
|
835
|
-
}
|
|
836
|
-
await apiRequest('PATCH', updatePath, {
|
|
837
|
-
data: item,
|
|
941
|
+
await apiRequest('PATCH', buildUpdatePath(existingId), {
|
|
942
|
+
data: body,
|
|
838
943
|
contextName: flags.context,
|
|
839
944
|
});
|
|
840
|
-
|
|
945
|
+
return { status: 'updated', id: existingId, identifier };
|
|
841
946
|
}
|
|
842
947
|
catch (updateError) {
|
|
843
948
|
const msg = updateError instanceof ApiError ? updateError.message : String(updateError);
|
|
844
|
-
|
|
949
|
+
return { status: 'error', id: existingId, identifier, error: `Update failed: ${msg}` };
|
|
845
950
|
}
|
|
846
951
|
}
|
|
847
|
-
|
|
848
|
-
results.push({ status: 'error', identifier, error: 'Conflict but no existingId returned' });
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
else {
|
|
852
|
-
const msg = error instanceof ApiError ? `(${error.status}) ${error.message}` : String(error);
|
|
853
|
-
results.push({ status: 'error', identifier, error: msg });
|
|
952
|
+
return { status: 'error', identifier, error: 'Conflict but no existingId returned' };
|
|
854
953
|
}
|
|
954
|
+
const msg = error instanceof ApiError ? `(${error.status}) ${error.message}` : String(error);
|
|
955
|
+
return { status: 'error', identifier, error: msg };
|
|
855
956
|
}
|
|
856
|
-
}
|
|
957
|
+
};
|
|
958
|
+
// Worker pool: 4 requests in flight at a time. Rows are emitted as each
|
|
959
|
+
// request resolves so output order may differ from input order.
|
|
960
|
+
const concurrency = 4;
|
|
961
|
+
const queue = [...items];
|
|
962
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
|
963
|
+
for (;;) {
|
|
964
|
+
const item = queue.shift();
|
|
965
|
+
if (!item)
|
|
966
|
+
return;
|
|
967
|
+
const r = await processItem(item);
|
|
968
|
+
results.push(r);
|
|
969
|
+
emitRow(r);
|
|
970
|
+
}
|
|
971
|
+
}));
|
|
857
972
|
// Summary counts
|
|
858
973
|
const created = results.filter(r => r.status === 'created').length;
|
|
859
974
|
const updated = results.filter(r => r.status === 'updated').length;
|
|
860
975
|
const errors = results.filter(r => r.status === 'error');
|
|
861
|
-
|
|
862
|
-
const outputFormat = flags.output || 'table';
|
|
863
|
-
if (outputFormat === 'json') {
|
|
976
|
+
if (useJson) {
|
|
864
977
|
this.log(JSON.stringify(results, null, 2));
|
|
865
978
|
}
|
|
866
|
-
else if (outputFormat === 'csv' || flags.csv) {
|
|
867
|
-
this.log('status,identifier,id,error');
|
|
868
|
-
for (const r of results) {
|
|
869
|
-
const err = (r.error || '').replace(/"/g, '""');
|
|
870
|
-
this.log(`${r.status},${r.identifier || ''},${r.id || ''},"${err}"`);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
else {
|
|
874
|
-
if (results.length > 0) {
|
|
875
|
-
const statusWidth = 8;
|
|
876
|
-
const identifierWidth = Math.max(10, ...results.map(r => (r.identifier || '').length));
|
|
877
|
-
this.log(`${'status'.padEnd(statusWidth)} ${'identifier'.padEnd(identifierWidth)} error`);
|
|
878
|
-
this.log(`${'-'.repeat(statusWidth)} ${'-'.repeat(identifierWidth)} ${'-'.repeat(50)}`);
|
|
879
|
-
for (const r of results) {
|
|
880
|
-
const status = r.status === 'created' ? 'created' : r.status === 'updated' ? 'updated' : 'error';
|
|
881
|
-
this.log(`${status.padEnd(statusWidth)} ${(r.identifier || '').padEnd(identifierWidth)} ${r.error || ''}`);
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
979
|
this.log('');
|
|
886
980
|
this.log(`Summary: ${created} created, ${updated} updated, ${errors.length} failed`);
|
|
887
981
|
}
|