@contrail/flexplm 1.7.4-alpha.458d29b → 1.7.4-alpha.57ee2a7

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.
@@ -15,6 +15,12 @@ export interface FCConfig {
15
15
  vibeEventEndpoint: string;
16
16
  csrfEndpoint: string;
17
17
  useDistinctRestEndPointForImages?: boolean;
18
+ /** Opt-in. When true, an item family that itself passes meetsCriteria() is published to the
19
+ * season even when every one of its options is still in a pre-development lifecycle stage. */
20
+ publishReleasedFamilyWithPreDevelopmentOptions?: boolean;
21
+ /** Only meaningful when publishReleasedFamilyWithPreDevelopmentOptions is true. When true, such a
22
+ * family is republished on EVERY publish, not only when one of its options actually changed. */
23
+ alwaysPublishReleasedFamilyWithPreDevelopmentOptions?: boolean;
18
24
  itemPreDevelopmentLifecycleStages: string[];
19
25
  identifierAtts?: {
20
26
  [key: string]: string[];
@@ -12,7 +12,12 @@ export declare class ItemFamilyChanges {
12
12
  colorDeletes: string[];
13
13
  colorUpdates: string[];
14
14
  colorUnchanged: string[];
15
- colorSuppressed: string[];
15
+ /** Options that changed but are still pre-development. They do NOT produce
16
+ * LCSSKUSeasonLink events - they only signal that the family needs republishing. */
17
+ preDevelopmentOptionChanges: string[];
18
+ /** True when at least one option row was tracked at family level only, because the option is
19
+ * pre-development while the family itself is released. */
20
+ hasPreDevelopmentOnlyOptions: boolean;
16
21
  assortmentItemFullChangeMap: Map<string, any>;
17
22
  assortmentItemDeleteMap: Map<string, any>;
18
23
  itemToFederatedIdMapping: Map<string, string>;
@@ -14,7 +14,12 @@ 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
+ /** Options that changed but are still pre-development. They do NOT produce
18
+ * LCSSKUSeasonLink events - they only signal that the family needs republishing. */
19
+ this.preDevelopmentOptionChanges = [];
20
+ /** True when at least one option row was tracked at family level only, because the option is
21
+ * pre-development while the family itself is released. */
22
+ this.hasPreDevelopmentOnlyOptions = false;
18
23
  this.assortmentItemFullChangeMap = new Map();
19
24
  this.assortmentItemDeleteMap = new Map();
20
25
  this.itemToFederatedIdMapping = new Map();
@@ -33,7 +38,8 @@ class ItemFamilyChanges {
33
38
  s += ' colorDeletes: [' + this.colorDeletes + ']\n';
34
39
  s += ' colorUpdates: [' + this.colorUpdates + ']\n';
35
40
  s += ' colorUnchanged: [' + this.colorUnchanged + ']\n';
36
- s += ' colorSuppressed: [' + this.colorSuppressed + ']\n';
41
+ s += ' preDevelopmentOptionChanges: [' + this.preDevelopmentOptionChanges + ']\n';
42
+ s += ' hasPreDevelopmentOnlyOptions: ' + this.hasPreDevelopmentOnlyOptions + '\n';
37
43
  s += 'assortmentItemFullChangeMap:\n';
38
44
  s += 'size: ' + this.assortmentItemFullChangeMap.size + '\n[';
39
45
  for (const key of this.assortmentItemFullChangeMap.keys()) {
@@ -62,6 +62,11 @@ export declare class BaseProcessPublishAssortment {
62
62
  getFullChangeAssortmentMap(fullChange: any): Map<string, object>;
63
63
  getDeleteChangesAssortmentMap(deleteChanges: any[]): Map<string, object>;
64
64
  getReleasedForDevelopmentItemAndFamilyIds(fullChange: any, deleteChanges: any): string[];
65
+ /** Adds item families that pass meetsCriteria() but whose option rows are all pre-development.
66
+ * Only ever ADDS family ids - never option ids, and never removes anything. */
67
+ protected addReleasedFamiliesOfPreDevelopmentOptions(aItems: any[], releasedForDevelopmentItemIds: string[]): void;
68
+ protected publishReleasedFamilyWithPreDevOptions(): boolean;
69
+ protected alwaysPublishReleasedFamilyWithPreDevOptions(): boolean;
65
70
  meetsCriteria(aItem: any): boolean;
66
71
  processPublish(pcd: PublishChangeData, changeDetail: any, fullChange: any, deleteChanges: any): Promise<{
67
72
  results: {
@@ -11,6 +11,7 @@ const fsPromise = require("fs/promises");
11
11
  const path = require("path");
12
12
  const app_framework_1 = require("@contrail/app-framework");
13
13
  const event_short_message_status_1 = require("../util/event-short-message-status");
14
+ const config_defaults_1 = require("../util/config-defaults");
14
15
  class BaseProcessPublishAssortment {
15
16
  constructor(_config, _dc, _mapFileUtil) {
16
17
  this.TTL = 30 * 24 * 60 * 60 * 1000; // 30 days
@@ -495,18 +496,13 @@ class BaseProcessPublishAssortment {
495
496
  const itemFamilyId = item?.itemFamilyId;
496
497
  if (meetsCriteria) {
497
498
  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)) {
499
+ if (!itemFamilySet.has(itemFamilyId) && itemId !== itemFamilyId) {
500
+ const familyItem = item?.itemFamily;
501
+ const familyItemMeetsCriteria = this.meetsCriteria({ item: familyItem });
502
+ if (familyItemMeetsCriteria) {
507
503
  releasedForDevelopmentItemIds.push(itemFamilyId);
508
504
  }
509
- itemFamilySet.add(itemFamilyId); // only mark resolved when we had data to judge
505
+ itemFamilySet.add(itemFamilyId);
510
506
  }
511
507
  }
512
508
  }
@@ -532,9 +528,43 @@ class BaseProcessPublishAssortment {
532
528
  }
533
529
  }
534
530
  }
531
+ if (this.publishReleasedFamilyWithPreDevOptions()) {
532
+ this.addReleasedFamiliesOfPreDevelopmentOptions(assortmentItemsArray, releasedForDevelopmentItemIds);
533
+ this.addReleasedFamiliesOfPreDevelopmentOptions(deleteChanges, releasedForDevelopmentItemIds);
534
+ }
535
535
  console.info('releasedForDevelopmentItemIds: ' + releasedForDevelopmentItemIds);
536
536
  return releasedForDevelopmentItemIds;
537
537
  }
538
+ /** Adds item families that pass meetsCriteria() but whose option rows are all pre-development.
539
+ * Only ever ADDS family ids - never option ids, and never removes anything. */
540
+ addReleasedFamiliesOfPreDevelopmentOptions(aItems, releasedForDevelopmentItemIds) {
541
+ for (const aItem of (aItems || [])) {
542
+ const item = aItem?.item;
543
+ const itemId = item?.id;
544
+ const itemFamilyId = item?.itemFamilyId;
545
+ if (!itemFamilyId || itemId === itemFamilyId) {
546
+ continue;
547
+ }
548
+ if (releasedForDevelopmentItemIds.includes(itemFamilyId)) {
549
+ continue;
550
+ }
551
+ if (this.meetsCriteria(aItem)) {
552
+ continue;
553
+ }
554
+ const familyItem = item?.itemFamily;
555
+ if (familyItem && this.meetsCriteria({ item: familyItem })) {
556
+ console.info('adding released family with pre-development options: ' + itemFamilyId);
557
+ releasedForDevelopmentItemIds.push(itemFamilyId);
558
+ }
559
+ }
560
+ }
561
+ publishReleasedFamilyWithPreDevOptions() {
562
+ return config_defaults_1.ConfigDefaults.isPropertyTrue(this.config?.publishReleasedFamilyWithPreDevelopmentOptions);
563
+ }
564
+ alwaysPublishReleasedFamilyWithPreDevOptions() {
565
+ return this.publishReleasedFamilyWithPreDevOptions()
566
+ && config_defaults_1.ConfigDefaults.isPropertyTrue(this.config?.alwaysPublishReleasedFamilyWithPreDevelopmentOptions);
567
+ }
538
568
  meetsCriteria(aItem) {
539
569
  const item = aItem?.item;
540
570
  const lifecycleStage = item?.lifecycleStage;
@@ -735,7 +765,12 @@ class BaseProcessPublishAssortment {
735
765
  for (const [itemId, aItem] of assortmentItemFullChangeMap) {
736
766
  const projectItem = aItem?.projectItem;
737
767
  const itemFamilyId = aItem?.item?.itemFamilyId;
738
- if (!pcd.releasedForDevelopmentItemIds.includes(itemFamilyId)) {
768
+ const familyInScope = pcd.releasedForDevelopmentItemIds.includes(itemFamilyId);
769
+ const itemInScope = pcd.releasedForDevelopmentItemIds.includes(itemId);
770
+ // Released family + pre-development option: track at family level only (no SKU events).
771
+ const familyOnly = !itemInScope && familyInScope && itemId !== itemFamilyId
772
+ && this.publishReleasedFamilyWithPreDevOptions();
773
+ if (!familyInScope || (!itemInScope && !familyOnly)) {
739
774
  continue;
740
775
  }
741
776
  const ifc = itemFamilyChanges.get(itemFamilyId) || new item_family_changes_1.ItemFamilyChanges(itemFamilyId, pcd.sinceDate);
@@ -743,24 +778,29 @@ class BaseProcessPublishAssortment {
743
778
  ifc.itemFamilyObject = itemId === itemFamilyId ? aItem?.item : aItem?.item?.itemFamily;
744
779
  itemFamilyChanges.set(itemFamilyId, ifc);
745
780
  }
781
+ else if (itemId === itemFamilyId && this.publishReleasedFamilyWithPreDevOptions()) {
782
+ // A pre-development option row may have created this bucket with the partial
783
+ // item.itemFamily; the family's own row carries the full item, so prefer it.
784
+ ifc.itemFamilyObject = aItem?.item;
785
+ }
746
786
  ifc.assortmentItemFullChangeMap.set(itemId, aItem);
747
787
  if (pcd.itemToFederatedIdMapping.has(itemId)) {
748
788
  ifc.itemToFederatedIdMapping.set(itemId, pcd.itemToFederatedIdMapping.get(itemId));
749
789
  }
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);
790
+ if (familyOnly) {
791
+ ifc.hasPreDevelopmentOnlyOptions = true;
792
+ if (addIds.includes(itemId) || deleteIds.includes(itemId) || updateIds.includes(itemId)
793
+ || (projectItem && new Date(projectItem.updatedOn) > pcd.sinceDate)) {
794
+ ifc.preDevelopmentOptionChanges.push(itemId);
795
+ }
796
+ else {
797
+ ifc.colorUnchanged.push(itemId);
798
+ }
799
+ if (projectItem && !projectItem?.roles && aItem?.item?.roles) {
800
+ projectItem.roles = aItem?.item?.roles;
760
801
  }
761
802
  continue;
762
803
  }
763
- // NEW Step - ends
764
804
  if (itemId === itemFamilyId) {
765
805
  if (addIds.includes(itemId))
766
806
  ifc.familyAdd = true;
@@ -776,12 +816,6 @@ class BaseProcessPublishAssortment {
776
816
  ifc.familyUpdate = true;
777
817
  }
778
818
  }
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
819
  if (projectItem && !projectItem?.roles && aItem?.item?.roles) {
786
820
  projectItem.roles = aItem?.item?.roles;
787
821
  }
@@ -805,7 +839,11 @@ class BaseProcessPublishAssortment {
805
839
  for (const [itemId, delEntity] of assortmentItemDeleteMap) {
806
840
  const itemFamilyId = delEntity?.item?.itemFamilyId;
807
841
  const item = delEntity?.item;
808
- if (!pcd.releasedForDevelopmentItemIds.includes(itemFamilyId) || !pcd.releasedForDevelopmentItemIds.includes(itemId)) {
842
+ const familyInScope = pcd.releasedForDevelopmentItemIds.includes(itemFamilyId);
843
+ const itemInScope = pcd.releasedForDevelopmentItemIds.includes(itemId);
844
+ const familyOnly = !itemInScope && familyInScope && itemId !== itemFamilyId
845
+ && this.publishReleasedFamilyWithPreDevOptions();
846
+ if (!familyInScope || (!itemInScope && !familyOnly)) {
809
847
  continue;
810
848
  }
811
849
  else {
@@ -835,6 +873,10 @@ class BaseProcessPublishAssortment {
835
873
  if (itemId === itemFamilyId) {
836
874
  ifc.familyDelete = true;
837
875
  }
876
+ else if (familyOnly) {
877
+ ifc.hasPreDevelopmentOnlyOptions = true;
878
+ ifc.preDevelopmentOptionChanges.push(itemId);
879
+ }
838
880
  else {
839
881
  ifc.colorDeletes.push(itemId);
840
882
  }
@@ -880,10 +922,10 @@ class BaseProcessPublishAssortment {
880
922
  const LCSSeason = Object.assign({}, seasonFed);
881
923
  const assortment = await this.getAssortment(assortmentId);
882
924
  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
884
925
  const familyAssortmentItem = itemFamilyChanges.familyAdd || itemFamilyChanges.familyUpdate || itemFamilyChanges.familyDelete
885
926
  || itemFamilyChanges.colorAdds.length > 0 || itemFamilyChanges.colorUpdates.length > 0 || itemFamilyChanges.colorDeletes.length > 0
886
- || itemFamilyChanges.colorSuppressed.length > 0
927
+ || itemFamilyChanges.preDevelopmentOptionChanges.length > 0
928
+ || (this.alwaysPublishReleasedFamilyWithPreDevOptions() && itemFamilyChanges.hasPreDevelopmentOnlyOptions)
887
929
  || (projectItem && this.updatedSinceDate(projectItem, itemFamilyChanges.sinceDate));
888
930
  //familyItemRemoved is used when adding the first option to an assortment
889
931
  //and will have updates for the family item.
@@ -1337,151 +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
- });
1484
- });
1485
1340
  describe('getProjectItem', () => {
1486
1341
  const config = {
1487
1342
  identifierAtts: {
@@ -1645,70 +1500,6 @@ describe('meetsCriteria', () => {
1645
1500
  expect(results).toBeTruthy();
1646
1501
  });
1647
1502
  });
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
- });
1712
1503
  describe('getEventsForItemFamilyChanges - conditional eventType', () => {
1713
1504
  const config = {
1714
1505
  identifierAtts: {
@@ -2083,3 +1874,259 @@ describe('sendToFlexPLM / handleVibeIQFile / sendPublishPayloadEvent', () => {
2083
1874
  });
2084
1875
  });
2085
1876
  });
1877
+ describe('publishReleasedFamilyWithPreDevelopmentOptions', () => {
1878
+ const FAMILY_ID = 'AERfdvdAt1HHEVvQ';
1879
+ const LIME_ID = 'limeGreenItemId__';
1880
+ const MAROON_ID = 'maroonItemId_____';
1881
+ const SINCE = new Date('2026-09-03T16:54:20.778Z');
1882
+ const BEFORE_SINCE = '2026-09-01T00:00:00.000Z';
1883
+ const AFTER_SINCE = '2026-09-03T17:00:00.000Z';
1884
+ /** The full family item, as it appears on the family's own assortment row. */
1885
+ const fullFamilyItem = {
1886
+ id: FAMILY_ID, itemFamilyId: FAMILY_ID, name: 'Sweatpant', itemNumber: 11,
1887
+ lifecycleStage: 'released', roles: ['family'], richFamilyOnlyField: 'PRESENT'
1888
+ };
1889
+ /** The partial family item, as hydrated onto an option row. */
1890
+ const partialFamilyItem = {
1891
+ id: FAMILY_ID, itemFamilyId: FAMILY_ID, name: 'Sweatpant', itemNumber: 11,
1892
+ lifecycleStage: 'released', roles: ['family']
1893
+ };
1894
+ const familyRow = () => ({
1895
+ entityType: 'assortment-item',
1896
+ id: 'ai-' + FAMILY_ID,
1897
+ itemId: FAMILY_ID,
1898
+ item: { ...fullFamilyItem },
1899
+ projectItem: { id: 'proj:' + FAMILY_ID, itemId: FAMILY_ID, updatedOn: BEFORE_SINCE, roles: ['family'] }
1900
+ });
1901
+ const optionRow = (itemId, optionName, projectItemUpdatedOn) => ({
1902
+ entityType: 'assortment-item',
1903
+ id: 'ai-' + itemId,
1904
+ itemId,
1905
+ item: {
1906
+ id: itemId, itemFamilyId: FAMILY_ID, name: 'Sweatpant', optionName,
1907
+ optionGroup: 'color', lifecycleStage: 'concept', roles: ['color', 'option'],
1908
+ itemFamily: { ...partialFamilyItem }
1909
+ },
1910
+ projectItem: { id: 'proj:' + itemId, itemId, updatedOn: projectItemUpdatedOn, roles: ['color', 'option'] },
1911
+ familyProjectItem: { id: 'proj:' + FAMILY_ID, itemId: FAMILY_ID, updatedOn: BEFORE_SINCE, roles: ['family'] }
1912
+ });
1913
+ const emptyDetail = { adds: [], deletes: [], updates: [], familyItemsRemoved: [] };
1914
+ const baseConfig = { identifierAtts: { LCSProduct: ['itemNumber'] }, itemPreDevelopmentLifecycleStages: ['concept'] };
1915
+ const makePpa2 = (flagOn, alwaysOn) => {
1916
+ const cfg = {
1917
+ ...baseConfig,
1918
+ publishReleasedFamilyWithPreDevelopmentOptions: flagOn,
1919
+ alwaysPublishReleasedFamilyWithPreDevelopmentOptions: alwaysOn
1920
+ };
1921
+ const mfu = new transform_data_1.MapFileUtil(new sdk_1.Entities());
1922
+ return new base_process_publish_assortment_1.BaseProcessPublishAssortment(cfg, new data_converter_1.DataConverter(cfg, mfu), mfu);
1923
+ };
1924
+ const makePpa = (flagOn) => makePpa2(flagOn, false);
1925
+ const makePcd = (releasedIds) => {
1926
+ const sf = { entityReference: 'assortment:11EZML5dMn3tREuT', objectClass: 'LCSSeason' };
1927
+ const pcd = new publish_change_data_1.PublishChangeData('11EZML5dMn3tREuT', sf, 'apc1', SINCE);
1928
+ pcd.itemToFederatedIdMapping = new Map();
1929
+ pcd.releasedForDevelopmentItemIds = releasedIds;
1930
+ return pcd;
1931
+ };
1932
+ const eventSeasonFed = { entityReference: 'assortment:a1', objectClass: 'LCSSeason' };
1933
+ const stubEventDeps = (ppa) => {
1934
+ jest.spyOn(ppa, 'getAssortment').mockResolvedValue({ id: 'a1', publishToFlexPLM: true });
1935
+ jest.spyOn(ppa, 'getSeasonalData').mockResolvedValue({});
1936
+ jest.spyOn(map_utils_1.MapUtil, 'applyTransformMap').mockImplementation(async (...args) => args[2]);
1937
+ jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getIdentifierProperties').mockResolvedValue(['itemNumber']);
1938
+ jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'getInformationalProperties').mockResolvedValue([]);
1939
+ jest.spyOn(type_conversion_utils_1.TypeConversionUtils, 'isOutboundCreatableFromEntity').mockResolvedValue(true);
1940
+ };
1941
+ afterEach(() => { jest.restoreAllMocks(); });
1942
+ /////////////////////////////////////////////////////////////////////////////
1943
+ // getReleasedForDevelopmentItemAndFamilyIds
1944
+ /////////////////////////////////////////////////////////////////////////////
1945
+ it('REGRESSION: flag off - released family with all-concept options stays out of scope', () => {
1946
+ const fullChange = { assortmentItems: [optionRow(LIME_ID, 'Lime Green', BEFORE_SINCE),
1947
+ optionRow(MAROON_ID, 'Maroon', BEFORE_SINCE)] };
1948
+ expect(makePpa(false).getReleasedForDevelopmentItemAndFamilyIds(fullChange, [])).toEqual([]);
1949
+ });
1950
+ it('flag on - released family enters scope; concept options do NOT', () => {
1951
+ const fullChange = { assortmentItems: [optionRow(LIME_ID, 'Lime Green', BEFORE_SINCE),
1952
+ optionRow(MAROON_ID, 'Maroon', BEFORE_SINCE)] };
1953
+ expect(makePpa(true).getReleasedForDevelopmentItemAndFamilyIds(fullChange, [])).toEqual([FAMILY_ID]);
1954
+ });
1955
+ it('flag on - concept family with concept options still stays out of scope', () => {
1956
+ const row = optionRow(LIME_ID, 'Lime Green', BEFORE_SINCE);
1957
+ row.item.itemFamily = { ...partialFamilyItem, lifecycleStage: 'concept' };
1958
+ expect(makePpa(true).getReleasedForDevelopmentItemAndFamilyIds({ assortmentItems: [row] }, [])).toEqual([]);
1959
+ });
1960
+ it('flag on - family reached only through a delete record', () => {
1961
+ const deleted = optionRow(MAROON_ID, 'Maroon', BEFORE_SINCE);
1962
+ expect(makePpa(true).getReleasedForDevelopmentItemAndFamilyIds({ assortmentItems: [] }, [deleted]))
1963
+ .toEqual([FAMILY_ID]);
1964
+ });
1965
+ it('flag on - two concept options of the same family add the family only once', () => {
1966
+ const fullChange = { assortmentItems: [optionRow(LIME_ID, 'Lime Green', BEFORE_SINCE),
1967
+ optionRow(MAROON_ID, 'Maroon', BEFORE_SINCE)] };
1968
+ expect(makePpa(true).getReleasedForDevelopmentItemAndFamilyIds(fullChange, [])).toHaveLength(1);
1969
+ });
1970
+ it('flag on - option row with no hydrated itemFamily is skipped without throwing', () => {
1971
+ const bare = optionRow(LIME_ID, 'Lime Green', BEFORE_SINCE);
1972
+ delete bare.item.itemFamily;
1973
+ expect(makePpa(true).getReleasedForDevelopmentItemAndFamilyIds({ assortmentItems: [bare] }, [])).toEqual([]);
1974
+ });
1975
+ it('released family WITH its own assortment row is in scope with or without the flag', () => {
1976
+ const rows = { assortmentItems: [familyRow(), optionRow(LIME_ID, 'Lime Green', BEFORE_SINCE)] };
1977
+ expect(makePpa(false).getReleasedForDevelopmentItemAndFamilyIds(rows, [])).toEqual([FAMILY_ID]);
1978
+ expect(makePpa(true).getReleasedForDevelopmentItemAndFamilyIds(rows, [])).toEqual([FAMILY_ID]);
1979
+ });
1980
+ it('string flag values from app config are honoured', () => {
1981
+ const fullChange = { assortmentItems: [optionRow(LIME_ID, 'Lime Green', BEFORE_SINCE)] };
1982
+ expect(makePpa('true').getReleasedForDevelopmentItemAndFamilyIds(fullChange, [])).toEqual([FAMILY_ID]);
1983
+ expect(makePpa('false').getReleasedForDevelopmentItemAndFamilyIds(fullChange, [])).toEqual([]);
1984
+ });
1985
+ /////////////////////////////////////////////////////////////////////////////
1986
+ // getItemFamilyChanges
1987
+ /////////////////////////////////////////////////////////////////////////////
1988
+ it('ACTION 2: deleting a concept option marks the released family for republish', () => {
1989
+ const ppa = makePpa(true);
1990
+ const remaining = optionRow(LIME_ID, 'Lime Green', BEFORE_SINCE);
1991
+ const deleted = optionRow(MAROON_ID, 'Maroon', BEFORE_SINCE);
1992
+ const pcd = makePcd([FAMILY_ID]);
1993
+ pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, emptyDetail, ppa.getFullChangeAssortmentMap({ assortmentItems: [remaining] }), ppa.getDeleteChangesAssortmentMap([deleted]));
1994
+ expect(pcd.itemFamilyChanges.size).toEqual(1);
1995
+ const ifc = pcd.itemFamilyChanges.get(FAMILY_ID);
1996
+ expect(ifc.preDevelopmentOptionChanges).toEqual([MAROON_ID]);
1997
+ expect(ifc.hasPreDevelopmentOnlyOptions).toBe(true);
1998
+ expect(ifc.colorDeletes).toEqual([]);
1999
+ expect(ifc.colorAdds).toEqual([]);
2000
+ expect(ifc.itemFamilyObject.id).toEqual(FAMILY_ID);
2001
+ });
2002
+ it('adding a concept option marks the family, not a colorAdd', () => {
2003
+ const ppa = makePpa(true);
2004
+ const added = optionRow(MAROON_ID, 'Maroon', BEFORE_SINCE);
2005
+ const pcd = makePcd([FAMILY_ID]);
2006
+ pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, { ...emptyDetail, adds: [{ id: MAROON_ID }] }, ppa.getFullChangeAssortmentMap({ assortmentItems: [added] }), new Map());
2007
+ const ifc = pcd.itemFamilyChanges.get(FAMILY_ID);
2008
+ expect(ifc.preDevelopmentOptionChanges).toEqual([MAROON_ID]);
2009
+ expect(ifc.colorAdds).toEqual([]);
2010
+ });
2011
+ it('ACTION 1: nothing changed - family bucket exists but signals no change', () => {
2012
+ const ppa = makePpa(true);
2013
+ const pcd = makePcd([FAMILY_ID]);
2014
+ const rows = { assortmentItems: [optionRow(LIME_ID, 'Lime Green', BEFORE_SINCE),
2015
+ optionRow(MAROON_ID, 'Maroon', BEFORE_SINCE)] };
2016
+ pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, emptyDetail, ppa.getFullChangeAssortmentMap(rows), new Map());
2017
+ const ifc = pcd.itemFamilyChanges.get(FAMILY_ID);
2018
+ expect(ifc.preDevelopmentOptionChanges).toEqual([]);
2019
+ expect(ifc.colorUnchanged.sort()).toEqual([LIME_ID, MAROON_ID].sort());
2020
+ });
2021
+ it('REGRESSION: flag off - concept option rows produce no ItemFamilyChanges at all', () => {
2022
+ const ppa = makePpa(false);
2023
+ const pcd = makePcd([FAMILY_ID]);
2024
+ pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, emptyDetail, ppa.getFullChangeAssortmentMap({ assortmentItems: [optionRow(LIME_ID, 'Lime Green', AFTER_SINCE)] }), new Map());
2025
+ expect(pcd.itemFamilyChanges.size).toEqual(0);
2026
+ });
2027
+ it('REGRESSION: a released option is still a colorAdd, never a preDevelopmentOptionChange', () => {
2028
+ const ppa = makePpa(true);
2029
+ const released = optionRow(LIME_ID, 'Lime Green', BEFORE_SINCE);
2030
+ released.item.lifecycleStage = 'development';
2031
+ const pcd = makePcd([FAMILY_ID, LIME_ID]);
2032
+ pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, { ...emptyDetail, adds: [{ id: LIME_ID }] }, ppa.getFullChangeAssortmentMap({ assortmentItems: [released] }), new Map());
2033
+ const ifc = pcd.itemFamilyChanges.get(FAMILY_ID);
2034
+ expect(ifc.colorAdds).toEqual([LIME_ID]);
2035
+ expect(ifc.preDevelopmentOptionChanges).toEqual([]);
2036
+ expect(ifc.hasPreDevelopmentOnlyOptions).toBe(false);
2037
+ });
2038
+ it('itemFamilyObject prefers the full family item when the OPTION row is iterated first', () => {
2039
+ const ppa = makePpa(true);
2040
+ const pcd = makePcd([FAMILY_ID]);
2041
+ const map = new Map();
2042
+ map.set(LIME_ID, optionRow(LIME_ID, 'Lime Green', BEFORE_SINCE));
2043
+ map.set(FAMILY_ID, familyRow());
2044
+ pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, emptyDetail, map, new Map());
2045
+ const ifc = pcd.itemFamilyChanges.get(FAMILY_ID);
2046
+ expect(ifc.itemFamilyObject.richFamilyOnlyField).toEqual('PRESENT');
2047
+ });
2048
+ it('itemFamilyObject is the full family item when the FAMILY row is iterated first', () => {
2049
+ const ppa = makePpa(true);
2050
+ const pcd = makePcd([FAMILY_ID]);
2051
+ const map = new Map();
2052
+ map.set(FAMILY_ID, familyRow());
2053
+ map.set(LIME_ID, optionRow(LIME_ID, 'Lime Green', BEFORE_SINCE));
2054
+ pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, emptyDetail, map, new Map());
2055
+ const ifc = pcd.itemFamilyChanges.get(FAMILY_ID);
2056
+ expect(ifc.itemFamilyObject.richFamilyOnlyField).toEqual('PRESENT');
2057
+ });
2058
+ it('delete record with a missing item entity is skipped safely', () => {
2059
+ const ppa = makePpa(true);
2060
+ const pcd = makePcd([FAMILY_ID]);
2061
+ const delMap = new Map();
2062
+ delMap.set('GHOST', { itemId: 'GHOST', item: undefined });
2063
+ pcd.itemFamilyChanges = ppa.getItemFamilyChanges(pcd, emptyDetail, new Map(), delMap);
2064
+ expect(pcd.itemFamilyChanges.size).toEqual(0);
2065
+ });
2066
+ /////////////////////////////////////////////////////////////////////////////
2067
+ // getEventsForItemFamilyChanges
2068
+ /////////////////////////////////////////////////////////////////////////////
2069
+ it('emits exactly one LCSProductSeasonLink and zero LCSSKUSeasonLink', async () => {
2070
+ const ppa = makePpa(true);
2071
+ const ifc = new item_family_changes_1.ItemFamilyChanges(FAMILY_ID, SINCE);
2072
+ ifc.itemFamilyObject = fullFamilyItem;
2073
+ ifc.hasPreDevelopmentOnlyOptions = true;
2074
+ ifc.preDevelopmentOptionChanges.push(MAROON_ID);
2075
+ ifc.assortmentItemFullChangeMap.set(LIME_ID, optionRow(LIME_ID, 'Lime Green', BEFORE_SINCE));
2076
+ stubEventDeps(ppa);
2077
+ const events = await ppa.getEventsForItemFamilyChanges(ifc, 'a1', eventSeasonFed, new Map());
2078
+ expect(events).toHaveLength(1);
2079
+ expect(events[0].objectClass).toBe('LCSProductSeasonLink');
2080
+ expect(events[0].eventType).toBe('UPSERT_ON_SEASON');
2081
+ expect(events[0].entityReference).toBe('item:' + FAMILY_ID);
2082
+ expect(events[0].LCSProduct['itemNumber']).toBe(11);
2083
+ });
2084
+ it('REGRESSION: an ItemFamilyChanges with no changes still emits nothing', async () => {
2085
+ const ppa = makePpa(true);
2086
+ const ifc = new item_family_changes_1.ItemFamilyChanges(FAMILY_ID, SINCE);
2087
+ ifc.itemFamilyObject = fullFamilyItem;
2088
+ ifc.colorUnchanged.push(LIME_ID, MAROON_ID);
2089
+ ifc.assortmentItemFullChangeMap.set(LIME_ID, optionRow(LIME_ID, 'Lime Green', BEFORE_SINCE));
2090
+ stubEventDeps(ppa);
2091
+ expect(await ppa.getEventsForItemFamilyChanges(ifc, 'a1', eventSeasonFed, new Map())).toHaveLength(0);
2092
+ });
2093
+ /////////////////////////////////////////////////////////////////////////////
2094
+ // alwaysPublishReleasedFamilyWithPreDevelopmentOptions
2095
+ /////////////////////////////////////////////////////////////////////////////
2096
+ const unchangedFamilyIfc = () => {
2097
+ const ifc = new item_family_changes_1.ItemFamilyChanges(FAMILY_ID, SINCE);
2098
+ ifc.itemFamilyObject = fullFamilyItem;
2099
+ ifc.hasPreDevelopmentOnlyOptions = true;
2100
+ ifc.colorUnchanged.push(LIME_ID, MAROON_ID);
2101
+ ifc.assortmentItemFullChangeMap.set(LIME_ID, optionRow(LIME_ID, 'Lime Green', BEFORE_SINCE));
2102
+ return ifc;
2103
+ };
2104
+ it('ACTION 1 with always-flag: unchanged family still emits one LCSProductSeasonLink', async () => {
2105
+ const ppa = makePpa2(true, true);
2106
+ stubEventDeps(ppa);
2107
+ const events = await ppa.getEventsForItemFamilyChanges(unchangedFamilyIfc(), 'a1', eventSeasonFed, new Map());
2108
+ expect(events).toHaveLength(1);
2109
+ expect(events[0].objectClass).toBe('LCSProductSeasonLink');
2110
+ });
2111
+ it('always-flag off: unchanged family emits nothing', async () => {
2112
+ const ppa = makePpa2(true, false);
2113
+ stubEventDeps(ppa);
2114
+ expect(await ppa.getEventsForItemFamilyChanges(unchangedFamilyIfc(), 'a1', eventSeasonFed, new Map()))
2115
+ .toHaveLength(0);
2116
+ });
2117
+ it('always-flag cannot act alone when the primary flag is off', async () => {
2118
+ const ppa = makePpa2(false, true);
2119
+ stubEventDeps(ppa);
2120
+ expect(await ppa.getEventsForItemFamilyChanges(unchangedFamilyIfc(), 'a1', eventSeasonFed, new Map()))
2121
+ .toHaveLength(0);
2122
+ });
2123
+ it('REGRESSION: always-flag does not republish an ordinary unchanged released family', async () => {
2124
+ const ppa = makePpa2(true, true);
2125
+ stubEventDeps(ppa);
2126
+ const ifc = new item_family_changes_1.ItemFamilyChanges(FAMILY_ID, SINCE);
2127
+ ifc.itemFamilyObject = fullFamilyItem;
2128
+ ifc.colorUnchanged.push(LIME_ID);
2129
+ ifc.assortmentItemFullChangeMap.set(LIME_ID, optionRow(LIME_ID, 'Lime Green', BEFORE_SINCE));
2130
+ expect(await ppa.getEventsForItemFamilyChanges(ifc, 'a1', eventSeasonFed, new Map())).toHaveLength(0);
2131
+ });
2132
+ });
@@ -20,6 +20,8 @@ export declare class ConfigDefaults {
20
20
  processAsItem: boolean;
21
21
  };
22
22
  useDistinctRestEndPointForImages: boolean;
23
+ publishReleasedFamilyWithPreDevelopmentOptions: boolean;
24
+ alwaysPublishReleasedFamilyWithPreDevelopmentOptions: boolean;
23
25
  csrfEndpoint: string;
24
26
  vibeEventEndpoint: string;
25
27
  payloadDefaultAsArray: boolean;
@@ -67,6 +67,8 @@ class ConfigDefaults {
67
67
  processAsItem: false,
68
68
  },
69
69
  useDistinctRestEndPointForImages: false,
70
+ publishReleasedFamilyWithPreDevelopmentOptions: false,
71
+ alwaysPublishReleasedFamilyWithPreDevelopmentOptions: false,
70
72
  csrfEndpoint: '/servlet/rest/security/csrf',
71
73
  vibeEventEndpoint: '/rfa/vibeiq/vibeEvents',
72
74
  payloadDefaultAsArray: true,
@@ -393,3 +393,50 @@ describe('all tests', () => {
393
393
  });
394
394
  });
395
395
  });
