@capillarytech/creatives-library 9.0.54-alpha.3 → 9.0.54

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.
@@ -63,6 +63,7 @@ export const ENABLE_AI_SUGGESTIONS = 'ENABLE_AI_SUGGESTIONS';
63
63
  export const AI_CONTENT_BOT_DISABLED = 'AI_CONTENT_BOT_DISABLED';
64
64
  export const AI_DOCUMENTATION_BOT_ENABLED = 'AI_DOCUMENTATION_BOT_ENABLED';
65
65
  export const ENABLE_PRODUCT_SUPPORT_VIDEOS = 'ENABLE_PRODUCT_SUPPORT_VIDEOS';
66
+ export const SUPPORT_ENGAGEMENT_MODULE = 'SUPPORT_ENGAGEMENT_MODULE';
66
67
  export const EMBEDDED = 'embedded';
67
68
  // --- Tag/Validation Constants ---
68
69
  export const CARD_RELATED_TAGS = [
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "9.0.54-alpha.3",
4
+ "version": "9.0.54",
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));
package/utils/common.js CHANGED
@@ -99,6 +99,10 @@ export const hasSupportCKEditor = Auth.hasFeatureAccess.bind(
99
99
  null,
100
100
  SUPPORT_CK_EDITOR,
101
101
  );
102
+ export const hasSupportEngagementModule = Auth.hasFeatureAccess.bind(
103
+ null,
104
+ SUPPORT_ENGAGEMENT_MODULE,
105
+ );
102
106
 
103
107
  export const hasGiftVoucherFeature = Auth.hasFeatureAccess.bind(
104
108
  null,
@@ -4276,13 +4276,13 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
4276
4276
  forceRender: true,
4277
4277
  tab: (
4278
4278
  <div className="form-tab-header" onInput={(e) => e.stopPropagation()}>
4279
- { this.state.translationLang !== 'ja-JP' && (
4279
+ {
4280
4280
  _.forEach(renderedContentSections, (value) => {
4281
4281
  if (value) {
4282
4282
  return value;
4283
4283
  }
4284
4284
  return value;
4285
- }))
4285
+ })
4286
4286
  }
4287
4287
  </div>
4288
4288
  ),
@@ -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,11 +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,
42
- CCS_CHANNEL_NAME_MAP,
43
- CCS_CONTENT_TRANSFORMS,
38
+ CHANNEL_CONTENT_KEY_MAP,
39
+ CHANNEL_DELIVERY_KEY_MAP,
40
+ CAMPAIGNS,
44
41
  } from './constants';
45
42
  import { getEnabledSteps } from './utils/getEnabledSteps';
46
43
  import messages from './messages';
@@ -67,25 +64,6 @@ const hasDeliverySettingForChannel = (channelSetting, channel) => {
67
64
  return !!settings && Object.values(settings).some((v) => v !== null && v !== '' && v !== undefined);
68
65
  };
69
66
 
