@shushed/helpers 0.0.204 → 0.0.205-fix-erp-729-20251209153116

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.
@@ -62,7 +62,6 @@ class AirtableHelper extends runtime_1.default {
62
62
  fieldsToMergeOn: (options.fieldsToMergeOn ?? [this.primaryKeyFieldName]).map(x => this.dictionary[x] || x),
63
63
  },
64
64
  returnFieldsByFieldId: true,
65
- typecast: options.typecast || false,
66
65
  records: currentBatch.map(x => {
67
66
  const recordId = x.$recordId;
68
67
  const fieldsWithoutRecordId = { ...x };
@@ -400,18 +400,15 @@ function transformMasterOrder(payload) {
400
400
  const desc = String(line?.description || '').trim().toLowerCase();
401
401
  return desc === 'delivery';
402
402
  };
403
- let invoicesNetMinor = 0;
404
403
  let invoicesGrossMinor = 0;
405
404
  (payload.invoices || []).forEach((inv) => {
406
405
  (inv.invoice_lines || []).forEach((line) => {
407
406
  if (!isDeliveryItem(line)) {
408
- invoicesNetMinor += toMinorUnits(Number(line?.amount || line?.amount_excl_vat || 0));
409
407
  invoicesGrossMinor += toMinorUnits(Number(line?.amount_including_vat || line?.amount_incl_vat || 0));
410
408
  }
411
409
  });
412
410
  });
413
411
  if (invoicesGrossMinor === 0 && (payload.invoices || []).length > 0) {
414
- invoicesNetMinor = toMinorUnits((payload.invoices || []).reduce((acc, inv) => acc + (inv.amount_excl_vat || 0), 0));
415
412
  invoicesGrossMinor = toMinorUnits((payload.invoices || []).reduce((acc, inv) => acc + (inv.amount_incl_vat || 0), 0));
416
413
  }
417
414
  if (invoicesGrossMinor === 0 && invoicedLineNos.size > 0 && sale0?.sale_lines) {
@@ -423,10 +420,8 @@ function transformMasterOrder(payload) {
423
420
  const lineNo = Number(l?.line_no || -1);
424
421
  return invoicedLineNos.has(lineNo);
425
422
  });
426
- invoicesNetMinor = toMinorUnits(invoicedSaleLines.reduce((acc, l) => acc + (Number(l?.amount || 0)), 0));
427
423
  invoicesGrossMinor = toMinorUnits(invoicedSaleLines.reduce((acc, l) => acc + (Number(l?.amount_including_vat || 0)), 0));
428
424
  }
429
- let nonInvSalesNetMinor = 0;
430
425
  let nonInvSalesGrossMinor = 0;
431
426
  if (sale0?.sale_lines && sale0.sale_lines.length > 0) {
432
427
  const allSaleLines = (sale0.sale_lines || []).filter((l) => {
@@ -436,26 +431,20 @@ function transformMasterOrder(payload) {
436
431
  return false;
437
432
  return true;
438
433
  });
439
- const totalSaleNetMinor = toMinorUnits(allSaleLines.reduce((acc, l) => acc + (Number(l?.amount || 0)), 0));
440
434
  const totalSaleGrossMinor = toMinorUnits(allSaleLines.reduce((acc, l) => acc + (Number(l?.amount_including_vat || 0)), 0));
441
435
  if (invoicesGrossMinor > 0) {
442
- nonInvSalesNetMinor = Math.max(0, totalSaleNetMinor - invoicesNetMinor);
443
436
  nonInvSalesGrossMinor = Math.max(0, totalSaleGrossMinor - invoicesGrossMinor);
444
437
  }
445
438
  else {
446
- nonInvSalesNetMinor = totalSaleNetMinor;
447
439
  nonInvSalesGrossMinor = totalSaleGrossMinor;
448
440
  }
449
441
  }
450
442
  else if (sale0 && (sale0.amount_excl_vat || sale0.amount_incl_vat)) {
451
- const saleNetMinor = toMinorUnits(sale0.amount_excl_vat || 0);
452
443
  const saleGrossMinor = toMinorUnits(sale0.amount_incl_vat || 0);
453
444
  if (invoicesGrossMinor > 0) {
454
- nonInvSalesNetMinor = Math.max(0, saleNetMinor - invoicesNetMinor);
455
445
  nonInvSalesGrossMinor = Math.max(0, saleGrossMinor - invoicesGrossMinor);
456
446
  }
457
447
  else {
458
- nonInvSalesNetMinor = saleNetMinor;
459
448
  nonInvSalesGrossMinor = saleGrossMinor;
460
449
  }
461
450
  }
@@ -468,32 +457,30 @@ function transformMasterOrder(payload) {
468
457
  const lineNo = Number(l?.line_no || -1);
469
458
  return !invoicedLineNos.has(lineNo);
470
459
  });
471
- nonInvSalesNetMinor = toMinorUnits(nonInvoicedSales.reduce((acc, l) => acc + (Number(l?.amount || 0)), 0));
472
460
  nonInvSalesGrossMinor = toMinorUnits(nonInvoicedSales.reduce((acc, l) => acc + (Number(l?.amount_including_vat || 0)), 0));
473
461
  }
