@contrail/flexplm 1.7.4-alpha.2fb7e1f → 1.7.4-alpha.458d29b

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,6 +12,7 @@ export declare class ItemFamilyChanges {
12
12
  colorDeletes: string[];
13
13
  colorUpdates: string[];
14
14
  colorUnchanged: string[];
15
+ colorSuppressed: string[];
15
16
  assortmentItemFullChangeMap: Map<string, any>;
16
17
  assortmentItemDeleteMap: Map<string, any>;
17
18
  itemToFederatedIdMapping: Map<string, string>;
@@ -14,6 +14,7 @@ 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
17
18
  this.assortmentItemFullChangeMap = new Map();
18
19
  this.assortmentItemDeleteMap = new Map();
19
20
  this.itemToFederatedIdMapping = new Map();
@@ -32,6 +33,7 @@ class ItemFamilyChanges {
32
33
  s += ' colorDeletes: [' + this.colorDeletes + ']\n';
33
34
  s += ' colorUpdates: [' + this.colorUpdates + ']\n';
34
35
  s += ' colorUnchanged: [' + this.colorUnchanged + ']\n';
36
+ s += ' colorSuppressed: [' + this.colorSuppressed + ']\n';
35
37
  s += 'assortmentItemFullChangeMap:\n';
36
38
  s += 'size: ' + this.assortmentItemFullChangeMap.size + '\n[';
37
39
  for (const key of this.assortmentItemFullChangeMap.keys()) {
@@ -29,8 +29,7 @@ class BaseProcessPublishAssortment {
29
29
  let publisher;
30
30
  const assortmentId = event.assortmentId;
31
31
  try {
32
- console.info('process-start: assortmentId: ' + assortmentId
33
- + ', assortmentPublishChangeId: ' + assortmentPublishChangeId);
32
+ console.info('process-start!');
34
33
  let seasonFed;
35
34
  try {
36
35
  seasonFed = await this.getSeasonFederation(assortmentId);
@@ -75,9 +74,9 @@ class BaseProcessPublishAssortment {
75
74
  e.publishInfo = publishInfo;
76
75
  }
77
76
  catch (e2) {
78
- console.error('getPublishInfo failed: ' + e2.message);
77
+ console.log('catch e2: ' + e2.message);
79
78
  }
80
- console.error('process failed: assortmentId: ' + assortmentId + ': ' + e.message);
79
+ console.log('catch e: ' + e.message);
81
80
  throw e;
82
81
  }
83
82
  }
@@ -316,17 +315,19 @@ class BaseProcessPublishAssortment {
316
315
  }
317
316
  async downloadHydratedChangeDetail(assortmentPublishChange) {
318
317
  try {
318
+ console.info('downloadHydratedChangeDetail-start');
319
319
  const link = assortmentPublishChange?.hydratedDetailDownloadLink;
320
320
  const response = await fetch(link);
321
321
  const data = await response.json();
322
322
  return data;
323
323
  }
324
324
  catch (e) {
325
- console.error('Error hydratedDetailDownloadLink: ' + e.message);
325
+ console.log('Error hydratedDetailDownloadLink: ' + e.message);
326
326
  }
327
327
  return undefined;
328
328
  }
329
329
  async downloadAssortmentBaseline(assortmentPublishChange) {
330
+ console.info('downloadAssortmentBaseline-start');
330
331
  try {
331
332
  const link = assortmentPublishChange?.assortmentBaselineDownloadLink;
332
333
  const response = await fetch(link);
@@ -334,12 +335,15 @@ class BaseProcessPublishAssortment {
334
335
  return data;
335
336
  }
336
337
  catch (e) {
337
- console.error('Error assortmentBaselineDownloadLink: ' + e.message);
338
+ console.log('Error assortmentBaselineDownloadLink: ' + e.message);
338
339
  }
339
340
  return undefined;
340
341
  }
341
342
  async getDeleteChanges(assortmentPublishChange, apcHistory, assortmentBaseline, sinceDate) {
343
+ console.info('getDeleteChanges(): ' + assortmentPublishChange?.id);
344
+ console.info(sinceDate);
342
345
  const currentAPCIndex = apcHistory.findIndex(pc => pc?.id === assortmentPublishChange?.id);
346
+ console.info(' currentAPCIndex: ' + currentAPCIndex);
343
347
  if (currentAPCIndex == 0) {
344
348
  return [];
345
349
  }
@@ -348,13 +352,16 @@ class BaseProcessPublishAssortment {
348
352
  const apcDeletes = await this.downloadDeleteChanges(assortmentPublishChange);
349
353
  let previousApcDate = Date.parse(apcCreatedOn);
350
354
  const sinceDateMilliseconds = sinceDate.getTime();
351
- console.info('getDeleteChanges(): apc: ' + assortmentPublishChange?.id
352
- + ', index: ' + currentAPCIndex + ', previousApcCreatedOn: ' + apcCreatedOn);
355
+ if (app_framework_1.Logger.isInfoOn()) {
356
+ console.info('apcCreatedOn: ' + apcCreatedOn);
357
+ console.info('sinceDate: ' + sinceDate);
358
+ console.info('sinceDateMilliseconds: ' + sinceDateMilliseconds);
359
+ console.info('apcDateMilliseconds: ' + previousApcDate);
360
+ }
353
361
  //if only 1 apc, no processing needed
354
362
  if (sinceDateMilliseconds !== previousApcDate) {
363
+ console.info('sinceDateMilliseconds !== apcDateMilliseconds');
355
364
  const currentAssortmentItemIds = this.getBaselineItemIds(assortmentBaseline);
356
- let scannedCount = 0;
357
- let addedCount = 0;
358
365
  const deleteIds = (apcDeletes.length === 0)
359
366
  ? []
360
367
  : apcDeletes.map(item => item?.itemId);
@@ -369,20 +376,19 @@ class BaseProcessPublishAssortment {
369
376
  ? apcHistory[i - 1]
370
377
  : undefined;
371
378
  const deleteChanges = await this.buildDeleteChanges(workingAPC, previousApc);
379
+ console.info('checking deleteChanges');
372
380
  for (const dItem of deleteChanges) {
373
381
  const dItemId = dItem?.itemId;
374
- scannedCount++;
382
+ console.info(dItemId);
375
383
  if (!currentAssortmentItemIds.includes(dItemId) && !deleteIds.includes(dItemId)) {
376
- addedCount++;
384
+ console.info('adding');
377
385
  deleteIds.push(dItemId);
378
386
  apcDeletes.push(dItem);
379
387
  }
380
388
  }
381
389
  }
382
390
  }
383
- console.info('getDeleteChanges(): baselineItems: ' + currentAssortmentItemIds.length
384
- + ', scanned ' + scannedCount + ' historical delete changes, added ' + addedCount
385
- + ', deleteIds: ' + deleteIds.length);
391
+ console.info('getDeleteChanges()-currentAssortmentItemIds: ' + currentAssortmentItemIds);
386
392
  console.info('getDeleteChanges()-deleteIds: ' + deleteIds);
387
393
  }
388
394
  return apcDeletes;
@@ -396,6 +402,7 @@ class BaseProcessPublishAssortment {
396
402
  return itemIds;
397
403
  }
398
404
  async downloadDeleteChanges(apc) {
405
+ console.info('deleteDataDownloadLink-start');
399
406
  try {
400
407
  const link = apc?.deleteDataDownloadLink;
401
408
  const response = await fetch(link);
@@ -403,17 +410,19 @@ class BaseProcessPublishAssortment {
403
410
  return data;
404
411
  }
405
412
  catch (e) {
406
- console.error('Error deleteDataDownloadLink: ' + e.message);
413
+ console.log('Error deleteDataDownloadLink: ' + e.message);
407
414
  }
408
415
  return undefined;
409
416
  }
410
417
  async buildDeleteChanges(apc, previousApc) {
418
+ console.info('buildDeleteChanges()');
411
419
  if (apc?.deleteDataDownloadLink) {
412
420
  const deleteChanges = await this.downloadDeleteChanges(apc);
413
421
  if (deleteChanges) {
414
422
  return deleteChanges;
415
423
  }
416
424
  }
425
+ console.info('pulling down full APC');
417
426
  apc = await this.downloadAssortmentPublishChange(apc?.assortmentId, apc?.id);
418
427
  if (apc?.deleteDataDownloadLink) {
419
428
  const deleteChanges = await this.downloadDeleteChanges(apc);
@@ -425,18 +434,19 @@ class BaseProcessPublishAssortment {
425
434
  throw new Error(BaseProcessPublishAssortment.NOT_ABLE_TO_PROCESS_DELETE_CHANGES);
426
435
  }
427
436
  let previousBaseline;
437
+ console.info('check previousApc');
428
438
  if (previousApc?.assortmentBaselineDownloadLink) {
429
439
  previousBaseline = await this.downloadAssortmentBaseline(previousApc);
430
440
  }
431
441
  else {
442
+ console.info('previousApc pulling down full APC');
432
443
  previousApc = await this.downloadAssortmentPublishChange(previousApc?.assortmentId, previousApc?.id);
433
444
  previousBaseline = await this.downloadAssortmentBaseline(previousApc);
434
445
  }
435
446
  const deleteIds = apc?.detail?.deletes.map(dItem => dItem?.id);
436
447
  //building deletes based on previous baseline; because some APCs don't have delete data
437
448
  const deleteArray = previousBaseline?.assortmentItems.filter(aItem => deleteIds.includes(aItem?.itemId));
438
- console.info('buildDeleteChanges(): no deleteDataDownloadLink on apc ' + apc?.id
439
- + '; rebuilding ' + deleteArray.length + ' deletes from previous baseline');
449
+ console.info('deleteArray.length: ' + deleteArray.length);
440
450
  return deleteArray;
441
451
  }
442
452
  async getItemFederatedIds( /*itemIds*/) {
@@ -474,6 +484,7 @@ class BaseProcessPublishAssortment {
474
484
  return deleteItems;
475
485
  }
476
486
  getReleasedForDevelopmentItemAndFamilyIds(fullChange, deleteChanges) {
487
+ console.info('getReleasedForDevelopmentItemAndFamilyIds');
477
488
  const releasedForDevelopmentItemIds = [];
478
489
  const itemFamilySet = new Set();
479
490
  const assortmentItemsArray = fullChange?.assortmentItems;
@@ -484,13 +495,18 @@ class BaseProcessPublishAssortment {
484
495
  const itemFamilyId = item?.itemFamilyId;
485
496
  if (meetsCriteria) {
486
497
  releasedForDevelopmentItemIds.push(itemId);
487
- if (!itemFamilySet.has(itemFamilyId) && itemId !== itemFamilyId) {
488
- const familyItem = item?.itemFamily;
489
- const familyItemMeetsCriteria = this.meetsCriteria({ item: familyItem });
490
- if (familyItemMeetsCriteria) {
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)) {
491
507
  releasedForDevelopmentItemIds.push(itemFamilyId);
492
508
  }
493
- itemFamilySet.add(itemFamilyId);
509
+ itemFamilySet.add(itemFamilyId); // only mark resolved when we had data to judge
494
510
  }
495
511
  }
496
512
  }
@@ -525,6 +541,7 @@ class BaseProcessPublishAssortment {
525
541
  return !!lifecycleStage && !this.config.itemPreDevelopmentLifecycleStages.includes(lifecycleStage);
526
542
  }
527
543
  async processPublish(pcd, changeDetail, fullChange, deleteChanges) {
544
+ console.info('processPublish-start');
528
545
  const event = {
529
546
  assortmentId: pcd.assortmentId,
530
547
  assortmentPublishChangeId: pcd.assortmentPublishChangeId
@@ -708,6 +725,7 @@ class BaseProcessPublishAssortment {
708
725
  + '-' + d.getUTCMilliseconds();
709
726
  }
710
727
  getItemFamilyChanges(pcd, changeDetail, assortmentItemFullChangeMap, assortmentItemDeleteMap) {
728
+ console.info('getItemFamilyChanges-start');
711
729
  const itemFamilyChanges = new Map();
712
730
  const { adds, deletes, updates, familyItemsRemoved } = changeDetail;
713
731
  const addIds = adds.map(item => item.id);
@@ -717,7 +735,7 @@ class BaseProcessPublishAssortment {
717
735
  for (const [itemId, aItem] of assortmentItemFullChangeMap) {
718
736
  const projectItem = aItem?.projectItem;
719
737
  const itemFamilyId = aItem?.item?.itemFamilyId;
720
- if (!pcd.releasedForDevelopmentItemIds.includes(itemFamilyId) || !pcd.releasedForDevelopmentItemIds.includes(itemId)) {
738
+ if (!pcd.releasedForDevelopmentItemIds.includes(itemFamilyId)) {
721
739
  continue;
722
740
  }
723
741
  const ifc = itemFamilyChanges.get(itemFamilyId) || new item_family_changes_1.ItemFamilyChanges(itemFamilyId, pcd.sinceDate);
@@ -729,6 +747,20 @@ class BaseProcessPublishAssortment {
729
747
  if (pcd.itemToFederatedIdMapping.has(itemId)) {
730
748
  ifc.itemToFederatedIdMapping.set(itemId, pcd.itemToFederatedIdMapping.get(itemId));
731
749
  }
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
732
764
  if (itemId === itemFamilyId) {
733
765
  if (addIds.includes(itemId))
734
766
  ifc.familyAdd = true;
@@ -744,6 +776,12 @@ class BaseProcessPublishAssortment {
744
776
  ifc.familyUpdate = true;
745
777
  }
746
778
  }
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
747
785
  if (projectItem && !projectItem?.roles && aItem?.item?.roles) {
748
786
  projectItem.roles = aItem?.item?.roles;
749
787
  }
@@ -772,7 +810,8 @@ class BaseProcessPublishAssortment {
772
810
  }
773
811
  else {
774
812
  if (!item) {
775
- console.error('Failed to find deleted item entity: itemFamilyId: ' + itemFamilyId + ', itemId: ' + itemId);
813
+ console.error('Failed to find deleted item entity');
814
+ console.error(' itemFamilyId: ' + itemFamilyId + ' -itemId: ' + itemId);
776
815
  continue;
777
816
  }
778
817
  }
@@ -809,6 +848,7 @@ class BaseProcessPublishAssortment {
809
848
  return itemFamilyChanges;
810
849
  }
811
850
  async getEventsForPublishChangeData(publishChangeData) {
851
+ console.info('getEventsForPublishChangeData-start');
812
852
  const seasonalPayloads = [];
813
853
  for (const itemFamilyChange of publishChangeData.itemFamilyChanges.values()) {
814
854
  const events = await this.getEventsForItemFamilyChanges(itemFamilyChange, publishChangeData.assortmentId, publishChangeData.seasonFed, publishChangeData.itemToFederatedIdMapping);
@@ -816,8 +856,6 @@ class BaseProcessPublishAssortment {
816
856
  seasonalPayloads.push(...events);
817
857
  }
818
858
  }
819
- console.info('getEventsForPublishChangeData: itemFamilies: '
820
- + publishChangeData.itemFamilyChanges.size + ', payloads: ' + seasonalPayloads.length);
821
859
  return seasonalPayloads;
822
860
  }
823
861
  /**Returns the events for a given ItemFamilyChanges object
@@ -837,12 +875,15 @@ class BaseProcessPublishAssortment {
837
875
  * @returns
838
876
  */
839
877
  async getEventsForItemFamilyChanges(itemFamilyChanges, assortmentId, seasonFed, itemToFederatedIdMapping) {
878
+ console.info('getEventsForItemFamilyChanges()');
840
879
  const events = [];
841
880
  const LCSSeason = Object.assign({}, seasonFed);
842
881
  const assortment = await this.getAssortment(assortmentId);
843
882
  const projectItem = this.getProjectItem(itemFamilyChanges, itemFamilyChanges.itemFamilyId);
883
+ // ---- NEW Step to enable ItemFamily publish ET-243: added color suppressed length to existing condition, no other changes on familyAssortmentItem boolean
844
884
  const familyAssortmentItem = itemFamilyChanges.familyAdd || itemFamilyChanges.familyUpdate || itemFamilyChanges.familyDelete
845
885
  || itemFamilyChanges.colorAdds.length > 0 || itemFamilyChanges.colorUpdates.length > 0 || itemFamilyChanges.colorDeletes.length > 0
886
+ || itemFamilyChanges.colorSuppressed.length > 0
846
887
  || (projectItem && this.updatedSinceDate(projectItem, itemFamilyChanges.sinceDate));
847
888
  //familyItemRemoved is used when adding the first option to an assortment
848
889
  //and will have updates for the family item.
@@ -1337,6 +1337,151 @@ 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
+ });
1484
+ });
1340
1485
  describe('getProjectItem', () => {
1341
1486
  const config = {
1342
1487
  identifierAtts: {
@@ -1500,6 +1645,70 @@ describe('meetsCriteria', () => {
1500
1645
  expect(results).toBeTruthy();
1501
1646
  });
1502
1647
  });
1648
+ describe('getReleasedForDevelopmentItemAndFamilyIds', () => {
1649
+ const familyId = 'AERfdvdAt1HHEVvQ';
1650
+ const maroonId = 'YwWKVoL_vGN8khLy';
1651
+ const limeId = 'rsmWhWiXRNv_XBJG';
1652
+ const buildPpa = (preDev = ['concept']) => {
1653
+ const config = {
1654
+ identifierAtts: { LCSSeason: ['flexPLMSeasonName'] },
1655
+ itemPreDevelopmentLifecycleStages: preDev
1656
+ };
1657
+ const mapFileUtil = new transform_data_1.MapFileUtil(new sdk_1.Entities());
1658
+ const dc = new data_converter_1.DataConverter(config, mapFileUtil);
1659
+ return new base_process_publish_assortment_1.BaseProcessPublishAssortment(config, dc, mapFileUtil);
1660
+ };
1661
+ const familyItem = (lifecycleStage = 'released') => ({
1662
+ id: familyId, itemFamilyId: familyId, lifecycleStage, name: 'Sweatpant'
1663
+ });
1664
+ // option row as the hydrated baseline actually delivers it: itemFamily present
1665
+ const optionRow = (id, lifecycleStage = 'concept', family = familyItem()) => ({
1666
+ itemId: id,
1667
+ item: { id, itemFamilyId: familyId, lifecycleStage, itemFamily: family }
1668
+ });
1669
+ const familyRow = (lifecycleStage = 'released') => ({
1670
+ itemId: familyId,
1671
+ item: familyItem(lifecycleStage)
1672
+ });
1673
+ const run = (ppa, assortmentItems) => ppa.getReleasedForDevelopmentItemAndFamilyIds({ assortmentItems }, []);
1674
+ it('surfaces the family when every option is pre-development', () => {
1675
+ const ppa = buildPpa();
1676
+ expect(run(ppa, [optionRow(maroonId), optionRow(limeId)])).toEqual([familyId]);
1677
+ });
1678
+ it('does not duplicate the family id when a family row and a released option both exist', () => {
1679
+ const ppa = buildPpa();
1680
+ const ids = run(ppa, [familyRow(), optionRow(limeId, 'released')]);
1681
+ expect(ids.filter(id => id === familyId)).toHaveLength(1);
1682
+ });
1683
+ it('still resolves the family when an earlier row lacks itemFamily hydration', () => {
1684
+ const ppa = buildPpa();
1685
+ const noFamilyHydration = {
1686
+ itemId: maroonId,
1687
+ item: { id: maroonId, itemFamilyId: familyId, lifecycleStage: 'concept' }
1688
+ };
1689
+ expect(run(ppa, [noFamilyHydration, optionRow(limeId)])).toEqual([familyId]);
1690
+ });
1691
+ it('returns nothing when the family itself is pre-development', () => {
1692
+ const ppa = buildPpa();
1693
+ expect(run(ppa, [optionRow(maroonId, 'concept', familyItem('concept'))])).toEqual([]);
1694
+ });
1695
+ it('includes a family-only row exactly once', () => {
1696
+ const ppa = buildPpa();
1697
+ expect(run(ppa, [familyRow()])).toEqual([familyId]);
1698
+ });
1699
+ it('includes a released option and its family, excluding the concept option', () => {
1700
+ const ppa = buildPpa();
1701
+ const ids = run(ppa, [optionRow(maroonId, 'concept'), optionRow(limeId, 'released')]);
1702
+ expect(ids).toContain(familyId);
1703
+ expect(ids).toContain(limeId);
1704
+ expect(ids).not.toContain(maroonId);
1705
+ });
1706
+ it('treats the stage list as case-sensitive (documents the Concept vs concept trap)', () => {
1707
+ const ppa = buildPpa(['Concept']); // as mis-configured in praveen-dev-1
1708
+ const ids = run(ppa, [optionRow(maroonId), optionRow(limeId)]);
1709
+ expect(ids).toEqual([maroonId, familyId, limeId]); // filter inert: everything qualifies
1710
+ });
1711
+ });
1503
1712
  describe('getEventsForItemFamilyChanges - conditional eventType', () => {
1504
1713
  const config = {
1505
1714
  identifierAtts: {
@@ -175,12 +175,18 @@ class DataConverter {
175
175
  }
176
176
  async getObjectReferenceValue(prop, newData, inflateObjRef = false) {
177
177
  const slug = prop['slug'];
178
+ if (app_framework_1.Logger.isDebugOn()) {
179
+ console.debug('getObjectReferenceValue-prop: ' + slug);
180
+ }
178
181
  let value = newData[slug];
179
182
  const entityType = prop['referencedTypeRootSlug'];
180
183
  const entityId = newData[slug + 'Id'];
181
184
  if ((!value || typeof value === 'string') && inflateObjRef) {
182
185
  if (entityId) {
183
186
  if (this.objRefCache[entityId]) {
187
+ if (app_framework_1.Logger.isDebugOn()) {
188
+ console.debug('cache hit: ' + entityId);
189
+ }
184
190
  return this.objRefCache[entityId];
185
191
  }
186
192
  const criteria = {
@@ -426,6 +432,9 @@ class DataConverter {
426
432
  return "";
427
433
  }
428
434
  if (this.objRefCache[ctx.cacheKey]) {
435
+ if (app_framework_1.Logger.isDebugOn()) {
436
+ console.debug(`object reference cache hit: ${ctx.cacheKey}`);
437
+ }
429
438
  return this.objRefCache[ctx.cacheKey];
430
439
  }
431
440
  const objectReferenceId = ctx.useIdentityService
@@ -625,6 +634,9 @@ class DataConverter {
625
634
  }
626
635
  let cacheUser = DataConverter.getFromStaticCache(nd.email);
627
636
  if (cacheUser) {
637
+ if (app_framework_1.Logger.isDebugOn()) {
638
+ console.debug('user cache hit: ' + nd.email);
639
+ }
628
640
  await this.processGroupMemberCheck(prop, nd.email);
629
641
  return cacheUser;
630
642
  }
@@ -694,12 +706,18 @@ class DataConverter {
694
706
  */
695
707
  async getUserListValue(prop, newData) {
696
708
  const slug = prop['slug'];
709
+ if (app_framework_1.Logger.isDebugOn()) {
710
+ console.debug('getUserListValue-prop: ' + slug);
711
+ }
697
712
  const entityId = newData[slug + 'Id'];
698
713
  if (!entityId) {
699
714
  return {};
700
715
  }
701
716
  const cacheUser = DataConverter.getFromStaticCache(entityId);
702
717
  if (cacheUser) {
718
+ if (app_framework_1.Logger.isDebugOn()) {
719
+ console.debug('user cache hit: ' + entityId);
720
+ }
703
721
  return Object.assign({}, cacheUser);
704
722
  }
705
723
  const user = await this.getUserById(entityId);
package/package.json CHANGED
@@ -1,66 +1,66 @@
1
- {
2
- "name": "@contrail/flexplm",
3
- "version": "1.7.4-alpha.2fb7e1f",
4
- "description": "Library used for integration with flexplm.",
5
- "main": "lib/index.js",
6
- "types": "lib/index.d.ts",
7
- "bin": {
8
- "flexplm-mapping": "lib/cli/index.js"
9
- },
10
- "files": [
11
- "lib/**/*",
12
- "scripts/copy-template.js"
13
- ],
14
- "scripts": {
15
- "build": "tsc; node scripts/copy-template.js",
16
- "build:win": "tsc && node scripts/copy-template.js",
17
- "format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"",
18
- "lint": "tslint -p tsconfig.json",
19
- "test": "jest",
20
- "test-watch": "jest --watch",
21
- "test-debug": "jest --runInBand",
22
- "test-coverage": "jest --coverage"
23
- },
24
- "keywords": [],
25
- "author": "VibeIQ",
26
- "license": "ISC",
27
- "devDependencies": {
28
- "@types/jest": "^29.5.2",
29
- "jest": "^29.5.0",
30
- "prettier": "^1.19.1",
31
- "ts-jest": "^29.1.1",
32
- "tslint": "^5.11.0",
33
- "tslint-config-prettier": "^1.18.0",
34
- "typescript": "^4.0.0"
35
- },
36
- "peerDependencies": {
37
- "typescript": ">=4.0.0"
38
- },
39
- "peerDependenciesMeta": {
40
- "typescript": {
41
- "optional": true
42
- }
43
- },
44
- "jest": {
45
- "moduleFileExtensions": [
46
- "js",
47
- "json",
48
- "ts"
49
- ],
50
- "rootDir": "src",
51
- "testRegex": ".spec.ts$",
52
- "transform": {
53
- "^.+\\.(t|j)s$": "ts-jest"
54
- },
55
- "coverageDirectory": "../coverage",
56
- "testEnvironment": "node"
57
- },
58
- "dependencies": {
59
- "@contrail/app-framework": "^1.4.3",
60
- "@contrail/sdk": "^1.5.10",
61
- "@contrail/transform-data": "^1.3.2",
62
- "@contrail/util": "^1.3.1",
63
- "axios": "^1.4.0",
64
- "p-limit": "^3.1.0"
65
- }
66
- }
1
+ {
2
+ "name": "@contrail/flexplm",
3
+ "version": "1.7.4-alpha.458d29b",
4
+ "description": "Library used for integration with flexplm.",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "bin": {
8
+ "flexplm-mapping": "lib/cli/index.js"
9
+ },
10
+ "files": [
11
+ "lib/**/*",
12
+ "scripts/copy-template.js"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc; node scripts/copy-template.js",
16
+ "build:win": "tsc && node scripts/copy-template.js",
17
+ "format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"",
18
+ "lint": "tslint -p tsconfig.json",
19
+ "test": "jest",
20
+ "test-watch": "jest --watch",
21
+ "test-debug": "jest --runInBand",
22
+ "test-coverage": "jest --coverage"
23
+ },
24
+ "keywords": [],
25
+ "author": "VibeIQ",
26
+ "license": "ISC",
27
+ "devDependencies": {
28
+ "@types/jest": "^29.5.2",
29
+ "jest": "^29.5.0",
30
+ "prettier": "^1.19.1",
31
+ "ts-jest": "^29.1.1",
32
+ "tslint": "^5.11.0",
33
+ "tslint-config-prettier": "^1.18.0",
34
+ "typescript": "^4.0.0"
35
+ },
36
+ "peerDependencies": {
37
+ "typescript": ">=4.0.0"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "typescript": {
41
+ "optional": true
42
+ }
43
+ },
44
+ "jest": {
45
+ "moduleFileExtensions": [
46
+ "js",
47
+ "json",
48
+ "ts"
49
+ ],
50
+ "rootDir": "src",
51
+ "testRegex": ".spec.ts$",
52
+ "transform": {
53
+ "^.+\\.(t|j)s$": "ts-jest"
54
+ },
55
+ "coverageDirectory": "../coverage",
56
+ "testEnvironment": "node"
57
+ },
58
+ "dependencies": {
59
+ "@contrail/app-framework": "^1.4.3",
60
+ "@contrail/sdk": "^1.5.10",
61
+ "@contrail/transform-data": "^1.3.2",
62
+ "@contrail/util": "^1.3.1",
63
+ "axios": "^1.4.0",
64
+ "p-limit": "^3.1.0"
65
+ }
66
+ }