70
- // createCommDefinition resolves (rather than rejects) with the CCS error envelope
71
- // for 4xx/5xx responses — see api.js's request()/checkStatus. A duplicate
72
- // referenceId within the org comes back as this specific 409.
73
- const isDuplicateReferenceIdError = (res) => res?.success === false && res?.status?.message === 'REFERENCE_ID_EXISTS';
74
-
75
- /**
76
- * CCS requires referenceId on create; the consumer-supplied Alert/comm name is
77
- * mandatory but referenceId is optional in every consumer's own form. Generate
78
- * a stable fallback from the name rather than failing the save.
79
- */
80
- const buildCcsReferenceId = (name) => {
81
- const slug = (name || 'COMM')
82
- .trim()
83
- .toUpperCase()
84
- .replace(/[^A-Z0-9]+/g, '_')
85
- .replace(/^_+|_+$/g, '') || 'COMM';
86
- return `${slug}_${Date.now()}`;
87
- };
88
-
89
67
  const CommunicationFlow = ({
90
68
  config,
91
69
  initialData,
@@ -113,7 +91,6 @@ const CommunicationFlow = ({
113
91
  };
114
92
  });
115
93
  const [validationErrors, setValidationErrors] = useState({});
116
- const [saveError, setSaveError] = useState(null);
117
94
 
118
95
  // Memoize enabled steps
119
96
  const enabledSteps = useMemo(() => getEnabledSteps(config), [config]);
@@ -173,88 +150,74 @@ const CommunicationFlow = ({
173
150
  }, []);
174
151
 
175
152
  const handleSave = useCallback(async () => {
176
- setSaveError(null);
177
153
  const aggregatedData = getAggregatedData();
178
154
  const shouldUseCCS = config?.useCCS !== false;
179
- let ccsCommDefinition = null;
180
155
 
181
156
  if (shouldUseCCS) {
182
- const isMultiChannel = [CHANNEL_PRIORITY, AB_TEST].includes(aggregatedData.communicationStrategy);
183
- const contentItem = (aggregatedData.contentItems || [])[0];
184
- // Consumer-supplied name (e.g. CapNotify's Alert Name field) — CommunicationFlow
185
- // has no name input of its own, so this is mandatory input from config.context.
186
- 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);
187
160
 
188
- // CCS create is SINGLE-strategy only this phase (D1); CHANNEL_PRIORITY/AB_TEST
189
- // carry multiple content items with no CCS equivalent yet.
190
- if (!isMultiChannel && contentItem && name) {
191
- const rawChannel = (contentItem.channel || '').toUpperCase();
192
- const channel = CCS_CHANNEL_NAME_MAP[rawChannel] || rawChannel;
193
- const contentKey = CCS_CHANNEL_CONTENT_KEY_MAP[channel];
194
- const deliveryKey = CCS_CHANNEL_DELIVERY_KEY_MAP[channel];
195
- const contentTransform = CCS_CONTENT_TRANSFORMS[channel];
196
- const contentPayload = contentTransform ? contentTransform(contentItem.templateData) : contentItem.templateData;
197
- const { dynamicControls = {} } = aggregatedData;
198
- const channelSettings = aggregatedData.deliverySetting?.channelSetting?.[channel] || {};
199
- const referenceId = config?.context?.referenceId || buildCcsReferenceId(name);
161
+ const channelContentKeyMap = CHANNEL_CONTENT_KEY_MAP;
162
+ const channelDeliveryKeyMap = CHANNEL_DELIVERY_KEY_MAP;
200
163
 
201
- const payload = {
202
- referenceId,
203
- name,
204
- description: config?.context?.description || undefined,
205
- strategyType: CCS_STRATEGY_TYPE_SINGLE,
206
- settings: {
207
- additionalSettings: {
208
- useTinyUrl: dynamicControls.useTinyUrl ?? false,
209
- encryptUrl: dynamicControls.sendToControlCustomers ?? false,
210
- linkTrackingEnabled: dynamicControls.overrideDailyLimit ?? false,
211
- userSubscriptionDisabled: dynamicControls.sendToBrandPocs ?? false,
212
- },
213
- executionParams: {},
214
- },
215
- singleChannelStrategy: {
216
- variant: {
217
- channel,
218
- // `channel` spreads LAST in both objects below — contentItem.templateData
219
- // (e.g. mobile push) carries the UI's own internal channel identifier
220
- // (MOBILEPUSH), which must never win over CCS's normalized enum value
221
- // (PUSH) once spread order puts it after.
222
- ...(contentKey && { [contentKey]: { ...contentPayload, channel } }),
223
- ...(deliveryKey && { [deliveryKey]: { channelSettings: { ...channelSettings, channel } } }),
224
- },
225
- },
226
- };
164
+ const contentItems = aggregatedData.contentItems || [];
165
+ const { dynamicControls = {} } = aggregatedData;
227
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) {
228
175
  try {
229
- const res = await createCommDefinition(payload);
230
- if (isDuplicateReferenceIdError(res)) {
231
- // Duplicate referenceId in this org — block the save so the user can
232
- // change it, rather than silently proceeding without a CCS comm.
233
- setSaveError(formatMessage(messages.duplicateReferenceIdError));
234
- return;
235
- }
236
- const data = res?.response?.data;
237
- if (data?.id) {
238
- ccsCommDefinition = {
239
- id: data.id,
240
- referenceId: data.referenceId,
241
- version: data.version?.version ?? 1,
242
- status: data.status,
243
- };
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);
244
213
  }
245
214
  } catch (error) {
246
- console.error('[CommunicationFlow] CCS createCommDefinition error:', error);
215
+ console.error('[CommunicationFlow] CCS createCentralCommsMetaId error:', error);
247
216
  }
248
- } else if (!name) {
249
- console.warn('[CommunicationFlow] Skipping CCS createCommDefinition — config.context.name is required');
250
- } else if (isMultiChannel) {
251
- console.warn('[CommunicationFlow] Skipping CCS createCommDefinition — CHANNEL_PRIORITY/AB_TEST strategies are not supported by CCS create yet (SINGLE only)');
252
- } else if (!contentItem) {
253
- console.warn('[CommunicationFlow] Skipping CCS createCommDefinition — no content item found (contentItems is empty)');
254
217
  }
255
218
  }
256
219
 
257
- onSave(ccsCommDefinition ? { ...aggregatedData, ccsCommDefinition } : aggregatedData);
220
+ onSave(aggregatedData);
258
221
  }, [getAggregatedData, config, onSave]);
259
222
 
260
223
  // Call onChange callback when stepData changes
@@ -354,11 +317,6 @@ const CommunicationFlow = ({
354
317
  {renderSteps()}
355
318
  {onSave && (
356
319
  <CapRow useLegacy className="communication-flow-container__footer">
357
- {saveError && (
358
- <CapLabel type="label2" className="communication-flow-container__save-error">
359
- {saveError}
360
- </CapLabel>
361
- )}
362
320
  <CapButton type="primary" onClick={handleSave} disabled={isSaveDisabled}>
363
321
  {formatMessage(messages.save)}
364
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 {
@@ -42,7 +41,7 @@ const resolveValue = (val) => {
42
41
 
43
42
  const SENDER_ID_RESOLVERS = {
44
43
  [SMS]: (setting) => resolveValue(setting?.gsmSenderId),
45
- [EMAIL]: (setting) => resolveValue(setting?.senderId),
44
+ [EMAIL]: (setting) => resolveValue(setting?.senderEmail),
46
45
  [VIBER]: (setting) => resolveValue(setting?.sender) || resolveValue(setting?.gsmSenderId),
47
46
  [WHATSAPP]: (setting) => resolveValue(setting?.senderMobNum),
48
47
  [RCS]: (setting) => resolveValue(setting?.senderMobNum) || resolveValue(setting?.rcsSender),
@@ -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,241 +359,82 @@ 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
- variant: expect.objectContaining({
393
- channel: 'SMS',
394
- smsMessageContent: { channel: 'SMS', message: 'Hello' },
395
- }),
396
- }),
386
+ centralCommsPayload: expect.objectContaining({ channel: 'SMS', module: 'CAMPAIGNS' }),
397
387
  }),
398
388
  );
399
389
  expect(onSave).toHaveBeenCalledTimes(1);
400
390
  });
401
391
 