474
- const returnsNetMinor = toMinorUnits((payload.credit_memos || []).reduce((acc, cm) => acc + (cm.amount_excl_vat || 0), 0));
475
- const returnsGrossMinor = toMinorUnits((payload.credit_memos || []).reduce((acc, cm) => acc + (cm.amount_incl_vat || 0), 0));
476
- const totalNetMinor = invoicesNetMinor + nonInvSalesNetMinor;
477
462
  const totalGrossMinor = invoicesGrossMinor + nonInvSalesGrossMinor;
478
- const total = {
479
- amount_net: moneyFromMinor(totalNetMinor),
480
- amount_gross: moneyFromMinor(totalGrossMinor),
481
- discount_amount_net: money(0),
482
- discount_amount_gross: money(0),
483
- discount_amount_percent: 0,
484
- };
485
- const return_total = (payload.credit_memos && payload.credit_memos.length > 0)
486
- ? {
487
- amount_net: moneyFromMinor(returnsNetMinor),
488
- amount_gross: moneyFromMinor(returnsGrossMinor),
489
- discount_amount_net: money(0),
490
- discount_amount_gross: money(0),
491
- discount_amount_percent: 0,
463
+ const invoicesNetMinorWithDelivery = toMinorUnits((payload.invoices || []).reduce((acc, inv) => acc + (inv.amount_excl_vat || 0), 0));
464
+ const invoicesGrossMinorWithDelivery = toMinorUnits((payload.invoices || []).reduce((acc, inv) => acc + (inv.amount_incl_vat || 0), 0));
465
+ let nonInvSalesNetMinorWithDelivery = 0;
466
+ let nonInvSalesGrossMinorWithDelivery = 0;
467
+ if (sale0 && (sale0.amount_excl_vat || sale0.amount_incl_vat)) {
468
+ const saleNetMinor = toMinorUnits(sale0.amount_excl_vat || 0);
469
+ const saleGrossMinor = toMinorUnits(sale0.amount_incl_vat || 0);
470
+ if (invoicesGrossMinorWithDelivery > 0) {
471
+ nonInvSalesNetMinorWithDelivery = Math.max(0, saleNetMinor - invoicesNetMinorWithDelivery);
472
+ nonInvSalesGrossMinorWithDelivery = Math.max(0, saleGrossMinor - invoicesGrossMinorWithDelivery);
492
473
  }
493
- : undefined;
494
- const net_total = {
495
- amount_net: moneyFromMinor(Math.max(0, totalNetMinor - returnsNetMinor)),
496
- amount_gross: moneyFromMinor(Math.max(0, totalGrossMinor - returnsGrossMinor)),
474
+ else {
475
+ nonInvSalesNetMinorWithDelivery = saleNetMinor;
476
+ nonInvSalesGrossMinorWithDelivery = saleGrossMinor;
477
+ }
478
+ }
479
+ const totalNetMinorWithDelivery = invoicesNetMinorWithDelivery + nonInvSalesNetMinorWithDelivery;
480
+ const totalGrossMinorWithDelivery = invoicesGrossMinorWithDelivery + nonInvSalesGrossMinorWithDelivery;
481
+ const total = {
482
+ amount_net: moneyFromMinor(totalNetMinorWithDelivery),
483
+ amount_gross: moneyFromMinor(totalGrossMinorWithDelivery),
497
484
  discount_amount_net: money(0),
498
485
  discount_amount_gross: money(0),
499
486
  discount_amount_percent: 0,
@@ -603,7 +590,7 @@ function transformMasterOrder(payload) {
603
590
  const merchReturnGrossFromCreditMemos = (payload.credit_memos || []).reduce((acc, cm) => {
604
591
  const lines = cm?.credit_memo_lines || [];
605
592
  let hasQualifying = false;
606
- const sum = lines.reduce((s, l) => {
593
+ const sumGross = lines.reduce((s, l) => {
607
594
  const itemNo = String(l?.item_no || l?.no || "");
608
595
  const desc = String(l?.description || "");
609
596
  const looksProduct = itemNo.includes('-') || desc.includes(' - ');
@@ -616,24 +603,36 @@ function transformMasterOrder(payload) {
616
603
  }
617
604
  return s;
618
605
  }, 0);
606
+ const sumNet = lines.reduce((s, l) => {
607
+ const itemNo = String(l?.item_no || l?.no || "");
608
+ const desc = String(l?.description || "");
609
+ const looksProduct = itemNo.includes('-') || desc.includes(' - ');
610
+ const typeStr = String(l?.type || "").toLowerCase();
611
+ const isItemish = typeStr.includes('item') || looksProduct;
612
+ const isRefund = itemNo.toUpperCase() === 'REFUND';
613
+ if ((l?.quantity || 0) > 0 && isItemish && !isRefund && !isDeliveryItem(l)) {
614
+ return s + (l?.amount || l?.amount_excl_vat || 0);
615
+ }
616
+ return s;
617
+ }, 0);
619
618
  if (!hasQualifying)
620
619
  return acc;
621
620
  if (cm?.amount_incl_vat != null && cm.amount_incl_vat > 0) {
622
- return acc + cm.amount_incl_vat;
621
+ return { gross: acc.gross + cm.amount_incl_vat, net: acc.net + (cm.amount_excl_vat || 0) };
623
622
  }
624
- return acc + sum;
625
- }, 0);
623
+ return { gross: acc.gross + sumGross, net: acc.net + sumNet };
624
+ }, { gross: 0, net: 0 });
626
625
  const merchReturnGrossFromReturnSales = (payload.sales || []).reduce((acc, sale) => {
627
626
  const docTypeRaw = String(sale?.document_type || sale?.documentType || "");
628
627
  const isReturnOrder = docTypeRaw.toLowerCase().includes('return');
629
628
  if (!isReturnOrder)
630
629
  return acc;
631
630
  if (sale?.amount_incl_vat != null && sale.amount_incl_vat > 0) {
632
- return acc + sale.amount_incl_vat;
631
+ return { gross: acc.gross + sale.amount_incl_vat, net: acc.net + (sale.amount_excl_vat || sale.amount || 0) };
633
632
  }
634
633
  const lines = sale?.sale_lines || [];
635
634
  let hasQualifying = false;
636
- const sum = lines.reduce((s, l) => {
635
+ const sumGross = lines.reduce((s, l) => {
637
636
  const itemNo = String(l?.item_no || l?.no || "");
638
637
  const desc = String(l?.description || "");
639
638
  const looksProduct = itemNo.includes('-') || desc.includes(' - ');
@@ -646,10 +645,46 @@ function transformMasterOrder(payload) {
646
645
  }
647
646
  return s;
648
647
  }, 0);
649
- return acc + (sum > 0 ? sum : (hasQualifying ? (sale?.amount_incl_vat || sale?.amount_including_vat || 0) : 0));
650
- }, 0);
651
- const merchReturnGross = merchReturnGrossFromCreditMemos + merchReturnGrossFromReturnSales;
648
+ const sumNet = lines.reduce((s, l) => {
649
+ const itemNo = String(l?.item_no || l?.no || "");
650
+ const desc = String(l?.description || "");
651
+ const looksProduct = itemNo.includes('-') || desc.includes(' - ');
652
+ const typeStr = String(l?.type || "").toLowerCase();
653
+ const isItemish = typeStr.includes('item') || looksProduct;
654
+ const isRefund = itemNo.toUpperCase() === 'REFUND';
655
+ if ((l?.quantity || 0) > 0 && isItemish && !isRefund && !isDeliveryItem(l)) {
656
+ return s + (l?.amount || l?.amount_excl_vat || 0);
657
+ }
658
+ return s;
659
+ }, 0);
660
+ if (sumGross > 0) {
661
+ return { gross: acc.gross + sumGross, net: acc.net + sumNet };
662
+ }
663
+ if (hasQualifying) {
664
+ return { gross: acc.gross + (sale?.amount_incl_vat || sale?.amount_including_vat || 0), net: acc.net + (sale?.amount_excl_vat || sale?.amount || 0) };
665
+ }
666
+ return acc;
667
+ }, { gross: 0, net: 0 });
668
+ const merchReturnGross = merchReturnGrossFromCreditMemos.gross + merchReturnGrossFromReturnSales.gross;
652
669
  const hasMerchReturn = merchReturnGross > 0;
670
+ const returnsNetMinor = toMinorUnits((payload.credit_memos || []).reduce((acc, cm) => acc + (cm.amount_excl_vat || 0), 0));
671
+ const returnsGrossMinor = toMinorUnits((payload.credit_memos || []).reduce((acc, cm) => acc + (cm.amount_incl_vat || 0), 0));
672
+ const return_total = (returnsGrossMinor > 0)
673
+ ? {
674
+ amount_net: moneyFromMinor(returnsNetMinor),
675
+ amount_gross: moneyFromMinor(returnsGrossMinor),
676
+ discount_amount_net: money(0),
677
+ discount_amount_gross: money(0),
678
+ discount_amount_percent: 0,
679
+ }
680
+ : undefined;
681
+ const net_total = {
682
+ amount_net: moneyFromMinor(Math.max(0, totalNetMinorWithDelivery - returnsNetMinor)),
683
+ amount_gross: moneyFromMinor(Math.max(0, totalGrossMinorWithDelivery - returnsGrossMinor)),
684
+ discount_amount_net: money(0),
685
+ discount_amount_gross: money(0),
686
+ discount_amount_percent: 0,
687
+ };
653
688
  const totalOrderGrossMinor = totalGrossMinor;
654
689
  const sourceLineMap = new Map();
655
690
  sourceLines.forEach((line) => {
@@ -73,14 +73,6 @@ class CentraHelper extends env_1.default {
73
73
  errors.push(...userErrors.map((x) => ({ message: x.message, path: x.path })));
74
74
  return { errors, limitExceeded: response.status === 429 };
75
75
  }
76
- if (resp.data && typeof resp.data === 'object') {
77
- for (const key in resp.data) {
78
- if (resp.data[key] && typeof resp.data[key] === 'object' && 'userErrors' in resp.data[key] && Array.isArray(resp.data[key].userErrors) && resp.data[key].userErrors.length > 0) {
79
- errors.push(...resp.data[key].userErrors.map((x) => ({ message: x.message, path: x.path })));
80
- return { errors, limitExceeded: response.status === 429 };
81
- }
82
- }
83
- }
84
76
  }
85
77
  if (response.status === 429) {
86
78
  resp.limitExceeded = true;
@@ -331,7 +323,7 @@ class CentraHelper extends env_1.default {
331
323
  } while (hasNextPage);
332
324
  return result;
333
325
  }
334
- async fetchCentraMarkets() {
326
+ async fetchCentraMarkets(externalIds) {
335
327
  const result = [];
336
328
  const limit = 200;
337
329
  let page = 0;
@@ -341,15 +333,15 @@ class CentraHelper extends env_1.default {
341
333
  method: 'POST',
342
334
  body: JSON.stringify({
343
335
  query: `
344
- query GetMarkets($limit: Int, $page: Int) {
345
- markets(limit: $limit, page: $page) {
336
+ query GetMarkets($limit: Int, $page: Int${externalIds ? `, $externalIds: MarketsFilter` : ''}) {
337
+ markets(${externalIds ? `where: $externalIds, ` : ''}limit: $limit, page: $page) {
346
338
  id
347
339
  externalId
348
340
  name
349
341
  }
350
342
  }
351
343
  `,
352
- variables: { page: page, limit: limit }
344
+ variables: { page: page, limit: limit, externalIds: { externalId: externalIds } }
353
345
  })
354
346
  });
355
347
  if (CentraHelper.isCentraErrors(data)) {
@@ -479,7 +471,7 @@ class CentraHelper extends env_1.default {
479
471
  nextCursor = null;
480
472
  }
481
473
  if (data && data.data?.productConnection?.edges?.length) {
482
- for (let i = 0; i < data.data.productConnection.edges.length; i++) {
474
+ for (let i = data.data.productConnection.edges.length; i < styleIds.length; i++) {
483
475
  const { node } = data.data.productConnection.edges[i];
484
476
  result[node.externalId] = node;
485
477
  }
@@ -505,15 +497,16 @@ class CentraHelper extends env_1.default {
505
497
  edges {
506
498
  node {
507
499
  id
500
+ externalId
508
501
  status
509
- uri
502
+ url
510
503
  metaTitle
511
504
  metaDescription
512
505
  name
513
506
  categories {
514
507
  id
515
508
  name
516
- uri
509
+ url
517
510
  isTopCategory
518
511
  }
519
512
  markets {
@@ -525,7 +518,7 @@ class CentraHelper extends env_1.default {
525
518
  externalId
526
519
  status
527
520
  }
528
- productVariants {
521
+ productVariant {
529
522
  id
530
523
  externalId
531
524
  status
@@ -572,8 +565,8 @@ class CentraHelper extends env_1.default {
572
565
  for (let i = 0; i < edges.length; i++) {
573
566
  const { node } = edges[i];
574
567
  if (inputIdType === 'variant') {
575
- result[+node.productVariants[0].id] = [];
576
- result[+node.productVariants[0].id].push(node);
568
+ result[+node.productVariant[0].id] = [];
569
+ result[+node.productVariant[0].id].push(node);
577
570
  }
578
571
  else {
579
572
  result[+node.product.id] = [];
@@ -797,45 +790,59 @@ class CentraHelper extends env_1.default {
797
790
  }
798
791
  return Object.assign({}, pricelistInCache, pricelistToSet);
799
792
  }
800
- async getCentraMarkets(alwaysFetch = false) {
793
+ async getCentraMarkets(externalIds, alwaysFetch = false) {
794
+ if (externalIds && !externalIds.length) {
795
+ return {};
796
+ }
801
797
  let marketInCache = {};
802
- let dedupedMarketNamesInCache = [];
798
+ let dedupedExternalIds = [];
803
799
  let marketsToFetch = null;
804
800
  if (!alwaysFetch) {
805
- const marketNamesInCache = (await this.get(this.getCacheKeyForMarkets(), 'env', {
806
- isEphemeral: true,
807
- encrypted: false,
808
- }).then(x => x ? JSON.parse(x) : null));
809
- if (marketNamesInCache) {
810
- dedupedMarketNamesInCache = marketNamesInCache.filter((x, index, self) => self.indexOf(x) === index);
811
- marketInCache = Object.fromEntries(Object.entries(await this.get(dedupedMarketNamesInCache.map(x => this.getCacheKeyForMarket(x)), 'env', {
801
+ if (!externalIds) {
802
+ externalIds = (await this.get(this.getCacheKeyForMarkets(), 'env', {
803
+ isEphemeral: true,
804
+ encrypted: false,
805
+ }).then(x => x ? JSON.parse(x) : null));
806
+ }
807
+ if (externalIds) {
808
+ dedupedExternalIds = externalIds.filter((x, index, self) => self.indexOf(x) === index);
809
+ marketInCache = Object.fromEntries(Object.entries(await this.get(dedupedExternalIds.map(x => this.getCacheKeyForMarket(x)), 'env', {
812
810
  isEphemeral: true,
813
811
  encrypted: false,
814
812
  })).map(([key, value]) => [key, value ? JSON.parse(value || 'null') : undefined]).filter(([_, value]) => value));
815
- marketsToFetch = dedupedMarketNamesInCache.filter(x => !marketInCache[x]);
813
+ marketsToFetch = dedupedExternalIds.filter(x => !marketInCache[x]);
816
814
  }
817
815
  }
818
816
  const marketToSet = {};
819
817
  if (!marketsToFetch || marketsToFetch.length) {
820
- const markets = await this.fetchCentraMarkets();
818
+ const markets = await this.fetchCentraMarkets(marketsToFetch);
821
819
  if (CentraHelper.isCentraErrors(markets)) {
822
- return new Error(`Failed to fetch markets: ${markets.errors.map((x) => x.message).join(', ')}`);
820
+ if (marketsToFetch) {
821
+ for (const marketExternalId of marketsToFetch) {
822
+ marketToSet[marketExternalId] = new Error(`Failed to fetch market ${marketExternalId}: ${markets.errors.map((x) => x.message).join(', ')}`);
823
+ }
824
+ }
825
+ else {
826
+ return new Error(`Failed to fetch markets: ${markets.errors.map((x) => x.message).join(', ')}`);
827
+ }
823
828
  }
824
829
  else {
825
830
  for (const market of markets) {
826
- marketToSet[market.name] = market;
831
+ marketToSet[market.externalId] = market;
827
832
  }
828
833
  await this.set(Object.entries(marketToSet).filter(([_, value]) => !(value instanceof Error)).map(([key, value]) => ({ name: this.getCacheKeyForMarket(key), value: JSON.stringify(value) })), 'env', {
829
834
  ephemeralMs: CACHE_EXPIRATION_MS,
830
835
  encrypted: false,
831
836
  });
832
- await this.set([{
833
- name: this.getCacheKeyForMarkets(),
834
- value: JSON.stringify(Object.keys(marketToSet)),
835
- }], 'env', {
836
- ephemeralMs: CACHE_EXPIRATION_MS,
837
- encrypted: false,
838
- });
837
+ if (!marketsToFetch) {
838
+ await this.set([{
839
+ name: this.getCacheKeyForMarkets(),
840
+ value: JSON.stringify(Object.keys(marketToSet)),
841
+ }], 'env', {
842
+ ephemeralMs: CACHE_EXPIRATION_MS,
843
+ encrypted: false,
844
+ });
845
+ }
839
846
  }
840
847
  }
841
848
  return Object.assign({}, marketInCache, marketToSet);
@@ -72,6 +72,11 @@ class DatoHelper extends runtime_1.default {
72
72
  return response;
73
73
  }
74
74
  async createAssetFromUpload(uploadId, opts, uploadCollectionId) {
75
+ const normalizedTags = Array.isArray(opts.tags) && opts.tags.length > 0
76
+ ? opts.tags
77
+ : ["integrations"];
78
+ const safeTitle = opts.title && opts.title.trim().length > 0 ? opts.title : "untitled";
79
+ const safeAlt = opts.alt && opts.alt.trim().length > 0 ? opts.alt : safeTitle;
75
80
  const body = {
76
81
  data: {
77
82
  type: 'upload',
@@ -80,13 +85,35 @@ class DatoHelper extends runtime_1.default {
80
85
  author: opts.author,
81
86
  copyright: opts.copyright,
82
87
  notes: opts.notes || null,
88
+ title: safeTitle,
89
+ alt: safeAlt,
83
90
  default_field_metadata: {
91
+ "en-GB": {
92
+ title: safeTitle,
93
+ alt: safeAlt,
94
+ custom_data: {
95
+ source: normalizedTags[0] || "uploaded-by-integrations",
96
+ tags: normalizedTags,
97
+ }
98
+ },
84
99
  en: {
85
- title: opts.title,
86
- alt: opts.alt,
100
+ title: safeTitle,
101
+ alt: safeAlt,
102
+ custom_data: {
103
+ source: normalizedTags[0] || "uploaded-by-integrations",
104
+ tags: normalizedTags,
105
+ }
106
+ },
107
+ "en-US": {
108
+ title: safeTitle,
109
+ alt: safeAlt,
110
+ custom_data: {
111
+ source: normalizedTags[0] || "uploaded-by-integrations",
112
+ tags: normalizedTags,
113
+ }
87
114
  }
88
115
  },
89
- tags: opts.tags || [],
116
+ tags: normalizedTags || [],
90
117
  },
91
118
  },
92
119
  };
@@ -155,8 +182,119 @@ class DatoHelper extends runtime_1.default {
155
182
  this.logging.error(timeoutMsg);
156
183
  throw new Error(timeoutMsg);
157
184
  }
185
+ updateUpload = async (options) => {
186
+ try {
187
+ this.logging.log(`Starting update upload - ${JSON.stringify(options)}`);
188
+ if (!options.id)
189
+ throw new Error("updateUpload requires 'id' in options");
190
+ const safeTitle = options.title && options.title.trim().length > 0
191
+ ? options.title.trim()
192
+ : options.filename?.trim().split(".")[0] || "untitled";
193
+ const safeAlt = options.alt && options.alt.trim().length > 0
194
+ ? options.alt.trim()
195
+ : safeTitle;
196
+ const normalizedTags = Array.isArray(options.tags) && options.tags.length > 0
197
+ ? options.tags
198
+ : ["integrations"];
199
+ const body = {
200
+ data: {
201
+ type: "upload",
202
+ id: options.id,
203
+ attributes: {
204
+ author: options.author,
205
+ copyright: options.copyright,
206
+ tags: normalizedTags,
207
+ notes: options.notes || null,
208
+ default_field_metadata: {
209
+ "en-GB": {
210
+ title: safeTitle,
211
+ alt: safeAlt,
212
+ custom_data: {
213
+ source: normalizedTags[0] || "uploaded-by-integrations",
214
+ tags: normalizedTags,
215
+ },
216
+ },
217
+ en: {
218
+ title: safeTitle,
219
+ alt: safeAlt,
220
+ custom_data: {
221
+ source: normalizedTags[0] || "uploaded-by-integrations",
222
+ tags: normalizedTags,
223
+ },
224
+ },
225
+ "en-US": {
226
+ title: safeTitle,
227
+ alt: safeAlt,
228
+ custom_data: {
229
+ source: normalizedTags[0] || "uploaded-by-integrations",
230
+ tags: normalizedTags,
231
+ },
232
+ },
233
+ },
234
+ },
235
+ },
236
+ };
237
+ if (options.uploadCollectionId) {
238
+ body.data.relationships = {
239
+ upload_collection: {
240
+ data: {
241
+ type: "upload_collection",
242
+ id: options.uploadCollectionId,
243
+ },
244
+ },
245
+ };
246
+ }
247
+ const response = await fetch(`${this.baseUrl}/uploads/${options.id}`, {
248
+ method: "PATCH",
249
+ headers: {
250
+ Authorization: `Bearer ${this.apiToken}`,
251
+ "X-Api-Version": "3",
252
+ "X-Environment": this.environment,
253
+ "Content-Type": "application/vnd.api+json",
254
+ Accept: "application/json",
255
+ },
256
+ body: JSON.stringify(body),
257
+ });
258
+ if (!response.ok) {
259
+ const errorText = await response.text();
260
+ this.logging.error(`Failed to update upload: ${response.statusText} - ${errorText}`);
261
+ throw new Error(`Failed to update upload: ${response.statusText} - ${errorText}`);
262
+ }
263
+ const updated = await response.json();
264
+ if (Array.isArray(updated.data) && updated.data[0]?.type === "api_error") {
265
+ const errorCode = updated.data[0].attributes?.code || "UNKNOWN_ERROR";
266
+ const errorMsg = `DatoCMS API error: ${errorCode}`;
267
+ this.logging.error(errorMsg);
268
+ throw new Error(errorMsg);
269
+ }
270
+ if (updated.data?.type === "job") {
271
+ this.logging.log(`Waiting for job completion: ${updated.data.id}`);
272
+ const jobPayload = await this.waitForJobCompletion(updated.data.id);
273
+ const assetId = jobPayload?.data?.id;
274
+ const assetUrl = jobPayload?.data?.attributes?.url;
275
+ if (!assetId || !assetUrl) {
276
+ throw new Error(`Job completed but payload missing expected fields: ${JSON.stringify(jobPayload)}`);
277
+ }
278
+ this.logging.log(`Job completed for upload: ${assetId}`);
279
+ return { success: true, assetId, upload: assetUrl };
280
+ }
281
+ if (updated.data?.type === "upload") {
282
+ const { id, attributes } = updated.data;
283
+ this.logging.log(`Upload updated successfully: ${id}`);
284
+ return { success: true, assetId: id, upload: attributes.url };
285
+ }
286
+ const errMsg = `Unexpected update response: ${JSON.stringify(updated)}`;
287
+ this.logging.error(errMsg);
288
+ throw new Error(errMsg);
289
+ }
290
+ catch (err) {
291
+ const msg = err?.message ?? JSON.stringify(err);
292
+ this.logging.error(`[updateUpload] failed: ${msg}`);
293
+ throw new Error(msg);
294
+ }
295
+ };
158
296
  async uploadFromUrl(options) {
159
- const { url, filename, uploadCollectionId } = options;
297
+ const { url, filename, uploadCollectionId, skipCreationIfAlreadyExists } = options;
160
298
  this.logging.log(`Starting upload from URL: ${url}`);
161
299
  const fileResponse = await fetch(url);
162
300
  if (!fileResponse.ok) {
@@ -164,9 +302,43 @@ class DatoHelper extends runtime_1.default {
164
302
  this.logging.error(errorMsg);
165
303
  throw new Error(errorMsg);
166
304
  }
167
- let finalFilename = filename || path_1.default.basename(new URL(url).pathname) || 'bs-' + this.triggerId + '-' + new Date().getTime();
168
- if (!path_1.default.extname(finalFilename) && fileResponse.headers.has('content-type')) {
169
- finalFilename += `.${(0, mime_types_1.extension)(fileResponse.headers.get('content-type'))}`;
305
+ let finalFilename = filename || path_1.default.basename(new URL(url).pathname) ||
306
+ `bs-${this.triggerId}-${Date.now()}`;
307
+ if (filename && !path_1.default.extname(finalFilename)) {
308
+ const urlObj = new URL(url);
309
+ const pathname = urlObj.pathname;
310
+ if (pathname.includes(".")) {
311
+ const urlExtension = path_1.default.extname(pathname);
312
+ if (urlExtension) {
313
+ finalFilename += urlExtension;
314
+ }
315
+ }
316
+ if (!path_1.default.extname(finalFilename) && fileResponse.headers.has('content-type')) {
317
+ const ext = (0, mime_types_1.extension)(fileResponse.headers.get('content-type'));
318
+ if (typeof ext === "string" && ext.length > 0) {
319
+ finalFilename += `.${ext}`;
320
+ }
321
+ }
322
+ }
323
+ else if (!filename) {
324
+ if (!path_1.default.extname(finalFilename) && fileResponse.headers.has('content-type')) {
325
+ const ext = (0, mime_types_1.extension)(fileResponse.headers.get('content-type'));
326
+ if (typeof ext === "string" && ext.length > 0) {
327
+ finalFilename += `.${ext}`;
328
+ }
329
+ }
330
+ }
331
+ if (skipCreationIfAlreadyExists) {
332
+ const existing = await this.findUploadByFilename(finalFilename);
333
+ if (existing) {
334
+ this.logging.log(`Skipping upload: existing asset found for "${finalFilename}"`);
335
+ return {
336
+ success: true,
337
+ assetId: existing.id,
338
+ upload: existing.attributes.url,
339
+ skipped: true,
340
+ };
341
+ }
170
342
  }
171
343
  const uploadRequest = await this.requestUploadParameters(finalFilename);
172
344
  const uploadId = uploadRequest.data.id;
@@ -183,6 +355,7 @@ class DatoHelper extends runtime_1.default {
183
355
  success: true,
184
356
  upload: assetPayload.data.attributes.url,
185
357
  assetId: assetPayload.data.id,
358
+ skipped: false,
186
359
  };
187
360
  }
188
361
  async uploadFromFile(options) {
@@ -264,7 +437,14 @@ class DatoHelper extends runtime_1.default {
264
437
  }
265
438
  async findUploadByFilename(filename) {
266
439
  try {
267
- const response = await fetch(`${this.baseUrl}/uploads?filter[filename]=${encodeURIComponent(filename)}`, {
440
+ if (!filename || typeof filename !== 'string') {
441
+ this.logging.log(`findUploadByFilename called with invalid filename: ${String(filename)}`);
442
+ return null;
443
+ }
444
+ const normalizedTarget = filename.trim().toLowerCase().replace(/\.[^.]+$/, '');
445
+ const query = encodeURIComponent(normalizedTarget);
446
+ this.logging.log(`Searching uploads with query="${normalizedTarget}" for filename="${filename}"`);
447
+ const response = await fetch(`${this.baseUrl}/uploads?filter[query]=${query}&page[limit]=100`, {
268
448
  headers: {
269
449
  'Authorization': `Bearer ${this.apiToken}`,
270
450
  'X-Api-Version': '3',
@@ -281,10 +461,32 @@ class DatoHelper extends runtime_1.default {
281
461
  this.logging.error(`Failed to parse JSON in findUploadByFilename: ${err.message}`);
282
462
  return null;
283
463
  });
284
- return data?.data?.[0] || null;
464
+ const uploads = data?.data || [];
465
+ if (!uploads.length) {
466
+ this.logging.log(`No uploads returned for query="${normalizedTarget}"`);
467
+ return null;
468
+ }
469
+ const targetFull = filename.trim().toLowerCase();
470
+ const match = uploads.find((u) => {
471
+ const storedFull = u?.attributes?.filename?.toLowerCase();
472
+ if (!storedFull)
473
+ return false;
474
+ if (storedFull === targetFull)
475
+ return true;
476
+ const storedNormalized = storedFull.replace(/\.[^.]+$/, '');
477
+ if (storedNormalized === normalizedTarget)
478
+ return true;
479
+ return false;
480
+ });
481
+ if (match) {
482
+ this.logging.log(`Found existing upload for "${filename}" -> asset_id=${match.id}, storedFilename=${match.attributes?.filename}`);
483
+ return match;
484
+ }
485
+ this.logging.log(`No exact filename match found among ${uploads.length} uploads for "${filename}" (query="${normalizedTarget}")`);
486
+ return null;
285
487
  }
286
488
  catch (err) {
287
- this.logging.error(`Error in findUploadByFilename: ${err.message}`);
489
+ this.logging.error(`Error in findUploadByFilename: ${err?.message ?? String(err)}`);
288
490
  return null;
289
491
  }
290
492
  }
@@ -399,5 +601,37 @@ class DatoHelper extends runtime_1.default {
399
601
  throw err;
400
602
  }
401
603
  }
604
+ async updateProductSwatch(productId, uploadId) {
605
+ try {
606
+ const res = await fetch(`${this.baseUrl}/items/${productId}`, {
607
+ method: "PATCH",
608
+ headers: {
609
+ Authorization: `Bearer ${this.apiToken}`,
610
+ "X-Api-Version": "3",
611
+ "X-Environment": this.environment,
612
+ "Content-Type": "application/vnd.api+json",
613
+ Accept: "application/json",
614
+ },
615
+ body: JSON.stringify({
616
+ data: {
617
+ type: "item",
618
+ id: productId,
619
+ attributes: {
620
+ swatch: { upload_id: uploadId },
621
+ },
622
+ },
623
+ }),
624
+ });
625
+ if (!res.ok) {
626
+ const errorText = await res.text();
627
+ throw new Error(`Failed to update product swatch: ${res.status} ${res.statusText} - ${errorText}`);
628
+ }
629
+ return await res.json();
630
+ }
631
+ catch (err) {
632
+ this.logging.error(`updateProductSwatch failed: ${err.message}`);
633
+ throw err;
634
+ }
635
+ }
402
636
  }
403
637
  exports.default = DatoHelper;
@@ -44,7 +44,6 @@ declare class AirtableHelper<T extends Record<string, string>, K extends keyof T
44
44
  }>, options?: {
45
45
  fieldsToMergeOn?: Array<keyof T>;
46
46
  primaryKeyWritable?: boolean;
47
- typecast?: boolean;
48
47
  }, callIdx?: number, collectedResult?: {
49
48
  updatedRecords: Array<string>;
50
49
  createdRecords: Array<string>;
@@ -19,18 +19,19 @@ export type BasicCentraProduct = {
19
19
  export type BasicCentraDisplay = {
20
20
  id: number;
21
21
  name: string;
22
+ externalId: string;
22
23
  status: string;
23
- uri: string;
24
+ url: string;
24
25
  metaTitle: string;
25
26
  metaDescription: string;
26
27
  categories: {
27
28
  id: number;
28
29
  name: string;
29
- uri: string;
30
+ url: string;
30
31
  isTopCategory: boolean;
31
32
  }[];
32
33
  markets: BasicCentraMarket[];
33
- productVariants: Array<{
34
+ productVariant: Array<{
34
35
  id: number;
35
36
  externalId: string;
36
37
  status: string;
@@ -76,6 +77,7 @@ export type BasicCentraCountry = {
76
77
  export type BasicCentraMarket = {
77
78
  id: number;
78
79
  name: string;
80
+ externalId: string;
79
81
  };
80
82
  export type BasicCentraVariant = {
81
83
  id: number;
@@ -156,14 +158,14 @@ export default class CentraHelper extends EnvEngine {
156
158
  fetchCentraWarehouses(externalIds?: string[] | undefined | null): Promise<CentraErrors | Array<BasicCentraWarehouse>>;
157
159
  fetchCentraCampaigns(names?: string[] | undefined | null): Promise<CentraErrors | Record<string, BasicCentraCampaign | Error>>;
158
160
  fetchPricelists(names?: string[] | undefined | null): Promise<CentraErrors | Array<BasicPricelist>>;
159
- fetchCentraMarkets(): Promise<CentraErrors | Array<BasicCentraMarket>>;
161
+ fetchCentraMarkets(externalIds?: string[] | undefined | null): Promise<CentraErrors | Array<BasicCentraMarket>>;
160
162
  fetchCentraSizeCharts(externalIds?: string[] | undefined | null): Promise<CentraErrors | Array<BasicCentraSizeChart>>;
161
163
  private fetchCentraProducts;
162
164
  private fetchCentraDisplays;
163
165
  private fetchCentraVariants;
164
166
  getCentraWarehouses(externalIds?: string[] | null | undefined, alwaysFetch?: boolean): Promise<Error | Record<string, Error | BasicCentraWarehouse>>;
165
167
  getCentraPricelists(names?: string[] | null | undefined, alwaysFetch?: boolean): Promise<Error | Record<string, Error | BasicPricelist>>;
166
- getCentraMarkets(alwaysFetch?: boolean): Promise<Error | Record<string, Error | BasicCentraMarket>>;
168
+ getCentraMarkets(externalIds?: string[] | null | undefined, alwaysFetch?: boolean): Promise<Error | Record<string, Error | BasicCentraMarket>>;
167
169
  getCentraCountries(iso2Codes?: string[] | null | undefined, alwaysFetch?: boolean): Promise<Error | Record<string, Error | BasicCentraCountry>>;
168
170
  getCentraSizeCharts(externalIds?: string[] | null | undefined, alwaysFetch?: boolean): Promise<Error | Record<string, Error | BasicCentraSizeChart>>;
169
171
  getCentraProducts(styleIds: string[]): Promise<Error | Record<string, Error | BasicCentraProduct>>;
@@ -8,6 +8,16 @@ interface UploadResult {
8
8
  success: boolean;
9
9
  upload: string;
10
10
  assetId?: string;
11
+ skipped?: boolean;
12
+ }
13
+ interface UploadReturn {
14
+ id: string;
15
+ type: string;
16
+ attributes: {
17
+ url: string;
18
+ filename: string;
19
+ [key: string]: any;
20
+ };
11
21
  }
12
22
  export default class DatoHelper extends Runtime {
13
23
  private apiToken;
@@ -19,6 +29,21 @@ export default class DatoHelper extends Runtime {
19
29
  private createAssetFromUpload;
20
30
  private checkJobResult;
21
31
  private waitForJobCompletion;
32
+ updateUpload: (options: {
33
+ id: string;
34
+ filename?: string;
35
+ tags: string[];
36
+ notes?: string;
37
+ title: string;
38
+ alt: string;
39
+ author: string;
40
+ copyright: string;
41
+ uploadCollectionId?: string;
42
+ }) => Promise<{
43
+ success: boolean;
44
+ assetId: string;
45
+ upload: string;
46
+ }>;
22
47
  uploadFromUrl(options: {
23
48
  url: string;
24
49
  copyright: string;
@@ -53,10 +78,11 @@ export default class DatoHelper extends Runtime {
53
78
  quality?: number;
54
79
  format?: 'jpg' | 'pjpg' | 'png' | 'webp' | 'avif' | 'gif' | 'jxl' | 'jp2' | 'jxr' | 'json' | 'blurhash' | (string & {});
55
80
  }): string;
56
- findUploadByFilename(filename: string): Promise<any>;
81
+ findUploadByFilename(filename: string): Promise<UploadReturn | null>;
57
82
  getItemTypeId(apiKey: string): Promise<string>;
58
83
  getProductBySku(sku: string): Promise<any>;
59
84
  createProduct(sku: string, productTypeId: string): Promise<any>;
60
85
  updateProductGallery(productId: string, gallery: any[]): Promise<any>;
86
+ updateProductSwatch(productId: string, uploadId: string): Promise<any>;
61
87
  }
62
88
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shushed/helpers",
3
- "version": "0.0.204",
3
+ "version": "0.0.205-fix-erp-729-20251209153116",
4
4
  "author": "",
5
5
  "license": "UNLICENSED",
6
6
  "description": "",