@contrail/flexplm 1.5.0-alpha.aaef470 → 1.5.1-alpha.14abddb

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
@@ -6,6 +6,12 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6
6
  Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## [Unreleased]
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
+
14
+ ## [1.5.0] - 2026-05-12
9
15
  ### Added
10
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.
11
17
  - Added optional identity-service lookup in `DataConverter.setObjectReferenceValue` for resolving inbound `object_reference` values. Enabled per referenced entity type via `config.search.<entityType>.useIdentityServiceForInboundData`. When enabled the reference is resolved via the identity service using a uniqueness pool key; otherwise behavior falls through to the existing `getAllObjectReferences` query path.
@@ -79,6 +79,7 @@ export declare class BaseProcessPublishAssortment {
79
79
  private sendToFlexPLM;
80
80
  private saveToLocalFile;
81
81
  private handleVibeIQFile;
82
+ sendPublishPayloadEvent(outboundEvent: any): Promise<void>;
82
83
  private getCurrentDateString;
83
84
  getItemFamilyChanges(pcd: PublishChangeData, changeDetail: any, assortmentItemFullChangeMap: Map<string, any>, assortmentItemDeleteMap: Map<string, any>): Map<string, ItemFamilyChanges>;
84
85
  getEventsForPublishChangeData(publishChangeData: PublishChangeData): Promise<SeasonalPayload[]>;
@@ -569,14 +569,19 @@ 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 [result] = await Promise.all([
580
+ flexPLMConnect.sendToFlexPLM(outboundPublishEvent),
581
+ this.sendPublishPayloadEvent(outboundPublishEvent)
582
+ ]);
583
+ result['outboundPublishEvent'] = outboundPublishEvent;
584
+ return result;
580
585
  }
