@capillarytech/creatives-library 9.0.54 → 9.0.55-alpha.1

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.
@@ -1,8 +1,8 @@
1
1
  import React from 'react';
2
2
 
3
3
  jest.mock('../../../services/api', () => ({
4
- createCentralCommsMetaId: jest.fn(),
5
- getCentralCommsMetaIds: jest.fn(),
4
+ createCommDefinition: jest.fn(),
5
+ editCommDefinition: jest.fn(),
6
6
  }));
7
7
 
8
8
  jest.mock('../../CreativesContainer', () => function MockCreativesContainer({
@@ -38,7 +38,7 @@ import { IntlProvider } from 'react-intl';
38
38
  import history from '../../../utils/history';
39
39
  import { initialReducer } from '../../../initialReducer';
40
40
  import CommunicationFlow from '../CommunicationFlow';
41
- import { createCentralCommsMetaId, getCentralCommsMetaIds } from '../../../services/api';
41
+ import { createCommDefinition, editCommDefinition } from '../../../services/api';
42
42
  import { getEnabledSteps } from '../utils/getEnabledSteps';
43
43
  import {
44
44
  CHANNELS,
@@ -359,82 +359,313 @@ describe('isSaveDisabled', () => {
359
359
  });
360
360
 
361
361
  describe('handleSave — CCS flow', () => {
362
+ const ccsConfig = { ...baseConfig, context: { name: 'Order Placed Notification' }, features: {} };
363
+
362
364
  beforeEach(() => {
363
- createCentralCommsMetaId.mockResolvedValue({ response: { data: { id: 'meta-123' } } });
364
- getCentralCommsMetaIds.mockResolvedValue({ response: { data: {} } });
365
+ createCommDefinition.mockResolvedValue({
366
+ response: { data: { id: 'cd_123', referenceId: 'ORDER_PLACED_123', status: 'DRAFT', version: { version: 1 } } },
367
+ });
365
368
  });
366
369
 
367
370
  afterEach(() => {
368
371
  jest.clearAllMocks();
369
372
  });
370
373
 
371
- it('calls createCentralCommsMetaId for each content item when useCCS is not false', async () => {
374
+ it('calls createCommDefinition with a SINGLE-strategy payload when useCCS is not false', async () => {
372
375
  const onSave = jest.fn();
373
376
  renderWithFlow({
374
377
  features: {},
378
+ config: ccsConfig,
379
+ // messageBody is the legacy editor's field name for the typed SMS text
380
+ // (see CreativesContainer/index.js's getCreativesData SMS case) — the
381
+ // SMS transform maps it to CCS's `message` field.
375
382
  initialData: {
376
- contentItems: [{ channel: 'sms', templateData: { smsBody: 'Hello' } }],
383
+ contentItems: [{ channel: 'sms', templateData: { messageBody: 'Hello' } }],
377
384
  },
378
385
  onSave,
379
386
  });
380
387
 
381
388
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
382
389
 
383
- await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalledTimes(1));
384
- expect(createCentralCommsMetaId).toHaveBeenCalledWith(
390
+ await waitFor(() => expect(createCommDefinition).toHaveBeenCalledTimes(1));
391
+ expect(createCommDefinition).toHaveBeenCalledWith(
385
392
  expect.objectContaining({
386
- centralCommsPayload: expect.objectContaining({ channel: 'SMS', module: 'CAMPAIGNS' }),
393
+ name: 'Order Placed Notification',
394
+ strategyType: 'SINGLE',
395
+ singleChannelStrategy: expect.objectContaining({
396
+ variant: expect.objectContaining({
397
+ channel: 'SMS',
398
+ smsMessageContent: { channel: 'SMS', message: 'Hello' },
399
+ }),
400
+ }),
387
401
  }),
388
402
  );
389
403
  expect(onSave).toHaveBeenCalledTimes(1);
390
404
  });
391
405
 
392
- it('calls getCentralCommsMetaIds when metaIds are returned from createCentralCommsMetaId', async () => {
406
+ it('transforms SMS content from the legacy editor shape (messageBody) to CCS\'s message field', async () => {
407
+ const onSave = jest.fn();
408
+ renderWithFlow({
409
+ features: {},
410
+ config: ccsConfig,
411
+ initialData: {
412
+ contentItems: [{
413
+ channel: 'SMS',
414
+ templateData: { messageBody: 'Hello {{name}}', channel: 'SMS' },
415
+ }],
416
+ },
417
+ onSave,
418
+ });
419
+
420
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
421
+
422
+ await waitFor(() => expect(createCommDefinition).toHaveBeenCalledTimes(1));
423
+ expect(createCommDefinition).toHaveBeenCalledWith(
424
+ expect.objectContaining({
425
+ singleChannelStrategy: expect.objectContaining({
426
+ variant: expect.objectContaining({
427
+ channel: 'SMS',
428
+ smsMessageContent: {
429
+ channel: 'SMS',
430
+ message: 'Hello {{name}}',
431
+ },
432
+ }),
433
+ }),
434
+ }),
435
+ );
436
+ });
437
+
438
+ it('does not carry the legacy messageBody key into smsMessageContent (regression guard against reverting to pass-through)', async () => {
439
+ const onSave = jest.fn();
440
+ renderWithFlow({
441
+ features: {},
442
+ config: ccsConfig,
443
+ initialData: {
444
+ contentItems: [{
445
+ channel: 'SMS',
446
+ templateData: { messageBody: 'Hello {{name}}', channel: 'SMS' },
447
+ }],
448
+ },
449
+ onSave,
450
+ });
451
+
452
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
453
+
454
+ await waitFor(() => expect(createCommDefinition).toHaveBeenCalledTimes(1));
455
+ const payload = createCommDefinition.mock.calls[0][0];
456
+ expect(payload.singleChannelStrategy.variant.smsMessageContent).not.toHaveProperty('messageBody');
457
+ });
458
+
459
+ it('transforms EMAIL content from the legacy editor shape (emailSubject) to CCS\'s messageSubject', async () => {
460
+ const onSave = jest.fn();
461
+ renderWithFlow({
462
+ features: {},
463
+ config: ccsConfig,
464
+ initialData: {
465
+ contentItems: [{
466
+ channel: 'EMAIL',
467
+ templateData: { emailSubject: 'Order Confirmed', emailBody: '<html>Hi</html>' },
468
+ }],
469
+ },
470
+ onSave,
471
+ });
472
+
473
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
474
+
475
+ await waitFor(() => expect(createCommDefinition).toHaveBeenCalledTimes(1));
476
+ expect(createCommDefinition).toHaveBeenCalledWith(
477
+ expect.objectContaining({
478
+ singleChannelStrategy: expect.objectContaining({
479
+ variant: expect.objectContaining({
480
+ channel: 'EMAIL',
481
+ emailMessageContent: {
482
+ channel: 'EMAIL',
483
+ messageSubject: 'Order Confirmed',
484
+ messageBody: '<html>Hi</html>',
485
+ },
486
+ }),
487
+ }),
488
+ }),
489
+ );
490
+ });
491
+
492
+ it('transforms WEBPUSH content from the legacy editor\'s messageContent.content wrapping to CCS\'s flat shape', async () => {
493
+ const onSave = jest.fn();
494
+ const cta = { type: 'SITE_URL', actionLink: 'https://example.com' };
495
+ const expandableDetails = { media: [{ url: 'https://example.com/banner.png', type: 'IMAGE' }], ctas: [] };
496
+ renderWithFlow({
497
+ features: {},
498
+ config: ccsConfig,
499
+ initialData: {
500
+ contentItems: [{
501
+ channel: 'WEBPUSH',
502
+ templateData: {
503
+ messageContent: {
504
+ content: {
505
+ channel: 'WEBPUSH',
506
+ accountId: 13792,
507
+ content: { title: 'Hi', message: 'Body', iconImageUrl: 'https://example.com/icon.png', cta, expandableDetails },
508
+ messageSubject: 'Subject',
509
+ offers: [],
510
+ },
511
+ },
512
+ },
513
+ }],
514
+ },
515
+ onSave,
516
+ });
517
+
518
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
519
+
520
+ await waitFor(() => expect(createCommDefinition).toHaveBeenCalledTimes(1));
521
+ expect(createCommDefinition).toHaveBeenCalledWith(
522
+ expect.objectContaining({
523
+ singleChannelStrategy: expect.objectContaining({
524
+ variant: expect.objectContaining({
525
+ channel: 'WEBPUSH',
526
+ webPushMessageContent: {
527
+ channel: 'WEBPUSH',
528
+ messageSubject: 'Subject',
529
+ accountId: 13792,
530
+ content: { title: 'Hi', message: 'Body', iconImageUrl: 'https://example.com/icon.png', cta, expandableDetails },
531
+ },
532
+ }),
533
+ }),
534
+ }),
535
+ );
536
+ });
537
+
538
+ it('normalizes the internal MOBILEPUSH channel to CCS\'s PUSH enum (keys stay mpush*), even when templateData/channelSetting carry their own stray channel field', async () => {
539
+ const onSave = jest.fn();
393
540
  renderWithFlow({
394
541
  features: {},
395
- initialData: { contentItems: [{ channel: 'EMAIL', templateData: {} }] },
542
+ config: ccsConfig,
543
+ initialData: {
544
+ // `channel: 'MOBILEPUSH'` inline on templateData reproduces the real
545
+ // production bug: contentItem.templateData carries the UI's own
546
+ // internal channel identifier, which must not win over the
547
+ // normalized 'PUSH' enum once spread into mpushMessageContent.
548
+ contentItems: [{ channel: 'MOBILEPUSH', templateData: { channel: 'MOBILEPUSH', messageSubject: 'Hi' } }],
549
+ // Likewise, deliverySetting.channelSetting.PUSH carrying its own
550
+ // stray `channel` key must not win over the normalized value once
551
+ // spread into mpushDeliverySettings.channelSettings.
552
+ deliverySetting: { channelSetting: { PUSH: { channel: 'MOBILEPUSH', domainId: 2002 } } },
553
+ },
554
+ onSave,
396
555
  });
397
556
 
398
557
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
399
558
 
400
- await waitFor(() => expect(getCentralCommsMetaIds).toHaveBeenCalledWith('meta-123'));
559
+ await waitFor(() => expect(createCommDefinition).toHaveBeenCalledTimes(1));
560
+ expect(createCommDefinition).toHaveBeenCalledWith(
561
+ expect.objectContaining({
562
+ singleChannelStrategy: expect.objectContaining({
563
+ variant: expect.objectContaining({
564
+ channel: 'PUSH',
565
+ mpushMessageContent: { channel: 'PUSH', messageSubject: 'Hi' },
566
+ mpushDeliverySettings: {
567
+ channelSettings: { channel: 'PUSH', domainId: 2002 },
568
+ },
569
+ }),
570
+ }),
571
+ }),
572
+ );
401
573
  });
402
574
 
403
- it('skips getCentralCommsMetaIds when response contains no id', async () => {
404
- createCentralCommsMetaId.mockResolvedValue({ response: { data: {} } });
575
+ it('merges ccsCommDefinition into the data passed to onSave when create succeeds', async () => {
576
+ const onSave = jest.fn();
405
577
  renderWithFlow({
406
578
  features: {},
579
+ config: ccsConfig,
407
580
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
581
+ onSave,
408
582
  });
409
583
 
410
584
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
411
585
 
412
- await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalled());
413
- expect(getCentralCommsMetaIds).not.toHaveBeenCalled();
586
+ await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
587
+ expect(onSave).toHaveBeenCalledWith(
588
+ expect.objectContaining({
589
+ ccsCommDefinition: { id: 'cd_123', referenceId: 'ORDER_PLACED_123', version: 1, status: 'DRAFT' },
590
+ }),
591
+ );
414
592
  });
415
593
 
416
- it('uses ouId and module from config.context when provided', async () => {
594
+ it('preserves a real version of 0 from CCS (not coerced to 1 by a falsy-zero fallback)', async () => {
595
+ const onSave = jest.fn();
596
+ createCommDefinition.mockResolvedValueOnce({
597
+ response: { data: { id: 'cd_123', referenceId: 'ORDER_PLACED_123', status: 'DRAFT', version: { version: 0 } } },
598
+ });
417
599
  renderWithFlow({
418
600
  features: {},
419
- config: { ...baseConfig, context: { ouId: 42, module: 'LOYALTY' }, features: {} },
601
+ config: ccsConfig,
420
602
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
603
+ onSave,
421
604
  });
422
605
 
423
606
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
424
607
 
425
- await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalled());
426
- expect(createCentralCommsMetaId).toHaveBeenCalledWith(
608
+ await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
609
+ expect(onSave).toHaveBeenCalledWith(
427
610
  expect.objectContaining({
428
- centralCommsPayload: expect.objectContaining({ ouId: 42, module: 'LOYALTY' }),
611
+ ccsCommDefinition: { id: 'cd_123', referenceId: 'ORDER_PLACED_123', version: 0, status: 'DRAFT' },
429
612
  }),
430
613
  );
431
614
  });
432
615
 
616
+ it('generates a referenceId from name when config.context.referenceId is absent', async () => {
617
+ // moduleMocks.js spies on Date.now globally, but this config's resetMocks:true
618
+ // clears that return value before every test — set it explicitly here.
619
+ jest.spyOn(Date, 'now').mockReturnValue(1612267539410);
620
+ renderWithFlow({
621
+ features: {},
622
+ config: ccsConfig,
623
+ initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
624
+ });
625
+
626
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
627
+
628
+ await waitFor(() => expect(createCommDefinition).toHaveBeenCalled());
629
+ const payload = createCommDefinition.mock.calls[0][0];
630
+ expect(payload.referenceId).toBe('ORDER_PLACED_NOTIFICATION_1612267539410');
631
+ });
632
+
633
+ it('uses config.context.referenceId and description when provided', async () => {
634
+ renderWithFlow({
635
+ features: {},
636
+ config: { ...ccsConfig, context: { ...ccsConfig.context, referenceId: 'ORDER_PLACED', description: 'desc' } },
637
+ initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
638
+ });
639
+
640
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
641
+
642
+ await waitFor(() => expect(createCommDefinition).toHaveBeenCalled());
643
+ expect(createCommDefinition).toHaveBeenCalledWith(
644
+ expect.objectContaining({ referenceId: 'ORDER_PLACED', description: 'desc' }),
645
+ );
646
+ });
647
+
433
648
  it('skips CCS entirely when useCCS is false', async () => {
434
649
  const onSave = jest.fn();
435
650
  renderWithFlow({
436
651
  features: {},
437
- config: { ...baseConfig, useCCS: false, features: {} },
652
+ config: { ...ccsConfig, useCCS: false },
653
+ initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
654
+ onSave,
655
+ });
656
+
657
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
658
+
659
+ await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
660
+ expect(createCommDefinition).not.toHaveBeenCalled();
661
+ expect(onSave).toHaveBeenCalledWith(expect.not.objectContaining({ ccsCommDefinition: expect.anything() }));
662
+ });
663
+
664
+ it('skips createCommDefinition when config.context.name is absent (mandatory)', async () => {
665
+ const onSave = jest.fn();
666
+ renderWithFlow({
667
+ features: {},
668
+ config: { ...baseConfig, features: {} },
438
669
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
439
670
  onSave,
440
671
  });
@@ -442,14 +673,34 @@ describe('handleSave — CCS flow', () => {
442
673
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
443
674
 
444
675
  await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
445
- expect(createCentralCommsMetaId).not.toHaveBeenCalled();
676
+ expect(createCommDefinition).not.toHaveBeenCalled();
677
+ });
678
+
679
+ it('skips createCommDefinition for multi-channel strategies (CHANNEL_PRIORITY/AB_TEST)', async () => {
680
+ const onSave = jest.fn();
681
+ renderWithFlow({
682
+ features: {},
683
+ config: ccsConfig,
684
+ initialData: {
685
+ communicationStrategy: CHANNEL_PRIORITY,
686
+ channels: ['SMS', 'EMAIL'],
687
+ contentItems: [{ channel: 'SMS', templateData: {} }, { channel: 'EMAIL', templateData: {} }],
688
+ },
689
+ onSave,
690
+ });
691
+
692
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
693
+
694
+ await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
695
+ expect(createCommDefinition).not.toHaveBeenCalled();
446
696
  });
447
697
 
448
- it('still calls onSave when createCentralCommsMetaId rejects', async () => {
449
- createCentralCommsMetaId.mockRejectedValue(new Error('Network error'));
698
+ it('still calls onSave (without ccsCommDefinition) when createCommDefinition rejects', async () => {
699
+ createCommDefinition.mockRejectedValue(new Error('Network error'));
450
700
  const onSave = jest.fn();
451
701
  renderWithFlow({
452
702
  features: {},
703
+ config: ccsConfig,
453
704
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
454
705
  onSave,
455
706
  });
@@ -457,12 +708,35 @@ describe('handleSave — CCS flow', () => {
457
708
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
458
709
 
459
710
  await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
711
+ expect(onSave).toHaveBeenCalledWith(expect.not.objectContaining({ ccsCommDefinition: expect.anything() }));
460
712
  });
461
713
 
462
- it('skips createCentralCommsMetaId when contentItems is empty', async () => {
714
+ it('blocks save and shows an error when createCommDefinition resolves with a duplicate REFERENCE_ID_EXISTS conflict', async () => {
715
+ createCommDefinition.mockResolvedValue({
716
+ success: false,
717
+ status: { isError: true, code: 409, message: 'REFERENCE_ID_EXISTS' },
718
+ message: 'REFERENCE_ID_EXISTS',
719
+ });
463
720
  const onSave = jest.fn();
464
721
  renderWithFlow({
465
722
  features: {},
723
+ config: ccsConfig,
724
+ initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
725
+ onSave,
726
+ });
727
+
728
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
729
+
730
+ await waitFor(() => expect(createCommDefinition).toHaveBeenCalledTimes(1));
731
+ expect(await screen.findByText(/already in use in your organization/i)).toBeInTheDocument();
732
+ expect(onSave).not.toHaveBeenCalled();
733
+ });
734
+
735
+ it('skips createCommDefinition when contentItems is empty', async () => {
736
+ const onSave = jest.fn();
737
+ renderWithFlow({
738
+ features: {},
739
+ config: ccsConfig,
466
740
  initialData: { contentItems: [] },
467
741
  onSave,
468
742
  });
@@ -470,12 +744,13 @@ describe('handleSave — CCS flow', () => {
470
744
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
471
745
 
472
746
  await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
473
- expect(createCentralCommsMetaId).not.toHaveBeenCalled();
747
+ expect(createCommDefinition).not.toHaveBeenCalled();
474
748
  });
475
749
 
476
- it('includes additionalSettings derived from dynamicControls in the payload', async () => {
750
+ it('places additionalSettings derived from dynamicControls under settings.additionalSettings (not delivery settings)', async () => {
477
751
  renderWithFlow({
478
752
  features: {},
753
+ config: ccsConfig,
479
754
  initialData: {
480
755
  contentItems: [{ channel: 'SMS', templateData: {} }],
481
756
  dynamicControls: {
@@ -489,14 +764,116 @@ describe('handleSave — CCS flow', () => {
489
764
 
490
765
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
491
766
 
492
- await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalled());
493
- const payload = createCentralCommsMetaId.mock.calls[0][0];
494
- expect(payload.centralCommsPayload.smsDeliverySettings.additionalSettings).toEqual({
767
+ await waitFor(() => expect(createCommDefinition).toHaveBeenCalled());
768
+ const payload = createCommDefinition.mock.calls[0][0];
769
+ expect(payload.settings.additionalSettings).toEqual({
495
770
  useTinyUrl: true,
496
771
  encryptUrl: true,
497
772
  linkTrackingEnabled: true,
498
773
  userSubscriptionDisabled: true,
499
774
  });
775
+ expect(payload.singleChannelStrategy.variant.smsDeliverySettings.additionalSettings).toBeUndefined();
776
+ });
777
+
778
+ it('includes channelSettings from deliverySetting.channelSetting in the payload', async () => {
779
+ renderWithFlow({
780
+ features: {},
781
+ config: ccsConfig,
782
+ initialData: {
783
+ contentItems: [{ channel: 'SMS', templateData: {} }],
784
+ deliverySetting: { channelSetting: { SMS: { domainId: 1001, gsmSenderId: 'BRAND1' } } },
785
+ },
786
+ });
787
+
788
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
789
+
790
+ await waitFor(() => expect(createCommDefinition).toHaveBeenCalled());
791
+ const payload = createCommDefinition.mock.calls[0][0];
792
+ expect(payload.singleChannelStrategy.variant.smsDeliverySettings.channelSettings).toEqual({
793
+ channel: 'SMS',
794
+ domainId: 1001,
795
+ gsmSenderId: 'BRAND1',
796
+ });
797
+ });
798
+
799
+ it('does not call editCommDefinition when config.context.existingCommDefinitionId is absent', async () => {
800
+ const onSave = jest.fn();
801
+ renderWithFlow({
802
+ features: {},
803
+ config: ccsConfig,
804
+ initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
805
+ onSave,
806
+ });
807
+
808
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
809
+
810
+ await waitFor(() => expect(createCommDefinition).toHaveBeenCalledTimes(1));
811
+ expect(editCommDefinition).not.toHaveBeenCalled();
812
+ });
813
+
814
+ it('calls editCommDefinition (not createCommDefinition) and merges the existing id into ccsCommDefinition when config.context.existingCommDefinitionId is set', async () => {
815
+ editCommDefinition.mockResolvedValue({
816
+ response: { data: { version: { version: 2 }, status: 'DRAFT' } },
817
+ });
818
+ const onSave = jest.fn();
819
+ renderWithFlow({
820
+ features: {},
821
+ config: {
822
+ ...ccsConfig,
823
+ context: { ...ccsConfig.context, referenceId: 'ORDER_PLACED', existingCommDefinitionId: 'cd_existing_1' },
824
+ },
825
+ initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
826
+ onSave,
827
+ });
828
+
829
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
830
+
831
+ await waitFor(() => expect(editCommDefinition).toHaveBeenCalledTimes(1));
832
+ expect(editCommDefinition).toHaveBeenCalledWith(
833
+ 'cd_existing_1',
834
+ expect.objectContaining({
835
+ strategyType: 'SINGLE',
836
+ settings: expect.anything(),
837
+ singleChannelStrategy: expect.anything(),
838
+ }),
839
+ );
840
+ expect(createCommDefinition).not.toHaveBeenCalled();
841
+
842
+ await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
843
+ expect(onSave).toHaveBeenCalledWith(
844
+ expect.objectContaining({
845
+ ccsCommDefinition: {
846
+ id: 'cd_existing_1',
847
+ referenceId: 'ORDER_PLACED',
848
+ version: 2,
849
+ status: 'DRAFT',
850
+ },
851
+ }),
852
+ );
853
+ });
854
+
855
+ it('blocks save and shows an error when editCommDefinition resolves with a duplicate REFERENCE_ID_EXISTS conflict', async () => {
856
+ editCommDefinition.mockResolvedValue({
857
+ success: false,
858
+ status: { isError: true, code: 409, message: 'REFERENCE_ID_EXISTS' },
859
+ message: 'REFERENCE_ID_EXISTS',
860
+ });
861
+ const onSave = jest.fn();
862
+ renderWithFlow({
863
+ features: {},
864
+ config: {
865
+ ...ccsConfig,
866
+ context: { ...ccsConfig.context, referenceId: 'ORDER_PLACED', existingCommDefinitionId: 'cd_existing_1' },
867
+ },
868
+ initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
869
+ onSave,
870
+ });
871
+
872
+ await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
873
+
874
+ await waitFor(() => expect(editCommDefinition).toHaveBeenCalledTimes(1));
875
+ expect(await screen.findByText(/already in use in your organization/i)).toBeInTheDocument();
876
+ expect(onSave).not.toHaveBeenCalled();
500
877
  });
501
878
  });
502
879
 
@@ -552,39 +929,34 @@ describe('optional chaining safety', () => {
552
929
  jest.clearAllMocks();
553
930
  });
554
931
 
555
- it('defaults ouId to -1 when config.context is absent', async () => {
556
- createCentralCommsMetaId.mockResolvedValue({ response: { data: { id: 'x' } } });
932
+ it('does not crash and skips CCS when config.context is entirely absent', async () => {
933
+ const onSave = jest.fn();
557
934
  renderWithFlow({
558
935
  features: {},
936
+ config: { ...baseConfig, features: {} },
559
937
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
938
+ onSave,
560
939
  });
561
940
 
562
941
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
563
942
 
564
- await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalled());
565
- expect(createCentralCommsMetaId).toHaveBeenCalledWith(
566
- expect.objectContaining({
567
- centralCommsPayload: expect.objectContaining({ ouId: -1 }),
568
- }),
569
- );
943
+ await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
944
+ expect(createCommDefinition).not.toHaveBeenCalled();
570
945
  });
571
946
 
572
- it('defaults module to consumer.toUpperCase() when config.context.module absent', async () => {
573
- createCentralCommsMetaId.mockResolvedValue({ response: { data: { id: 'x' } } });
947
+ it('does not crash when config.context is present but empty', async () => {
948
+ const onSave = jest.fn();
574
949
  renderWithFlow({
575
950
  features: {},
576
- config: { ...baseConfig, consumer: 'loyalty', features: {} },
951
+ config: { ...baseConfig, context: {}, features: {} },
577
952
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
953
+ onSave,
578
954
  });
579
955
 
580
956
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
581
957
 
582
- await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalled());
583
- expect(createCentralCommsMetaId).toHaveBeenCalledWith(
584
- expect.objectContaining({
585
- centralCommsPayload: expect.objectContaining({ module: 'LOYALTY' }),
586
- }),
587
- );
958
+ await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
959
+ expect(createCommDefinition).not.toHaveBeenCalled();
588
960
  });
589
961
 
590
962
  it('initializes channel from config.channel when initialData.channel is absent', () => {
@@ -108,6 +108,22 @@ describe('CommunicationFlowCard', () => {
108
108
  fireEvent.click(screen.getByText('Add creatives'));
109
109
  expect(screen.getByTestId('comm-flow-mock')).toHaveAttribute('data-mode', 'create');
110
110
  });
111
+
112
+ it('disables the Add creatives button when disabled prop is true', () => {
113
+ renderCard({ disabled: true, disabledTooltip: 'Enter an Alert name to add content' });
114
+ expect(screen.getByText('Add creatives').closest('button')).toBeDisabled();
115
+ });
116
+
117
+ it('does not open SlideBox when Add creatives is clicked while disabled', () => {
118
+ renderCard({ disabled: true, disabledTooltip: 'Enter an Alert name to add content' });
119
+ fireEvent.click(screen.getByText('Add creatives'));
120
+ expect(screen.queryByTestId('slide-box')).not.toBeInTheDocument();
121
+ });
122
+
123
+ it('Add creatives button is enabled by default (disabled prop absent)', () => {
124
+ renderCard();
125
+ expect(screen.getByText('Add creatives').closest('button')).not.toBeDisabled();
126
+ });
111
127
  });
112
128
 
113
129
  describe('configured state (initialData provided)', () => {
@@ -231,11 +247,11 @@ describe('CommunicationFlowCard', () => {
231
247
  expect(screen.getByText(/Sender ID/)).toBeInTheDocument();
232
248
  });
233
249
 
234
- it('shows senderEmail for EMAIL with Sender ID label', () => {
250
+ it('shows senderId for EMAIL with Sender ID label', () => {
235
251
  renderCard({
236
252
  initialData: makeSavedData({
237
253
  contentItems: [{ channel: 'EMAIL', templateData: {} }],
238
- deliverySetting: { channelSetting: { EMAIL: { senderEmail: 'test@example.com' } } },
254
+ deliverySetting: { channelSetting: { EMAIL: { senderId: 'test@example.com' } } },
239
255
  }),
240
256
  });
241
257
  expect(screen.getByText(/test@example.com/)).toBeInTheDocument();