@capillarytech/creatives-library 9.0.56-alpha.7 → 9.0.56-betaa.0

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.
Files changed (82) hide show
  1. package/constants/unified.js +4 -2
  2. package/package.json +1 -1
  3. package/services/api.js +0 -20
  4. package/utils/common.js +12 -0
  5. package/utils/templateVarUtils.js +0 -16
  6. package/utils/tests/templateVarUtils.test.js +0 -44
  7. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +0 -13
  8. package/v2Components/CommonTestAndPreview/actions.js +0 -13
  9. package/v2Components/CommonTestAndPreview/constants.js +0 -8
  10. package/v2Components/CommonTestAndPreview/index.js +3 -55
  11. package/v2Components/CommonTestAndPreview/reducer.js +0 -20
  12. package/v2Components/CommonTestAndPreview/sagas.js +0 -28
  13. package/v2Components/CommonTestAndPreview/selectors.js +0 -6
  14. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +0 -106
  15. package/v2Components/CommonTestAndPreview/tests/actions.test.js +0 -15
  16. package/v2Components/CommonTestAndPreview/tests/constants.test.js +1 -9
  17. package/v2Components/CommonTestAndPreview/tests/index.test.js +0 -101
  18. package/v2Components/CommonTestAndPreview/tests/reducer.test.js +0 -41
  19. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +0 -84
  20. package/v2Components/CommonTestAndPreview/tests/selectors.test.js +0 -12
  21. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +320 -46
  22. package/v2Components/FormBuilder/Functional/channels/mobilepush/buildSubmitPayload.js +10 -0
  23. package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +82 -0
  24. package/v2Components/FormBuilder/Functional/channels/mobilepush/getEditorErrorDescriptor.js +74 -0
  25. package/v2Components/FormBuilder/Functional/channels/mobilepush/index.js +28 -0
  26. package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +19 -0
  27. package/v2Components/FormBuilder/Functional/channels/mobilepush/runSubmitPipeline.js +45 -0
  28. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/getEditorErrorDescriptor.test.js +162 -0
  29. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/modals.test.js +37 -0
  30. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/runSubmitPipeline.test.js +70 -0
  31. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/validate.test.js +274 -0
  32. package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +251 -0
  33. package/v2Components/FormBuilder/Functional/channels/registry.js +2 -0
  34. package/v2Components/FormBuilder/Functional/constants.js +43 -8
  35. package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +175 -28
  36. package/v2Components/FormBuilder/Functional/core/store/formReducer.js +75 -6
  37. package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +10 -20
  38. package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +9 -1
  39. package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +20 -3
  40. package/v2Components/FormBuilder/Functional/layout/Section.js +6 -1
  41. package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +89 -0
  42. package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +440 -0
  43. package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +9 -2
  44. package/v2Components/FormBuilder/Functional/tests/mpush.crossFlowParity.test.js +430 -0
  45. package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +282 -0
  46. package/v2Components/FormBuilder/Functional/tests/mpush.editContainer.parity.test.js +141 -0
  47. package/v2Components/FormBuilder/Functional/tests/mpush.engine.test.js +306 -0
  48. package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +151 -0
  49. package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +342 -0
  50. package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +17 -0
  51. package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +110 -0
  52. package/v2Components/FormBuilder/index.js +34 -12
  53. package/v2Components/FormBuilder/tests/entryGate.test.js +94 -7
  54. package/v2Components/FormBuilder/tests/mpush.characterization.test.js +459 -0
  55. package/v2Components/TestAndPreviewSlidebox/index.js +0 -9
  56. package/v2Containers/CommunicationFlow/CommunicationFlow.js +75 -140
  57. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +2 -18
  58. package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +27 -58
  59. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +51 -503
  60. package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +4 -40
  61. package/v2Containers/CommunicationFlow/constants.js +4 -65
  62. package/v2Containers/CommunicationFlow/index.js +23 -16
  63. package/v2Containers/CommunicationFlow/messages.js +3 -11
  64. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +11 -24
  65. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +0 -4
  66. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +4 -176
  67. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +0 -3
  68. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +0 -6
  69. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +0 -25
  70. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +4 -11
  71. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +5 -19
  72. package/v2Containers/CreativesContainer/index.js +3 -2
  73. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +5 -5
  74. package/v2Containers/CreativesContainer/tests/index.test.js +0 -12
  75. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +19 -115
  76. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +3 -27
  77. package/v2Containers/WebPush/Create/preview/WebPushPreview.js +12 -17
  78. package/v2Containers/WebPush/Create/preview/tests/WebPushPreview.test.js +0 -5
  79. package/v2Containers/Whatsapp/index.js +3 -6
  80. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +34 -306
  81. package/v2Containers/mockdata.js +0 -25
  82. package/v2Containers/CommunicationFlow/Tests/constants.test.js +0 -85