581
586
  async saveToLocalFile(events, eventType) {
582
587
  let results = {};
@@ -588,15 +593,16 @@ class BaseProcessPublishAssortment {
588
593
  const eventDirName = 'events-output';
589
594
  const fileName = `${path.sep}${eventDirName}${path.sep}events-${dateString}.json`;
590
595
  fileHandle = await fsPromise.open('.' + fileName, 'a');
591
- const asyncEvent = {
596
+ const outboundPublishEvent = {
592
597
  taskId: this.config.taskId,
593
598
  eventType,
594
599
  objectClass: 'LCSSeason',
595
600
  events
596
601
  };
597
- await fileHandle.writeFile(JSON.stringify(asyncEvent));
602
+ await fileHandle.writeFile(JSON.stringify(outboundPublishEvent));
598
603
  results = {
599
604
  message: 'Successfully Saved File',
605
+ outboundPublishEvent,
600
606
  fileLocation: dirName + fileName
601
607
  };
602
608
  }
@@ -612,7 +618,7 @@ class BaseProcessPublishAssortment {
612
618
  const eventBuffer = Buffer.from(JSON.stringify(events), 'utf-8');
613
619
  const fileName = 'ASYNC_PUBLISH_SEASON-events.json';
614
620
  const uploadFile = await new sdk_1.Files().createAndUploadFileFromBuffer(eventBuffer, 'application/json', fileName, null, this.TTL);
615
- const asyncEvent = {
621
+ const outboundPublishEvent = {
616
622
  taskId: this.config.taskId,
617
623
  eventType,
618
624
  objectClass: 'LCSSeason',
@@ -622,15 +628,39 @@ class BaseProcessPublishAssortment {
622
628
  };
623
629
  if (sendMode === 'vibeiqfile') {
624
630
  const flexPLMConnect = new flexplm_connect_1.FlexPLMConnect(this.config);
625
- return await flexPLMConnect.sendToFlexPLM(asyncEvent);
631
+ const [result] = await Promise.all([
632
+ flexPLMConnect.sendToFlexPLM(outboundPublishEvent),
633
+ this.sendPublishPayloadEvent(outboundPublishEvent)
634
+ ]);
635
+ return { ...result, outboundPublishEvent };
626
636
  }
627
637
  else {
638
+ await this.sendPublishPayloadEvent(outboundPublishEvent);
628
639
  return {
629
640
  message: 'Successfully Uploaded File.',
630
- asyncEvent
641
+ outboundPublishEvent
631
642
  };
632
643
  }
633
644
  }
645
+ async sendPublishPayloadEvent(outboundEvent) {
646
+ console.info('sendPublishPayloadEvent()');
647
+ let initialEvent = {};
648
+ if (this.config?.event) {
649
+ initialEvent = (typeof this.config?.event === 'string')
650
+ ? JSON.parse(this.config?.event)
651
+ : this.config?.event;
652
+ }
653
+ const eventBody = {
654
+ originSystemType: 'VibeIQ',
655
+ objectClass: 'AssortmentPublishedToFlexPLM',
656
+ outboundEvent,
657
+ initialEvent
658
+ };
659
+ await new sdk_1.Entities().create({
660
+ entityName: 'external-event',
661
+ object: eventBody
662
+ });
663
+ }
634
664
  getCurrentDateString() {
635
665
  const d = new Date();
636
666
  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,122 @@ 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('sendToFlexPLM merges outboundPublishEvent into result and calls 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('handleVibeIQFile (vibeiqfile) merges outboundPublishEvent into FlexPLM result and calls sendPublishPayloadEvent', async () => {
1742
+ const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
1743
+ const flexResponse = { status: 200, data: { ok: true } };
1744
+ const spyFlex = jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM')
1745
+ .mockResolvedValue(flexResponse);
1746
+ const spyEvent = jest.spyOn(bppa, 'sendPublishPayloadEvent')
1747
+ .mockResolvedValue(undefined);
1748
+ const result = await bppa.handleVibeIQFile(events, eventType, 'vibeiqfile');
1749
+ expect(fileUploadCalls).toHaveLength(1);
1750
+ expect(fileUploadCalls[0].fileName).toBe('ASYNC_PUBLISH_SEASON-events.json');
1751
+ expect(fileUploadCalls[0].mimeType).toBe('application/json');
1752
+ const passedOutboundPublishEvent = spyFlex.mock.calls[0][0];
1753
+ expect(passedOutboundPublishEvent.taskId).toBe('task-abc');
1754
+ expect(passedOutboundPublishEvent.eventType).toBe(eventType);
1755
+ expect(passedOutboundPublishEvent.objectClass).toBe('LCSSeason');
1756
+ expect(passedOutboundPublishEvent.eventsFileId).toBe('file-123');
1757
+ expect(passedOutboundPublishEvent.eventsDownloadLink).toBe('https://download.url');
1758
+ expect(passedOutboundPublishEvent.eventsAdminDownloadLink).toBe('https://admin.download.url');
1759
+ expect(spyFlex).toHaveBeenCalledTimes(1);
1760
+ expect(spyEvent).toHaveBeenCalledTimes(1);
1761
+ expect(spyEvent).toHaveBeenCalledWith(passedOutboundPublishEvent);
1762
+ expect(result).toEqual({ ...flexResponse, outboundPublishEvent: passedOutboundPublishEvent });
1763
+ });
1764
+ it('handleVibeIQFile (vibeiqfile-dontsendtoflexplm) skips FlexPLM but still calls sendPublishPayloadEvent', async () => {
1765
+ const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
1766
+ const spyFlex = jest.spyOn(flexplm_connect_1.FlexPLMConnect.prototype, 'sendToFlexPLM')
1767
+ .mockResolvedValue({});
1768
+ const spyEvent = jest.spyOn(bppa, 'sendPublishPayloadEvent')
1769
+ .mockResolvedValue(undefined);
1770
+ const result = await bppa.handleVibeIQFile(events, eventType, 'vibeiqfile-dontsendtoflexplm');
1771
+ expect(spyFlex).not.toHaveBeenCalled();
1772
+ expect(spyEvent).toHaveBeenCalledTimes(1);
1773
+ const passedOutboundPublishEvent = spyEvent.mock.calls[0][0];
1774
+ expect(passedOutboundPublishEvent.eventsFileId).toBe('file-123');
1775
+ expect(result).toEqual({
1776
+ message: 'Successfully Uploaded File.',
1777
+ outboundPublishEvent: passedOutboundPublishEvent
1778
+ });
1779
+ });
1780
+ it('sendPublishPayloadEvent creates an external-event with initialEvent parsed from config', async () => {
1781
+ const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
1782
+ const outboundEvent = { foo: 'bar' };
1783
+ await bppa.sendPublishPayloadEvent(outboundEvent);
1784
+ expect(createCallArg).toEqual({
1785
+ entityName: 'external-event',
1786
+ object: {
1787
+ originSystemType: 'VibeIQ',
1788
+ objectClass: 'AssortmentPublishedToFlexPLM',
1789
+ outboundEvent,
1790
+ initialEvent: { sourceEventId: 'src-1' }
1791
+ }
1792
+ });
1793
+ });
1794
+ it('sendPublishPayloadEvent defaults initialEvent to {} when config has no event', async () => {
1795
+ const bareConfig = { taskId: 'task-x' };
1796
+ const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(bareConfig, dc, mapFileUtil);
1797
+ const outboundEvent = { hello: 'world' };
1798
+ await bppa.sendPublishPayloadEvent(outboundEvent);
1799
+ expect(createCallArg).toEqual({
1800
+ entityName: 'external-event',
1801
+ object: {
1802
+ originSystemType: 'VibeIQ',
1803
+ objectClass: 'AssortmentPublishedToFlexPLM',
1804
+ outboundEvent,
1805
+ initialEvent: {}
1806
+ }
1807
+ });
1808
+ });
1809
+ });
@@ -32,7 +32,7 @@ export declare class DataConverter {
32
32
  private buildObjectReferenceContext;
33
33
  private lookupObjectReferenceViaIdentityService;
34
34
  private lookupObjectReferenceViaQuery;
35
- private assertSingleObjectReference;
35
+ private pickSingleResult;
36
36
  getAllObjectReferences(entityType: string, rootTypeCriteria: any, postProcessCriteria?: any): Promise<any[]>;
37
37
  checkKeysAndValues(criteria: any, arrayOfObjects: any, entityTypePath: any): any[];
38
38
  filterOutArchivedAndTrashedEntities(entities: any[]): any[];
@@ -417,25 +417,27 @@ class DataConverter {
417
417
  entityName: 'identity',
418
418
  criteria: { poolKey, propertyName, propertyValue }
419
419
  }) ?? [];
420
- return this.assertSingleObjectReference(identityResults, ctx.combinedCriteria, (r) => r.entityReference.split(':')[1]);
420
+ const match = this.pickSingleResult(identityResults, ctx.combinedCriteria);
421
+ return match ? match.entityReference.split(':')[1] : "";
421
422
  }
422
423
  async lookupObjectReferenceViaQuery(ctx) {
423
424
  let arrObjectReferences = await this.getAllObjectReferences(ctx.entityType, ctx.rootTypeCriteria);
424
425
  if (ctx.entityType !== ctx.entityTypePath) {
425
426
  arrObjectReferences = this.checkKeysAndValues(ctx.typeCriteria, arrObjectReferences, ctx.entityTypePath);
426
427
  }
427
- return this.assertSingleObjectReference(arrObjectReferences, ctx.combinedCriteria, (r) => r.id);
428
+ const match = this.pickSingleResult(arrObjectReferences, ctx.combinedCriteria);
429
+ return match ? match.id : "";
428
430
  }
429
- assertSingleObjectReference(results, combinedCriteria, getId) {
431
+ pickSingleResult(results, combinedCriteria) {
430
432
  if (!results.length) {
431
433
  console.warn(`The passed in object reference criteria ${JSON.stringify(combinedCriteria)} didn't match any entities.`);
432
- return "";
434
+ return undefined;
433
435
  }
434
436
  if (results.length > 1) {
435
437
  console.warn(`The passed in object reference criteria has duplicate records found ${JSON.stringify(combinedCriteria)}.`);
436
- return "";
438
+ return undefined;
437
439
  }
438
- return getId(results[0]);
440
+ return results[0];
439
441
  }
440
442
  async getAllObjectReferences(entityType, rootTypeCriteria, postProcessCriteria = null) {
441
443
  const entities = new sdk_1.Entities();
@@ -826,7 +826,7 @@ describe('setObjectReferenceValue - identity service', () => {
826
826
  checkSpy.mockRestore();
827
827
  }
828
828
  });
829
- it('assertSingleObjectReference single result - returns id via callback', async () => {
829
+ it('pickSingleResult single result - identity branch parses id from entityReference', async () => {
830
830
  const config = baseConfig();
831
831
  config.search = { item: { useIdentityServiceForInboundData: true } };
832
832
  const dc = new data_converter_1.DataConverter(config, new transform_data_1.MapFileUtil(new sdk_1.Entities()));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrail/flexplm",
3
- "version": "1.5.0-alpha.aaef470",
3
+ "version": "1.5.1-alpha.14abddb",
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,147 @@ 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('sendToFlexPLM merges outboundPublishEvent into result and calls 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, '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('handleVibeIQFile (vibeiqfile) merges outboundPublishEvent into FlexPLM result and calls sendPublishPayloadEvent', async () => {
2071
+ const bppa = new BaseProcessPublishAssortment(config, dc, mapFileUtil);
2072
+ const flexResponse = { status: 200, data: { ok: true } };
2073
+
2074
+ const spyFlex = jest.spyOn(FlexPLMConnect.prototype, 'sendToFlexPLM')
2075
+ .mockResolvedValue(flexResponse as any);
2076
+ const spyEvent = jest.spyOn(bppa, 'sendPublishPayloadEvent')
2077
+ .mockResolvedValue(undefined as any);
2078
+
2079
+ const result = await (bppa as any).handleVibeIQFile(events, eventType, 'vibeiqfile');
2080
+
2081
+ expect(fileUploadCalls).toHaveLength(1);
2082
+ expect(fileUploadCalls[0].fileName).toBe('ASYNC_PUBLISH_SEASON-events.json');
2083
+ expect(fileUploadCalls[0].mimeType).toBe('application/json');
2084
+
2085
+ const passedOutboundPublishEvent = spyFlex.mock.calls[0][0] as any;
2086
+ expect(passedOutboundPublishEvent.taskId).toBe('task-abc');
2087
+ expect(passedOutboundPublishEvent.eventType).toBe(eventType);
2088
+ expect(passedOutboundPublishEvent.objectClass).toBe('LCSSeason');
2089
+ expect(passedOutboundPublishEvent.eventsFileId).toBe('file-123');
2090
+ expect(passedOutboundPublishEvent.eventsDownloadLink).toBe('https://download.url');
2091
+ expect(passedOutboundPublishEvent.eventsAdminDownloadLink).toBe('https://admin.download.url');
2092
+
2093
+ expect(spyFlex).toHaveBeenCalledTimes(1);
2094
+ expect(spyEvent).toHaveBeenCalledTimes(1);
2095
+ expect(spyEvent).toHaveBeenCalledWith(passedOutboundPublishEvent);
2096
+ expect(result).toEqual({ ...flexResponse, outboundPublishEvent: passedOutboundPublishEvent });
2097
+ });
2098
+
2099
+ it('handleVibeIQFile (vibeiqfile-dontsendtoflexplm) skips FlexPLM but still calls sendPublishPayloadEvent', async () => {
2100
+ const bppa = new BaseProcessPublishAssortment(config, dc, mapFileUtil);
2101
+
2102
+ const spyFlex = jest.spyOn(FlexPLMConnect.prototype, 'sendToFlexPLM')
2103
+ .mockResolvedValue({} as any);
2104
+ const spyEvent = jest.spyOn(bppa, 'sendPublishPayloadEvent')
2105
+ .mockResolvedValue(undefined as any);
2106
+
2107
+ const result = await (bppa as any).handleVibeIQFile(events, eventType, 'vibeiqfile-dontsendtoflexplm');
2108
+
2109
+ expect(spyFlex).not.toHaveBeenCalled();
2110
+ expect(spyEvent).toHaveBeenCalledTimes(1);
2111
+
2112
+ const passedOutboundPublishEvent = spyEvent.mock.calls[0][0] as any;
2113
+ expect(passedOutboundPublishEvent.eventsFileId).toBe('file-123');
2114
+ expect(result).toEqual({
2115
+ message: 'Successfully Uploaded File.',
2116
+ outboundPublishEvent: passedOutboundPublishEvent
2117
+ });
2118
+ });
2119
+
2120
+ it('sendPublishPayloadEvent creates an external-event with initialEvent parsed from config', async () => {
2121
+ const bppa = new BaseProcessPublishAssortment(config, dc, mapFileUtil);
2122
+ const outboundEvent = { foo: 'bar' };
2123
+
2124
+ await bppa.sendPublishPayloadEvent(outboundEvent);
2125
+
2126
+ expect(createCallArg).toEqual({
2127
+ entityName: 'external-event',
2128
+ object: {
2129
+ originSystemType: 'VibeIQ',
2130
+ objectClass: 'AssortmentPublishedToFlexPLM',
2131
+ outboundEvent,
2132
+ initialEvent: { sourceEventId: 'src-1' }
2133
+ }
2134
+ });
2135
+ });
2136
+
2137
+ it('sendPublishPayloadEvent defaults initialEvent to {} when config has no event', async () => {
2138
+ const bareConfig = { taskId: 'task-x' } as unknown as FCConfig;
2139
+ const bppa = new BaseProcessPublishAssortment(bareConfig, dc, mapFileUtil);
2140
+ const outboundEvent = { hello: 'world' };
2141
+
2142
+ await bppa.sendPublishPayloadEvent(outboundEvent);
2143
+
2144
+ expect(createCallArg).toEqual({
2145
+ entityName: 'external-event',
2146
+ object: {
2147
+ originSystemType: 'VibeIQ',
2148
+ objectClass: 'AssortmentPublishedToFlexPLM',
2149
+ outboundEvent,
2150
+ initialEvent: {}
2151
+ }
2152
+ });
2153
+ });
1992
2154
  });
@@ -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,12 @@ export class BaseProcessPublishAssortment {
693
693
  };
694
694
 
695
695
  const flexPLMConnect = new FlexPLMConnect(this.config);
696
- return await flexPLMConnect.sendToFlexPLM(asyncEvent);
696
+ const [result] = await Promise.all([
697
+ flexPLMConnect.sendToFlexPLM(outboundPublishEvent),
698
+ this.sendPublishPayloadEvent(outboundPublishEvent)
699
+ ]);
700
+ result['outboundPublishEvent'] = outboundPublishEvent;
701
+ return result;
697
702
  }
698
703
 
699
704
  private async saveToLocalFile(events: SeasonalPayload[], eventType: string) {
@@ -708,17 +713,18 @@ export class BaseProcessPublishAssortment {
708
713
  const fileName = `${path.sep}${eventDirName}${path.sep}events-${dateString}.json`;
709
714
  fileHandle = await fsPromise.open('.' + fileName, 'a');
710
715
 
711
- const asyncEvent: AsyncEventsPayloadType = {
716
+ const outboundPublishEvent: AsyncEventsPayloadType = {
712
717
  taskId: this.config.taskId,
713
718
  eventType,
714
719
  objectClass: 'LCSSeason',
715
720
  events
716
721
  };
717
722
 
718
- await fileHandle.writeFile(JSON.stringify(asyncEvent));
723
+ await fileHandle.writeFile(JSON.stringify(outboundPublishEvent));
719
724
 
720
725
  results = {
721
726
  message: 'Successfully Saved File',
727
+ outboundPublishEvent,
722
728
  fileLocation: dirName + fileName
723
729
  };
724
730
  } catch (e) {
@@ -735,26 +741,53 @@ export class BaseProcessPublishAssortment {
735
741
  const fileName = 'ASYNC_PUBLISH_SEASON-events.json';
736
742
  const uploadFile = await new Files().createAndUploadFileFromBuffer(eventBuffer, 'application/json', fileName, null, this.TTL);
737
743
 
738
- const asyncEvent: AsyncEventsPayloadType = {
744
+ const outboundPublishEvent: AsyncEventsPayloadType = {
739
745
  taskId: this.config.taskId,
740
746
  eventType,
741
747
  objectClass: 'LCSSeason',
742
748
  eventsFileId: uploadFile.id,
743
749
  eventsDownloadLink: uploadFile.downloadUrl,
744
750
  eventsAdminDownloadLink: uploadFile.adminDownloadUrl
745
- };
751
+ };
746
752
 
747
753
  if (sendMode === 'vibeiqfile') {
748
754
  const flexPLMConnect = new FlexPLMConnect(this.config);
749
- return await flexPLMConnect.sendToFlexPLM(asyncEvent);
755
+ const [result] = await Promise.all([
756
+ flexPLMConnect.sendToFlexPLM(outboundPublishEvent),
757
+ this.sendPublishPayloadEvent(outboundPublishEvent)
758
+ ]);
759
+ return { ...result, outboundPublishEvent };
750
760
  } else {
761
+ await this.sendPublishPayloadEvent(outboundPublishEvent);
751
762
  return {
752
763
  message: 'Successfully Uploaded File.',
753
- asyncEvent
764
+ outboundPublishEvent
754
765
  };
755
766
  }
756
767
  }
757
768
 
769
+ public async sendPublishPayloadEvent(outboundEvent: any){
770
+ console.info('sendPublishPayloadEvent()');
771
+ let initialEvent = {};
772
+ if(this.config?.event){
773
+ initialEvent = (typeof this.config?.event === 'string')
774
+ ? JSON.parse(this.config?.event)
775
+ : this.config?.event;
776
+ }
777
+ const eventBody = {
778
+ originSystemType: 'VibeIQ',
779
+ objectClass: 'AssortmentPublishedToFlexPLM',
780
+ outboundEvent,
781
+ initialEvent
782
+ };
783
+
784
+ await new Entities().create({
785
+ entityName: 'external-event',
786
+ object: eventBody
787
+ });
788
+ }
789
+
790
+
758
791
  private getCurrentDateString() {
759
792
  const d = new Date();
760
793
  return '' + d.getUTCFullYear()
@@ -947,7 +947,7 @@ describe('setObjectReferenceValue - identity service', () => {
947
947
  }
948
948
  });
949
949
 
950
- it('assertSingleObjectReference single result - returns id via callback', async () => {
950
+ it('pickSingleResult single result - identity branch parses id from entityReference', async () => {
951
951
  const config = baseConfig();
952
952
  config.search = { item: { useIdentityServiceForInboundData: true } };
953
953
  const dc = new DataConverter(config, new MapFileUtil(new Entities()));
@@ -520,7 +520,8 @@ export class DataConverter {
520
520
  criteria: { poolKey, propertyName, propertyValue }
521
521
  }) ?? [];
522
522
 
523
- return this.assertSingleObjectReference(identityResults, ctx.combinedCriteria, (r) => r.entityReference.split(':')[1]);
523
+ const match = this.pickSingleResult(identityResults, ctx.combinedCriteria);
524
+ return match ? match.entityReference.split(':')[1] : "";
524
525
  }
525
526
 
526
527
  private async lookupObjectReferenceViaQuery(ctx: ObjectReferenceContext): Promise<string> {
@@ -528,19 +529,20 @@ export class DataConverter {
528
529
  if (ctx.entityType !== ctx.entityTypePath) {
529
530
  arrObjectReferences = this.checkKeysAndValues(ctx.typeCriteria, arrObjectReferences, ctx.entityTypePath);
530
531
  }
531
- return this.assertSingleObjectReference(arrObjectReferences, ctx.combinedCriteria, (r) => r.id);
532
+ const match = this.pickSingleResult(arrObjectReferences, ctx.combinedCriteria);
533
+ return match ? match.id : "";
532
534
  }
533
535
 
534
- private assertSingleObjectReference(results: any[], combinedCriteria: any, getId: (r: any) => string): string {
536
+ private pickSingleResult(results: any[], combinedCriteria: any): any | undefined {
535
537
  if (!results.length) {
536
538
  console.warn(`The passed in object reference criteria ${JSON.stringify(combinedCriteria)} didn't match any entities.`);
537
- return "";
539
+ return undefined;
538
540
  }
539
541
  if (results.length > 1) {
540
542
  console.warn(`The passed in object reference criteria has duplicate records found ${JSON.stringify(combinedCriteria)}.`);
541
- return "";
543
+ return undefined;
542
544
  }
543
- return getId(results[0]);
545
+ return results[0];
544
546
  }
545
547
 
546
548
  /**