402
- it('transforms EMAIL content from the legacy editor shape (emailSubject) to CCS\'s messageSubject', async () => {
403
- const onSave = jest.fn();
404
- renderWithFlow({
405
- features: {},
406
- config: ccsConfig,
407
- initialData: {
408
- contentItems: [{
409
- channel: 'EMAIL',
410
- templateData: { emailSubject: 'Order Confirmed', emailBody: '<html>Hi</html>' },
411
- }],
412
- },
413
- onSave,
414
- });
415
-
416
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
417
-
418
- await waitFor(() => expect(createCommDefinition).toHaveBeenCalledTimes(1));
419
- expect(createCommDefinition).toHaveBeenCalledWith(
420
- expect.objectContaining({
421
- singleChannelStrategy: expect.objectContaining({
422
- variant: expect.objectContaining({
423
- channel: 'EMAIL',
424
- emailMessageContent: {
425
- channel: 'EMAIL',
426
- messageSubject: 'Order Confirmed',
427
- messageBody: '<html>Hi</html>',
428
- },
429
- }),
430
- }),
431
- }),
432
- );
433
- });
434
-
435
- it('transforms WEBPUSH content from the legacy editor\'s messageContent.content wrapping to CCS\'s flat shape', async () => {
436
- const onSave = jest.fn();
437
- const cta = { type: 'SITE_URL', actionLink: 'https://example.com' };
438
- const expandableDetails = { media: [{ url: 'https://example.com/banner.png', type: 'IMAGE' }], ctas: [] };
439
- renderWithFlow({
440
- features: {},
441
- config: ccsConfig,
442
- initialData: {
443
- contentItems: [{
444
- channel: 'WEBPUSH',
445
- templateData: {
446
- messageContent: {
447
- content: {
448
- channel: 'WEBPUSH',
449
- accountId: 13792,
450
- content: { title: 'Hi', message: 'Body', iconImageUrl: 'https://example.com/icon.png', cta, expandableDetails },
451
- messageSubject: 'Subject',
452
- offers: [],
453
- },
454
- },
455
- },
456
- }],
457
- },
458
- onSave,
459
- });
460
-
461
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
462
-
463
- await waitFor(() => expect(createCommDefinition).toHaveBeenCalledTimes(1));
464
- expect(createCommDefinition).toHaveBeenCalledWith(
465
- expect.objectContaining({
466
- singleChannelStrategy: expect.objectContaining({
467
- variant: expect.objectContaining({
468
- channel: 'WEBPUSH',
469
- webPushMessageContent: {
470
- channel: 'WEBPUSH',
471
- messageSubject: 'Subject',
472
- accountId: 13792,
473
- content: { title: 'Hi', message: 'Body', iconImageUrl: 'https://example.com/icon.png', cta, expandableDetails },
474
- },
475
- }),
476
- }),
477
- }),
478
- );
479
- });
480
-
481
- 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 () => {
482
- const onSave = jest.fn();
392
+ it('calls getCentralCommsMetaIds when metaIds are returned from createCentralCommsMetaId', async () => {
483
393
  renderWithFlow({
484
394
  features: {},
485
- config: ccsConfig,
486
- initialData: {
487
- // `channel: 'MOBILEPUSH'` inline on templateData reproduces the real
488
- // production bug: contentItem.templateData carries the UI's own
489
- // internal channel identifier, which must not win over the
490
- // normalized 'PUSH' enum once spread into mpushMessageContent.
491
- contentItems: [{ channel: 'MOBILEPUSH', templateData: { channel: 'MOBILEPUSH', messageSubject: 'Hi' } }],
492
- // Likewise, deliverySetting.channelSetting.PUSH carrying its own
493
- // stray `channel` key must not win over the normalized value once
494
- // spread into mpushDeliverySettings.channelSettings.
495
- deliverySetting: { channelSetting: { PUSH: { channel: 'MOBILEPUSH', domainId: 2002 } } },
496
- },
497
- onSave,
395
+ initialData: { contentItems: [{ channel: 'EMAIL', templateData: {} }] },
498
396
  });
499
397
 
500
398
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
501
399
 
502
- await waitFor(() => expect(createCommDefinition).toHaveBeenCalledTimes(1));
503
- expect(createCommDefinition).toHaveBeenCalledWith(
504
- expect.objectContaining({
505
- singleChannelStrategy: expect.objectContaining({
506
- variant: expect.objectContaining({
507
- channel: 'PUSH',
508
- mpushMessageContent: { channel: 'PUSH', messageSubject: 'Hi' },
509
- mpushDeliverySettings: {
510
- channelSettings: { channel: 'PUSH', domainId: 2002 },
511
- },
512
- }),
513
- }),
514
- }),
515
- );
400
+ await waitFor(() => expect(getCentralCommsMetaIds).toHaveBeenCalledWith('meta-123'));
516
401
  });
517
402
 
518
- it('merges ccsCommDefinition into the data passed to onSave when create succeeds', async () => {
519
- const onSave = jest.fn();
403
+ it('skips getCentralCommsMetaIds when response contains no id', async () => {
404
+ createCentralCommsMetaId.mockResolvedValue({ response: { data: {} } });
520
405
  renderWithFlow({
521
406
  features: {},
522
- config: ccsConfig,
523
407
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
524
- onSave,
525
408
  });
526
409
 
527
410
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
528
411
 
529
- await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
530
- expect(onSave).toHaveBeenCalledWith(
531
- expect.objectContaining({
532
- ccsCommDefinition: { id: 'cd_123', referenceId: 'ORDER_PLACED_123', version: 1, status: 'DRAFT' },
533
- }),
534
- );
412
+ await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalled());
413
+ expect(getCentralCommsMetaIds).not.toHaveBeenCalled();
535
414
  });
536
415
 
537
- it('preserves a real version of 0 from CCS (not coerced to 1 by a falsy-zero fallback)', async () => {
538
- const onSave = jest.fn();
539
- createCommDefinition.mockResolvedValueOnce({
540
- response: { data: { id: 'cd_123', referenceId: 'ORDER_PLACED_123', status: 'DRAFT', version: { version: 0 } } },
541
- });
416
+ it('uses ouId and module from config.context when provided', async () => {
542
417
  renderWithFlow({
543
418
  features: {},
544
- config: ccsConfig,
419
+ config: { ...baseConfig, context: { ouId: 42, module: 'LOYALTY' }, features: {} },
545
420
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
546
- onSave,
547
421
  });
548
422
 
549
423
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
550
424
 
551
- await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
552
- expect(onSave).toHaveBeenCalledWith(
425
+ await waitFor(() => expect(createCentralCommsMetaId).toHaveBeenCalled());
426
+ expect(createCentralCommsMetaId).toHaveBeenCalledWith(
553
427
  expect.objectContaining({
554
- ccsCommDefinition: { id: 'cd_123', referenceId: 'ORDER_PLACED_123', version: 0, status: 'DRAFT' },
428
+ centralCommsPayload: expect.objectContaining({ ouId: 42, module: 'LOYALTY' }),
555
429
  }),
556
430
  );
557
431
  });
558
432
 
559
- it('generates a referenceId from name when config.context.referenceId is absent', async () => {
560
- // moduleMocks.js spies on Date.now globally, but this config's resetMocks:true
561
- // clears that return value before every test — set it explicitly here.
562
- jest.spyOn(Date, 'now').mockReturnValue(1612267539410);
563
- renderWithFlow({
564
- features: {},
565
- config: ccsConfig,
566
- initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
567
- });
568
-
569
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
570
-
571
- await waitFor(() => expect(createCommDefinition).toHaveBeenCalled());
572
- const payload = createCommDefinition.mock.calls[0][0];
573
- expect(payload.referenceId).toBe('ORDER_PLACED_NOTIFICATION_1612267539410');
574
- });
575
-
576
- it('uses config.context.referenceId and description when provided', async () => {
577
- renderWithFlow({
578
- features: {},
579
- config: { ...ccsConfig, context: { ...ccsConfig.context, referenceId: 'ORDER_PLACED', description: 'desc' } },
580
- initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
581
- });
582
-
583
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
584
-
585
- await waitFor(() => expect(createCommDefinition).toHaveBeenCalled());
586
- expect(createCommDefinition).toHaveBeenCalledWith(
587
- expect.objectContaining({ referenceId: 'ORDER_PLACED', description: 'desc' }),
588
- );
589
- });
590
-
591
433
  it('skips CCS entirely when useCCS is false', async () => {
592
434
  const onSave = jest.fn();
593
435
  renderWithFlow({
594
436
  features: {},
595
- config: { ...ccsConfig, useCCS: false },
437
+ config: { ...baseConfig, useCCS: false, features: {} },
596
438
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
597
439
  onSave,
598
440
  });
@@ -600,15 +442,14 @@ describe('handleSave — CCS flow', () => {
600
442
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
601
443
 
602
444
  await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
603
- expect(createCommDefinition).not.toHaveBeenCalled();
604
- expect(onSave).toHaveBeenCalledWith(expect.not.objectContaining({ ccsCommDefinition: expect.anything() }));
445
+ expect(createCentralCommsMetaId).not.toHaveBeenCalled();
605
446
  });
606
447
 
607
- 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'));
608
450
  const onSave = jest.fn();
609
451
  renderWithFlow({
610
452
  features: {},
611
- config: { ...baseConfig, features: {} },
612
453
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
613
454
  onSave,
614
455
  });
@@ -616,70 +457,12 @@ describe('handleSave — CCS flow', () => {
616
457
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
617
458
 
618
459
  await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
619
- expect(createCommDefinition).not.toHaveBeenCalled();
620
- });
621
-
622
- it('skips createCommDefinition for multi-channel strategies (CHANNEL_PRIORITY/AB_TEST)', async () => {
623
- const onSave = jest.fn();
624
- renderWithFlow({
625
- features: {},
626
- config: ccsConfig,
627
- initialData: {
628
- communicationStrategy: CHANNEL_PRIORITY,
629
- channels: ['SMS', 'EMAIL'],
630
- contentItems: [{ channel: 'SMS', templateData: {} }, { channel: 'EMAIL', templateData: {} }],
631
- },
632
- onSave,
633
- });
634
-
635
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
636
-
637
- await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
638
- expect(createCommDefinition).not.toHaveBeenCalled();
639
460
  });
640
461
 
641
- it('still calls onSave (without ccsCommDefinition) when createCommDefinition rejects', async () => {
642
- createCommDefinition.mockRejectedValue(new Error('Network error'));
462
+ it('skips createCentralCommsMetaId when contentItems is empty', async () => {
643
463
  const onSave = jest.fn();
644
464
  renderWithFlow({
645
465
  features: {},
646
- config: ccsConfig,
647
- initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
648
- onSave,
649
- });
650
-
651
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
652
-
653
- await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
654
- expect(onSave).toHaveBeenCalledWith(expect.not.objectContaining({ ccsCommDefinition: expect.anything() }));
655
- });
656
-
657
- it('blocks save and shows an error when createCommDefinition resolves with a duplicate REFERENCE_ID_EXISTS conflict', async () => {
658
- createCommDefinition.mockResolvedValue({
659
- success: false,
660
- status: { isError: true, code: 409, message: 'REFERENCE_ID_EXISTS' },
661
- message: 'REFERENCE_ID_EXISTS',
662
- });
663
- const onSave = jest.fn();
664
- renderWithFlow({
665
- features: {},
666
- config: ccsConfig,
667
- initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
668
- onSave,
669
- });
670
-
671
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
672
-
673
- await waitFor(() => expect(createCommDefinition).toHaveBeenCalledTimes(1));
674
- expect(await screen.findByText(/already in use in your organization/i)).toBeInTheDocument();
675
- expect(onSave).not.toHaveBeenCalled();
676
- });
677
-
678
- it('skips createCommDefinition when contentItems is empty', async () => {
679
- const onSave = jest.fn();
680
- renderWithFlow({
681
- features: {},
682
- config: ccsConfig,
683
466
  initialData: { contentItems: [] },
684
467
  onSave,
685
468
  });
@@ -687,13 +470,12 @@ describe('handleSave — CCS flow', () => {
687
470
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
688
471
 
689
472
  await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
690
- expect(createCommDefinition).not.toHaveBeenCalled();
473
+ expect(createCentralCommsMetaId).not.toHaveBeenCalled();
691
474
  });
692
475
 
693
- it('places additionalSettings derived from dynamicControls under settings.additionalSettings (not delivery settings)', async () => {
476
+ it('includes additionalSettings derived from dynamicControls in the payload', async () => {
694
477
  renderWithFlow({
695
478
  features: {},
696
- config: ccsConfig,
697
479
  initialData: {
698
480
  contentItems: [{ channel: 'SMS', templateData: {} }],
699
481
  dynamicControls: {
@@ -707,36 +489,14 @@ describe('handleSave — CCS flow', () => {
707
489
 
708
490
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
709
491
 
710
- await waitFor(() => expect(createCommDefinition).toHaveBeenCalled());
711
- const payload = createCommDefinition.mock.calls[0][0];
712
- 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({
713
495
  useTinyUrl: true,
714
496
  encryptUrl: true,
715
497
  linkTrackingEnabled: true,
716
498
  userSubscriptionDisabled: true,
717
499
  });
718
- expect(payload.singleChannelStrategy.variant.smsDeliverySettings.additionalSettings).toBeUndefined();
719
- });
720
-
721
- it('includes channelSettings from deliverySetting.channelSetting in the payload', async () => {
722
- renderWithFlow({
723
- features: {},
724
- config: ccsConfig,
725
- initialData: {
726
- contentItems: [{ channel: 'SMS', templateData: {} }],
727
- deliverySetting: { channelSetting: { SMS: { domainId: 1001, gsmSenderId: 'BRAND1' } } },
728
- },
729
- });
730
-
731
- await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
732
-
733
- await waitFor(() => expect(createCommDefinition).toHaveBeenCalled());
734
- const payload = createCommDefinition.mock.calls[0][0];
735
- expect(payload.singleChannelStrategy.variant.smsDeliverySettings.channelSettings).toEqual({
736
- channel: 'SMS',
737
- domainId: 1001,
738
- gsmSenderId: 'BRAND1',
739
- });
740
500
  });
741
501
  });
742
502
 
@@ -792,34 +552,39 @@ describe('optional chaining safety', () => {
792
552
  jest.clearAllMocks();
793
553
  });
794
554
 
795
- it('does not crash and skips CCS when config.context is entirely absent', async () => {
796
- const onSave = jest.fn();
555
+ it('defaults ouId to -1 when config.context is absent', async () => {
556
+ createCentralCommsMetaId.mockResolvedValue({ response: { data: { id: 'x' } } });
797
557
  renderWithFlow({
798
558
  features: {},
799
- config: { ...baseConfig, features: {} },
800
559
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
801
- onSave,
802
560
  });
803
561
 
804
562
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
805
563
 
806
- await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
807
- 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
+ );
808
570
  });
809
571
 
810
- it('does not crash when config.context is present but empty', async () => {
811
- const onSave = jest.fn();
572
+ it('defaults module to consumer.toUpperCase() when config.context.module absent', async () => {
573
+ createCentralCommsMetaId.mockResolvedValue({ response: { data: { id: 'x' } } });
812
574
  renderWithFlow({
813
575
  features: {},
814
- config: { ...baseConfig, context: {}, features: {} },
576
+ config: { ...baseConfig, consumer: 'loyalty', features: {} },
815
577
  initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
816
- onSave,
817
578
  });
818
579
 
819
580
  await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
820
581
 
821
- await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
822
- 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
+ );
823
588
  });
824
589
 
825
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)', () => {
@@ -247,11 +231,11 @@ describe('CommunicationFlowCard', () => {
247
231
  expect(screen.getByText(/Sender ID/)).toBeInTheDocument();
248
232
  });
249
233
 
250
- it('shows senderId for EMAIL with Sender ID label', () => {
234
+ it('shows senderEmail for EMAIL with Sender ID label', () => {
251
235
  renderCard({
252
236
  initialData: makeSavedData({
253
237
  contentItems: [{ channel: 'EMAIL', templateData: {} }],
254
- deliverySetting: { channelSetting: { EMAIL: { senderId: 'test@example.com' } } },
238
+ deliverySetting: { channelSetting: { EMAIL: { senderEmail: 'test@example.com' } } },
255
239
  }),
256
240
  });
257
241
  expect(screen.getByText(/test@example.com/)).toBeInTheDocument();
@@ -38,86 +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 sample payloads, verified against real create responses). Kept
48
- // separate from CHANNEL_CONTENT_KEY_MAP/CHANNEL_DELIVERY_KEY_MAP below, which
49
- // are the legacy messageMeta payload's keys.
50
- export const CCS_CHANNEL_CONTENT_KEY_MAP = {
51
- SMS: 'smsMessageContent',
52
- EMAIL: 'emailMessageContent',
53
- WHATSAPP: 'whatsappMessageContent',
54
- // CCS's channel enum value for mobile push is 'PUSH' (verified against real
55
- // create responses), even though the content/delivery keys stay 'mpush*'.
56
- PUSH: 'mpushMessageContent',
57
- INAPP: 'inAppMessageContent',
58
- ANDROID: 'androidMessageContent',
59
- IOS: 'iosMessageContent',
60
- ZALO: 'zaloMessageContent',
61
- VIBER: 'viberMessageContent',
62
- LINE: 'lineMessageContent',
63
- WEBPUSH: 'webPushMessageContent',
64
- RCS: 'rcsMessageContent',
65
- };
66
-
67
- // contentItem.templateData is built by the legacy CreativesContainer content
68
- // editor (messageMeta-oriented field names) — for channels whose legacy field
69
- // names don't match CCS's own content schema, transform before sending rather
70
- // than dumping templateData as-is. EMAIL: legacy stores the subject as
71
- // `emailSubject` and the HTML body as `emailBody` (see
72
- // extractContentForPreview.js and CreativesContainer/index.js, where the Bee
73
- // editor's output is written back into templateData.emailBody — also
74
- // mirrored by the existing legacy emailMessageContent builder in
75
- // utils/transformerUtils.js). CCS expects `messageSubject`/`messageBody`.
76
- export const CCS_CONTENT_TRANSFORMS = {
77
- EMAIL: (templateData = {}) => ({
78
- messageSubject: templateData.emailSubject,
79
- messageBody: templateData.emailBody || templateData.emailHtml,
80
- }),
81
- // WEBPUSH: the legacy editor nests the real data under
82
- // messageContent.content (see CommunicationFlowCard.js's own comment on
83
- // this same wrapping, for the edit-mode case) — extracting it flat here
84
- // matches CCS's shape ({ messageSubject, accountId, content }); `offers`
85
- // (a legacy-only field) is deliberately dropped, not part of CCS's schema.
86
- WEBPUSH: (templateData = {}) => {
87
- const inner = templateData.messageContent?.content || templateData;
88
- return {
89
- messageSubject: inner.messageSubject,
90
- accountId: inner.accountId,
91
- content: inner.content,
92
- };
93
- },
94
- };
95
-
96
- export const CCS_CHANNEL_DELIVERY_KEY_MAP = {
97
- SMS: 'smsDeliverySettings',
98
- EMAIL: 'emailDeliverySettings',
99
- WHATSAPP: 'whatsappDeliverySettings',
100
- PUSH: 'mpushDeliverySettings',
101
- INAPP: 'inAppDeliverySettings',
102
- ANDROID: 'androidDeliverySettings',
103
- IOS: 'iosDeliverySettings',
104
- ZALO: 'zaloDeliverySettings',
105
- VIBER: 'viberDeliverySettings',
106
- LINE: 'lineDeliverySettings',
107
- WEBPUSH: 'webPushDeliverySettings',
108
- RCS: 'rcsDeliverySettings',
109
- };
110
-
111
- // The UI's own internal channel identifier for mobile push is 'MOBILEPUSH'
112
- // (see CreativesContainer/constants.js MOBILE_PUSH), but CCS's channel enum
113
- // value is 'PUSH' (verified against real create responses — the content/
114
- // delivery KEYS still say 'mpush*', only the enum VALUE is 'PUSH') —
115
- // normalize before building the CCS payload. Every other channel identifier
116
- // matches CCS as-is.
117
- export const CCS_CHANNEL_NAME_MAP = {
118
- MOBILEPUSH: 'PUSH',
119
- };
120
-
121
41
  // Channel config - single source of truth for CreativesContainer and TemplatesV2
122
42
  // paneKey: TemplatesV2 defaultPanes object key (for channelsToHide)
123
43
  // channelProp: CreativesContainer channel prop (must match pane.key for tab to be active)
@@ -312,4 +232,4 @@ export const INCENTIVE_TYPES = [
312
232
  {
313
233
  value: 'badges', label: <FormattedMessage {...messages.badges} />, isActive: false, disabled: false,
314
234
  },
315
- ];
235
+ ];
@@ -9,6 +9,7 @@
9
9
  import React from 'react';
10
10
  import PropTypes from 'prop-types';
11
11
  import CommunicationFlow from './CommunicationFlow';
12
+ import { hasSupportEngagementModule } from '../../utils/common';
12
13
 
13
14
  const CommunicationFlowContainer = ({
14
15
  config,
@@ -17,16 +18,22 @@ const CommunicationFlowContainer = ({
17
18
  onCancel,
18
19
  onChange,
19
20
  ...otherProps
20
- }) => (
21
- <CommunicationFlow
22
- config={config}
23
- initialData={initialData}
24
- onSave={onSave}
25
- onCancel={onCancel}
26
- onChange={onChange}
27
- {...otherProps}
28
- />
29
- );
21
+ }) => {
22
+ if (!hasSupportEngagementModule()) {
23
+ return null;
24
+ }
25
+
26
+ return (
27
+ <CommunicationFlow
28
+ config={config}
29
+ initialData={initialData}
30
+ onSave={onSave}
31
+ onCancel={onCancel}
32
+ onChange={onChange}
33
+ {...otherProps}
34
+ />
35
+ );
36
+ };
30
37
 
31
38
  CommunicationFlowContainer.propTypes = {
32
39
  config: PropTypes.shape({
@@ -80,13 +87,11 @@ CommunicationFlowContainer.propTypes = {
80
87
  controls: PropTypes.array,
81
88
  }),
82
89
  }),
83
- context: PropTypes.object, // ouId, campaignId, programId; name (required for CCS create),
84
- // referenceId (optional, auto-generated from name if absent), description (optional).
85
- 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.
86
92
  }).isRequired,
87
93
  initialData: PropTypes.object, // for edit/preview mode
88
- onSave: PropTypes.func.isRequired, // (data) => void - called when user saves; data.ccsCommDefinition
89
- // ({id, referenceId, version, status}) is present when the CCS create succeeded
94
+ onSave: PropTypes.func.isRequired, // (data) => void - called when user saves
90
95
  onCancel: PropTypes.func.isRequired, // () => void - called when user cancels
91
96
  onChange: PropTypes.func, // (data) => void - optional, called on data changes
92
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
  };
@@ -29,7 +29,6 @@ import {
29
29
  getEmailDefaultsForDomainId,
30
30
  getSmsDefaultsForDomainId,
31
31
  getRcsAccountName,
32
- getEmailDomainGatewayMapIdByDomainId,
33
32
  } from './deliverySettingsConfig';
34
33
  import messages from '../../messages';
35
34
  import './SenderDetails.scss';
@@ -152,11 +151,6 @@ const SenderDetails = ({
152
151
  saveValues.whatsappDomainId = matchedDomain?.domainProperties?.id ?? matchedDomain?.domainId ?? matchedDomain?.id;
153
152
  }
154
153
  }
155
- // Include EMAIL domainGatewayMapId derived from the selected domain for persistence -
156
- // CCS needs it alongside domainId, but it's not a user-facing field.
157
- if (entity && fieldValues.emailDomain != null && fieldValues.emailDomain !== '') {
158
- saveValues.emailDomainGatewayMapId = getEmailDomainGatewayMapIdByDomainId(entity, fieldValues.emailDomain);
159
- }
160
154
  onSave(saveValues);
161
155
  }
162
156
  onClose();
@@ -83,7 +83,6 @@ const ENTITIES = {
83
83
  emailTwoDomains: {
84
84
  EMAIL: [
85
85
  {
86
- id: 'gwmap-a',
87
86
  domainProperties: {
88
87
  id: 'em-a',
89
88
  domainName: 'Mail Alpha',
@@ -102,7 +101,6 @@ const ENTITIES = {
102
101
  },
103
102
  },
104
103
  {
105
- id: 'gwmap-b',
106
104
  domainProperties: {
107
105
  id: 'em-b',
108
106
  domainName: 'Mail Beta',
@@ -324,29 +322,6 @@ describe('SenderDetails', () => {
324
322
  });
325
323
  });
326
324
 
327
- it('persists emailDomainGatewayMapId derived from the selected email domain on save', async () => {
328
- const { onSave } = renderSenderDetails({
329
- channels: ['EMAIL'],
330
- preloadedDomainProperties: ENTITIES.emailTwoDomains,
331
- });
332
- const root = document.querySelector('.sender-details');
333
- await waitFor(() => expect(within(root).getByText('Mail Alpha')).toBeInTheDocument());
334
-
335
- await openSelectAndChoose(root, 0, 'Mail Beta');
336
- await waitFor(() => expect(within(root).getByText('beta@other.com')).toBeInTheDocument());
337
-
338
- const saveBtn = within(root).getByRole('button', { name: /save changes/i });
339
- await waitFor(() => expect(saveBtn).not.toBeDisabled());
340
- fireEvent.click(saveBtn);
341
-
342
- expect(onSave).toHaveBeenCalledWith(
343
- expect.objectContaining({
344
- emailDomain: 'em-b',
345
- emailDomainGatewayMapId: 'gwmap-b',
346
- }),
347
- );
348
- });
349
-
350
325
  it('resets email domain row back to the first gateway', async () => {
351
326
  renderSenderDetails({
352
327
  channels: ['EMAIL'],
@@ -438,7 +438,6 @@ describe('deliverySettingsConfig — user-facing flows', () => {
438
438
  smsDomain: 'd1',
439
439
  smsSenderId: 'g1',
440
440
  emailDomain: 'ed',
441
- emailDomainGatewayMapId: 'gwmap-1',
442
441
  emailSenderId: 'mail@x.com',
443
442
  emailSenderName: 'N',
444
443
  emailReplyToId: 'r@x.com',
@@ -456,10 +455,9 @@ describe('deliverySettingsConfig — user-facing flows', () => {
456
455
  SMS: { domainId: 'd1', gsmSenderId: 'g1' },
457
456
  EMAIL: {
458
457
  domainId: 'ed',
459
- domainGatewayMapId: 'gwmap-1',
460
- senderId: 'mail@x.com',
458
+ senderEmail: 'mail@x.com',
461
459
  senderLabel: 'N',
462
- replyToId: 'r@x.com',
460
+ senderReplyTo: 'r@x.com',
463
461
  },
464
462
  WHATSAPP: { domainId: 'wd', senderMobNum: '+w' },
465
463
  RCS: { senderMobNum: '+r', rcsSender: '+r' },
@@ -474,10 +472,9 @@ describe('deliverySettingsConfig — user-facing flows', () => {
474
472
  SMS: { domainId: 'd', gsmSenderId: 'g' },
475
473
  EMAIL: {
476
474
  domainId: 'ed',
477
- domainGatewayMapId: 'gwmap-2',
478
- senderId: 'e@e.com',
475
+ senderEmail: 'e@e.com',
479
476
  senderLabel: 'L',
480
- replyToId: 'r@e.com',
477
+ senderReplyTo: 'r@e.com',
481
478
  },
482
479
  WHATSAPP: { senderMobNum: '+w', domainId: 'wid' },
483
480
  RCS: { rcsSender: '+r' },
@@ -489,10 +486,6 @@ describe('deliverySettingsConfig — user-facing flows', () => {
489
486
  smsDomain: 'd',
490
487
  smsSenderId: 'g',
491
488
  emailDomain: 'ed',
492
- emailDomainGatewayMapId: 'gwmap-2',
493
- emailSenderId: 'e@e.com',
494
- emailSenderName: 'L',
495
- emailReplyToId: 'r@e.com',
496
489
  whatsappDomainId: 'wid',
497
490
  viberSenderId: 'vx',
498
491
  });
@@ -181,14 +181,6 @@ const getEmailDomainValue = (entity) => {
181
181
  return emailData.domainProperties?.id ?? emailData.domainId ?? emailData.id ?? '';
182
182
  };
183
183
 
184
- // CCS's domainGatewayMapId is the domainProperties API's own outer `id`
185
- // (distinct from `domainProperties.id`, which is the numeric domainId) - the
186
- // domain-gateway-mapping id for the selected domain entry.
187
- const getEmailDomainGatewayMapIdByDomainId = (entity, domainId) => {
188
- const domainEntry = getEmailDomainById(entity, domainId);
189
- return domainEntry?.id ?? '';
190
- };
191
-
192
184
  /** Domain name for summary display (parseEntityForDisplay) */
193
185
  const getEmailDomainNameForDisplay = (entity) => {
194
186
  const emailData = getEmailData(entity);
@@ -270,8 +262,6 @@ const getEmailDefaultsForDomain = (domainData) => {
270
262
  * context.fieldValues has current form state (e.g. selected emailDomain)
271
263
  * Aligned with adiona-ui/cap-campaigns-v2: default = contactInfo item with default:true, or first
272
264
  */
273
- export { getEmailDomainGatewayMapIdByDomainId };
274
-
275
265
  export const getDefaultValueForField = (fieldKey, entity, context = {}) => {
276
266
  if (!entity) return '';
277
267
  const fieldValues = context?.fieldValues || {};
@@ -584,11 +574,7 @@ export const parseEntityForDisplay = (entity, context = {}) => {
584
574
  senderDetails: entity,
585
575
  smsSenderId: getSmsSenderIdValue(entity),
586
576
  smsDomain: getSmsDomainValue(entity),
587
- // emailDomain here is the domain NAME (for display only) - buildChannelSettingFromFieldValues
588
- // needs the numeric domainId/domainGatewayMapId, sourced separately below.
589
577
  emailDomain: getEmailDomainNameForDisplay(entity),
590
- emailDomainId: getEmailDomainValue(entity),
591
- emailDomainGatewayMapId: getEmailDomainGatewayMapIdByDomainId(entity, getEmailDomainValue(entity)),
592
578
  emailSenderId: getEmailSenderIdValue(entity),
593
579
  viberSenderId: getSenderIdFromContactInfo(entity, 'VIBER'),
594
580
  zaloSenderId: getSenderIdFromContactInfo(entity, 'ZALO'),
@@ -614,14 +600,10 @@ export const buildChannelSettingFromFieldValues = (fieldValues = {}) => {
614
600
  }
615
601
  if (fieldValues.emailDomain != null || fieldValues.emailSenderId != null || fieldValues.emailSenderName != null || fieldValues.emailReplyToId != null) {
616
602
  channelSetting.EMAIL = {
617
- // emailDomainId (numeric, from the auto-save-defaults path via parseEntityForDisplay)
618
- // takes priority over emailDomain, which is numeric when sourced from SenderDetails'
619
- // own field state but a display NAME string when sourced from parseEntityForDisplay.
620
- domainId: fieldValues.emailDomainId ?? fieldValues.emailDomain,
621
- domainGatewayMapId: fieldValues.emailDomainGatewayMapId,
622
- senderId: fieldValues.emailSenderId,
603
+ domainId: fieldValues.emailDomain,
604
+ senderEmail: fieldValues.emailSenderId,
623
605
  senderLabel: fieldValues.emailSenderName,
624
- replyToId: fieldValues.emailReplyToId,
606
+ senderReplyTo: fieldValues.emailReplyToId,
625
607
  };
626
608
  }
627
609
  if (fieldValues.whatsappSenderNumber != null || fieldValues.whatsappAccount != null) {
@@ -691,10 +673,9 @@ export const parseChannelSettingForDisplay = (channelSetting = {}) => {
691
673
  }
692
674
  if (channelSetting.EMAIL) {
693
675
  out.emailDomain = channelSetting.EMAIL.domainId;
694
- out.emailDomainGatewayMapId = channelSetting.EMAIL.domainGatewayMapId;
695
- out.emailSenderId = channelSetting.EMAIL.senderId;
676
+ out.emailSenderId = channelSetting.EMAIL.senderEmail;
696
677
  out.emailSenderName = channelSetting.EMAIL.senderLabel;
697
- out.emailReplyToId = channelSetting.EMAIL.replyToId;
678
+ out.emailReplyToId = channelSetting.EMAIL.senderReplyTo;
698
679
  }
699
680
  if (channelSetting.WHATSAPP) {
700
681
  out.whatsappSenderNumber = channelSetting.WHATSAPP.senderMobNum;
@@ -1139,8 +1139,7 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
1139
1139
  this.props.globalActions.addMessageToQueue(message);
1140
1140
  } else {
1141
1141
  const name = data.file.name.split('.');
1142
- const blob = data.file.slice(0, -1, 'image');
1143
- const newFile = new File([blob], `${name[0]}${Date.now()}.${name[1]}`, {type: 'image'});
1142
+ const newFile = new File([data.file], `${name[0]}${Date.now()}.${name[1]}`, {type: 'image'});
1144
1143
  this.props.actions.uploadAsset(newFile, data.type, data.fileParams);
1145
1144
  }
1146
1145
  };