396
+ describe('publishReleasedFamilyWithPreDevelopmentOptions config', () => {
397
+ const config = {
398
+ apiHost: 'http://test.com',
399
+ userName: 'vibeiq',
400
+ password: 'vibeiq',
401
+ plmEnviornment: 'SB'
402
+ };
403
+ it('publishReleasedFamilyWithPreDevelopmentOptions-get default', async () => {
404
+ const startConfig = Object.assign({}, config);
405
+ const fcConfig = await config_defaults_1.ConfigDefaults.setConfigDefaults(startConfig);
406
+ expect(fcConfig['publishReleasedFamilyWithPreDevelopmentOptions']).toBe(false);
407
+ });
408
+ it('alwaysPublishReleasedFamilyWithPreDevelopmentOptions-get default', async () => {
409
+ const startConfig = Object.assign({}, config);
410
+ const fcConfig = await config_defaults_1.ConfigDefaults.setConfigDefaults(startConfig);
411
+ expect(fcConfig['alwaysPublishReleasedFamilyWithPreDevelopmentOptions']).toBe(false);
412
+ });
413
+ it('publishReleasedFamilyWithPreDevelopmentOptions-set boolean true', async () => {
414
+ const startConfig = Object.assign({}, config);
415
+ startConfig['publishReleasedFamilyWithPreDevelopmentOptions'] = true;
416
+ const fcConfig = await config_defaults_1.ConfigDefaults.setConfigDefaults(startConfig);
417
+ expect(config_defaults_1.ConfigDefaults.isPropertyTrue(fcConfig['publishReleasedFamilyWithPreDevelopmentOptions'])).toBe(true);
418
+ });
419
+ it('publishReleasedFamilyWithPreDevelopmentOptions-set string true from app config', async () => {
420
+ const startConfig = Object.assign({}, config);
421
+ startConfig['publishReleasedFamilyWithPreDevelopmentOptions'] = 'true';
422
+ const fcConfig = await config_defaults_1.ConfigDefaults.setConfigDefaults(startConfig);
423
+ expect(config_defaults_1.ConfigDefaults.isPropertyTrue(fcConfig['publishReleasedFamilyWithPreDevelopmentOptions'])).toBe(true);
424
+ });
425
+ it('publishReleasedFamilyWithPreDevelopmentOptions-string false stays off', async () => {
426
+ const startConfig = Object.assign({}, config);
427
+ startConfig['publishReleasedFamilyWithPreDevelopmentOptions'] = 'false';
428
+ const fcConfig = await config_defaults_1.ConfigDefaults.setConfigDefaults(startConfig);
429
+ expect(config_defaults_1.ConfigDefaults.isPropertyTrue(fcConfig['publishReleasedFamilyWithPreDevelopmentOptions'])).toBe(false);
430
+ });
431
+ it('both flags can be delivered through complexConfig', async () => {
432
+ const startConfig = Object.assign({}, config, {
433
+ complexConfig: {
434
+ publishReleasedFamilyWithPreDevelopmentOptions: true,
435
+ alwaysPublishReleasedFamilyWithPreDevelopmentOptions: 'true'
436
+ }
437
+ });
438
+ const fcConfig = await config_defaults_1.ConfigDefaults.setConfigDefaults(startConfig);
439
+ expect(config_defaults_1.ConfigDefaults.isPropertyTrue(fcConfig['publishReleasedFamilyWithPreDevelopmentOptions'])).toBe(true);
440
+ expect(config_defaults_1.ConfigDefaults.isPropertyTrue(fcConfig['alwaysPublishReleasedFamilyWithPreDevelopmentOptions'])).toBe(true);
441
+ });
442
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrail/flexplm",
3
- "version": "1.7.4-alpha.458d29b",
3
+ "version": "1.7.4-alpha.57ee2a7",
4
4
  "description": "Library used for integration with flexplm.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",