@contrail/flexplm 1.5.0 → 1.5.1-alpha.2e74ff1

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/CHANGELOG.md CHANGED
@@ -7,6 +7,10 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.5.1] - 2026-05-14
11
+ ### Added
12
+ - Added sending an external event with the publish payload and trigger key of `VibeIQ|AssortmentPublishedToFlexPLM` to enable secondary events to run on the event / data which was generated. Also includes the payload in the data which can be passed to the next action.
13
+
10
14
  ## [1.5.0] - 2026-05-12
11
15
  ### Added
12
16
  - Added support for Inbound `LCSMaterial` to sync to the entity class `item` with type path `item:material` and `itemNumber` as identifier. This is controlled by an `LCSMaterial.processAsItem` (default `false`) config default.
@@ -77,8 +77,10 @@ export declare class BaseProcessPublishAssortment {
77
77
  getResultsCount(events: any): any;
78
78
  private sendEvents;
79
79
  private sendToFlexPLM;
80
+ private buildPublishError;
80
81
  private saveToLocalFile;
81
82
  private handleVibeIQFile;
83
+ private sendPublishPayloadEvent;
82
84
  private getCurrentDateString;
83
85
  getItemFamilyChanges(pcd: PublishChangeData, changeDetail: any, assortmentItemFullChangeMap: Map<string, any>, assortmentItemDeleteMap: Map<string, any>): Map<string, ItemFamilyChanges>;
84
86
  getEventsForPublishChangeData(publishChangeData: PublishChangeData): Promise<SeasonalPayload[]>;
@@ -569,14 +569,37 @@ class BaseProcessPublishAssortment {
569
569
  }
570
570
  }
