@contrail/flexplm 1.7.4-alpha.ad2bc4e → 1.7.4-alpha.b0c1671

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.
@@ -12,7 +12,6 @@ export declare class ItemFamilyChanges {
12
12
  colorDeletes: string[];
13
13
  colorUpdates: string[];
14
14
  colorUnchanged: string[];
15
- colorSuppressed: string[];
16
15
  assortmentItemFullChangeMap: Map<string, any>;
17
16
  assortmentItemDeleteMap: Map<string, any>;
18
17
  itemToFederatedIdMapping: Map<string, string>;
@@ -14,7 +14,6 @@ class ItemFamilyChanges {
14
14
  this.colorDeletes = [];
15
15
  this.colorUpdates = [];
16
16
  this.colorUnchanged = [];
17
- this.colorSuppressed = []; // changed this window, but excluded by the lifecycle filter
18
17
  this.assortmentItemFullChangeMap = new Map();
19
18
  this.assortmentItemDeleteMap = new Map();
20
19
  this.itemToFederatedIdMapping = new Map();
@@ -33,7 +32,6 @@ class ItemFamilyChanges {
33
32
  s += ' colorDeletes: [' + this.colorDeletes + ']\n';
34
33
  s += ' colorUpdates: [' + this.colorUpdates + ']\n';
35
34
  s += ' colorUnchanged: [' + this.colorUnchanged + ']\n';
36
- s += ' colorSuppressed: [' + this.colorSuppressed + ']\n';
37
35
  s += 'assortmentItemFullChangeMap:\n';
38
36
  s += 'size: ' + this.assortmentItemFullChangeMap.size + '\n[';
39
37
  for (const key of this.assortmentItemFullChangeMap.keys()) {
@@ -15,7 +15,7 @@ class BaseProcessPublishAssortment {
15
15
  constructor(_config, _dc, _mapFileUtil) {
16
16
  this.TTL = 30 * 24 * 60 * 60 * 1000; // 30 days
17
17
  this.cache = {
18
- carriedFromSeason: {},
18
+ carriedFromSeason: {}
19
19
  };
20
20
  this.config = _config;
21
21
  this.dc = _dc;
@@ -46,13 +46,13 @@ class BaseProcessPublishAssortment {
46
46
  console.log(message);
47
47
  const output = {
48
48
  results: { message, event },
49
- skip_await: true,
49
+ skip_await: true
50
50
  };
51
51
  return output;
52
52
  }
53
53
  apcHistory = await this.getApcHistory(assortmentId);
54
54
  const sinceDate = await this.getSinceDate(assortmentId, assortmentPublishChangeId, apcHistory);
55
- // Get detail information
55
+ //Get detail information
56
56
  const assortmentPublishChange = await this.downloadAssortmentPublishChange(assortmentId, assortmentPublishChangeId);
57
57
  publisher = this.getPublisher(assortmentPublishChange);
58
58
  const changeDetail = await this.downloadHydratedChangeDetail(assortmentPublishChange);
@@ -91,7 +91,7 @@ class BaseProcessPublishAssortment {
91
91
  versionComments: apc?.versionComments,
92
92
  publishDate: apc?.createdOn,
93
93
  publisher,
94
- versionHistoryNumber,
94
+ versionHistoryNumber
95
95
  };
96
96
  return publishInfo;
97
97
  }
@@ -111,15 +111,15 @@ class BaseProcessPublishAssortment {
111
111
  return 'unknown';
112
112
  }
113
113
  const createdOnDate = new Date(createdOnString);
114
- createdOnDate.setMonth(createdOnDate.getMonth() - 1); // subtract 1 month
114
+ createdOnDate.setMonth(createdOnDate.getMonth() - 1); //subtract 1 month
115
115
  const createdOnStringMinus1 = createdOnDate.toISOString();
116
116
  const createdOn = 'ISGREATERTHAN ' + createdOnStringMinus1;
117
117
  const snapshots = await new sdk_1.Entities().get({
118
118
  entityName: 'entity-snapshot',
119
119
  criteria: {
120
120
  entityReference,
121
- createdOn,
122
- },
121
+ createdOn
122
+ }
123
123
  });
124
124
  const assortmentPublishChangeId = apc?.id;
125
125
  const snapshot = snapshots.find(snap => snap?.assortmentChangeId === assortmentPublishChangeId);
@@ -169,7 +169,7 @@ class BaseProcessPublishAssortment {
169
169
  }
170
170
  this.assortment = await new sdk_1.Entities().get({
171
171
  entityName: 'assortment',
172
- id: assortmentId,
172
+ id: assortmentId
173
173
  });
174
174
  return this.assortment;
175
175
  }
@@ -214,9 +214,9 @@ class BaseProcessPublishAssortment {
214
214
  return sdk_1.Request.request(resource, {
215
215
  method: 'GET',
216
216
  headers: {
217
- "Accept": 'application/json',
218
- 'Content-Type': 'application/json',
219
- },
217
+ Accept: 'application/json',
218
+ 'Content-Type': 'application/json'
219
+ }
220
220
  });
221
221
  }
222
222
  updatedSinceDate(entity, sinceDate) {
@@ -287,7 +287,7 @@ class BaseProcessPublishAssortment {
287
287
  }
288
288
  }
289
289
  }
290
- dateArray.sort((a, b) => Date.parse(a) - Date.parse(b));
290
+ dateArray.sort((a, b) => { return Date.parse(a) - Date.parse(b); });
291
291
  const arrIndex = (dateArray.length - pastPublishCount > 0) ? dateArray.length - pastPublishCount : 0;
292
292
  sinceDate = dateArray[arrIndex];
293
293
  return sinceDate;
@@ -308,9 +308,9 @@ class BaseProcessPublishAssortment {
308
308
  return await sdk_1.Request.request(resourceString, {
309
309
  method: 'GET',
310
310
  Headers: {
311
- "Accept": 'application/json',
312
- 'Content-Type': 'application/json',
313
- },
311
+ Accept: 'application/json',
312
+ 'Content-Type': 'application/json'
313
+ }
314
314
  });
315
315
  }