@@ -1,8 +1,8 @@
1
1
  import React from 'react';
2
2
 
3
3
  jest.mock('../../../services/api', () => ({
4
- createCommDefinition: jest.fn(),
5
- editCommDefinition: jest.fn(),
4
+ createCentralCommsMetaId: jest.fn(),
5
+ getCentralCommsMetaIds: 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 { createCommDefinition, editCommDefinition } from '../../../services/api';
41
+ import { createCentralCommsMetaId, getCentralCommsMetaIds } from '../../../services/api';
42
42
  import { getEnabledSteps } from '../utils/getEnabledSteps';
43
43
  import {
44
44
  CHANNELS,
@@ -192,13 +192,13 @@ describe('CommunicationFlow', () => {
192
192
  it('hides channel selection and dynamic controls until a communication strategy is set', async () => {
193
193
  renderWithFlow({ features: FEATURES.strategyContentDynamic });
194
194
 
195
- expect(screen.queryByText(/add creative/i)).not.toBeInTheDocument();
195
+ expect(screen.queryByText(/content template/i)).not.toBeInTheDocument();
196
196
  expect(screen.queryByText(/other controls/i)).not.toBeInTheDocument();
197
197
 
198
198
  await selectCommunicationStrategy('Single template');
199
199
 
200
200
  await waitFor(() => {
201
- expect(screen.getAllByText(/add creative/i).length).toBeGreaterThan(0);
201
+ expect(screen.getAllByText(/content template/i).length).toBeGreaterThan(0);
202
202
  });
203
203
  expect(screen.getByText(/other controls/i)).toBeInTheDocument();
204
204
  });
@@ -359,295 +359,82 @@ describe('isSaveDisabled', () => {
359
359
  });
360
360
 
361
361
  describe('handleSave — CCS flow', () => {
362
- const ccsConfig = { ...baseConfig, context: { name: 'Order Placed Notification' }, features: {} };
363
-
364
362
  beforeEach(() => {
365
- createCommDefinition.mockResolvedValue({
366
- response: { data: { id: 'cd_123', referenceId: 'ORDER_PLACED_123', status: 'DRAFT', version: { version: 1 } } },
367
- });
363
+ createCentralCommsMetaId.mockResolvedValue({ response: { data: { id: 'meta-123' } } });
364
+ getCentralCommsMetaIds.mockResolvedValue({ response: { data: {} } });
368
365
  });
369
366
 
370
367
  afterEach(() => {
371
368
  jest.clearAllMocks();
372
369
  });
373
370
 
374
- it('calls createCommDefinition with a SINGLE-strategy payload when useCCS is not false', async () => {
371
+ it('calls createCentralCommsMetaId for each content item when useCCS is not false', async () => {
375
372
  const onSave = jest.fn();
376
373
  renderWithFlow({
377
374
  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.
382
375
  initialData: {
383
- contentItems: [{ channel: 'sms', templateData: { messageBody: 'Hello' } }],
376
+ contentItems: [{ channel: 'sms', templateData: { smsBody: 'Hello' } }],
384
377
  },
385
378
  onSave,
386
379
  });
387
380
 
388
381
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
389
382
 
390
- await waitFor(() => expect(createCommDefinition).toHaveBeenCalledTimes(1));
391
- expect(createCommDefinition).toHaveBeenCalledWith(
383
+ await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalledTimes(1));
384
+ expect(createCentralCommsMetaId).toHaveBeenCalledWith(
392
385
  expect.objectContaining({
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
- }),
386
+ centralCommsPayload: expect.objectContaining({ channel: 'SMS', module: 'CAMPAIGNS' }),
401
387
  }),
402
388
  );
403
389
  expect(onSave).toHaveBeenCalledTimes(1);
404
390
  });
405
391
 
406
- it('transforms SMS content from the legacy editor shape (messageBody) to CCS\'s message field', async () => {
407
- const onSave = jest.fn();
392
+ it('calls getCentralCommsMetaIds when metaIds are returned from createCentralCommsMetaId', async () => {
408
393
  renderWithFlow({
409
394
  features: {},
410
- config: ccsConfig,
411
- initialData: {
412
- contentItems: [{
413
- channel: 'SMS',
414
- templateData: { messageBody: 'Hello {{name}}', channel: 'SMS' },
415
- }],
416
- },
417
- onSave,
395
+ initialData: { contentItems: [{ channel: 'EMAIL', templateData: {} }] },
418
396
  });
419
397
 
420
398
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
421
399
 
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
- );
400
+ await waitFor(() => expect(getCentralCommsMetaIds).toHaveBeenCalledWith('meta-123'));
436
401
  });
437
402
 
438
- it('does not carry the legacy messageBody key into smsMessageContent (regression guard against reverting to pass-through)', async () => {
439
- const onSave = jest.fn();
403
+ it('skips getCentralCommsMetaIds when response contains no id', async () => {
404
+ createCentralCommsMetaId.mockResolvedValue({ response: { data: {} } });
440
405
  renderWithFlow({
441
406
  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();
540
- renderWithFlow({
541
- features: {},
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,
555
- });
556
-
557
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
558
-
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
- );
573
- });
574
-
575
- it('merges ccsCommDefinition into the data passed to onSave when create succeeds', async () => {
576
- const onSave = jest.fn();
577
- renderWithFlow({
578
- features: {},
579
- config: ccsConfig,
580
407
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
581
- onSave,
582
408
  });
583
409
 
584
410
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
585
411
 
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
- );
412
+ await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalled());
413
+ expect(getCentralCommsMetaIds).not.toHaveBeenCalled();
592
414
  });
593
415
 
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
- });
416
+ it('uses ouId and module from config.context when provided', async () => {
599
417
  renderWithFlow({
600
418
  features: {},
601
- config: ccsConfig,
419
+ config: { ...baseConfig, context: { ouId: 42, module: 'LOYALTY' }, features: {} },
602
420
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
603
- onSave,
604
421
  });
605
422
 
606
423
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
607
424
 
608
- await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
609
- expect(onSave).toHaveBeenCalledWith(
425
+ await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalled());
426
+ expect(createCentralCommsMetaId).toHaveBeenCalledWith(
610
427
  expect.objectContaining({
611
- ccsCommDefinition: { id: 'cd_123', referenceId: 'ORDER_PLACED_123', version: 0, status: 'DRAFT' },
428
+ centralCommsPayload: expect.objectContaining({ ouId: 42, module: 'LOYALTY' }),
612
429
  }),
613
430
  );
614
431
  });
615
432
 
616
- it('sends an empty referenceId/description when config.context does not supply them', async () => {
617
- renderWithFlow({
618
- features: {},
619
- config: ccsConfig,
620
- initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
621
- });
622
-
623
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
624
-
625
- await waitFor(() => expect(createCommDefinition).toHaveBeenCalled());
626
- const payload = createCommDefinition.mock.calls[0][0];
627
- expect(payload.referenceId).toBe('');
628
- expect(payload.description).toBe('');
629
- });
630
-
631
- it('uses config.context.referenceId and description when provided', async () => {
632
- renderWithFlow({
633
- features: {},
634
- config: { ...ccsConfig, context: { ...ccsConfig.context, referenceId: 'ORDER_PLACED', description: 'desc' } },
635
- initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
636
- });
637
-
638
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
639
-
640
- await waitFor(() => expect(createCommDefinition).toHaveBeenCalled());
641
- expect(createCommDefinition).toHaveBeenCalledWith(
642
- expect.objectContaining({ referenceId: 'ORDER_PLACED', description: 'desc' }),
643
- );
644
- });
645
-
646
433
  it('skips CCS entirely when useCCS is false', async () => {
647
434
  const onSave = jest.fn();
648
435
  renderWithFlow({
649
436
  features: {},
650
- config: { ...ccsConfig, useCCS: false },
437
+ config: { ...baseConfig, useCCS: false, features: {} },
651
438
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
652
439
  onSave,
653
440
  });
@@ -655,15 +442,14 @@ describe('handleSave — CCS flow', () => {
655
442
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
656
443
 
657
444
  await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
658
- expect(createCommDefinition).not.toHaveBeenCalled();
659
- expect(onSave).toHaveBeenCalledWith(expect.not.objectContaining({ ccsCommDefinition: expect.anything() }));
445
+ expect(createCentralCommsMetaId).not.toHaveBeenCalled();
660
446
  });
661
447
 
662
- it('skips createCommDefinition when config.context.name is absent (mandatory)', async () => {
448
+ it('still calls onSave when createCentralCommsMetaId rejects', async () => {
449
+ createCentralCommsMetaId.mockRejectedValue(new Error('Network error'));
663
450
  const onSave = jest.fn();
664
451
  renderWithFlow({
665
452
  features: {},
666
- config: { ...baseConfig, features: {} },
667
453
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
668
454
  onSave,
669
455
  });
@@ -671,102 +457,12 @@ describe('handleSave — CCS flow', () => {
671
457
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
672
458
 
673
459
  await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
674
- expect(createCommDefinition).not.toHaveBeenCalled();
675
- });
676
-
677
- it('skips createCommDefinition for multi-channel strategies (CHANNEL_PRIORITY/AB_TEST)', async () => {
678
- const onSave = jest.fn();
679
- renderWithFlow({
680
- features: {},
681
- config: ccsConfig,
682
- initialData: {
683
- communicationStrategy: CHANNEL_PRIORITY,
684
- channels: ['SMS', 'EMAIL'],
685
- contentItems: [{ channel: 'SMS', templateData: {} }, { channel: 'EMAIL', templateData: {} }],
686
- },
687
- onSave,
688
- });
689
-
690
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
691
-
692
- await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
693
- expect(createCommDefinition).not.toHaveBeenCalled();
694
- });
695
-
696
- it('blocks save and shows a generic error when createCommDefinition rejects (regression: this used to silently call onSave without ccsCommDefinition and no user-facing feedback — Send for approval would then just be disabled with zero explanation why)', async () => {
697
- createCommDefinition.mockRejectedValue(new Error('Network error'));
698
- const onSave = jest.fn();
699
- renderWithFlow({
700
- features: {},
701
- config: ccsConfig,
702
- initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
703
- onSave,
704
- });
705
-
706
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
707
-
708
- await waitFor(() => expect(screen.getByText('Something went wrong while saving your content. Please try again.')).toBeInTheDocument());
709
- expect(onSave).not.toHaveBeenCalled();
710
460
  });
711
461
 
712
- it('blocks save and shows a generic error when createCommDefinition resolves with a network-level failure (api.js resolves isError instead of rejecting on a 401/403/etc)', async () => {
713
- createCommDefinition.mockResolvedValue({ isError: true, status: 401 });
462
+ it('skips createCentralCommsMetaId when contentItems is empty', async () => {
714
463
  const onSave = jest.fn();
715
464
  renderWithFlow({
716
465
  features: {},
717
- config: ccsConfig,
718
- initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
719
- onSave,
720
- });
721
-
722
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
723
-
724
- await waitFor(() => expect(screen.getByText('Something went wrong while saving your content. Please try again.')).toBeInTheDocument());
725
- expect(onSave).not.toHaveBeenCalled();
726
- });
727
-
728
- it('blocks save and shows a generic error when createCommDefinition resolves with no usable data (e.g. missing id)', async () => {
729
- createCommDefinition.mockResolvedValue({ response: { data: {} } });
730
- const onSave = jest.fn();
731
- renderWithFlow({
732
- features: {},
733
- config: ccsConfig,
734
- initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
735
- onSave,
736
- });
737
-
738
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
739
-
740
- await waitFor(() => expect(screen.getByText('Something went wrong while saving your content. Please try again.')).toBeInTheDocument());
741
- expect(onSave).not.toHaveBeenCalled();
742
- });
743
-
744
- it('blocks save and shows an error when createCommDefinition resolves with a duplicate REFERENCE_ID_EXISTS conflict', async () => {
745
- createCommDefinition.mockResolvedValue({
746
- success: false,
747
- status: { isError: true, code: 409, message: 'REFERENCE_ID_EXISTS' },
748
- message: 'REFERENCE_ID_EXISTS',
749
- });
750
- const onSave = jest.fn();
751
- renderWithFlow({
752
- features: {},
753
- config: ccsConfig,
754
- initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
755
- onSave,
756
- });
757
-
758
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
759
-
760
- await waitFor(() => expect(createCommDefinition).toHaveBeenCalledTimes(1));
761
- expect(await screen.findByText(/already in use in your organization/i)).toBeInTheDocument();
762
- expect(onSave).not.toHaveBeenCalled();
763
- });
764
-
765
- it('skips createCommDefinition when contentItems is empty', async () => {
766
- const onSave = jest.fn();
767
- renderWithFlow({
768
- features: {},
769
- config: ccsConfig,
770
466
  initialData: { contentItems: [] },
771
467
  onSave,
772
468
  });
@@ -774,13 +470,12 @@ describe('handleSave — CCS flow', () => {
774
470
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
775
471
 
776
472
  await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
777
- expect(createCommDefinition).not.toHaveBeenCalled();
473
+ expect(createCentralCommsMetaId).not.toHaveBeenCalled();
778
474
  });
779
475
 
780
- it('places additionalSettings derived from dynamicControls under settings.additionalSettings (not delivery settings)', async () => {
476
+ it('includes additionalSettings derived from dynamicControls in the payload', async () => {
781
477
  renderWithFlow({
782
478
  features: {},
783
- config: ccsConfig,
784
479
  initialData: {
785
480
  contentItems: [{ channel: 'SMS', templateData: {} }],
786
481
  dynamicControls: {
@@ -794,118 +489,14 @@ describe('handleSave — CCS flow', () => {
794
489
 
795
490
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
796
491
 
797
- await waitFor(() => expect(createCommDefinition).toHaveBeenCalled());
798
- const payload = createCommDefinition.mock.calls[0][0];
799
- expect(payload.settings.additionalSettings).toEqual({
492
+ await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalled());
493
+ const payload = createCentralCommsMetaId.mock.calls[0][0];
494
+ expect(payload.centralCommsPayload.smsDeliverySettings.additionalSettings).toEqual({
800
495
  useTinyUrl: true,
801
496
  encryptUrl: true,
802
497
  linkTrackingEnabled: true,
803
498
  userSubscriptionDisabled: true,
804
499
  });
805
- expect(payload.singleChannelStrategy.variant.smsDeliverySettings.additionalSettings).toBeUndefined();
806
- });
807
-
808
- it('includes channelSettings from deliverySetting.channelSetting in the payload', async () => {
809
- renderWithFlow({
810
- features: {},
811
- config: ccsConfig,
812
- initialData: {
813
- contentItems: [{ channel: 'SMS', templateData: {} }],
814
- deliverySetting: { channelSetting: { SMS: { domainId: 1001, gsmSenderId: 'BRAND1' } } },
815
- },
816
- });
817
-
818
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
819
-
820
- await waitFor(() => expect(createCommDefinition).toHaveBeenCalled());
821
- const payload = createCommDefinition.mock.calls[0][0];
822
- expect(payload.singleChannelStrategy.variant.smsDeliverySettings.channelSettings).toEqual({
823
- channel: 'SMS',
824
- domainId: 1001,
825
- gsmSenderId: 'BRAND1',
826
- });
827
- });
828
-
829
- it('does not call editCommDefinition when config.context.existingCommDefinitionId is absent', async () => {
830
- const onSave = jest.fn();
831
- renderWithFlow({
832
- features: {},
833
- config: ccsConfig,
834
- initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
835
- onSave,
836
- });
837
-
838
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
839
-
840
- await waitFor(() => expect(createCommDefinition).toHaveBeenCalledTimes(1));
841
- expect(editCommDefinition).not.toHaveBeenCalled();
842
- });
843
-
844
- it('calls editCommDefinition (not createCommDefinition) and merges the existing id into ccsCommDefinition when config.context.existingCommDefinitionId is set', async () => {
845
- editCommDefinition.mockResolvedValue({
846
- response: { data: { version: { version: 2 }, status: 'DRAFT' } },
847
- });
848
- const onSave = jest.fn();
849
- renderWithFlow({
850
- features: {},
851
- config: {
852
- ...ccsConfig,
853
- context: { ...ccsConfig.context, referenceId: 'ORDER_PLACED', description: 'desc', existingCommDefinitionId: 'cd_existing_1' },
854
- },
855
- initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
856
- onSave,
857
- });
858
-
859
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
860
-
861
- await waitFor(() => expect(editCommDefinition).toHaveBeenCalledTimes(1));
862
- expect(editCommDefinition).toHaveBeenCalledWith(
863
- 'cd_existing_1',
864
- expect.objectContaining({
865
- referenceId: 'ORDER_PLACED',
866
- description: 'desc',
867
- strategyType: 'SINGLE',
868
- settings: expect.anything(),
869
- singleChannelStrategy: expect.anything(),
870
- }),
871
- );
872
- expect(createCommDefinition).not.toHaveBeenCalled();
873
-
874
- await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
875
- expect(onSave).toHaveBeenCalledWith(
876
- expect.objectContaining({
877
- ccsCommDefinition: {
878
- id: 'cd_existing_1',
879
- referenceId: 'ORDER_PLACED',
880
- version: 2,
881
- status: 'DRAFT',
882
- },
883
- }),
884
- );
885
- });
886
-
887
- it('blocks save and shows an error when editCommDefinition resolves with a duplicate REFERENCE_ID_EXISTS conflict', async () => {
888
- editCommDefinition.mockResolvedValue({
889
- success: false,
890
- status: { isError: true, code: 409, message: 'REFERENCE_ID_EXISTS' },
891
- message: 'REFERENCE_ID_EXISTS',
892
- });
893
- const onSave = jest.fn();
894
- renderWithFlow({
895
- features: {},
896
- config: {
897
- ...ccsConfig,
898
- context: { ...ccsConfig.context, referenceId: 'ORDER_PLACED', existingCommDefinitionId: 'cd_existing_1' },
899
- },
900
- initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
901
- onSave,
902
- });
903
-
904
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
905
-
906
- await waitFor(() => expect(editCommDefinition).toHaveBeenCalledTimes(1));
907
- expect(await screen.findByText(/already in use in your organization/i)).toBeInTheDocument();
908
- expect(onSave).not.toHaveBeenCalled();
909
500
  });
910
501
  });
911
502
 
@@ -961,34 +552,39 @@ describe('optional chaining safety', () => {
961
552
  jest.clearAllMocks();
962
553
  });
963
554
 
964
- it('does not crash and skips CCS when config.context is entirely absent', async () => {
965
- const onSave = jest.fn();
555
+ it('defaults ouId to -1 when config.context is absent', async () => {
556
+ createCentralCommsMetaId.mockResolvedValue({ response: { data: { id: 'x' } } });
966
557
  renderWithFlow({
967
558
  features: {},
968
- config: { ...baseConfig, features: {} },
969
559
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
970
- onSave,
971
560
  });
972
561
 
973
562
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
974
563
 
975
- await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
976
- expect(createCommDefinition).not.toHaveBeenCalled();
564
+ await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalled());
565
+ expect(createCentralCommsMetaId).toHaveBeenCalledWith(
566
+ expect.objectContaining({
567
+ centralCommsPayload: expect.objectContaining({ ouId: -1 }),
568
+ }),
569
+ );
977
570
  });
978
571
 
979
- it('does not crash when config.context is present but empty', async () => {
980
- const onSave = jest.fn();
572
+ it('defaults module to consumer.toUpperCase() when config.context.module absent', async () => {
573
+ createCentralCommsMetaId.mockResolvedValue({ response: { data: { id: 'x' } } });
981
574
  renderWithFlow({
982
575
  features: {},
983
- config: { ...baseConfig, context: {}, features: {} },
576
+ config: { ...baseConfig, consumer: 'loyalty', features: {} },
984
577
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
985
- onSave,
986
578
  });
987
579
 
988
580
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
989
581
 
990
- await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
991
- expect(createCommDefinition).not.toHaveBeenCalled();
582
+ await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalled());
583
+ expect(createCentralCommsMetaId).toHaveBeenCalledWith(
584
+ expect.objectContaining({
585
+ centralCommsPayload: expect.objectContaining({ module: 'LOYALTY' }),
586
+ }),
587
+ );
992
588
  });
993
589
 
994
590
  it('initializes channel from config.channel when initialData.channel is absent', () => {
@@ -1032,51 +628,3 @@ describe('optional chaining safety', () => {
1032
628
  expect(screen.getByRole('button', { name: /^save$/i })).toBeDisabled();
1033
629
  });
1034
630
  });
1035
-
1036
- describe('step dividers', () => {
1037
- it('renders a divider after CommunicationStrategyStep and after ChannelSelectionStep when both render together', () => {
1038
- const { container } = renderWithFlow({
1039
- features: {
1040
- communicationStrategyData: { required: true, options: STRATEGY_OPTIONS },
1041
- contentTemplateData: { required: true, channels: CHANNELS },
1042
- },
1043
- initialData: {
1044
- communicationStrategy: SINGLE_TEMPLATE,
1045
- contentItems: [{ channel: 'SMS', templateData: {} }],
1046
- },
1047
- });
1048
- // Each rendered step (CommunicationStrategyStep, ChannelSelectionStep) emits its
1049
- // own trailing divider.
1050
- expect(container.querySelectorAll('.ant-divider')).toHaveLength(2);
1051
- });
1052
-
1053
- it('omits the divider after ChannelSelectionStep when no content has been added yet', () => {
1054
- const { container } = renderWithFlow({
1055
- features: {
1056
- communicationStrategyData: { required: true, options: STRATEGY_OPTIONS },
1057
- contentTemplateData: { required: true, channels: CHANNELS },
1058
- },
1059
- initialData: {
1060
- communicationStrategy: SINGLE_TEMPLATE,
1061
- contentItems: [],
1062
- },
1063
- });
1064
- // Neither divider renders — both CommunicationStrategyStep's and ChannelSelectionStep's
1065
- // own trailing dividers are conditional on content existing, to avoid a dangling line
1066
- // around an empty "Add creative" state.
1067
- expect(container.querySelectorAll('.ant-divider')).toHaveLength(0);
1068
- });
1069
-
1070
- it('renders the divider after CommunicationStrategyStep unconditionally when no CHANNEL_SELECTION step follows', () => {
1071
- const { container } = renderWithFlow({
1072
- features: {
1073
- communicationStrategyData: { required: true, options: STRATEGY_OPTIONS },
1074
- // contentTemplateData.required omitted — no CHANNEL_SELECTION step in this flow.
1075
- },
1076
- initialData: {
1077
- communicationStrategy: SINGLE_TEMPLATE,
1078
- },
1079
- });
1080
- expect(container.querySelectorAll('.ant-divider')).toHaveLength(1);
1081
- });
1082
- });