@capillarytech/creatives-library 9.0.49-alpha.1 → 9.0.49

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "9.0.49-alpha.1",
4
+ "version": "9.0.49",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
package/services/api.js CHANGED
@@ -701,15 +701,6 @@ export const createCentralCommsMetaId = (payload, metaType = TRANSACTION) => {
701
701
  return request(url, getAPICallObject('POST', payload, false, false, false, true));
702
702
  };
703
703
 
704
- // CCS CommDefinition API (new Veyron /api/v1/commdefinition surface, proxied
705
- // through cap-creatives-api's /comm-definitions). Used by CommunicationFlow's
706
- // own Save action — separate from the legacy messageMeta functions above,
707
- // which remain in use by CreativesContainer's own save flow (Cap/sagas.js).
708
- export const createCommDefinition = (payload) => {
709
- const url = `${API_ENDPOINT}/comm-definitions`;
710
- return request(url, getAPICallObject('POST', payload, false, false, false, true));
711
- };
712
-
713
704
  export const getCentralCommsMetaIds = (metaIds, metaType = TRANSACTION) => {
714
705
  const url = `${API_ENDPOINT}/common/central-comms/meta-id/${metaType}?metaIds=${metaIds}`;
715
706
  return request(url, getAPICallObject('GET', null, false, false, false, true));
@@ -17,11 +17,10 @@ import { createStructuredSelector } from 'reselect';
17
17
  import CapRow from '@capillarytech/cap-ui-library/CapRow';
18
18
  import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
19
19
  import CapButton from '@capillarytech/cap-ui-library/CapButton';
20
- import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
21
20
  // import injectSaga from '../../utils/injectSaga'; // cap-coupons flows disabled
22
21
  // import injectReducer from '../../utils/injectReducer';
23
22
  import { makeSelectAuthenticated } from '../Cap/selectors';
24
- import { createCommDefinition } from '../../services/api';
23
+ import { createCentralCommsMetaId, getCentralCommsMetaIds } from '../../services/api';
25
24
  import DynamicControlsStep from './steps/DynamicControlsStep';
26
25
  import MessageTypeStep from './steps/MessageTypeStep';
27
26
  import CommunicationStrategyStep from './steps/CommunicationStrategyStep';
@@ -36,9 +35,9 @@ import {
36
35
  CHANNELS,
37
36
  INCENTIVE_TYPES,
38
37
  DYNAMIC_CONTROLS_CONFIG,
39
- CCS_STRATEGY_TYPE_SINGLE,
40
- CCS_CHANNEL_CONTENT_KEY_MAP,
41
- CCS_CHANNEL_DELIVERY_KEY_MAP,
38
+ CHANNEL_CONTENT_KEY_MAP,
39
+ CHANNEL_DELIVERY_KEY_MAP,
40
+ CAMPAIGNS,
42
41
  } from './constants';
43
42
  import { getEnabledSteps } from './utils/getEnabledSteps';
44
43
  import messages from './messages';
@@ -65,25 +64,6 @@ const hasDeliverySettingForChannel = (channelSetting, channel) => {
65
64
  return !!settings && Object.values(settings).some((v) => v !== null && v !== '' && v !== undefined);
66
65
  };
67
66
 
68
- // createCommDefinition resolves (rather than rejects) with the CCS error envelope
69
- // for 4xx/5xx responses — see api.js's request()/checkStatus. A duplicate
70
- // referenceId within the org comes back as this specific 409.
71
- const isDuplicateReferenceIdError = (res) => res?.success === false && res?.status?.message === 'REFERENCE_ID_EXISTS';
72
-
73
- /**
74
- * CCS requires referenceId on create; the consumer-supplied Alert/comm name is
75
- * mandatory but referenceId is optional in every consumer's own form. Generate
76
- * a stable fallback from the name rather than failing the save.
77
- */
78
- const buildCcsReferenceId = (name) => {
79
- const slug = (name || 'COMM')
80
- .trim()
81
- .toUpperCase()
82
- .replace(/[^A-Z0-9]+/g, '_')
83
- .replace(/^_+|_+$/g, '') || 'COMM';
84
- return `${slug}_${Date.now()}`;
85
- };
86
-
87
67
  const CommunicationFlow = ({
88
68
  config,
89
69
  initialData,
@@ -111,7 +91,6 @@ const CommunicationFlow = ({
111
91
  };
112
92
  });
113
93
  const [validationErrors, setValidationErrors] = useState({});
114
- const [saveError, setSaveError] = useState(null);
115
94
 
116
95
  // Memoize enabled steps
117
96
  const enabledSteps = useMemo(() => getEnabledSteps(config), [config]);
@@ -171,75 +150,74 @@ const CommunicationFlow = ({
171
150
  }, []);
172
151
 
173
152
  const handleSave = useCallback(async () => {
174
- setSaveError(null);
175
153
  const aggregatedData = getAggregatedData();
176
154
  const shouldUseCCS = config?.useCCS !== false;
177
- let ccsCommDefinition = null;
178
155
 
179
156
  if (shouldUseCCS) {
180
- const isMultiChannel = [CHANNEL_PRIORITY, AB_TEST].includes(aggregatedData.communicationStrategy);
181
- const contentItem = (aggregatedData.contentItems || [])[0];
182
- // Consumer-supplied name (e.g. CapNotify's Alert Name field) — CommunicationFlow
183
- // has no name input of its own, so this is mandatory input from config.context.
184
- const name = config?.context?.name;
157
+ const ouId = config?.context?.ouId || -1;
158
+ const module = config?.context?.module
159
+ || (config?.consumer ? config.consumer.toUpperCase() : CAMPAIGNS);
185
160
 
186
- // CCS create is SINGLE-strategy only this phase (D1); CHANNEL_PRIORITY/AB_TEST
187
- // carry multiple content items with no CCS equivalent yet.
188
- if (!isMultiChannel && contentItem && name) {
189
- const channel = (contentItem.channel || '').toUpperCase();
190
- const contentKey = CCS_CHANNEL_CONTENT_KEY_MAP[channel];
191
- const deliveryKey = CCS_CHANNEL_DELIVERY_KEY_MAP[channel];
192
- const { dynamicControls = {} } = aggregatedData;
193
- const channelSettings = aggregatedData.deliverySetting?.channelSetting?.[channel] || {};
194
- const referenceId = config?.context?.referenceId || buildCcsReferenceId(name);
161
+ const channelContentKeyMap = CHANNEL_CONTENT_KEY_MAP;
162
+ const channelDeliveryKeyMap = CHANNEL_DELIVERY_KEY_MAP;
195
163
 
196
- const payload = {
197
- referenceId,
198
- name,
199
- description: config?.context?.description || undefined,
200
- strategyType: CCS_STRATEGY_TYPE_SINGLE,
201
- settings: {
202
- additionalSettings: {
203
- useTinyUrl: dynamicControls.useTinyUrl ?? false,
204
- encryptUrl: dynamicControls.sendToControlCustomers ?? false,
205
- linkTrackingEnabled: dynamicControls.overrideDailyLimit ?? false,
206
- userSubscriptionDisabled: dynamicControls.sendToBrandPocs ?? false,
207
- },
208
- executionParams: {},
209
- },
210
- singleChannelStrategy: {
211
- channel,
212
- ...(contentKey && { [contentKey]: contentItem.templateData }),
213
- ...(deliveryKey && { [deliveryKey]: { channelSettings: { channel, ...channelSettings } } }),
214
- },
215
- };
164
+ const contentItems = aggregatedData.contentItems || [];
165
+ const { dynamicControls = {} } = aggregatedData;
216
166
 
167
+ const additionalSettings = {
168
+ useTinyUrl: dynamicControls.useTinyUrl ?? false,
169
+ encryptUrl: dynamicControls.sendToControlCustomers ?? false,
170
+ linkTrackingEnabled: dynamicControls.overrideDailyLimit ?? false,
171
+ userSubscriptionDisabled: dynamicControls.sendToBrandPocs ?? false,
172
+ };
173
+
174
+ if (contentItems.length > 0) {
217
175
  try {
218
- const res = await createCommDefinition(payload);
219
- if (isDuplicateReferenceIdError(res)) {
220
- // Duplicate referenceId in this org — block the save so the user can
221
- // change it, rather than silently proceeding without a CCS comm.
222
- setSaveError(formatMessage(messages.duplicateReferenceIdError));
223
- return;
224
- }
225
- const data = res?.response?.data;
226
- if (data?.id) {
227
- ccsCommDefinition = {
228
- id: data.id,
229
- referenceId: data.referenceId,
230
- version: data.version?.version || 1,
231
- status: data.status,
232
- };
176
+ const responses = await Promise.all(
177
+ contentItems.map((item) => {
178
+ const channel = (item.channel || '').toUpperCase();
179
+ const contentKey = channelContentKeyMap[channel];
180
+ const deliveryKey = channelDeliveryKeyMap[channel];
181
+ const payload = {
182
+ centralCommsPayload: {
183
+ ouId,
184
+ channel,
185
+ module,
186
+ executionParams: {},
187
+ clientName: 'EMF',
188
+ ...(contentKey && {
189
+ [contentKey]: { channel, ...item.templateData },
190
+ }),
191
+ ...(deliveryKey && {
192
+ [deliveryKey]: {
193
+ additionalSettings,
194
+ channelSettings: {
195
+ channel,
196
+ ...(aggregatedData.deliverySetting?.channelSetting?.[channel] || {}),
197
+ },
198
+ },
199
+ }),
200
+ },
201
+ };
202
+ return createCentralCommsMetaId(payload);
203
+ }),
204
+ );
205
+
206
+ const metaIds = responses
207
+ .map((res) => res?.response?.data?.id)
208
+ .filter(Boolean)
209
+ .join(',');
210
+
211
+ if (metaIds) {
212
+ const getResponse = await getCentralCommsMetaIds(metaIds);
233
213
  }
234
214
  } catch (error) {
235
- console.error('[CommunicationFlow] CCS createCommDefinition error:', error);
215
+ console.error('[CommunicationFlow] CCS createCentralCommsMetaId error:', error);
236
216
  }
237
- } else if (!name) {
238
- console.warn('[CommunicationFlow] Skipping CCS createCommDefinition — config.context.name is required');
239
217
  }
240
218
  }
241
219
 
242
- onSave(ccsCommDefinition ? { ...aggregatedData, ccsCommDefinition } : aggregatedData);
220
+ onSave(aggregatedData);
243
221
  }, [getAggregatedData, config, onSave]);
244
222
 
245
223
  // Call onChange callback when stepData changes
@@ -339,11 +317,6 @@ const CommunicationFlow = ({
339
317
  {renderSteps()}
340
318
  {onSave && (
341
319
  <CapRow useLegacy className="communication-flow-container__footer">
342
- {saveError && (
343
- <CapLabel type="label2" className="communication-flow-container__save-error">
344
- {saveError}
345
- </CapLabel>
346
- )}
347
320
  <CapButton type="primary" onClick={handleSave} disabled={isSaveDisabled}>
348
321
  {formatMessage(messages.save)}
349
322
  </CapButton>
@@ -19,7 +19,6 @@ import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
19
19
  import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
20
20
  import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
21
21
  import CapSlideBox from '@capillarytech/cap-ui-library/CapSlideBox';
22
- import CapTooltip from '@capillarytech/cap-ui-library/CapTooltip';
23
22
  import CommunicationFlow from './index';
24
23
  import { CHANNELS, DEFAULT_COMMUNICATION_STRATEGY_OPTIONS, DYNAMIC_CONTROLS_CONFIG } from './constants';
25
24
  import {
@@ -75,8 +74,6 @@ const CommunicationFlowCard = ({
75
74
  onChange,
76
75
  cap,
77
76
  intl,
78
- disabled,
79
- disabledTooltip,
80
77
  }) => {
81
78
  const { formatMessage } = intl || {};
82
79
  const [showSlideBox, setShowSlideBox] = useState(false);
@@ -94,9 +91,8 @@ const CommunicationFlowCard = ({
94
91
  }, [onCancel]);
95
92
 
96
93
  const handleOpen = useCallback(() => {
97
- if (disabled) return;
98
94
  setShowSlideBox(true);
99
- }, [disabled]);
95
+ }, []);
100
96
 
101
97
  const firstItem = savedData?.contentItems?.[0];
102
98
  const channelConfig = firstItem
@@ -186,17 +182,9 @@ const CommunicationFlowCard = ({
186
182
  <CapImage src={addCreativesIllustration} />
187
183
  </CapColumn>
188
184
  <CapColumn span={14} className="empty-card-action-col">
189
- {disabled && disabledTooltip ? (
190
- <CapTooltip title={disabledTooltip}>
191
- <CapButton type="secondary" disabled>
192
- {formatMessage(messages.addCreatives)}
193
- </CapButton>
194
- </CapTooltip>
195
- ) : (
196
- <CapButton type="secondary" onClick={handleOpen} disabled={disabled}>
197
- {formatMessage(messages.addCreatives)}
198
- </CapButton>
199
- )}
185
+ <CapButton type="secondary" onClick={handleOpen}>
186
+ {formatMessage(messages.addCreatives)}
187
+ </CapButton>
200
188
  </CapColumn>
201
189
  </CapRow>
202
190
  </CapCard>
@@ -239,8 +227,6 @@ CommunicationFlowCard.propTypes = {
239
227
  onChange: PropTypes.func,
240
228
  cap: PropTypes.object,
241
229
  intl: PropTypes.object.isRequired,
242
- disabled: PropTypes.bool, // Disables the "Add creatives" trigger (e.g. until a consumer-required field is filled)
243
- disabledTooltip: PropTypes.node, // Shown on hover when disabled is true
244
230
  };
245
231
 
246
232
  CommunicationFlowCard.defaultProps = {
@@ -249,8 +235,6 @@ CommunicationFlowCard.defaultProps = {
249
235
  onCancel: null,
250
236
  onChange: null,
251
237
  cap: null,
252
- disabled: false,
253
- disabledTooltip: null,
254
238
  };
255
239
 
256
240
  export default injectIntl(CommunicationFlowCard);
@@ -1,7 +1,8 @@
1
1
  import React from 'react';
2
2
 
3
3
  jest.mock('../../../services/api', () => ({
4
- createCommDefinition: jest.fn(),
4
+ createCentralCommsMetaId: jest.fn(),
5
+ getCentralCommsMetaIds: jest.fn(),
5
6
  }));
6
7
 
7
8
  jest.mock('../../CreativesContainer', () => function MockCreativesContainer({
@@ -37,7 +38,7 @@ import { IntlProvider } from 'react-intl';
37
38
  import history from '../../../utils/history';
38
39
  import { initialReducer } from '../../../initialReducer';
39
40
  import CommunicationFlow from '../CommunicationFlow';
40
- import { createCommDefinition } from '../../../services/api';
41
+ import { createCentralCommsMetaId, getCentralCommsMetaIds } from '../../../services/api';
41
42
  import { getEnabledSteps } from '../utils/getEnabledSteps';
42
43
  import {
43
44
  CHANNELS,
@@ -358,93 +359,74 @@ describe('isSaveDisabled', () => {
358
359
  });
359
360
 
360
361
  describe('handleSave — CCS flow', () => {
361
- const ccsConfig = { ...baseConfig, context: { name: 'Order Placed Notification' }, features: {} };
362
-
363
362
  beforeEach(() => {
364
- createCommDefinition.mockResolvedValue({
365
- response: { data: { id: 'cd_123', referenceId: 'ORDER_PLACED_123', status: 'DRAFT', version: { version: 1 } } },
366
- });
363
+ createCentralCommsMetaId.mockResolvedValue({ response: { data: { id: 'meta-123' } } });
364
+ getCentralCommsMetaIds.mockResolvedValue({ response: { data: {} } });
367
365
  });
368
366
 
369
367
  afterEach(() => {
370
368
  jest.clearAllMocks();
371
369
  });
372
370
 
373
- 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 () => {
374
372
  const onSave = jest.fn();
375
373
  renderWithFlow({
376
374
  features: {},
377
- config: ccsConfig,
378
375
  initialData: {
379
- contentItems: [{ channel: 'sms', templateData: { message: 'Hello' } }],
376
+ contentItems: [{ channel: 'sms', templateData: { smsBody: 'Hello' } }],
380
377
  },
381
378
  onSave,
382
379
  });
383
380
 
384
381
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
385
382
 
386
- await waitFor(() => expect(createCommDefinition).toHaveBeenCalledTimes(1));
387
- expect(createCommDefinition).toHaveBeenCalledWith(
383
+ await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalledTimes(1));
384
+ expect(createCentralCommsMetaId).toHaveBeenCalledWith(
388
385
  expect.objectContaining({
389
- name: 'Order Placed Notification',
390
- strategyType: 'SINGLE',
391
- singleChannelStrategy: expect.objectContaining({
392
- channel: 'SMS',
393
- smsMessageContent: { message: 'Hello' },
394
- }),
386
+ centralCommsPayload: expect.objectContaining({ channel: 'SMS', module: 'CAMPAIGNS' }),
395
387
  }),
396
388
  );
397
389
  expect(onSave).toHaveBeenCalledTimes(1);
398
390
  });
399
391
 
400
- it('merges ccsCommDefinition into the data passed to onSave when create succeeds', async () => {
401
- const onSave = jest.fn();
392
+ it('calls getCentralCommsMetaIds when metaIds are returned from createCentralCommsMetaId', async () => {
402
393
  renderWithFlow({
403
394
  features: {},
404
- config: ccsConfig,
405
- initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
406
- onSave,
395
+ initialData: { contentItems: [{ channel: 'EMAIL', templateData: {} }] },
407
396
  });
408
397
 
409
398
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
410
399
 
411
- await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
412
- expect(onSave).toHaveBeenCalledWith(
413
- expect.objectContaining({
414
- ccsCommDefinition: { id: 'cd_123', referenceId: 'ORDER_PLACED_123', version: 1, status: 'DRAFT' },
415
- }),
416
- );
400
+ await waitFor(() => expect(getCentralCommsMetaIds).toHaveBeenCalledWith('meta-123'));
417
401
  });
418
402
 
419
- it('generates a referenceId from name when config.context.referenceId is absent', async () => {
420
- // moduleMocks.js spies on Date.now globally, but this config's resetMocks:true
421
- // clears that return value before every test — set it explicitly here.
422
- jest.spyOn(Date, 'now').mockReturnValue(1612267539410);
403
+ it('skips getCentralCommsMetaIds when response contains no id', async () => {
404
+ createCentralCommsMetaId.mockResolvedValue({ response: { data: {} } });
423
405
  renderWithFlow({
424
406
  features: {},
425
- config: ccsConfig,
426
407
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
427
408
  });
428
409
 
429
410
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
430
411
 
431
- await waitFor(() => expect(createCommDefinition).toHaveBeenCalled());
432
- const payload = createCommDefinition.mock.calls[0][0];
433
- expect(payload.referenceId).toBe('ORDER_PLACED_NOTIFICATION_1612267539410');
412
+ await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalled());
413
+ expect(getCentralCommsMetaIds).not.toHaveBeenCalled();
434
414
  });
435
415
 
436
- it('uses config.context.referenceId and description when provided', async () => {
416
+ it('uses ouId and module from config.context when provided', async () => {
437
417
  renderWithFlow({
438
418
  features: {},
439
- config: { ...ccsConfig, context: { ...ccsConfig.context, referenceId: 'ORDER_PLACED', description: 'desc' } },
419
+ config: { ...baseConfig, context: { ouId: 42, module: 'LOYALTY' }, features: {} },
440
420
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
441
421
  });
442
422
 
443
423
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
444
424
 
445
- await waitFor(() => expect(createCommDefinition).toHaveBeenCalled());
446
- expect(createCommDefinition).toHaveBeenCalledWith(
447
- expect.objectContaining({ referenceId: 'ORDER_PLACED', description: 'desc' }),
425
+ await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalled());
426
+ expect(createCentralCommsMetaId).toHaveBeenCalledWith(
427
+ expect.objectContaining({
428
+ centralCommsPayload: expect.objectContaining({ ouId: 42, module: 'LOYALTY' }),
429
+ }),
448
430
  );
449
431
  });
450
432
 
@@ -452,7 +434,7 @@ describe('handleSave — CCS flow', () => {
452
434
  const onSave = jest.fn();
453
435
  renderWithFlow({
454
436
  features: {},
455
- config: { ...ccsConfig, useCCS: false },
437
+ config: { ...baseConfig, useCCS: false, features: {} },
456
438
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
457
439
  onSave,
458
440
  });
@@ -460,50 +442,14 @@ describe('handleSave — CCS flow', () => {
460
442
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
461
443
 
462
444
  await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
463
- expect(createCommDefinition).not.toHaveBeenCalled();
464
- expect(onSave).toHaveBeenCalledWith(expect.not.objectContaining({ ccsCommDefinition: expect.anything() }));
465
- });
466
-
467
- it('skips createCommDefinition when config.context.name is absent (mandatory)', async () => {
468
- const onSave = jest.fn();
469
- renderWithFlow({
470
- features: {},
471
- config: { ...baseConfig, features: {} },
472
- initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
473
- onSave,
474
- });
475
-
476
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
477
-
478
- await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
479
- expect(createCommDefinition).not.toHaveBeenCalled();
480
- });
481
-
482
- it('skips createCommDefinition for multi-channel strategies (CHANNEL_PRIORITY/AB_TEST)', async () => {
483
- const onSave = jest.fn();
484
- renderWithFlow({
485
- features: {},
486
- config: ccsConfig,
487
- initialData: {
488
- communicationStrategy: CHANNEL_PRIORITY,
489
- channels: ['SMS', 'EMAIL'],
490
- contentItems: [{ channel: 'SMS', templateData: {} }, { channel: 'EMAIL', templateData: {} }],
491
- },
492
- onSave,
493
- });
494
-
495
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
496
-
497
- await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
498
- expect(createCommDefinition).not.toHaveBeenCalled();
445
+ expect(createCentralCommsMetaId).not.toHaveBeenCalled();
499
446
  });
500
447
 
501
- it('still calls onSave (without ccsCommDefinition) when createCommDefinition rejects', async () => {
502
- createCommDefinition.mockRejectedValue(new Error('Network error'));
448
+ it('still calls onSave when createCentralCommsMetaId rejects', async () => {
449
+ createCentralCommsMetaId.mockRejectedValue(new Error('Network error'));
503
450
  const onSave = jest.fn();
504
451
  renderWithFlow({
505
452
  features: {},
506
- config: ccsConfig,
507
453
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
508
454
  onSave,
509
455
  });
@@ -511,35 +457,12 @@ describe('handleSave — CCS flow', () => {
511
457
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
512
458
 
513
459
  await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
514
- expect(onSave).toHaveBeenCalledWith(expect.not.objectContaining({ ccsCommDefinition: expect.anything() }));
515
460
  });
516
461
 
517
- it('blocks save and shows an error when createCommDefinition resolves with a duplicate REFERENCE_ID_EXISTS conflict', async () => {
518
- createCommDefinition.mockResolvedValue({
519
- success: false,
520
- status: { isError: true, code: 409, message: 'REFERENCE_ID_EXISTS' },
521
- message: 'REFERENCE_ID_EXISTS',
522
- });
462
+ it('skips createCentralCommsMetaId when contentItems is empty', async () => {
523
463
  const onSave = jest.fn();
524
464
  renderWithFlow({
525
465
  features: {},
526
- config: ccsConfig,
527
- initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
528
- onSave,
529
- });
530
-
531
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
532
-
533
- await waitFor(() => expect(createCommDefinition).toHaveBeenCalledTimes(1));
534
- expect(await screen.findByText(/already in use in your organization/i)).toBeInTheDocument();
535
- expect(onSave).not.toHaveBeenCalled();
536
- });
537
-
538
- it('skips createCommDefinition when contentItems is empty', async () => {
539
- const onSave = jest.fn();
540
- renderWithFlow({
541
- features: {},
542
- config: ccsConfig,
543
466
  initialData: { contentItems: [] },
544
467
  onSave,
545
468
  });
@@ -547,13 +470,12 @@ describe('handleSave — CCS flow', () => {
547
470
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
548
471
 
549
472
  await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
550
- expect(createCommDefinition).not.toHaveBeenCalled();
473
+ expect(createCentralCommsMetaId).not.toHaveBeenCalled();
551
474
  });
552
475
 
553
- it('places additionalSettings derived from dynamicControls under settings.additionalSettings (not delivery settings)', async () => {
476
+ it('includes additionalSettings derived from dynamicControls in the payload', async () => {
554
477
  renderWithFlow({
555
478
  features: {},
556
- config: ccsConfig,
557
479
  initialData: {
558
480
  contentItems: [{ channel: 'SMS', templateData: {} }],
559
481
  dynamicControls: {
@@ -567,36 +489,14 @@ describe('handleSave — CCS flow', () => {
567
489
 
568
490
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
569
491
 
570
- await waitFor(() => expect(createCommDefinition).toHaveBeenCalled());
571
- const payload = createCommDefinition.mock.calls[0][0];
572
- 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({
573
495
  useTinyUrl: true,
574
496
  encryptUrl: true,
575
497
  linkTrackingEnabled: true,
576
498
  userSubscriptionDisabled: true,
577
499
  });
578
- expect(payload.singleChannelStrategy.smsDeliverySettings.additionalSettings).toBeUndefined();
579
- });
580
-
581
- it('includes channelSettings from deliverySetting.channelSetting in the payload', async () => {
582
- renderWithFlow({
583
- features: {},
584
- config: ccsConfig,
585
- initialData: {
586
- contentItems: [{ channel: 'SMS', templateData: {} }],
587
- deliverySetting: { channelSetting: { SMS: { domainId: 1001, gsmSenderId: 'BRAND1' } } },
588
- },
589
- });
590
-
591
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
592
-
593
- await waitFor(() => expect(createCommDefinition).toHaveBeenCalled());
594
- const payload = createCommDefinition.mock.calls[0][0];
595
- expect(payload.singleChannelStrategy.smsDeliverySettings.channelSettings).toEqual({
596
- channel: 'SMS',
597
- domainId: 1001,
598
- gsmSenderId: 'BRAND1',
599
- });
600
500
  });
601
501
  });
602
502
 
@@ -652,34 +552,39 @@ describe('optional chaining safety', () => {
652
552
  jest.clearAllMocks();
653
553
  });
654
554
 
655
- it('does not crash and skips CCS when config.context is entirely absent', async () => {
656
- const onSave = jest.fn();
555
+ it('defaults ouId to -1 when config.context is absent', async () => {
556
+ createCentralCommsMetaId.mockResolvedValue({ response: { data: { id: 'x' } } });
657
557
  renderWithFlow({
658
558
  features: {},
659
- config: { ...baseConfig, features: {} },
660
559
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
661
- onSave,
662
560
  });
663
561
 
664
562
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
665
563
 
666
- await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
667
- 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
+ );
668
570
  });
669
571
 
670
- it('does not crash when config.context is present but empty', async () => {
671
- const onSave = jest.fn();
572
+ it('defaults module to consumer.toUpperCase() when config.context.module absent', async () => {
573
+ createCentralCommsMetaId.mockResolvedValue({ response: { data: { id: 'x' } } });
672
574
  renderWithFlow({
673
575
  features: {},
674
- config: { ...baseConfig, context: {}, features: {} },
576
+ config: { ...baseConfig, consumer: 'loyalty', features: {} },
675
577
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
676
- onSave,
677
578
  });
678
579
 
679
580
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
680
581
 
681
- await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
682
- 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
+ );
683
588
  });
684
589
 
685
590
  it('initializes channel from config.channel when initialData.channel is absent', () => {
@@ -108,22 +108,6 @@ 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
- });
127
111
  });
128
112
 
129
113
  describe('configured state (initialData provided)', () => {
@@ -38,46 +38,6 @@ export const CHANNEL_PRIORITY = 'CHANNEL_PRIORITY';
38
38
  export const AB_TEST = 'AB_TEST';
39
39
  export const SINGLE_TEMPLATE = 'SINGLE_TEMPLATE';
40
40
 
41
- // CCS CommDefinition strategyType — distinct from SINGLE_TEMPLATE above, which
42
- // is this UI's own communicationStrategy value. Only SINGLE is supported this
43
- // phase; CCS create is skipped entirely for CHANNEL_PRIORITY/AB_TEST.
44
- export const CCS_STRATEGY_TYPE_SINGLE = 'SINGLE';
45
-
46
- // Mirrors CCS's SingleChannelStrategy field names exactly (Cap Notify API
47
- // Design doc, "Channel Strategy overview" / class dump). Kept separate from
48
- // CHANNEL_CONTENT_KEY_MAP/CHANNEL_DELIVERY_KEY_MAP below, which are the legacy
49
- // messageMeta payload's keys and do not match CCS naming for every channel
50
- // (e.g. legacy uses lowercase 'mpushMessageContent'; CCS uses 'mPushMessageContent').
51
- export const CCS_CHANNEL_CONTENT_KEY_MAP = {
52
- SMS: 'smsMessageContent',
53
- EMAIL: 'emailMessageContent',
54
- WHATSAPP: 'whatsappMessageContent',
55
- MPUSH: 'mPushMessageContent',
56
- INAPP: 'inAppMessageContent',
57
- ANDROID: 'androidMessageContent',
58
- IOS: 'iosMessageContent',
59
- ZALO: 'zaloMessageContent',
60
- VIBER: 'viberMessageContent',
61
- LINE: 'lineMessageContent',
62
- WEBPUSH: 'webPushMessageContent',
63
- RCS: 'rcsMessageContent',
64
- };
65
-
66
- export const CCS_CHANNEL_DELIVERY_KEY_MAP = {
67
- SMS: 'smsDeliverySettings',
68
- EMAIL: 'emailDeliverySettings',
69
- WHATSAPP: 'whatsappDeliverySettings',
70
- MPUSH: 'mPushDeliverySettings',
71
- INAPP: 'inAppDeliverySettings',
72
- ANDROID: 'androidDeliverySettings',
73
- IOS: 'iosDeliverySettings',
74
- ZALO: 'zaloDeliverySettings',
75
- VIBER: 'viberDeliverySettings',
76
- LINE: 'lineDeliverySettings',
77
- WEBPUSH: 'webPushDeliverySettings',
78
- RCS: 'rcsDeliverySettings',
79
- };
80
-
81
41
  // Channel config - single source of truth for CreativesContainer and TemplatesV2
82
42
  // paneKey: TemplatesV2 defaultPanes object key (for channelsToHide)
83
43
  // channelProp: CreativesContainer channel prop (must match pane.key for tab to be active)
@@ -87,13 +87,11 @@ CommunicationFlowContainer.propTypes = {
87
87
  controls: PropTypes.array,
88
88
  }),
89
89
  }),
90
- context: PropTypes.object, // ouId, campaignId, programId; name (required for CCS create),
91
- // referenceId (optional, auto-generated from name if absent), description (optional).
92
- useCCS: PropTypes.bool, // If false, skips the CCS createCommDefinition call on save. Defaults to true.
90
+ context: PropTypes.object, // ouId, campaignId, programId, etc.
91
+ useCCS: PropTypes.bool, // If false, skips CCS bulk-claim-approve on save. Defaults to true.
93
92
  }).isRequired,
94
93
  initialData: PropTypes.object, // for edit/preview mode
95
- onSave: PropTypes.func.isRequired, // (data) => void - called when user saves; data.ccsCommDefinition
96
- // ({id, referenceId, version, status}) is present when the CCS create succeeded
94
+ onSave: PropTypes.func.isRequired, // (data) => void - called when user saves
97
95
  onCancel: PropTypes.func.isRequired, // () => void - called when user cancels
98
96
  onChange: PropTypes.func, // (data) => void - optional, called on data changes
99
97
  };
@@ -363,8 +363,4 @@ export default {
363
363
  id: `${prefix}.senderNotConfiguredError`,
364
364
  defaultMessage: 'Selected domain gateway id is not correct. Please change the domain id or contact the gateway team to register them with Capillary.',
365
365
  },
366
- duplicateReferenceIdError: {
367
- id: `${prefix}.duplicateReferenceIdError`,
368
- defaultMessage: 'This reference ID is already in use in your organization. Please use a different reference ID.',
369
- },
370
366
  };
@@ -2051,21 +2051,11 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2051
2051
  const isCardArchiveEligible = isArchivalEnabled && this.isChannelArchiveEligible(currentChannel, cardWhatsappStatus, cardRcsStatus);
2052
2052
  const isArchivedMode = isArchivalEnabled && get(this.props, 'Templates.isArchivedMode', false);
2053
2053
  const isAnyArchiveInProgress = isArchivalEnabled && !!(get(this.props, 'Templates.archiveInProgress') || get(this.props, 'Templates.unarchiveInProgress') || get(this.props, 'Templates.bulkArchiveInProgress') || get(this.props, 'Templates.bulkUnarchiveInProgress'));
2054
- const smsBaseForLegacyCheck = template?.versions?.base || {};
2055
- const updatedSmsEditorForLegacy = smsBaseForLegacyCheck['updated-sms-editor'];
2056
- const updatedSmsEditorForLegacyJoined = Array.isArray(updatedSmsEditorForLegacy)
2057
- ? updatedSmsEditorForLegacy.join('')
2058
- : updatedSmsEditorForLegacy;
2059
- const smsBodyForLegacyCheck = `${updatedSmsEditorForLegacyJoined || ''}${smsBaseForLegacyCheck['sms-editor'] || ''}`;
2060
- const isDltLegacyTemplate = currentChannel === SMS
2061
- && isTraiDltFeature
2062
- && !this.props.isFullMode
2063
- && DLT_LEGACY_VAR_REGEX.test(smsBodyForLegacyCheck);
2064
2054
  const templateData = {
2065
2055
  key: `${currentChannel}-card-${template?.name}`,
2066
2056
  title: (
2067
2057
  <span className="template-card-title" title={template?.name}>
2068
- {isCardArchiveEligible && this.renderCardSelectionCheckbox({ templateId: template._id, selectedIds: selectedIdsArrayForCard, isDisabled: isAnyArchiveInProgress || isDltLegacyTemplate })}
2058
+ {isCardArchiveEligible && this.renderCardSelectionCheckbox({ templateId: template._id, selectedIds: selectedIdsArrayForCard, isDisabled: isAnyArchiveInProgress })}
2069
2059
  <CapLabel.CapLabelInline type="label1" title={template?.name} className="template-card-name">
2070
2060
  {template?.name}
2071
2061
  {currentChannel === INAPP && (
@@ -2125,39 +2115,20 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
2125
2115
  })()
2126
2116
  ],
2127
2117
  hoverOption: isArchivedMode || !this.canPerform(PERMISSIONS.CREATIVE_EDIT) ? null : (
2128
- isDltLegacyTemplate ? (
2129
- <CapTooltip
2130
- title={this.props.intl.formatMessage(messages.smsLegacyBlockedTooltip)}
2131
- >
2132
- <CapLabel.CapLabelInline>
2133
- <CapButton
2134
- className={
2135
- this.props.isFullMode
2136
- ? `edit-${channelLowerCase}`
2137
- : `select-${channelLowerCase}`
2138
- }
2139
- disabled
2140
- >
2141
- {hoverButtonText}
2142
- </CapButton>
2143
- </CapLabel.CapLabelInline>
2144
- </CapTooltip>
2145
- ) : (
2146
- <CapButton
2147
- className={
2148
- this.props.isFullMode
2149
- ? `edit-${channelLowerCase}`
2150
- : `select-${channelLowerCase}`
2151
- }
2152
- onClick={e =>
2153
- handlers.handleEditClick(e, template, undefined, undefined, {
2154
- account: this.state.selectedAccount
2155
- })
2156
- }
2157
- >
2158
- {hoverButtonText}
2159
- </CapButton>
2160
- )
2118
+ <CapButton
2119
+ className={
2120
+ this.props.isFullMode
2121
+ ? `edit-${channelLowerCase}`
2122
+ : `select-${channelLowerCase}`
2123
+ }
2124
+ onClick={e =>
2125
+ handlers.handleEditClick(e, template, undefined, undefined, {
2126
+ account: this.state.selectedAccount
2127
+ })
2128
+ }
2129
+ >
2130
+ {hoverButtonText}
2131
+ </CapButton>
2161
2132
  )
2162
2133
  };
2163
2134
  const {
@@ -3603,22 +3574,6 @@ export class Templates extends React.Component { // eslint-disable-line react/pr
3603
3574
  CapNotification.error({ message: this.props.intl.formatMessage(messages.cannotEditArchivedTemplate) });
3604
3575
  return;
3605
3576
  }
3606
- if (!this.props.isFullMode
3607
- && this.checkDLTfeatureEnable()
3608
- && (this.state.channel || '').toLowerCase() === SMS_LOWERCASE) {
3609
- const smsBase = template?.versions?.base || {};
3610
- const updatedSms = smsBase['updated-sms-editor'];
3611
- const updatedSmsJoined = Array.isArray(updatedSms) ? updatedSms.join('') : updatedSms;
3612
- // Check BOTH candidate fields — some legacy templates keep the body only in `sms-editor`
3613
- // while others have it in `updated-sms-editor`. Concatenating catches either.
3614
- const smsBody = `${updatedSmsJoined || ''}${smsBase['sms-editor'] || ''}`;
3615
- if (DLT_LEGACY_VAR_REGEX.test(smsBody)) {
3616
- CapNotification.error({
3617
- message: this.props.intl.formatMessage(messages.smsLegacyBlockedTooltip),
3618
- });
3619
- return;
3620
- }
3621
- }
3622
3577
  if (modeType && modeType !== undefined) {
3623
3578
  this.setState({modeType});
3624
3579
  }
@@ -546,10 +546,6 @@ export default defineMessages({
546
546
  id: `${scope}.smsLegacyFormatBadge`,
547
547
  defaultMessage: 'Legacy format',
548
548
  },
549
- "smsLegacyBlockedTooltip": {
550
- id: `${scope}.smsLegacyBlockedTooltip`,
551
- defaultMessage: "This template can't be used until re-registered with typed variables on the DLT portal.",
552
- },
553
549
  "uploadTemplate": {
554
550
  id: `${scope}.uploadTemplate`,
555
551
  defaultMessage: 'Upload template',
@@ -3161,7 +3161,7 @@ const isAuthenticationTemplate = isEqual(templateCategory, WHATSAPP_CATEGORIES.a
3161
3161
  : templateMsgText;
3162
3162
 
3163
3163
  // Get varMapped from editData or empty object
3164
- const varMappedValue = editData?.varMapped || {};
3164
+ const varMappedValue = varMap || {};
3165
3165
 
3166
3166
  return {
3167
3167
  // Preview structure (for WhatsAppPreviewContent component)