316
316
  async downloadHydratedChangeDetail(assortmentPublishChange) {
@@ -358,7 +358,7 @@ class BaseProcessPublishAssortment {
358
358
  console.info('sinceDateMilliseconds: ' + sinceDateMilliseconds);
359
359
  console.info('apcDateMilliseconds: ' + previousApcDate);
360
360
  }
361
- // if only 1 apc, no processing needed
361
+ //if only 1 apc, no processing needed
362
362
  if (sinceDateMilliseconds !== previousApcDate) {
363
363
  console.info('sinceDateMilliseconds !== apcDateMilliseconds');
364
364
  const currentAssortmentItemIds = this.getBaselineItemIds(assortmentBaseline);
@@ -444,7 +444,7 @@ class BaseProcessPublishAssortment {
444
444
  previousBaseline = await this.downloadAssortmentBaseline(previousApc);
445
445
  }
446
446
  const deleteIds = apc?.detail?.deletes.map(dItem => dItem?.id);
447
- // building deletes based on previous baseline; because some APCs don't have delete data
447
+ //building deletes based on previous baseline; because some APCs don't have delete data
448
448
  const deleteArray = previousBaseline?.assortmentItems.filter(aItem => deleteIds.includes(aItem?.itemId));
449
449
  console.info('deleteArray.length: ' + deleteArray.length);
450
450
  return deleteArray;
@@ -495,18 +495,13 @@ class BaseProcessPublishAssortment {
495
495
  const itemFamilyId = item?.itemFamilyId;
496
496
  if (meetsCriteria) {
497
497
  releasedForDevelopmentItemIds.push(itemId);
498
- }
499
- // ---- NEW (Step 5): family resolution moved OUT of the meetsCriteria block.
500
- // A family whose options are ALL pre-development has no other route into this
501
- // list when it has no assortment-item row of its own. ----
502
- if (!itemFamilySet.has(itemFamilyId) && itemId !== itemFamilyId) {
503
- const familyItem = item?.itemFamily;
504
- if (familyItem) {
505
- if (this.meetsCriteria({ item: familyItem })
506
- && !releasedForDevelopmentItemIds.includes(itemFamilyId)) {
498
+ if (!itemFamilySet.has(itemFamilyId) && itemId !== itemFamilyId) {
499
+ const familyItem = item?.itemFamily;
500
+ const familyItemMeetsCriteria = this.meetsCriteria({ item: familyItem });
501
+ if (familyItemMeetsCriteria) {
507
502
  releasedForDevelopmentItemIds.push(itemFamilyId);
508
503
  }
509
- itemFamilySet.add(itemFamilyId); // only mark resolved when we had data to judge
504
+ itemFamilySet.add(itemFamilyId);
510
505
  }
511
506
  }
512
507
  }
@@ -544,7 +539,7 @@ class BaseProcessPublishAssortment {
544
539
  console.info('processPublish-start');
545
540
  const event = {
546
541
  assortmentId: pcd.assortmentId,
547
- assortmentPublishChangeId: pcd.assortmentPublishChangeId,
542
+ assortmentPublishChangeId: pcd.assortmentPublishChangeId
548
543
  };
549
544
  const assortmentItemFullChangeMap = this.getFullChangeAssortmentMap(fullChange);
550
545
  const assortmentItemDeleteMap = this.getDeleteChangesAssortmentMap(deleteChanges);
@@ -559,7 +554,7 @@ class BaseProcessPublishAssortment {
559
554
  console.log(message);
560
555
  return {
561
556
  results: { message, event },
562
- skip_await: true,
557
+ skip_await: true
563
558
  };
564
559
  }
565
560
  const results = await this.sendEvents(events);
@@ -604,7 +599,7 @@ class BaseProcessPublishAssortment {
604
599
  taskId: this.config.taskId,
605
600
  eventType,
606
601
  objectClass: 'LCSSeason',
607
- events,
602
+ events
608
603
  };
609
604
  const flexPLMConnect = new flexplm_connect_1.FlexPLMConnect(this.config);
610
605
  const result = await this.sendAndRecordPublishEvent(flexPLMConnect, outboundPublishEvent);
@@ -650,13 +645,13 @@ class BaseProcessPublishAssortment {
650
645
  taskId: this.config.taskId,
651
646
  eventType,
652
647
  objectClass: 'LCSSeason',
653
- events,
648
+ events
654
649
  };
655
650
  await fileHandle.writeFile(JSON.stringify(outboundPublishEvent));
656
651
  results = {
657
652
  message: 'Successfully Saved File',
658
653
  outboundPublishEvent,
659
- fileLocation: dirName + fileName,
654
+ fileLocation: dirName + fileName
660
655
  };
661
656
  }
662
657
  catch (e) {
@@ -677,7 +672,7 @@ class BaseProcessPublishAssortment {
677
672
  objectClass: 'LCSSeason',
678
673
  eventsFileId: uploadFile.id,
679
674
  eventsDownloadLink: uploadFile.downloadUrl,
680
- eventsAdminDownloadLink: uploadFile.adminDownloadUrl,
675
+ eventsAdminDownloadLink: uploadFile.adminDownloadUrl
681
676
  };
682
677
  if (sendMode === 'vibeiqfile') {
683
678
  const flexPLMConnect = new flexplm_connect_1.FlexPLMConnect(this.config);
@@ -691,7 +686,7 @@ class BaseProcessPublishAssortment {
691
686
  await this.sendPublishPayloadEvent(outboundPublishEvent);
692
687
  return {
693
688
  message: 'Successfully Uploaded File.',
694
- outboundPublishEvent,
689
+ outboundPublishEvent
695
690
  };
696
691
  }
697
692
  }
@@ -707,11 +702,11 @@ class BaseProcessPublishAssortment {
707
702
  originSystemType: 'VibeIQ',
708
703
  objectClass: 'AssortmentPublishedToFlexPLM',
709
704
  outboundPublishEvent,
710
- initialEvent,
705
+ initialEvent
711
706
  };
712
707
  await new sdk_1.Entities().create({
713
708
  entityName: 'external-event',
714
- object: eventBody,
709
+ object: eventBody
715
710
  });
716
711
  }
717
712
  getCurrentDateString() {
@@ -735,7 +730,7 @@ class BaseProcessPublishAssortment {
735
730
  for (const [itemId, aItem] of assortmentItemFullChangeMap) {
736
731
  const projectItem = aItem?.projectItem;
737
732
  const itemFamilyId = aItem?.item?.itemFamilyId;
738
- if (!pcd.releasedForDevelopmentItemIds.includes(itemFamilyId)) {
733
+ if (!pcd.releasedForDevelopmentItemIds.includes(itemFamilyId) || !pcd.releasedForDevelopmentItemIds.includes(itemId)) {
739
734
  continue;
740
735
  }
741
736
  const ifc = itemFamilyChanges.get(itemFamilyId) || new item_family_changes_1.ItemFamilyChanges(itemFamilyId, pcd.sinceDate);
@@ -747,20 +742,6 @@ class BaseProcessPublishAssortment {
747
742
  if (pcd.itemToFederatedIdMapping.has(itemId)) {
748
743
  ifc.itemToFederatedIdMapping.set(itemId, pcd.itemToFederatedIdMapping.get(itemId));
749
744
  }
750
- // ---- NEW Step to enable ItemFamily publish ET-243: option is pre-development. It must not produce an
751
- // LCSSKUSeasonLink, but a change to it still makes the family publishable. ----
752
- if (!pcd.releasedForDevelopmentItemIds.includes(itemId)) {
753
- const changedInWindow = addIds.includes(itemId)
754
- || deleteIds.includes(itemId)
755
- || updateIds.includes(itemId)
756
- || this.updatedSinceDate(projectItem, pcd.sinceDate)
757
- || this.updatedSinceDate(aItem?.item, pcd.sinceDate);
758
- if (changedInWindow) {
759
- ifc.colorSuppressed.push(itemId);
760
- }
761
- continue;
762
- }
763
- // NEW Step - ends
764
745
  if (itemId === itemFamilyId) {
765
746
  if (addIds.includes(itemId))
766
747
  ifc.familyAdd = true;
@@ -776,12 +757,6 @@ class BaseProcessPublishAssortment {
776
757
  ifc.familyUpdate = true;
777
758
  }
778
759
  }
779
- // NEW Step to enable ItemFamily publish ET-243: item-level edits (incl. lifecycleStage) must publish
780
- // the family. Guarded so the "first option added" flow stays suppressed. ----
781
- if (!ifc.familyItemRemoved && this.updatedSinceDate(aItem?.item, pcd.sinceDate)) {
782
- ifc.familyUpdate = true;
783
- }
784
- // NEW Step - ends
785
760
  if (projectItem && !projectItem?.roles && aItem?.item?.roles) {
786
761
  projectItem.roles = aItem?.item?.roles;
787
762
  }
@@ -838,41 +813,7 @@ class BaseProcessPublishAssortment {
838
813
  else {
839
814
  ifc.colorDeletes.push(itemId);
840
815
  }
841
- } // End deletes for loop
842
- // ---- ET-243: the family's own row left the assortment while the family still has
843
- // option rows. The platform reports this in changeDetail.familyItemsRemoved, and the
844
- // row is in neither the baseline nor the delete-data file, so neither loop above can
845
- // see it. `deletes` is folded in as a fallback in case a removal is ever classified
846
- // there while still missing from the delete-data file. ----
847
- for (const removedRow of [...familyItemsRemoved, ...deletes]) {
848
- const itemId = removedRow?.itemId ?? removedRow?.id;
849
- const item = removedRow?.item;
850
- if (!itemId || !item)
851
- continue;
852
- const itemFamilyId = item?.itemFamilyId ?? itemId;
853
- if (itemId !== itemFamilyId)
854
- continue; // family rows only
855
- if (assortmentItemFullChangeMap.has(itemId))
856
- continue; // still in baseline -> loop 1
857
- if (assortmentItemDeleteMap.has(itemId))
858
- continue; // hard delete -> loop 2
859
- if (!pcd.releasedForDevelopmentItemIds.includes(itemFamilyId))
860
- continue;
861
- if (!this.meetsCriteria(removedRow))
862
- continue;
863
- let ifc = itemFamilyChanges.get(itemFamilyId);
864
- if (!ifc) {
865
- ifc = new item_family_changes_1.ItemFamilyChanges(itemFamilyId, pcd.sinceDate);
866
- itemFamilyChanges.set(itemFamilyId, ifc);
867
- }
868
- if (!ifc.itemFamilyObject) {
869
- ifc.itemFamilyObject = item;
870
- }
871
- if (removedRow?.projectItem) {
872
- ifc.assortmentItemFullChangeMap.set(itemId, removedRow);
873
- }
874
- ifc.familyItemRemoved = true;
875
- }
816
+ } //End deletes for loop
876
817
  if (app_framework_1.Logger.isDebugOn()) {
877
818
  console.debug('returning size: ' + itemFamilyChanges.size);
878
819
  for (const [key, value] of itemFamilyChanges) {
@@ -914,17 +855,13 @@ class BaseProcessPublishAssortment {
914
855
  const LCSSeason = Object.assign({}, seasonFed);
915
856
  const assortment = await this.getAssortment(assortmentId);
916
857
  const projectItem = this.getProjectItem(itemFamilyChanges, itemFamilyChanges.itemFamilyId);
917
- // ---- NEW Step to enable ItemFamily publish ET-243: added color suppressed length to existing condition and familyItemRemovedFlag
918
858
  const familyAssortmentItem = itemFamilyChanges.familyAdd || itemFamilyChanges.familyUpdate || itemFamilyChanges.familyDelete
919
- || itemFamilyChanges.familyItemRemoved
920
859
  || itemFamilyChanges.colorAdds.length > 0 || itemFamilyChanges.colorUpdates.length > 0 || itemFamilyChanges.colorDeletes.length > 0
921
- || itemFamilyChanges.colorSuppressed.length > 0
922
860
  || (projectItem && this.updatedSinceDate(projectItem, itemFamilyChanges.sinceDate));
923
- // familyItemRemoved: the family's own assortment row left the assortment while the
924
- // family still has option rows. The family stays in the season, so this publishes an
925
- // LCSProductSeasonLink update and no LCSSKUSeasonLink.
861
+ //familyItemRemoved is used when adding the first option to an assortment
862
+ //and will have updates for the family item.
926
863
  if (familyAssortmentItem) {
927
- // Product-season add
864
+ //Product-season add
928
865
  const entityReference = itemFamilyChanges.itemFamilyId;
929
866
  const prodEntityData = (itemFamilyChanges.assortmentItemFullChangeMap.has(entityReference))
930
867
  ? itemFamilyChanges.assortmentItemFullChangeMap.get(entityReference)?.item
@@ -940,7 +877,7 @@ class BaseProcessPublishAssortment {
940
877
  entityReference: 'item:' + entityReference,
941
878
  LCSSeason,
942
879
  LCSProduct,
943
- data: seasonalData,
880
+ data: seasonalData
944
881
  };
945
882
  if ('ASSORTMENT' === projectItem.addedFromSource && projectItem.addedFromAssortment) {
946
883
  const carriedFromSeason = await this.getCarriedFromSeason(projectItem);
@@ -950,9 +887,9 @@ class BaseProcessPublishAssortment {
950
887
  }
951
888
  events.push(psUpsert);
952
889
  }
953
- // colorway-season adds
954
- // colorAdds
955
- // colorUpdates
890
+ //colorway-season adds
891
+ //colorAdds
892
+ //colorUpdates
956
893
  const colorwayChanges = [];
957
894
  colorwayChanges.push(...itemFamilyChanges.colorAdds);
958
895
  colorwayChanges.push(...itemFamilyChanges.colorUpdates);
@@ -975,7 +912,7 @@ class BaseProcessPublishAssortment {
975
912
  entityReference: 'item:' + entityReference,
976
913
  LCSSeason,
977
914
  LCSSKU,
978
- data: seasonalData,
915
+ data: seasonalData
979
916
  };
980
917
  if ('ASSORTMENT' === projectItem.addedFromSource && projectItem.addedFromAssortment) {
981
918
  const carriedFromSeason = await this.getCarriedFromSeason(projectItem);
@@ -991,10 +928,10 @@ class BaseProcessPublishAssortment {
991
928
  let projectItem = {};
992
929
  if (itemFamilyChanges?.assortmentItemFullChangeMap.has(id)) {
993
930
  const aItem = itemFamilyChanges?.assortmentItemFullChangeMap.get(id);
994
- projectItem = aItem?.projectItem ?? {};
931
+ projectItem = aItem?.projectItem;
995
932
  }
996
933
  /////////////////////////////////////////////////////////////////////////////
997
- // Get from option assortmentItem because family not in assortmentItemFullChangeMap:start
934
+ //Get from option assortmentItem because family not in assortmentItemFullChangeMap:start
998
935
  /////////////////////////////////////////////////////////////////////////////
999
936
  if (id === itemFamilyChanges.itemFamilyId && Object.keys(projectItem).length == 0) {
1000
937
  for (const asstItem of itemFamilyChanges.assortmentItemFullChangeMap.values()) {
@@ -1005,7 +942,7 @@ class BaseProcessPublishAssortment {
1005
942
  }
1006
943
  }
1007
944
  /////////////////////////////////////////////////////////////////////////////
1008
- // Get from option assortmentItem because family not in assortmentItemFullChangeMap:end
945
+ //Get from option assortmentItem because family not in assortmentItemFullChangeMap:end
1009
946
  /////////////////////////////////////////////////////////////////////////////
1010
947
  return projectItem;
1011
948
  }
@@ -1055,7 +992,7 @@ class BaseProcessPublishAssortment {
1055
992
  if (this.cache.carriedFromSeason[addedFromAssortment]) {
1056
993
  return this.cache.carriedFromSeason[addedFromAssortment];
1057
994
  }
1058
- let seasonFederation;
995
+ let seasonFederation = undefined;
1059
996
  const assortmentEntity = await this.getAssormentEntityFromId(addedFromAssortment);
1060
997
  if (assortmentEntity) {
1061
998
  seasonFederation = await this.getSeasonFederationFromAssortment(assortmentEntity);
@@ -1068,7 +1005,7 @@ class BaseProcessPublishAssortment {
1068
1005
  try {
1069
1006
  assortment = await new sdk_1.Entities().get({
1070
1007
  entityName: 'assortment',
1071
- id: assortmentId,
1008
+ id: assortmentId
1072
1009
  });
1073
1010
  }
1074
1011
  catch (e) {
@@ -1082,7 +1019,7 @@ class BaseProcessPublishAssortment {
1082
1019
  let seasonObj = {
1083
1020
  entityReference: 'assortment:' + assortment.id,
1084
1021
  objectClass: 'LCSSeason',
1085
- flexPLMTypePath,
1022
+ flexPLMTypePath
1086
1023
  };
1087
1024
  try {
1088
1025
  const assortmentObj = await this.dc.getFlexPLMObjectData(assortment, [], true);
@@ -1337,176 +1337,6 @@ describe('getItemFamilyChanges', () => {
1337
1337
  expect(ifc.colorDeletes.length).toEqual(1);
1338
1338
  });
1339
1339
  });
1340
- describe('getItemFamilyChanges - lifecycle-suppressed options', () => {
1341
- const familyId = 'AERfdvdAt1HHEVvQ';
1342
- const maroonId = 'YwWKVoL_vGN8khLy';
1343
- const limeId = 'rsmWhWiXRNv_XBJG';
1344
- const sinceDate = new Date('2026-08-01T00:00:00.000Z');
1345
- const afterSince = '2026-08-26T00:00:00.000Z';
1346
- const beforeSince = '2026-05-11T00:00:00.000Z';
1347
- const noDetailChanges = () => ({ adds: [], deletes: [], updates: [], familyItemsRemoved: [] });
1348
- const buildPpa = () => {
1349
- const config = { itemPreDevelopmentLifecycleStages: ['concept'] };
1350
- const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
1351
- const dc = new data_converter_1.DataConverter(config, mapFileUtil);
1352
- return new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
1353
- };
1354
- const buildPcd = (releasedIds) => {
1355
- const seasonFed = {
1356
- entityReference: 'assortment:11EZML5dMn3tREuT',
1357
- objectClass: 'LCSSeason'
1358
- };
1359
- const pcd = new publish_change_data_1.PublishChangeData('11EZML5dMn3tREuT', seasonFed, 'Pg3y2x5IclYxk3hM', sinceDate);
1360
- pcd.itemToFederatedIdMapping = new Map();
1361
- releasedIds.forEach(id => pcd.releasedForDevelopmentItemIds.push(id));
1362
- return pcd;
1363
- };
1364
- const familyItem = (updatedOn = beforeSince, lifecycleStage = 'released') => ({
1365
- id: familyId, itemFamilyId: familyId, lifecycleStage, name: 'Sweatpant', updatedOn
1366
- });
1367
- const familyRow = (updatedOn = beforeSince, lifecycleStage = 'released') => ({
1368
- itemId: familyId,
1369
- item: familyItem(updatedOn, lifecycleStage),
1370
- projectItem: { id: 'pi-' + familyId, updatedOn: beforeSince }
1371
- });
1372
- const optionRow = (id, updatedOn, lifecycleStage = 'concept') => ({
1373
- itemId: id,
1374
- item: { id, itemFamilyId: familyId, lifecycleStage, updatedOn, itemFamily: familyItem() },
1375
- projectItem: { id: 'pi-' + id, updatedOn: beforeSince },
1376
- familyProjectItem: { id: 'pi-' + familyId, updatedOn: beforeSince }
1377
- });
1378
- it('records changed concept options as suppressed, not as colour updates', () => {
1379
- const ppa = buildPpa();
1380
- const fullChangeMap = ppa.getFullChangeAssortmentMap({
1381
- assortmentItems: [familyRow(), optionRow(maroonId, afterSince), optionRow(limeId, afterSince)]
1382
- });
1383
- const pcd = buildPcd([familyId]);
1384
- pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, noDetailChanges(), fullChangeMap, new Map());
1385
- expect(pcd.itemFamilyChanges.size).toEqual(1);
1386
- const ifc = pcd.itemFamilyChanges.get(familyId);
1387
- expect(ifc.colorSuppressed).toEqual([maroonId, limeId]);
1388
- expect(ifc.colorAdds.length).toEqual(0);
1389
- expect(ifc.colorUpdates.length).toEqual(0);
1390
- expect(ifc.colorDeletes.length).toEqual(0);
1391
- expect(ifc.familyUpdate).toBe(false); // family itself unchanged
1392
- });
1393
- it('keeps the family reachable when only concept option rows are in the baseline', () => {
1394
- const ppa = buildPpa();
1395
- const assortmentItems = [optionRow(maroonId, afterSince), optionRow(limeId, afterSince)];
1396
- const fullChangeMap = ppa.getFullChangeAssortmentMap({ assortmentItems });
1397
- // derive the gate the way production does, rather than seeding it
1398
- const releasedIds = ppa.getReleasedForDevelopmentItemAndFamilyIds({ assortmentItems }, []);
1399
- expect(releasedIds).toEqual([familyId]);
1400
- const pcd = buildPcd(releasedIds);
1401
- pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, noDetailChanges(), fullChangeMap, new Map());
1402
- expect(pcd.itemFamilyChanges.size).toEqual(1);
1403
- const ifc = pcd.itemFamilyChanges.get(familyId);
1404
- expect(ifc.itemFamilyObject.id).toEqual(familyId);
1405
- expect(ifc.colorSuppressed.length).toEqual(2);
1406
- expect(ppa.getProjectItem(ifc, familyId)).toEqual({ id: 'pi-' + familyId, updatedOn: beforeSince });
1407
- });
1408
- it('does not suppress-record an unchanged concept option', () => {
1409
- const ppa = buildPpa();
1410
- const fullChangeMap = ppa.getFullChangeAssortmentMap({
1411
- assortmentItems: [familyRow(), optionRow(maroonId, beforeSince)]
1412
- });
1413
- const pcd = buildPcd([familyId]);
1414
- pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, noDetailChanges(), fullChangeMap, new Map());
1415
- const ifc = pcd.itemFamilyChanges.get(familyId);
1416
- expect(ifc.colorSuppressed.length).toEqual(0);
1417
- });
1418
- it('picks up a concept option listed in the change detail even when timestamps are stale', () => {
1419
- const ppa = buildPpa();
1420
- const fullChangeMap = ppa.getFullChangeAssortmentMap({
1421
- assortmentItems: [familyRow(), optionRow(maroonId, beforeSince)]
1422
- });
1423
- const details = noDetailChanges();
1424
- details.updates = [{ id: maroonId }];
1425
- const pcd = buildPcd([familyId]);
1426
- pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, details, fullChangeMap, new Map());
1427
- expect(pcd.itemFamilyChanges.get(familyId).colorSuppressed).toEqual([maroonId]);
1428
- });
1429
- it('skips everything when the family itself is pre-development', () => {
1430
- const ppa = buildPpa();
1431
- const fullChangeMap = ppa.getFullChangeAssortmentMap({
1432
- assortmentItems: [familyRow(afterSince, 'concept'), optionRow(maroonId, afterSince)]
1433
- });
1434
- const pcd = buildPcd([]); // nothing released
1435
- pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, noDetailChanges(), fullChangeMap, new Map());
1436
- expect(pcd.itemFamilyChanges.size).toEqual(0);
1437
- });
1438
- it('still classifies released options normally', () => {
1439
- const ppa = buildPpa();
1440
- const fullChangeMap = ppa.getFullChangeAssortmentMap({
1441
- assortmentItems: [familyRow(), optionRow(maroonId, afterSince, 'released')]
1442
- });
1443
- const details = noDetailChanges();
1444
- details.adds = [{ id: maroonId }];
1445
- const pcd = buildPcd([familyId, maroonId]);
1446
- pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, details, fullChangeMap, new Map());
1447
- const ifc = pcd.itemFamilyChanges.get(familyId);
1448
- expect(ifc.colorAdds).toEqual([maroonId]);
1449
- expect(ifc.colorSuppressed.length).toEqual(0);
1450
- });
1451
- it('publishes a family-only row whose lifecycle just flipped (item-level edit)', () => {
1452
- const ppa = buildPpa();
1453
- const fullChangeMap = ppa.getFullChangeAssortmentMap({
1454
- assortmentItems: [familyRow(afterSince)] // item.updatedOn moved, projectItem did not
1455
- });
1456
- const pcd = buildPcd([familyId]);
1457
- pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, noDetailChanges(), fullChangeMap, new Map());
1458
- const ifc = pcd.itemFamilyChanges.get(familyId);
1459
- expect(ifc.familyUpdate).toBe(true); // fails without Step 3
1460
- expect(ifc.colorSuppressed.length).toEqual(0);
1461
- });
1462
- it('leaves an unchanged family-only row with no flags', () => {
1463
- const ppa = buildPpa();
1464
- const fullChangeMap = ppa.getFullChangeAssortmentMap({ assortmentItems: [familyRow()] });
1465
- const pcd = buildPcd([familyId]);
1466
- pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, noDetailChanges(), fullChangeMap, new Map());
1467
- const ifc = pcd.itemFamilyChanges.get(familyId);
1468
- expect(ifc.familyAdd).toBe(false);
1469
- expect(ifc.familyUpdate).toBe(false);
1470
- expect(ifc.familyDelete).toBe(false);
1471
- expect(ifc.colorSuppressed.length).toEqual(0);
1472
- });
1473
- it('does not set familyUpdate when the family item was removed', () => {
1474
- const ppa = buildPpa();
1475
- const fullChangeMap = ppa.getFullChangeAssortmentMap({ assortmentItems: [familyRow(afterSince)] });
1476
- const details = noDetailChanges();
1477
- details.familyItemsRemoved = [{ itemId: familyId }]; // note: itemId, not id
1478
- const pcd = buildPcd([familyId]);
1479
- pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, details, fullChangeMap, new Map());
1480
- const ifc = pcd.itemFamilyChanges.get(familyId);
1481
- expect(ifc.familyItemRemoved).toBe(true);
1482
- expect(ifc.familyUpdate).toBe(false); // fails if the !familyItemRemoved guard is dropped
1483
- expect(ifc.assortmentItemFullChangeMap.size).toEqual(1); // third pass must not re-insert the row
1484
- });
1485
- it('flags the family when its row is removed but option rows remain', () => {
1486
- const ppa = buildPpa();
1487
- const assortmentItems = [optionRow(maroonId, beforeSince), optionRow(limeId, beforeSince, 'released')];
1488
- const fullChangeMap = ppa.getFullChangeAssortmentMap({ assortmentItems });
1489
- const details = { ...noDetailChanges(), familyItemsRemoved: [{ itemId: familyId, item: familyItem() }] };
1490
- const pcd = buildPcd([familyId, limeId]);
1491
- pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, details, fullChangeMap, new Map());
1492
- const ifc = pcd.itemFamilyChanges.get(familyId);
1493
- expect(ifc.familyItemRemoved).toBe(true); // false before the third pass
1494
- expect(ifc.itemFamilyObject.id).toEqual(familyId);
1495
- expect(ifc.colorDeletes.length).toEqual(0); // no SKU event should follow
1496
- expect(ifc.assortmentItemFullChangeMap.has(familyId)).toBe(false); // removed row carries no projectItem
1497
- });
1498
- it('leaves a hard-deleted family row to the delete pass', () => {
1499
- const ppa = buildPpa();
1500
- const fullChangeMap = ppa.getFullChangeAssortmentMap({ assortmentItems: [] });
1501
- const deleteMap = new Map([[familyId, familyRow()]]);
1502
- const details = { ...noDetailChanges(), familyItemsRemoved: [{ itemId: familyId, item: familyItem() }] };
1503
- const pcd = buildPcd([familyId]);
1504
- pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, details, fullChangeMap, deleteMap);
1505
- const ifc = pcd.itemFamilyChanges.get(familyId);
1506
- expect(ifc.familyDelete).toBe(true); // set by the delete pass
1507
- expect(ifc.familyItemRemoved).toBe(false); // third pass skipped it - no double handling
1508
- });
1509
- });
1510
1340
  describe('getProjectItem', () => {
1511
1341
  const config = {
1512
1342
  identifierAtts: {
@@ -1670,70 +1500,6 @@ describe('meetsCriteria', () => {
1670
1500
  expect(results).toBeTruthy();
1671
1501
  });
1672
1502
  });
1673
- describe('getReleasedForDevelopmentItemAndFamilyIds', () => {
1674
- const familyId = 'AERfdvdAt1HHEVvQ';
1675
- const maroonId = 'YwWKVoL_vGN8khLy';
1676
- const limeId = 'rsmWhWiXRNv_XBJG';
1677
- const buildPpa = (preDev = ['concept']) => {
1678
- const config = {
1679
- identifierAtts: { LCSSeason: ['flexPLMSeasonName'] },
1680
- itemPreDevelopmentLifecycleStages: preDev
1681
- };
1682
- const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
1683
- const dc = new data_converter_1.DataConverter(config, mapFileUtil);
1684
- return new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
1685
- };
1686
- const familyItem = (lifecycleStage = 'released') => ({
1687
- id: familyId, itemFamilyId: familyId, lifecycleStage, name: 'Sweatpant'
1688
- });
1689
- // option row as the hydrated baseline actually delivers it: itemFamily present
1690
- const optionRow = (id, lifecycleStage = 'concept', family = familyItem()) => ({
1691
- itemId: id,
1692
- item: { id, itemFamilyId: familyId, lifecycleStage, itemFamily: family }
1693
- });
1694
- const familyRow = (lifecycleStage = 'released') => ({
1695
- itemId: familyId,
1696
- item: familyItem(lifecycleStage)
1697
- });
1698
- const run = (ppa, assortmentItems) => ppa.getReleasedForDevelopmentItemAndFamilyIds({ assortmentItems }, []);
1699
- it('surfaces the family when every option is pre-development', () => {
1700
- const ppa = buildPpa();
1701
- expect(run(ppa, [optionRow(maroonId), optionRow(limeId)])).toEqual([familyId]);
1702
- });
1703
- it('does not duplicate the family id when a family row and a released option both exist', () => {
1704
- const ppa = buildPpa();
1705
- const ids = run(ppa, [familyRow(), optionRow(limeId, 'released')]);
1706
- expect(ids.filter(id => id === familyId)).toHaveLength(1);
1707
- });
1708
- it('still resolves the family when an earlier row lacks itemFamily hydration', () => {
1709
- const ppa = buildPpa();
1710
- const noFamilyHydration = {
1711
- itemId: maroonId,
1712
- item: { id: maroonId, itemFamilyId: familyId, lifecycleStage: 'concept' }
1713
- };
1714
- expect(run(ppa, [noFamilyHydration, optionRow(limeId)])).toEqual([familyId]);
1715
- });
1716
- it('returns nothing when the family itself is pre-development', () => {
1717
- const ppa = buildPpa();
1718
- expect(run(ppa, [optionRow(maroonId, 'concept', familyItem('concept'))])).toEqual([]);
1719
- });
1720
- it('includes a family-only row exactly once', () => {
1721
- const ppa = buildPpa();
1722
- expect(run(ppa, [familyRow()])).toEqual([familyId]);
1723
- });
1724
- it('includes a released option and its family, excluding the concept option', () => {
1725
- const ppa = buildPpa();
1726
- const ids = run(ppa, [optionRow(maroonId, 'concept'), optionRow(limeId, 'released')]);
1727
- expect(ids).toContain(familyId);
1728
- expect(ids).toContain(limeId);
1729
- expect(ids).not.toContain(maroonId);
1730
- });
1731
- it('treats the stage list as case-sensitive (documents the Concept vs concept trap)', () => {
1732
- const ppa = buildPpa(['Concept']); // as mis-configured in praveen-dev-1
1733
- const ids = run(ppa, [optionRow(maroonId), optionRow(limeId)]);
1734
- expect(ids).toEqual([maroonId, familyId, limeId]); // filter inert: everything qualifies
1735
- });
1736
- });
1737
1503
  describe('getEventsForItemFamilyChanges - conditional eventType', () => {
1738
1504
  const config = {
1739
1505
  identifierAtts: {
@@ -1939,35 +1705,6 @@ describe('getEventsForItemFamilyChanges - conditional eventType', () => {
1939
1705
  // Verify SKU event called with assortment entity
1940
1706
  expect(type_conversion_utils_1.TypeConversionUtils.isOutboundCreatableFromEntity).toHaveBeenNthCalledWith(2, undefined, mapFileUtil, colorProjectItem, { item: itemData, assortment });
1941
1707
  });
1942
- it('should emit only an LCSProductSeasonLink when the family row was removed', async () => {
1943
- const bppa = new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
1944
- const itemFamilyId = 'AERfdvdAt1HHEVvQ';
1945
- const prodEntityData = { id: itemFamilyId, identifier: 'FAM-11', itemNumber: 'FAM-11' };
1946
- //const projectItem = { id: 'proj-fam', updatedOn: '2023-01-15T00:00:00.000Z' };
1947
- const projectItem = { id: 'proj-fam', updatedOn: '2022-12-01T00:00:00.000Z' };
1948
- const assortment = { id: 'assort123', name: 'Fall 2023 Assortment', publishToFlexPLM: true };
1949
- const seasonFed = {
1950
- entityReference: 'assortment:assort123',
1951
- objectClass: 'LCSSeason',
1952
- flexPLMSeasonName: 'Fall 2023'
1953
- };
1954
- const itemFamilyChanges = new item_family_changes_1.ItemFamilyChanges(itemFamilyId, new Date('2023-01-01T00:00:00.000Z'));
1955
- itemFamilyChanges.familyItemRemoved = true; // the only trigger
1956
- itemFamilyChanges.colorUnchanged.push('rsmWhWiXRNv_XBJG'); // surviving option must not emit
1957
- itemFamilyChanges.itemFamilyObject = prodEntityData;
1958
- jest.spyOn(bppa, 'getAssortment').mockResolvedValue(assortment);
1959
- jest.spyOn(bppa, 'getProjectItem').mockReturnValue(projectItem);
1960
- jest.spyOn(bppa, 'getSeasonalData').mockResolvedValue({ someData: 'value' });
1961
- jest.spyOn(map_utils_1.MapUtil, 'applyTransformMap').mockResolvedValue({ transformedData: 'value' });
1962
- jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierProperties').mockResolvedValue(['identifier']);
1963
- jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getInformationalProperties').mockResolvedValue([]);
1964
- jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'isOutboundCreatableFromEntity').mockResolvedValue(false);
1965
- const events = await bppa.getEventsForItemFamilyChanges(itemFamilyChanges, 'assort123', seasonFed, new Map());
1966
- expect(events).toHaveLength(1);
1967
- expect(events[0].objectClass).toBe('LCSProductSeasonLink');
1968
- expect(events[0].eventType).toBe('UPDATE_ON_SEASON');
1969
- expect(events[0].entityReference).toBe('item:' + itemFamilyId);
1970
- });
1971
1708
  });
1972
1709
  describe('sendToFlexPLM / handleVibeIQFile / sendPublishPayloadEvent', () => {
1973
1710
  const config = {
@@ -63,7 +63,6 @@ class FlexPLMConnect {
63
63
  const nonce_key = body.items[0].attributes.nonce_key;
64
64
  const nonce = body.items[0].attributes.nonce;
65
65
  console.info('nonce_key: ' + nonce_key);
66
- console.info('nonce: ' + nonce);
67
66
  return {
68
67
  nonce_key,
69
68
  nonce
@@ -89,7 +88,10 @@ class FlexPLMConnect {
89
88
  console.info('Request:');
90
89
  console.info('vibeEventsURL: ' + vibeEventsURL);
91
90
  const logOptions = JSON.parse(JSON.stringify(xferOptions));
92
- logOptions['headers']['Authorization'] = logOptions?.headers?.Authorization.substring(0, 9);
91
+ if (logOptions?.headers?.Authorization)
92
+ logOptions.headers.Authorization = '***masked***';
93
+ if (csrf?.nonce_key)
94
+ logOptions.headers[csrf.nonce_key] = '***masked***';
93
95
  console.info('csrfOptions: ' + JSON.stringify(logOptions));
94
96
  console.info('Making call to xfer data to FlexPLM');
95
97
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const flexplm_connect_1 = require("./flexplm-connect");
4
+ const app_framework_1 = require("@contrail/app-framework");
4
5
  const mockJsonData = { items: [{ id: '123', name: 'test' }] };
5
6
  const mockResponse = (status, json = mockJsonData) => ({
6
7
  status,
@@ -87,3 +88,22 @@ describe('FlexPLMConnect.getRequest', () => {
87
88
  await expect(connect.getRequest()).rejects.toThrow('Error getting json data from FlexPLM: Invalid JSON');
88
89
  });
89
90
  });
91
+ describe('FlexPLMConnect.sendRequest', () => {
92
+ it('should mask csrf nonce value in logged request options', async () => {
93
+ const loggerIsInfoOnSpy = jest.spyOn(app_framework_1.Logger, 'isInfoOn').mockReturnValue(true);
94
+ const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation();
95
+ const csrf = { nonce_key: 'CSRF_NONCE', nonce: 'raw-secret-nonce' };
96
+ const payload = { id: '123' };
97
+ const connect = new flexplm_connect_1.FlexPLMConnect(createConfig());
98
+ await connect.sendRequest(csrf, payload);
99
+ const csrfOptionsLogMessages = consoleInfoSpy.mock.calls
100
+ .map(([message]) => String(message ?? ''))
101
+ .filter(message => message.startsWith('csrfOptions: '));
102
+ const sendRequestOptionsLogMessage = csrfOptionsLogMessages.find(message => message.includes('"CSRF_NONCE":'));
103
+ expect(sendRequestOptionsLogMessage).toBeDefined();
104
+ expect(sendRequestOptionsLogMessage ?? '').not.toContain('raw-secret-nonce');
105
+ expect(sendRequestOptionsLogMessage ?? '').toContain('"CSRF_NONCE":"***masked***"');
106
+ loggerIsInfoOnSpy.mockRestore();
107
+ consoleInfoSpy.mockRestore();
108
+ });
109
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrail/flexplm",
3
- "version": "1.7.4-alpha.ad2bc4e",
3
+ "version": "1.7.4-alpha.b0c1671",
4
4
  "description": "Library used for integration with flexplm.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",