571
571
  async sendToFlexPLM(events, eventType) {
572
- const asyncEvent = {
572
+ const outboundPublishEvent = {
573
573
  taskId: this.config.taskId,
574
574
  eventType,
575
575
  objectClass: 'LCSSeason',
576
576
  events
577
577
  };
578
578
  const flexPLMConnect = new flexplm_connect_1.FlexPLMConnect(this.config);
579
- return await flexPLMConnect.sendToFlexPLM(asyncEvent);
579
+ const [sendResult, eventResult] = await Promise.allSettled([
580
+ flexPLMConnect.sendToFlexPLM(outboundPublishEvent),
581
+ this.sendPublishPayloadEvent(outboundPublishEvent)
582
+ ]);
583
+ if (sendResult.status === 'rejected' || eventResult.status === 'rejected') {
584
+ throw this.buildPublishError(sendResult, eventResult, outboundPublishEvent);
585
+ }
586
+ const result = sendResult.value;
587
+ const isResultObject = typeof result === 'object' && result !== null;
588
+ return isResultObject
589
+ ? { ...result, outboundPublishEvent }
590
+ : { result, outboundPublishEvent };
591
+ }
592
+ buildPublishError(sendResult, eventResult, outboundPublishEvent) {
593
+ const baseReason = sendResult.status === 'rejected' ? sendResult.reason : eventResult.reason;
594
+ const error = baseReason instanceof Error ? baseReason : new Error(String(baseReason));
595
+ error.outboundPublishEvent = outboundPublishEvent;
596
+ error.sendToFlexPLMResult = sendResult.status === 'fulfilled'
597
+ ? sendResult.value
598
+ : { error: sendResult.reason?.message ?? String(sendResult.reason) };
599
+ error.sendPublishPayloadEventResult = eventResult.status === 'fulfilled'
600
+ ? eventResult.value
601
+ : { error: eventResult.reason?.message ?? String(eventResult.reason) };
602
+ return error;
580
603
  }
581
604
  async saveToLocalFile(events, eventType) {
582
605
  let results = {};
@@ -588,15 +611,16 @@ class BaseProcessPublishAssortment {
588
611
  const eventDirName = 'events-output';
589
612
  const fileName = `${path.sep}${eventDirName}${path.sep}events-${dateString}.json`;
590
613
  fileHandle = await fsPromise.open('.' + fileName, 'a');
591
- const asyncEvent = {
614
+ const outboundPublishEvent = {
592
615
  taskId: this.config.taskId,
593
616
  eventType,
594
617
  objectClass: 'LCSSeason',
595
618
  events
596
619
  };
597
- await fileHandle.writeFile(JSON.stringify(asyncEvent));
620
+ await fileHandle.writeFile(JSON.stringify(outboundPublishEvent));
598
621
  results = {
599
622
  message: 'Successfully Saved File',
623
+ outboundPublishEvent,
600
624
  fileLocation: dirName + fileName
601
625
  };
602
626
  }
@@ -612,7 +636,7 @@ class BaseProcessPublishAssortment {
612
636
  const eventBuffer = Buffer.from(JSON.stringify(events), 'utf-8');
613
637
  const fileName = 'ASYNC_PUBLISH_SEASON-events.json';
614
638
  const uploadFile = await new sdk_1.Files().createAndUploadFileFromBuffer(eventBuffer, 'application/json', fileName, null, this.TTL);
615
- const asyncEvent = {
639
+ const outboundPublishEvent = {
616
640
  taskId: this.config.taskId,
617
641
  eventType,
618
642
  objectClass: 'LCSSeason',
@@ -622,15 +646,46 @@ class BaseProcessPublishAssortment {
622
646
  };
623
647
  if (sendMode === 'vibeiqfile') {
624
648
  const flexPLMConnect = new flexplm_connect_1.FlexPLMConnect(this.config);
625
- return await flexPLMConnect.sendToFlexPLM(asyncEvent);
649
+ const [sendResult, eventResult] = await Promise.allSettled([
650
+ flexPLMConnect.sendToFlexPLM(outboundPublishEvent),
651
+ this.sendPublishPayloadEvent(outboundPublishEvent)
652
+ ]);
653
+ if (sendResult.status === 'rejected' || eventResult.status === 'rejected') {
654
+ throw this.buildPublishError(sendResult, eventResult, outboundPublishEvent);
655
+ }
656
+ const result = sendResult.value;
657
+ const isResultObject = typeof result === 'object' && result !== null;
658
+ return isResultObject
659
+ ? { ...result, outboundPublishEvent }
660
+ : { result, outboundPublishEvent };
626
661
  }
627
662
  else {
663
+ await this.sendPublishPayloadEvent(outboundPublishEvent);
628
664
  return {
629
665
  message: 'Successfully Uploaded File.',
630
- asyncEvent
666
+ outboundPublishEvent
631
667
  };
632
668
  }
633
669
  }
670
+ async sendPublishPayloadEvent(outboundPublishEvent) {
671
+ console.info('sendPublishPayloadEvent()');
672
+ let initialEvent = {};
673
+ if (this.config?.event) {
674
+ initialEvent = (typeof this.config?.event === 'string')
675
+ ? JSON.parse(this.config?.event)
676
+ : this.config?.event;
677
+ }
678
+ const eventBody = {
679
+ originSystemType: 'VibeIQ',
680
+ objectClass: 'AssortmentPublishedToFlexPLM',
681
+ outboundPublishEvent,
682
+ initialEvent
683
+ };
684
+ await new sdk_1.Entities().create({
685
+ entityName: 'external-event',
686
+ object: eventBody
687
+ });
688
+ }
634
689
  getCurrentDateString() {
635
690
  const d = new Date();
636
691
  return '' + d.getUTCFullYear()
@@ -9,6 +9,7 @@ const sdk_1 = require("@contrail/sdk");
9
9
  const type_conversion_utils_1 = require("../util/type-conversion-utils");
10
10
  const map_utils_1 = require("../util/map-utils");
11
11
  const item_family_changes_1 = require("../interfaces/item-family-changes");
12
+ const flexplm_connect_1 = require("../util/flexplm-connect");
12
13
  let federatedId = '';
13
14
  jest.mock('../util/data-converter', () => {
14
15
  return {
@@ -30,6 +31,13 @@ jest.mock('../util/federation', () => {
30
31
  });
31
32
  let entityObject = {};
32
33
  let getOptionsObject = {};
34
+ let createCallArg = undefined;
35
+ let fileUploadCalls = [];
36
+ let fileUploadResult = {
37
+ id: 'file-123',
38
+ downloadUrl: 'https://download.url',
39
+ adminDownloadUrl: 'https://admin.download.url'
40
+ };
33
41
  jest.mock('@contrail/sdk', () => {
34
42
  return {
35
43
  Entities: class {
@@ -37,6 +45,18 @@ jest.mock('@contrail/sdk', () => {
37
45
  getOptionsObject = _getOtionsObject;
38
46
  return entityObject;
39
47
  }
48
+ create(arg) {
49
+ createCallArg = arg;
50
+ return Promise.resolve({});
51
+ }
52
+ },
53
+ Files: class {
54
+ createAndUploadFileFromBuffer(buffer, mimeType, fileName, _x, ttl) {
55
+ fileUploadCalls.push({ buffer, mimeType, fileName, ttl });
56
+ return Promise.resolve(fileUploadResult);
57
+ }
58
+ },
59
+ Request: class {
40
60
  }
41
61
  };
42
62
  });
@@ -1668,3 +1688,195 @@ describe('getEventsForItemFamilyChanges - conditional eventType', () => {
1668
1688
  expect(type_conversion_utils_1.TypeConversionUtils.isOutboundCreatableFromEntity).toHaveBeenNthCalledWith(2, undefined, mapFileUtil, colorProjectItem, { item: itemData, assortment });
1669
1689
  });
1670
1690
  });
1691
+ describe('sendToFlexPLM / handleVibeIQFile / sendPublishPayloadEvent', () => {
1692
+ const config = {
1693
+ taskId: 'task-abc',
1694
+ event: '{"sourceEventId":"src-1"}'
1695
+ };
1696
+ const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
1697
+ const dc = new data_converter_1.DataConverter(config, mapFileUtil);
1698
+ const events = [{ objectClass: 'LCSProductSeasonLink' }];
1699
+ const eventType = 'ASYNC_PUBLISH_SEASON';
1700
+ beforeEach(() => {
1701
+ createCallArg = undefined;
1702
+ fileUploadCalls = [];
1703
+ fileUploadResult = {
1704
+ id: 'file-123',
1705
+ downloadUrl: 'https://download.url',
1706
+ adminDownloadUrl: 'https://admin.download.url'
1707
+ };
1708
+ });
1709
+ afterEach(() => {
1710
+ jest.restoreAllMocks();
1711
+ });
1712
+ it('should merge outboundPublishEvent into result and call sendPublishPayloadEvent in parallel', async () => {
1713
+ const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
1714
+ const flexResponse = { status: 200, data: { ok: true } };
1715
+ let flexResolved = false;
1716
+ let eventResolved = false;
1717
+ const spyFlex = jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM')
1718
+ .mockImplementation(async () => {
1719
+ await new Promise(r => setTimeout(r, 5));
1720
+ flexResolved = true;
1721
+ return flexResponse;
1722
+ });
1723
+ const spyEvent = jest.spyOn(bppa, 'sendPublishPayloadEvent')
1724
+ .mockImplementation(async () => {
1725
+ await new Promise(r => setTimeout(r, 5));
1726
+ eventResolved = true;
1727
+ });
1728
+ const result = await bppa.sendToFlexPLM(events, eventType);
1729
+ expect(spyFlex).toHaveBeenCalledTimes(1);
1730
+ expect(spyEvent).toHaveBeenCalledTimes(1);
1731
+ expect(flexResolved).toBe(true);
1732
+ expect(eventResolved).toBe(true);
1733
+ const passedOutboundPublishEvent = spyFlex.mock.calls[0][0];
1734
+ expect(passedOutboundPublishEvent.taskId).toBe('task-abc');
1735
+ expect(passedOutboundPublishEvent.eventType).toBe(eventType);
1736
+ expect(passedOutboundPublishEvent.objectClass).toBe('LCSSeason');
1737
+ expect(passedOutboundPublishEvent.events).toBe(events);
1738
+ expect(spyEvent).toHaveBeenCalledWith(passedOutboundPublishEvent);
1739
+ expect(result).toEqual({ ...flexResponse, outboundPublishEvent: passedOutboundPublishEvent });
1740
+ });
1741
+ it('sendToFlexPLM throws when flexPLMConnect.sendToFlexPLM fails, attaching outboundPublishEvent and both results', async () => {
1742
+ const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
1743
+ const flexError = new Error('flex failed');
1744
+ const eventResponse = { eventOk: true };
1745
+ jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM').mockRejectedValue(flexError);
1746
+ jest.spyOn(bppa, 'sendPublishPayloadEvent').mockResolvedValue(eventResponse);
1747
+ await expect(bppa.sendToFlexPLM(events, eventType)).rejects.toMatchObject({
1748
+ message: 'flex failed',
1749
+ outboundPublishEvent: expect.objectContaining({ taskId: 'task-abc', eventType, objectClass: 'LCSSeason', events }),
1750
+ sendToFlexPLMResult: { error: 'flex failed' },
1751
+ sendPublishPayloadEventResult: eventResponse
1752
+ });
1753
+ });
1754
+ it('sendToFlexPLM throws when sendPublishPayloadEvent fails, with flex result attached', async () => {
1755
+ const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
1756
+ const flexResponse = { status: 200 };
1757
+ const eventError = new Error('event failed');
1758
+ jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM').mockResolvedValue(flexResponse);
1759
+ jest.spyOn(bppa, 'sendPublishPayloadEvent').mockRejectedValue(eventError);
1760
+ await expect(bppa.sendToFlexPLM(events, eventType)).rejects.toMatchObject({
1761
+ message: 'event failed',
1762
+ outboundPublishEvent: expect.objectContaining({ taskId: 'task-abc', eventType }),
1763
+ sendToFlexPLMResult: flexResponse,
1764
+ sendPublishPayloadEventResult: { error: 'event failed' }
1765
+ });
1766
+ });
1767
+ it('sendToFlexPLM throws the flexPLMConnect error when both fail', async () => {
1768
+ const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
1769
+ const flexError = new Error('flex failed');
1770
+ const eventError = new Error('event failed');
1771
+ jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM').mockRejectedValue(flexError);
1772
+ jest.spyOn(bppa, 'sendPublishPayloadEvent').mockRejectedValue(eventError);
1773
+ await expect(bppa.sendToFlexPLM(events, eventType)).rejects.toBe(flexError);
1774
+ expect(flexError.outboundPublishEvent).toBeDefined();
1775
+ expect(flexError.sendToFlexPLMResult).toEqual({ error: 'flex failed' });
1776
+ expect(flexError.sendPublishPayloadEventResult).toEqual({ error: 'event failed' });
1777
+ });
1778
+ it('handleVibeIQFile (vibeiqfile) throws when flexPLMConnect.sendToFlexPLM fails, with both results attached', async () => {
1779
+ const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
1780
+ const flexError = new Error('flex failed');
1781
+ const eventResponse = { eventOk: true };
1782
+ jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM').mockRejectedValue(flexError);
1783
+ jest.spyOn(bppa, 'sendPublishPayloadEvent').mockResolvedValue(eventResponse);
1784
+ await expect(bppa.handleVibeIQFile(events, eventType, 'vibeiqfile')).rejects.toMatchObject({
1785
+ message: 'flex failed',
1786
+ outboundPublishEvent: expect.objectContaining({ eventsFileId: 'file-123' }),
1787
+ sendToFlexPLMResult: { error: 'flex failed' },
1788
+ sendPublishPayloadEventResult: eventResponse
1789
+ });
1790
+ });
1791
+ it('handleVibeIQFile (vibeiqfile) throws when sendPublishPayloadEvent fails, with flex result attached', async () => {
1792
+ const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
1793
+ const flexResponse = { status: 200 };
1794
+ const eventError = new Error('event failed');
1795
+ jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM').mockResolvedValue(flexResponse);
1796
+ jest.spyOn(bppa, 'sendPublishPayloadEvent').mockRejectedValue(eventError);
1797
+ await expect(bppa.handleVibeIQFile(events, eventType, 'vibeiqfile')).rejects.toMatchObject({
1798
+ message: 'event failed',
1799
+ outboundPublishEvent: expect.objectContaining({ eventsFileId: 'file-123' }),
1800
+ sendToFlexPLMResult: flexResponse,
1801
+ sendPublishPayloadEventResult: { error: 'event failed' }
1802
+ });
1803
+ });
1804
+ it('handleVibeIQFile (vibeiqfile) throws the flexPLMConnect error when both fail', async () => {
1805
+ const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
1806
+ const flexError = new Error('flex failed');
1807
+ const eventError = new Error('event failed');
1808
+ jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM').mockRejectedValue(flexError);
1809
+ jest.spyOn(bppa, 'sendPublishPayloadEvent').mockRejectedValue(eventError);
1810
+ await expect(bppa.handleVibeIQFile(events, eventType, 'vibeiqfile')).rejects.toBe(flexError);
1811
+ expect(flexError.sendToFlexPLMResult).toEqual({ error: 'flex failed' });
1812
+ expect(flexError.sendPublishPayloadEventResult).toEqual({ error: 'event failed' });
1813
+ });
1814
+ it('should merge outboundPublishEvent into FlexPLM result and call sendPublishPayloadEvent when mode is vibeiqfile', async () => {
1815
+ const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
1816
+ const flexResponse = { status: 200, data: { ok: true } };
1817
+ const spyFlex = jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM')
1818
+ .mockResolvedValue(flexResponse);
1819
+ const spyEvent = jest.spyOn(bppa, 'sendPublishPayloadEvent')
1820
+ .mockResolvedValue(undefined);
1821
+ const result = await bppa.handleVibeIQFile(events, eventType, 'vibeiqfile');
1822
+ expect(fileUploadCalls).toHaveLength(1);
1823
+ expect(fileUploadCalls[0].fileName).toBe('ASYNC_PUBLISH_SEASON-events.json');
1824
+ expect(fileUploadCalls[0].mimeType).toBe('application/json');
1825
+ const passedOutboundPublishEvent = spyFlex.mock.calls[0][0];
1826
+ expect(passedOutboundPublishEvent.taskId).toBe('task-abc');
1827
+ expect(passedOutboundPublishEvent.eventType).toBe(eventType);
1828
+ expect(passedOutboundPublishEvent.objectClass).toBe('LCSSeason');
1829
+ expect(passedOutboundPublishEvent.eventsFileId).toBe('file-123');
1830
+ expect(passedOutboundPublishEvent.eventsDownloadLink).toBe('https://download.url');
1831
+ expect(passedOutboundPublishEvent.eventsAdminDownloadLink).toBe('https://admin.download.url');
1832
+ expect(spyFlex).toHaveBeenCalledTimes(1);
1833
+ expect(spyEvent).toHaveBeenCalledTimes(1);
1834
+ expect(spyEvent).toHaveBeenCalledWith(passedOutboundPublishEvent);
1835
+ expect(result).toEqual({ ...flexResponse, outboundPublishEvent: passedOutboundPublishEvent });
1836
+ });
1837
+ it('should skip FlexPLM but still call sendPublishPayloadEvent when mode is vibeiqfile-dontsendtoflexplm', async () => {
1838
+ const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
1839
+ const spyFlex = jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM')
1840
+ .mockResolvedValue({});
1841
+ const spyEvent = jest.spyOn(bppa, 'sendPublishPayloadEvent')
1842
+ .mockResolvedValue(undefined);
1843
+ const result = await bppa.handleVibeIQFile(events, eventType, 'vibeiqfile-dontsendtoflexplm');
1844
+ expect(spyFlex).not.toHaveBeenCalled();
1845
+ expect(spyEvent).toHaveBeenCalledTimes(1);
1846
+ const passedOutboundPublishEvent = spyEvent.mock.calls[0][0];
1847
+ expect(passedOutboundPublishEvent.eventsFileId).toBe('file-123');
1848
+ expect(result).toEqual({
1849
+ message: 'Successfully Uploaded File.',
1850
+ outboundPublishEvent: passedOutboundPublishEvent
1851
+ });
1852
+ });
1853
+ it('should create an external-event with initialEvent parsed from config', async () => {
1854
+ const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
1855
+ const outboundPublishEvent = { foo: 'bar' };
1856
+ await bppa.sendPublishPayloadEvent(outboundPublishEvent);
1857
+ expect(createCallArg).toEqual({
1858
+ entityName: 'external-event',
1859
+ object: {
1860
+ originSystemType: 'VibeIQ',
1861
+ objectClass: 'AssortmentPublishedToFlexPLM',
1862
+ outboundPublishEvent,
1863
+ initialEvent: { sourceEventId: 'src-1' }
1864
+ }
1865
+ });
1866
+ });
1867
+ it('should default initialEvent to {} when config has no event', async () => {
1868
+ const bareConfig = { taskId: 'task-x' };
1869
+ const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(bareConfig, dc, mapFileUtil);
1870
+ const outboundPublishEvent = { hello: 'world' };
1871
+ await bppa.sendPublishPayloadEvent(outboundPublishEvent);
1872
+ expect(createCallArg).toEqual({
1873
+ entityName: 'external-event',
1874
+ object: {
1875
+ originSystemType: 'VibeIQ',
1876
+ objectClass: 'AssortmentPublishedToFlexPLM',
1877
+ outboundPublishEvent,
1878
+ initialEvent: {}
1879
+ }
1880
+ });
1881
+ });
1882
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrail/flexplm",
3
- "version": "1.5.0",
3
+ "version": "1.5.1-alpha.2e74ff1",
4
4
  "description": "Library used for integration with flexplm.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -8,6 +8,7 @@ import { Entities } from '@contrail/sdk';
8
8
  import { TypeConversionUtils } from '../util/type-conversion-utils';
9
9
  import { MapUtil } from '../util/map-utils';
10
10
  import { ItemFamilyChanges } from '../interfaces/item-family-changes';
11
+ import { FlexPLMConnect } from '../util/flexplm-connect';
11
12
 
12
13
  let federatedId = '';
13
14
  jest.mock('../util/data-converter', () => {
@@ -32,6 +33,13 @@ jest.mock('../util/federation', () => {
32
33
  });
33
34
  let entityObject = {};
34
35
  let getOptionsObject = {};
36
+ let createCallArg: any = undefined;
37
+ let fileUploadCalls: any[] = [];
38
+ let fileUploadResult: any = {
39
+ id: 'file-123',
40
+ downloadUrl: 'https://download.url',
41
+ adminDownloadUrl: 'https://admin.download.url'
42
+ };
35
43
  jest.mock('@contrail/sdk', () => {
36
44
  return {
37
45
  Entities: class{
@@ -39,7 +47,18 @@ jest.mock('@contrail/sdk', () => {
39
47
  getOptionsObject = _getOtionsObject;
40
48
  return entityObject;
41
49
  }
42
- }
50
+ create(arg){
51
+ createCallArg = arg;
52
+ return Promise.resolve({});
53
+ }
54
+ },
55
+ Files: class{
56
+ createAndUploadFileFromBuffer(buffer, mimeType, fileName, _x, ttl){
57
+ fileUploadCalls.push({ buffer, mimeType, fileName, ttl });
58
+ return Promise.resolve(fileUploadResult);
59
+ }
60
+ },
61
+ Request: class{}
43
62
  };
44
63
  });
45
64
  describe('process publish assortment', () => {
@@ -1989,4 +2008,238 @@ describe('getEventsForItemFamilyChanges - conditional eventType', () => {
1989
2008
  );
1990
2009
  });
1991
2010
 
2011
+ });
2012
+
2013
+ describe('sendToFlexPLM / handleVibeIQFile / sendPublishPayloadEvent', () => {
2014
+ const config = {
2015
+ taskId: 'task-abc',
2016
+ event: '{"sourceEventId":"src-1"}'
2017
+ } as unknown as FCConfig;
2018
+ const mapFileUtil = new MapFileUtil(new Entities());
2019
+ const dc = new DataConverter(config, mapFileUtil);
2020
+ const events = [{ objectClass: 'LCSProductSeasonLink' }] as any;
2021
+ const eventType = 'ASYNC_PUBLISH_SEASON';
2022
+
2023
+ beforeEach(() => {
2024
+ createCallArg = undefined;
2025
+ fileUploadCalls = [];
2026
+ fileUploadResult = {
2027
+ id: 'file-123',
2028
+ downloadUrl: 'https://download.url',
2029
+ adminDownloadUrl: 'https://admin.download.url'
2030
+ };
2031
+ });
2032
+ afterEach(() => {
2033
+ jest.restoreAllMocks();
2034
+ });
2035
+
2036
+ it('should merge outboundPublishEvent into result and call sendPublishPayloadEvent in parallel', async () => {
2037
+ const bppa = new BaseProcessPublishAssortment(config, dc, mapFileUtil);
2038
+ const flexResponse = { status: 200, data: { ok: true } };
2039
+
2040
+ let flexResolved = false;
2041
+ let eventResolved = false;
2042
+ const spyFlex = jest.spyOn(FlexPLMConnect.prototype, 'sendToFlexPLM')
2043
+ .mockImplementation(async () => {
2044
+ await new Promise(r => setTimeout(r, 5));
2045
+ flexResolved = true;
2046
+ return flexResponse as any;
2047
+ });
2048
+ const spyEvent = jest.spyOn(bppa as any, 'sendPublishPayloadEvent')
2049
+ .mockImplementation(async () => {
2050
+ await new Promise(r => setTimeout(r, 5));
2051
+ eventResolved = true;
2052
+ });
2053
+
2054
+ const result = await (bppa as any).sendToFlexPLM(events, eventType);
2055
+
2056
+ expect(spyFlex).toHaveBeenCalledTimes(1);
2057
+ expect(spyEvent).toHaveBeenCalledTimes(1);
2058
+ expect(flexResolved).toBe(true);
2059
+ expect(eventResolved).toBe(true);
2060
+
2061
+ const passedOutboundPublishEvent = spyFlex.mock.calls[0][0] as any;
2062
+ expect(passedOutboundPublishEvent.taskId).toBe('task-abc');
2063
+ expect(passedOutboundPublishEvent.eventType).toBe(eventType);
2064
+ expect(passedOutboundPublishEvent.objectClass).toBe('LCSSeason');
2065
+ expect(passedOutboundPublishEvent.events).toBe(events);
2066
+ expect(spyEvent).toHaveBeenCalledWith(passedOutboundPublishEvent);
2067
+ expect(result).toEqual({ ...flexResponse, outboundPublishEvent: passedOutboundPublishEvent });
2068
+ });
2069
+
2070
+ it('sendToFlexPLM throws when flexPLMConnect.sendToFlexPLM fails, attaching outboundPublishEvent and both results', async () => {
2071
+ const bppa = new BaseProcessPublishAssortment(config, dc, mapFileUtil);
2072
+ const flexError = new Error('flex failed');
2073
+ const eventResponse = { eventOk: true };
2074
+
2075
+ jest.spyOn(FlexPLMConnect.prototype, 'sendToFlexPLM').mockRejectedValue(flexError);
2076
+ jest.spyOn(bppa as any, 'sendPublishPayloadEvent').mockResolvedValue(eventResponse as any);
2077
+
2078
+ await expect((bppa as any).sendToFlexPLM(events, eventType)).rejects.toMatchObject({
2079
+ message: 'flex failed',
2080
+ outboundPublishEvent: expect.objectContaining({ taskId: 'task-abc', eventType, objectClass: 'LCSSeason', events }),
2081
+ sendToFlexPLMResult: { error: 'flex failed' },
2082
+ sendPublishPayloadEventResult: eventResponse
2083
+ });
2084
+ });
2085
+
2086
+ it('sendToFlexPLM throws when sendPublishPayloadEvent fails, with flex result attached', async () => {
2087
+ const bppa = new BaseProcessPublishAssortment(config, dc, mapFileUtil);
2088
+ const flexResponse = { status: 200 };
2089
+ const eventError = new Error('event failed');
2090
+
2091
+ jest.spyOn(FlexPLMConnect.prototype, 'sendToFlexPLM').mockResolvedValue(flexResponse as any);
2092
+ jest.spyOn(bppa as any, 'sendPublishPayloadEvent').mockRejectedValue(eventError);
2093
+
2094
+ await expect((bppa as any).sendToFlexPLM(events, eventType)).rejects.toMatchObject({
2095
+ message: 'event failed',
2096
+ outboundPublishEvent: expect.objectContaining({ taskId: 'task-abc', eventType }),
2097
+ sendToFlexPLMResult: flexResponse,
2098
+ sendPublishPayloadEventResult: { error: 'event failed' }
2099
+ });
2100
+ });
2101
+
2102
+ it('sendToFlexPLM throws the flexPLMConnect error when both fail', async () => {
2103
+ const bppa = new BaseProcessPublishAssortment(config, dc, mapFileUtil);
2104
+ const flexError = new Error('flex failed');
2105
+ const eventError = new Error('event failed');
2106
+
2107
+ jest.spyOn(FlexPLMConnect.prototype, 'sendToFlexPLM').mockRejectedValue(flexError);
2108
+ jest.spyOn(bppa as any, 'sendPublishPayloadEvent').mockRejectedValue(eventError);
2109
+
2110
+ await expect((bppa as any).sendToFlexPLM(events, eventType)).rejects.toBe(flexError);
2111
+ expect((flexError as any).outboundPublishEvent).toBeDefined();
2112
+ expect((flexError as any).sendToFlexPLMResult).toEqual({ error: 'flex failed' });
2113
+ expect((flexError as any).sendPublishPayloadEventResult).toEqual({ error: 'event failed' });
2114
+ });
2115
+
2116
+ it('handleVibeIQFile (vibeiqfile) throws when flexPLMConnect.sendToFlexPLM fails, with both results attached', async () => {
2117
+ const bppa = new BaseProcessPublishAssortment(config, dc, mapFileUtil);
2118
+ const flexError = new Error('flex failed');
2119
+ const eventResponse = { eventOk: true };
2120
+
2121
+ jest.spyOn(FlexPLMConnect.prototype, 'sendToFlexPLM').mockRejectedValue(flexError);
2122
+ jest.spyOn(bppa as any, 'sendPublishPayloadEvent').mockResolvedValue(eventResponse as any);
2123
+
2124
+ await expect((bppa as any).handleVibeIQFile(events, eventType, 'vibeiqfile')).rejects.toMatchObject({
2125
+ message: 'flex failed',
2126
+ outboundPublishEvent: expect.objectContaining({ eventsFileId: 'file-123' }),
2127
+ sendToFlexPLMResult: { error: 'flex failed' },
2128
+ sendPublishPayloadEventResult: eventResponse
2129
+ });
2130
+ });
2131
+
2132
+ it('handleVibeIQFile (vibeiqfile) throws when sendPublishPayloadEvent fails, with flex result attached', async () => {
2133
+ const bppa = new BaseProcessPublishAssortment(config, dc, mapFileUtil);
2134
+ const flexResponse = { status: 200 };
2135
+ const eventError = new Error('event failed');
2136
+
2137
+ jest.spyOn(FlexPLMConnect.prototype, 'sendToFlexPLM').mockResolvedValue(flexResponse as any);
2138
+ jest.spyOn(bppa as any, 'sendPublishPayloadEvent').mockRejectedValue(eventError);
2139
+
2140
+ await expect((bppa as any).handleVibeIQFile(events, eventType, 'vibeiqfile')).rejects.toMatchObject({
2141
+ message: 'event failed',
2142
+ outboundPublishEvent: expect.objectContaining({ eventsFileId: 'file-123' }),
2143
+ sendToFlexPLMResult: flexResponse,
2144
+ sendPublishPayloadEventResult: { error: 'event failed' }
2145
+ });
2146
+ });
2147
+
2148
+ it('handleVibeIQFile (vibeiqfile) throws the flexPLMConnect error when both fail', async () => {
2149
+ const bppa = new BaseProcessPublishAssortment(config, dc, mapFileUtil);
2150
+ const flexError = new Error('flex failed');
2151
+ const eventError = new Error('event failed');
2152
+
2153
+ jest.spyOn(FlexPLMConnect.prototype, 'sendToFlexPLM').mockRejectedValue(flexError);
2154
+ jest.spyOn(bppa as any, 'sendPublishPayloadEvent').mockRejectedValue(eventError);
2155
+
2156
+ await expect((bppa as any).handleVibeIQFile(events, eventType, 'vibeiqfile')).rejects.toBe(flexError);
2157
+ expect((flexError as any).sendToFlexPLMResult).toEqual({ error: 'flex failed' });
2158
+ expect((flexError as any).sendPublishPayloadEventResult).toEqual({ error: 'event failed' });
2159
+ });
2160
+
2161
+ it('should merge outboundPublishEvent into FlexPLM result and call sendPublishPayloadEvent when mode is vibeiqfile', async () => {
2162
+ const bppa = new BaseProcessPublishAssortment(config, dc, mapFileUtil);
2163
+ const flexResponse = { status: 200, data: { ok: true } };
2164
+
2165
+ const spyFlex = jest.spyOn(FlexPLMConnect.prototype, 'sendToFlexPLM')
2166
+ .mockResolvedValue(flexResponse as any);
2167
+ const spyEvent = jest.spyOn(bppa as any, 'sendPublishPayloadEvent')
2168
+ .mockResolvedValue(undefined as any);
2169
+
2170
+ const result = await (bppa as any).handleVibeIQFile(events, eventType, 'vibeiqfile');
2171
+
2172
+ expect(fileUploadCalls).toHaveLength(1);
2173
+ expect(fileUploadCalls[0].fileName).toBe('ASYNC_PUBLISH_SEASON-events.json');
2174
+ expect(fileUploadCalls[0].mimeType).toBe('application/json');
2175
+
2176
+ const passedOutboundPublishEvent = spyFlex.mock.calls[0][0] as any;
2177
+ expect(passedOutboundPublishEvent.taskId).toBe('task-abc');
2178
+ expect(passedOutboundPublishEvent.eventType).toBe(eventType);
2179
+ expect(passedOutboundPublishEvent.objectClass).toBe('LCSSeason');
2180
+ expect(passedOutboundPublishEvent.eventsFileId).toBe('file-123');
2181
+ expect(passedOutboundPublishEvent.eventsDownloadLink).toBe('https://download.url');
2182
+ expect(passedOutboundPublishEvent.eventsAdminDownloadLink).toBe('https://admin.download.url');
2183
+
2184
+ expect(spyFlex).toHaveBeenCalledTimes(1);
2185
+ expect(spyEvent).toHaveBeenCalledTimes(1);
2186
+ expect(spyEvent).toHaveBeenCalledWith(passedOutboundPublishEvent);
2187
+ expect(result).toEqual({ ...flexResponse, outboundPublishEvent: passedOutboundPublishEvent });
2188
+ });
2189
+
2190
+ it('should skip FlexPLM but still call sendPublishPayloadEvent when mode is vibeiqfile-dontsendtoflexplm', async () => {
2191
+ const bppa = new BaseProcessPublishAssortment(config, dc, mapFileUtil);
2192
+
2193
+ const spyFlex = jest.spyOn(FlexPLMConnect.prototype, 'sendToFlexPLM')
2194
+ .mockResolvedValue({} as any);
2195
+ const spyEvent = jest.spyOn(bppa as any, 'sendPublishPayloadEvent')
2196
+ .mockResolvedValue(undefined as any);
2197
+
2198
+ const result = await (bppa as any).handleVibeIQFile(events, eventType, 'vibeiqfile-dontsendtoflexplm');
2199
+
2200
+ expect(spyFlex).not.toHaveBeenCalled();
2201
+ expect(spyEvent).toHaveBeenCalledTimes(1);
2202
+
2203
+ const passedOutboundPublishEvent = spyEvent.mock.calls[0][0] as any;
2204
+ expect(passedOutboundPublishEvent.eventsFileId).toBe('file-123');
2205
+ expect(result).toEqual({
2206
+ message: 'Successfully Uploaded File.',
2207
+ outboundPublishEvent: passedOutboundPublishEvent
2208
+ });
2209
+ });
2210
+
2211
+ it('should create an external-event with initialEvent parsed from config', async () => {
2212
+ const bppa = new BaseProcessPublishAssortment(config, dc, mapFileUtil);
2213
+ const outboundPublishEvent = { foo: 'bar' };
2214
+
2215
+ await (bppa as any).sendPublishPayloadEvent(outboundPublishEvent);
2216
+
2217
+ expect(createCallArg).toEqual({
2218
+ entityName: 'external-event',
2219
+ object: {
2220
+ originSystemType: 'VibeIQ',
2221
+ objectClass: 'AssortmentPublishedToFlexPLM',
2222
+ outboundPublishEvent,
2223
+ initialEvent: { sourceEventId: 'src-1' }
2224
+ }
2225
+ });
2226
+ });
2227
+
2228
+ it('should default initialEvent to {} when config has no event', async () => {
2229
+ const bareConfig = { taskId: 'task-x' } as unknown as FCConfig;
2230
+ const bppa = new BaseProcessPublishAssortment(bareConfig, dc, mapFileUtil);
2231
+ const outboundPublishEvent = { hello: 'world' };
2232
+
2233
+ await (bppa as any).sendPublishPayloadEvent(outboundPublishEvent);
2234
+
2235
+ expect(createCallArg).toEqual({
2236
+ entityName: 'external-event',
2237
+ object: {
2238
+ originSystemType: 'VibeIQ',
2239
+ objectClass: 'AssortmentPublishedToFlexPLM',
2240
+ outboundPublishEvent,
2241
+ initialEvent: {}
2242
+ }
2243
+ });
2244
+ });
1992
2245
  });
@@ -685,7 +685,7 @@ export class BaseProcessPublishAssortment {
685
685
  }
686
686
 
687
687
  private async sendToFlexPLM(events: SeasonalPayload[], eventType: string) {
688
- const asyncEvent: AsyncEventsPayloadType = {
688
+ const outboundPublishEvent: AsyncEventsPayloadType = {
689
689
  taskId: this.config.taskId,
690
690
  eventType,
691
691
  objectClass: 'LCSSeason',
@@ -693,7 +693,37 @@ export class BaseProcessPublishAssortment {
693
693
  };
694
694
 
695
695
  const flexPLMConnect = new FlexPLMConnect(this.config);
696
- return await flexPLMConnect.sendToFlexPLM(asyncEvent);
696
+ const [sendResult, eventResult] = await Promise.allSettled([
697
+ flexPLMConnect.sendToFlexPLM(outboundPublishEvent),
698
+ this.sendPublishPayloadEvent(outboundPublishEvent)
699
+ ]);
700
+
701
+ if (sendResult.status === 'rejected' || eventResult.status === 'rejected') {
702
+ throw this.buildPublishError(sendResult, eventResult, outboundPublishEvent);
703
+ }
704
+
705
+ const result = sendResult.value;
706
+ const isResultObject = typeof result === 'object' && result !== null;
707
+ return isResultObject
708
+ ? { ...result, outboundPublishEvent }
709
+ : { result, outboundPublishEvent };
710
+ }
711
+
712
+ private buildPublishError(
713
+ sendResult: PromiseSettledResult<any>,
714
+ eventResult: PromiseSettledResult<any>,
715
+ outboundPublishEvent: AsyncEventsPayloadType
716
+ ): Error {
717
+ const baseReason = sendResult.status === 'rejected' ? sendResult.reason : (eventResult as PromiseRejectedResult).reason;
718
+ const error: any = baseReason instanceof Error ? baseReason : new Error(String(baseReason));
719
+ error.outboundPublishEvent = outboundPublishEvent;
720
+ error.sendToFlexPLMResult = sendResult.status === 'fulfilled'
721
+ ? sendResult.value
722
+ : { error: sendResult.reason?.message ?? String(sendResult.reason) };
723
+ error.sendPublishPayloadEventResult = eventResult.status === 'fulfilled'
724
+ ? eventResult.value
725
+ : { error: eventResult.reason?.message ?? String(eventResult.reason) };
726
+ return error;
697
727
  }
698
728
 
699
729
  private async saveToLocalFile(events: SeasonalPayload[], eventType: string) {
@@ -708,17 +738,18 @@ export class BaseProcessPublishAssortment {
708
738
  const fileName = `${path.sep}${eventDirName}${path.sep}events-${dateString}.json`;
709
739
  fileHandle = await fsPromise.open('.' + fileName, 'a');
710
740
 
711
- const asyncEvent: AsyncEventsPayloadType = {
741
+ const outboundPublishEvent: AsyncEventsPayloadType = {
712
742
  taskId: this.config.taskId,
713
743
  eventType,
714
744
  objectClass: 'LCSSeason',
715
745
  events
716
746
  };
717
747
 
718
- await fileHandle.writeFile(JSON.stringify(asyncEvent));
748
+ await fileHandle.writeFile(JSON.stringify(outboundPublishEvent));
719
749
 
720
750
  results = {
721
751
  message: 'Successfully Saved File',
752
+ outboundPublishEvent,
722
753
  fileLocation: dirName + fileName
723
754
  };
724
755
  } catch (e) {
@@ -735,26 +766,62 @@ export class BaseProcessPublishAssortment {
735
766
  const fileName = 'ASYNC_PUBLISH_SEASON-events.json';
736
767
  const uploadFile = await new Files().createAndUploadFileFromBuffer(eventBuffer, 'application/json', fileName, null, this.TTL);
737
768
 
738
- const asyncEvent: AsyncEventsPayloadType = {
769
+ const outboundPublishEvent: AsyncEventsPayloadType = {
739
770
  taskId: this.config.taskId,
740
771
  eventType,
741
772
  objectClass: 'LCSSeason',
742
773
  eventsFileId: uploadFile.id,
743
774
  eventsDownloadLink: uploadFile.downloadUrl,
744
775
  eventsAdminDownloadLink: uploadFile.adminDownloadUrl
745
- };
776
+ };
746
777
 
747
778
  if (sendMode === 'vibeiqfile') {
748
779
  const flexPLMConnect = new FlexPLMConnect(this.config);
749
- return await flexPLMConnect.sendToFlexPLM(asyncEvent);
780
+ const [sendResult, eventResult] = await Promise.allSettled([
781
+ flexPLMConnect.sendToFlexPLM(outboundPublishEvent),
782
+ this.sendPublishPayloadEvent(outboundPublishEvent)
783
+ ]);
784
+
785
+ if (sendResult.status === 'rejected' || eventResult.status === 'rejected') {
786
+ throw this.buildPublishError(sendResult, eventResult, outboundPublishEvent);
787
+ }
788
+
789
+ const result = sendResult.value;
790
+ const isResultObject = typeof result === 'object' && result !== null;
791
+ return isResultObject
792
+ ? { ...result, outboundPublishEvent }
793
+ : { result, outboundPublishEvent };
750
794
  } else {
795
+ await this.sendPublishPayloadEvent(outboundPublishEvent);
751
796
  return {
752
797
  message: 'Successfully Uploaded File.',
753
- asyncEvent
798
+ outboundPublishEvent
754
799
  };
755
800
  }
756
801
  }
757
802
 
803
+ private async sendPublishPayloadEvent(outboundPublishEvent: AsyncEventsPayloadType){
804
+ console.info('sendPublishPayloadEvent()');
805
+ let initialEvent = {};
806
+ if (this.config?.event){
807
+ initialEvent = (typeof this.config?.event === 'string')
808
+ ? JSON.parse(this.config?.event)
809
+ : this.config?.event;
810
+ }
811
+ const eventBody = {
812
+ originSystemType: 'VibeIQ',
813
+ objectClass: 'AssortmentPublishedToFlexPLM',
814
+ outboundPublishEvent,
815
+ initialEvent
816
+ };
817
+
818
+ await new Entities().create({
819
+ entityName: 'external-event',
820
+ object: eventBody
821
+ });
822
+ }
823
+
824
+
758
825
  private getCurrentDateString() {
759
826
  const d = new Date();
760
827
  return '' + d.getUTCFullYear()