@capillarytech/creatives-library 8.0.317 → 8.0.319

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/constants/unified.js +1 -0
  2. package/package.json +1 -1
  3. package/services/api.js +6 -0
  4. package/services/tests/api.test.js +7 -0
  5. package/utils/common.js +6 -1
  6. package/v2Containers/CommunicationFlow/CommunicationFlow.js +291 -0
  7. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +25 -0
  8. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +255 -0
  9. package/v2Containers/CommunicationFlow/constants.js +200 -0
  10. package/v2Containers/CommunicationFlow/index.js +102 -0
  11. package/v2Containers/CommunicationFlow/messages.js +346 -0
  12. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +522 -0
  13. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +170 -0
  14. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +796 -0
  15. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/index.js +5 -0
  16. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +95 -0
  17. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/Tests/CommunicationStrategyStep.test.js +133 -0
  18. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/index.js +5 -0
  19. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +289 -0
  20. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.scss +70 -0
  21. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +319 -0
  22. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.scss +69 -0
  23. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +616 -0
  24. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +577 -0
  25. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +1111 -0
  26. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +696 -0
  27. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/index.js +7 -0
  28. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.js +102 -0
  29. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.scss +36 -0
  30. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/Tests/DynamicControlsStep.test.js +91 -0
  31. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/index.js +5 -0
  32. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/MessageTypeStep.js +86 -0
  33. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/Tests/MessageTypeStep.test.js +100 -0
  34. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/index.js +5 -0
  35. package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +30 -0
  36. package/v2Containers/CreativesContainer/constants.js +3 -0
  37. package/v2Containers/CreativesContainer/index.js +1 -1
  38. package/v2Containers/Rcs/index.js +4 -2
@@ -0,0 +1,696 @@
1
+ /**
2
+ * Delivery Settings Config - Single source for channel field definitions
3
+ *
4
+ * API → UI mapping (aligned with adiona-ui / cap-campaigns-v2 SenderDetails):
5
+ *
6
+ * SMS:
7
+ * - SMS Domain → domainProperties (select; label=domainName, value=domainId); Sender ID filtered by selected domain
8
+ * - Sender ID → contactInfo type gsm_sender_id (fallback cdma_sender_id); options only from selected domain
9
+ *
10
+ * EMAIL:
11
+ * - Email Domain → domainProperties (select; label=domainName, value=domainId); Sender ID/Reply-to/Sender name filtered by selected domain
12
+ * - Sender ID → contactInfo type sender_id; value = email address
13
+ * - Sender name → contactInfo type sender_id .label (display name)
14
+ * - Reply-to ID → contactInfo type reply_to_id; value = reply email
15
+ *
16
+ * WHATSAPP / RCS:
17
+ * - Sender number → contactInfo type gsm_sender_id (or cdma_sender_id); .value = phone number
18
+ *
19
+ * VIBER / ZALO / LINE:
20
+ * - Sender ID → contactInfo type gsm_sender_id (fallback cdma_sender_id); .value
21
+ *
22
+ * Entity structure from domainProperties API:
23
+ * { SMS: [...], EMAIL: [...], WHATSAPP: [...], VIBER: [...], ZALO: [...], LINE: [...] }
24
+ * Each channel array contains domain objects with domainProperties.contactInfo
25
+ */
26
+ import {
27
+ SMS, EMAIL, WHATSAPP, VIBER, RCS,
28
+ } from '../../../CreativesContainer/constants';
29
+
30
+ // --- Generic helpers for API response structure ---
31
+ const toArray = (val) => {
32
+ if (Array.isArray(val)) return val;
33
+ if (val) return [val];
34
+ return [];
35
+ };
36
+
37
+ const getChannelData = (entity, channelKey) => {
38
+ const domainList = toArray(entity?.[channelKey]);
39
+ return domainList[0] || null;
40
+ };
41
+
42
+ /**
43
+ * Pick the preferred contact entry for a given type:
44
+ * returns the first entry flagged as default, or the first valid entry, or null.
45
+ * Replaces the invalid single-argument .sort() pattern (Array.sort requires a
46
+ * two-argument comparator; a single-argument form produces undefined behaviour).
47
+ */
48
+ const pickPreferredContact = (contactInfoList, type) => {
49
+ const validMatches = contactInfoList.filter(
50
+ (contactEntry) => contactEntry?.type === type && contactEntry?.valid,
51
+ );
52
+ return validMatches.find((contactEntry) => contactEntry?.default) || validMatches[0] || null;
53
+ };
54
+
55
+ /** Extract value from contactInfo by type (e.g. gsm_sender_id, sender_id) */
56
+ const getValueFromContactInfo = (channelData, preferredType, fallbackTypes = []) => {
57
+ const contactInfo = channelData?.domainProperties?.contactInfo || [];
58
+ if (!Array.isArray(contactInfo)) return '';
59
+ const types = [preferredType, ...fallbackTypes];
60
+ const match = types.reduce((found, senderType) => {
61
+ if (found) return found;
62
+ return pickPreferredContact(contactInfo, senderType);
63
+ }, null);
64
+ return match ? (match.value || match.label) : '';
65
+ };
66
+
67
+ /** Extract all options from contactInfo by type */
68
+ const getOptionsFromContactInfo = (channelData, preferredType, fallbackTypes = []) => {
69
+ const contactInfo = channelData?.domainProperties?.contactInfo || [];
70
+ if (!Array.isArray(contactInfo)) return [];
71
+ const types = [preferredType, ...fallbackTypes];
72
+ const seen = new Set();
73
+ return types.reduce((accumulatedOptions, senderType) => {
74
+ const contactMatches = contactInfo.filter((contactEntry) => contactEntry?.type === senderType && contactEntry?.valid) || [];
75
+ contactMatches.forEach((contactMatch) => {
76
+ const senderValue = contactMatch.value || contactMatch.label;
77
+ const label = contactMatch.label || contactMatch.value;
78
+ if (senderValue && !seen.has(senderValue)) {
79
+ seen.add(senderValue);
80
+ accumulatedOptions.push({ label, value: senderValue });
81
+ }
82
+ });
83
+ return accumulatedOptions;
84
+ }, []);
85
+ };
86
+
87
+ // --- SMS extractors ---
88
+ const getSmsData = (entity) => getChannelData(entity, 'SMS');
89
+
90
+ /** Find domain by id in SMS array */
91
+ const getSmsDomainById = (entity, domainId) => {
92
+ const smsDomainList = toArray(entity?.SMS);
93
+ if (domainId == null) return smsDomainList[0] || null;
94
+ const idStr = String(domainId);
95
+ return smsDomainList.find((domainEntry) => {
96
+ const dId = domainEntry.domainProperties?.id ?? domainEntry.domainId ?? domainEntry.id;
97
+ return dId != null && String(dId) === idStr;
98
+ }) || smsDomainList[0] || null;
99
+ };
100
+
101
+ // SMS Domain options (label = domainName, value = domainId) - unique by domainName (like Email)
102
+ const getSmsDomainOptions = (entity) => {
103
+ const smsDomainList = toArray(entity?.SMS);
104
+ const seen = new Set();
105
+ const options = [];
106
+ smsDomainList.forEach((domainEntry, domainIndex) => {
107
+ const domainId = domainEntry.domainProperties?.id ?? domainEntry.domainId ?? domainEntry.id ?? String(domainIndex);
108
+ const label = domainEntry.domainProperties?.domainName || domainEntry.domainName || domainEntry.domainProperties?.hostName;
109
+ if (!seen.has(label)) {
110
+ seen.add(label);
111
+ options.push({ label, value: domainId });
112
+ }
113
+ });
114
+ return options;
115
+ };
116
+
117
+ const getSmsDomainValue = (entity) => {
118
+ const smsData = getSmsData(entity);
119
+ if (!smsData) return '';
120
+ return smsData.domainProperties?.id ?? smsData.domainId ?? smsData.id ?? '';
121
+ };
122
+
123
+ // Sender ID - options only from selected domain (like Email)
124
+ const getSmsSenderIdOptions = (entity, context = {}) => {
125
+ const selectedDomainId = context?.fieldValues?.smsDomain;
126
+ const domainData = selectedDomainId ? getSmsDomainById(entity, selectedDomainId) : getSmsData(entity);
127
+ if (!domainData) return [];
128
+ return getOptionsFromContactInfo(domainData, 'gsm_sender_id', ['cdma_sender_id']);
129
+ };
130
+
131
+ const getSmsSenderIdValue = (entity) => {
132
+ const smsData = getSmsData(entity);
133
+ return getValueFromContactInfo(smsData, 'gsm_sender_id', ['cdma_sender_id']);
134
+ };
135
+
136
+ /** Get default sender ID for a domain - for SMS domain change / reset */
137
+ const getSmsDefaultForDomain = (domainData) => {
138
+ if (!domainData) return '';
139
+ return getValueFromContactInfo(domainData, 'gsm_sender_id', ['cdma_sender_id']);
140
+ };
141
+
142
+ export const getSmsDefaultsForDomainId = (entity, domainId) => {
143
+ if (!entity) return { senderId: '' };
144
+ const domainData = getSmsDomainById(entity, domainId);
145
+ return { senderId: getSmsDefaultForDomain(domainData) };
146
+ };
147
+
148
+ // --- EMAIL extractors ---
149
+ const getEmailData = (entity) => getChannelData(entity, 'EMAIL');
150
+
151
+ /** Find domain by id in EMAIL array */
152
+ const getEmailDomainById = (entity, domainId) => {
153
+ const emailDomainList = toArray(entity?.EMAIL);
154
+ if (domainId == null) return emailDomainList[0] || null;
155
+ const idStr = String(domainId);
156
+ return emailDomainList.find((domainEntry) => {
157
+ const dId = domainEntry.domainProperties?.id ?? domainEntry.domainId ?? domainEntry.id;
158
+ return dId != null && String(dId) === idStr;
159
+ }) || emailDomainList[0] || null;
160
+ };
161
+
162
+ // Email Domain options (label = domainName, value = domainId) - unique by domainName
163
+ const getEmailDomainOptions = (entity) => {
164
+ const emailDomainList = toArray(entity?.EMAIL);
165
+ const seen = new Set();
166
+ const options = [];
167
+ emailDomainList.forEach((domainEntry, domainIndex) => {
168
+ const domainId = domainEntry.domainProperties?.id ?? domainEntry.domainId ?? domainEntry.id ?? String(domainIndex);
169
+ const label = domainEntry.domainProperties?.domainName || domainEntry.domainName || domainEntry.domainProperties?.hostName;
170
+ if (!seen.has(label)) {
171
+ seen.add(label);
172
+ options.push({ label, value: domainId });
173
+ }
174
+ });
175
+ return options;
176
+ };
177
+
178
+ const getEmailDomainValue = (entity) => {
179
+ const emailData = getEmailData(entity);
180
+ if (!emailData) return '';
181
+ return emailData.domainProperties?.id ?? emailData.domainId ?? emailData.id ?? '';
182
+ };
183
+
184
+ /** Domain name for summary display (parseEntityForDisplay) */
185
+ const getEmailDomainNameForDisplay = (entity) => {
186
+ const emailData = getEmailData(entity);
187
+ if (!emailData) return '';
188
+ return emailData.domainProperties?.domainName || emailData.domainName || emailData.domainProperties?.hostName || '';
189
+ };
190
+
191
+ // Sender ID (from sender_id - email address). Options only from selected domain - no cross-domain selection.
192
+ const getEmailSenderIdOptions = (entity, context = {}) => {
193
+ const selectedDomainId = context?.fieldValues?.emailDomain;
194
+ const domainData = selectedDomainId ? getEmailDomainById(entity, selectedDomainId) : getEmailData(entity);
195
+ if (!domainData) return [];
196
+ const options = getOptionsFromContactInfo(domainData, 'sender_id', []);
197
+ return options?.map((option) => ({ label: option.value || option.label, value: option.value || option.label }));
198
+ };
199
+
200
+ const getEmailSenderIdValue = (entity) => {
201
+ const emailData = getEmailData(entity);
202
+ return getValueFromContactInfo(emailData, 'sender_id', []);
203
+ };
204
+
205
+ // Sender name (from sender_id label). Options only from selected domain - no cross-domain selection.
206
+ const getEmailSenderNameOptions = (entity, context = {}) => {
207
+ const selectedDomainId = context?.fieldValues?.emailDomain;
208
+ const domainData = selectedDomainId ? getEmailDomainById(entity, selectedDomainId) : getEmailData(entity);
209
+ if (!domainData) return [];
210
+ const contactInfo = domainData?.domainProperties?.contactInfo || [];
211
+ const seen = new Set();
212
+ const options = [];
213
+ contactInfo
214
+ .filter((contact) => contact?.type === 'sender_id' && contact?.valid)
215
+ .forEach((contact) => {
216
+ const value = contact?.label || contact?.value;
217
+ if (value && !seen.has(value)) {
218
+ seen.add(value);
219
+ options.push({ label: value, value });
220
+ }
221
+ });
222
+ return options;
223
+ };
224
+
225
+ const getEmailSenderNameValue = (entity) => {
226
+ const emailData = getEmailData(entity);
227
+ const contactInfo = emailData?.domainProperties?.contactInfo || [];
228
+ const contactMatch = pickPreferredContact(contactInfo, 'sender_id');
229
+ return contactMatch ? (contactMatch.label || contactMatch.value) : '';
230
+ };
231
+
232
+ // Reply-to ID. Options only from selected domain - no cross-domain selection.
233
+ const getEmailReplyToIdOptions = (entity, context = {}) => {
234
+ const selectedDomainId = context?.fieldValues?.emailDomain;
235
+ const domainData = selectedDomainId ? getEmailDomainById(entity, selectedDomainId) : getEmailData(entity);
236
+ if (!domainData) return [];
237
+ const options = getOptionsFromContactInfo(domainData, 'reply_to_id', []);
238
+ return options?.map((option) => ({ label: option.value || option.label, value: option.value || option.label }));
239
+ };
240
+
241
+ const getEmailReplyToIdValue = (entity) => {
242
+ const emailData = getEmailData(entity);
243
+ return getValueFromContactInfo(emailData, 'reply_to_id', []);
244
+ };
245
+
246
+ /** Get default values for a domain (contactInfo items with default: true) - adiona-ui/cap-campaigns-v2 pattern */
247
+ const getEmailDefaultsForDomain = (domainData) => {
248
+ if (!domainData) return { senderId: '', senderName: '', replyToId: '' };
249
+ const contactInfo = domainData?.domainProperties?.contactInfo || [];
250
+ const findDefault = (type) => pickPreferredContact(contactInfo, type);
251
+ const senderMatch = findDefault('sender_id');
252
+ const replyMatch = findDefault('reply_to_id');
253
+ return {
254
+ senderId: senderMatch ? (senderMatch.value || senderMatch.label) : '',
255
+ senderName: senderMatch ? (senderMatch.label || senderMatch.value) : '',
256
+ replyToId: replyMatch ? (replyMatch.value || replyMatch.label) : '',
257
+ };
258
+ };
259
+
260
+ /**
261
+ * Get default value for a field (for Reset). (entity, context) => value
262
+ * context.fieldValues has current form state (e.g. selected emailDomain)
263
+ * Aligned with adiona-ui/cap-campaigns-v2: default = contactInfo item with default:true, or first
264
+ */
265
+ export const getDefaultValueForField = (fieldKey, entity, context = {}) => {
266
+ if (!entity) return '';
267
+ const fieldValues = context?.fieldValues || {};
268
+ if (fieldKey === 'smsDomain' || fieldKey === 'emailDomain') {
269
+ const options = fieldKey === 'emailDomain' ? getEmailDomainOptions(entity) : getSmsDomainOptions(entity);
270
+ return options?.[0]?.value ?? (fieldKey === 'emailDomain' ? getEmailDomainValue(entity) : getSmsDomainValue(entity)) ?? '';
271
+ }
272
+ if (fieldKey === 'smsSenderId') {
273
+ const domainId = fieldValues.smsDomain || getSmsDomainValue(entity);
274
+ const domainData = getSmsDomainById(entity, domainId);
275
+ return getSmsDefaultForDomain(domainData);
276
+ }
277
+ if (fieldKey === 'emailSenderId' || fieldKey === 'emailSenderName' || fieldKey === 'emailReplyToId') {
278
+ const domainId = fieldValues.emailDomain || getEmailDomainValue(entity);
279
+ const domainData = getEmailDomainById(entity, domainId);
280
+ const defs = getEmailDefaultsForDomain(domainData);
281
+ if (fieldKey === 'emailSenderId') return defs.senderId;
282
+ if (fieldKey === 'emailSenderName') return defs.senderName;
283
+ if (fieldKey === 'emailReplyToId') return defs.replyToId;
284
+ }
285
+ return '';
286
+ };
287
+
288
+ /** Get defaults for domain change (populate sender id, reply to, sender name when domain changes) */
289
+ export const getEmailDefaultsForDomainId = (entity, domainId) => {
290
+ if (!entity) return { senderId: '', senderName: '', replyToId: '' };
291
+ const domainData = getEmailDomainById(entity, domainId);
292
+ return getEmailDefaultsForDomain(domainData);
293
+ };
294
+
295
+ // --- WHATSAPP sender number (WABA-bound via sourceAccountIdentifier) ---
296
+ // The domainProperties API returns WHATSAPP domains with:
297
+ // connectionProperties.sourceAccountIdentifier (WABA ID)
298
+ // contactInfo with gsm_sender_id entries (phone numbers for that WABA)
299
+ // The WhatsApp template (in contentItems) provides sourceAccountIdentifier to bind to the right domain.
300
+
301
+ /** Extract sourceAccountIdentifier from a domain entry */
302
+ const getDomainSourceAccountId = (domainData) => {
303
+ const { connectionProperties = {} } = domainData?.domainProperties || {};
304
+ const { sourceAccountIdentifier = '', wabaId = '', userid = '' } = connectionProperties;
305
+ return sourceAccountIdentifier || wabaId || userid || '';
306
+ };
307
+
308
+ /** Find the WHATSAPP domain matching a sourceAccountIdentifier */
309
+ const getWhatsappDomainBySourceAccount = (entity, sourceAccountId) => {
310
+ const whatsappDomains = toArray(entity?.WHATSAPP);
311
+ if (!sourceAccountId) return whatsappDomains?.length ? whatsappDomains[0] : null;
312
+ const matchedDomain = whatsappDomains.find((domain) => getDomainSourceAccountId(domain) === sourceAccountId);
313
+ return matchedDomain || (whatsappDomains?.length ? whatsappDomains[0] : null);
314
+ };
315
+
316
+ /** Get WhatsApp account name from the matched domain */
317
+ export const getWhatsappAccountName = (entity, sourceAccountId) => {
318
+ const domain = getWhatsappDomainBySourceAccount(entity, sourceAccountId);
319
+ if (!domain) return '';
320
+ return domain.domainProperties?.domainName || domain.domainName || domain.label || '';
321
+ };
322
+
323
+ /** Get sender number value from the WABA-matched domain */
324
+ const getWhatsappSenderNumberValue = (entity, context = {}) => {
325
+ const sourceAccountId = context?.whatsappSourceAccountId || '';
326
+ const domain = getWhatsappDomainBySourceAccount(entity, sourceAccountId);
327
+ if (!domain) return '';
328
+ return getValueFromContactInfo(domain, 'gsm_sender_id', ['cdma_sender_id', 'whatsapp_sender_id']);
329
+ };
330
+
331
+ /** Get sender number options filtered to the WABA-matched domain only.
332
+ * Aligned with campaigns/adiona: both label and value use the phone number. */
333
+ const getWhatsappSenderNumberOptions = (entity, context = {}) => {
334
+ const sourceAccountId = context?.whatsappSourceAccountId || '';
335
+ const domain = getWhatsappDomainBySourceAccount(entity, sourceAccountId);
336
+ if (!domain) return [];
337
+ const options = getOptionsFromContactInfo(domain, 'gsm_sender_id', ['cdma_sender_id', 'whatsapp_sender_id']);
338
+ return options?.map((option) => ({ label: option.value || option.label, value: option.value || option.label }));
339
+ };
340
+
341
+ const getRcsSenderNumberValue = (entity) => {
342
+ const channelData = getChannelData(entity, 'RCS');
343
+ if (!channelData) return '';
344
+ return getValueFromContactInfo(channelData, 'gsm_sender_id', ['cdma_sender_id']);
345
+ };
346
+
347
+ /** Get RCS account name from first domain (like campaigns-v2: rcsSender display) */
348
+ export const getRcsAccountName = (entity) => {
349
+ const rcsData = getChannelData(entity, 'RCS');
350
+ if (!rcsData) return '';
351
+ return rcsData.domainProperties?.domainName || rcsData.domainName || rcsData.label || '';
352
+ };
353
+
354
+
355
+ // --- VIBER account (disabled) + sender ID ---
356
+ // Aligned with adiona-ui/cap-campaigns-v2: domainId + sender (VIBER_SENDER_ID = 'sender')
357
+ // API entity: VIBER array with domainProperties.{ id, domainName, contactInfo } per domain
358
+ // When domainProperties.id is 0/missing (e.g. Iris DGM), use domainEntry.id (domain gateway mapping id)
359
+ const getViberDomainId = (domainEntry) => {
360
+ const dpId = domainEntry.domainProperties?.id;
361
+ if (dpId != null && dpId !== 0) return dpId;
362
+ return domainEntry.domainId ?? domainEntry.id ?? '';
363
+ };
364
+
365
+ const getViberAccountValue = (entity) => {
366
+ const viberData = getChannelData(entity, 'VIBER');
367
+ if (!viberData) return '';
368
+ return getViberDomainId(viberData) || '';
369
+ };
370
+
371
+ const getViberAccountOptions = (entity, context = {}) => {
372
+ const viberDomainList = toArray(entity?.VIBER);
373
+ const seen = new Set();
374
+ const options = [];
375
+ viberDomainList.forEach((viberDomainEntry) => {
376
+ const domainId = getViberDomainId(viberDomainEntry);
377
+ const label = viberDomainEntry.domainProperties?.domainName
378
+ || viberDomainEntry.domainName
379
+ || viberDomainEntry.domainProperties?.hostName
380
+ || viberDomainEntry.label;
381
+ if (domainId !== '' && domainId != null && !seen.has(domainId)) {
382
+ seen.add(domainId);
383
+ options.push({ label, value: domainId });
384
+ }
385
+ });
386
+ // Use saved account from form state (editing existing campaign) or fall back to API-derived value.
387
+ // Prepends a synthetic entry when the saved account is no longer present in the API options
388
+ // (e.g. after domain deactivation), preserving the user's existing selection.
389
+ const currentVal = context?.fieldValues?.viberAccount || getViberAccountValue(entity);
390
+ if (currentVal && !options.some((option) => option.value === currentVal)) {
391
+ const firstDomain = viberDomainList[0];
392
+ const firstLabel = firstDomain?.domainProperties?.domainName
393
+ || firstDomain?.domainName
394
+ || String(currentVal);
395
+ return [{ label: firstLabel, value: currentVal }, ...options];
396
+ }
397
+ return options;
398
+ };
399
+
400
+ // --- VIBER/ZALO/LINE sender ID (gsm_sender_id from contactInfo) ---
401
+ // VIBER may use viber_sender_id in some API responses; others use gsm_sender_id
402
+ const getSenderIdFromContactInfo = (entity, channelKey) => {
403
+ const channelData = getChannelData(entity, channelKey);
404
+ const fallbacks = channelKey === 'VIBER'
405
+ ? ['cdma_sender_id', 'viber_sender_id']
406
+ : ['cdma_sender_id'];
407
+ return getValueFromContactInfo(channelData, 'gsm_sender_id', fallbacks);
408
+ };
409
+
410
+ const getSenderIdOptionsFromContactInfo = (entity, channelKey) => {
411
+ const channelDomainList = toArray(entity?.[channelKey]);
412
+ const seen = new Set();
413
+ const options = [];
414
+ const fallbacks = channelKey === 'VIBER' ? ['cdma_sender_id', 'viber_sender_id'] : ['cdma_sender_id'];
415
+ channelDomainList.forEach((domainEntry) => {
416
+ getOptionsFromContactInfo(domainEntry, 'gsm_sender_id', fallbacks).forEach((senderOption) => {
417
+ if (senderOption.value && !seen.has(senderOption.value)) {
418
+ seen.add(senderOption.value);
419
+ options.push(senderOption);
420
+ }
421
+ });
422
+ });
423
+ const currentVal = getSenderIdFromContactInfo(entity, channelKey);
424
+ /* istanbul ignore next -- synthetic prepend only occurs when persisted sender ID is absent from the newly loaded API options (stale save); valid API data always includes the current value */
425
+ if (currentVal && !options.some((senderOption) => senderOption.value === currentVal)) {
426
+ return [{ label: currentVal, value: currentVal }, ...options];
427
+ }
428
+ return options;
429
+ };
430
+
431
+ // --- Field type constants ---
432
+ export const FIELD_TYPE = {
433
+ SELECT: 'select',
434
+ INPUT: 'input',
435
+ DISPLAY: 'display', // Read-only label + value (e.g. Email Domain)
436
+ };
437
+
438
+ /**
439
+ * Channel field config
440
+ */
441
+ export const DELIVERY_SETTINGS_FIELDS = [
442
+ {
443
+ channel: 'SMS',
444
+ fieldKey: 'smsDomain',
445
+ messageKey: 'smsDomain',
446
+ type: FIELD_TYPE.SELECT,
447
+ getValue: getSmsDomainValue,
448
+ getOptions: getSmsDomainOptions,
449
+ },
450
+ {
451
+ channel: 'SMS',
452
+ fieldKey: 'smsSenderId',
453
+ messageKey: 'senderIdLabel',
454
+ type: FIELD_TYPE.SELECT,
455
+ getValue: getSmsSenderIdValue,
456
+ getOptions: getSmsSenderIdOptions,
457
+ },
458
+ // EMAIL - order: Email Domain (select with Reset), Sender ID, Sender name, Reply-to ID (selects)
459
+ {
460
+ channel: 'EMAIL',
461
+ fieldKey: 'emailDomain',
462
+ messageKey: 'emailDomainLabel',
463
+ type: FIELD_TYPE.SELECT,
464
+ getValue: getEmailDomainValue,
465
+ getOptions: getEmailDomainOptions,
466
+ },
467
+ {
468
+ channel: 'EMAIL',
469
+ fieldKey: 'emailSenderId',
470
+ messageKey: 'senderIdLabel',
471
+ type: FIELD_TYPE.SELECT,
472
+ getValue: getEmailSenderIdValue,
473
+ getOptions: getEmailSenderIdOptions,
474
+ },
475
+ {
476
+ channel: 'EMAIL',
477
+ fieldKey: 'emailSenderName',
478
+ messageKey: 'emailSenderName',
479
+ type: FIELD_TYPE.SELECT,
480
+ getValue: getEmailSenderNameValue,
481
+ getOptions: getEmailSenderNameOptions,
482
+ },
483
+ {
484
+ channel: 'EMAIL',
485
+ fieldKey: 'emailReplyToId',
486
+ messageKey: 'emailReplyToId',
487
+ type: FIELD_TYPE.SELECT,
488
+ getValue: getEmailReplyToIdValue,
489
+ getOptions: getEmailReplyToIdOptions,
490
+ },
491
+ // WHATSAPP - Business Account (disabled, driven by template) + Sender Number (filtered by WABA)
492
+ {
493
+ channel: 'WHATSAPP',
494
+ fieldKey: 'whatsappAccount',
495
+ messageKey: 'whatsappBusinessAccount',
496
+ type: FIELD_TYPE.SELECT,
497
+ disabled: true,
498
+ getValue: () => '',
499
+ getOptions: () => [],
500
+ },
501
+ {
502
+ channel: 'WHATSAPP',
503
+ fieldKey: 'whatsappSenderNumber',
504
+ messageKey: 'senderNumberLabel',
505
+ type: FIELD_TYPE.SELECT,
506
+ getValue: (entity, context) => getWhatsappSenderNumberValue(entity, context),
507
+ getOptions: (entity, context) => getWhatsappSenderNumberOptions(entity, context),
508
+ },
509
+ // RCS - Account (disabled) + Sender Number
510
+ {
511
+ channel: 'RCS',
512
+ fieldKey: 'rcsAccount',
513
+ messageKey: 'rcsAccountLabel',
514
+ type: FIELD_TYPE.SELECT,
515
+ disabled: true,
516
+ getValue: () => '',
517
+ getOptions: () => [],
518
+ },
519
+ // RCS sender number: display-only in summary (DeliverySettingsSection); no select/reset in slidebox
520
+ // VIBER - Viber account (disabled) + Sender ID (select with Reset) - per Figma
521
+ {
522
+ channel: 'VIBER',
523
+ fieldKey: 'viberAccount',
524
+ messageKey: 'viberAccountLabel',
525
+ type: FIELD_TYPE.SELECT,
526
+ disabled: true,
527
+ getValue: getViberAccountValue,
528
+ getOptions: (entity, context) => getViberAccountOptions(entity, context),
529
+ },
530
+ {
531
+ channel: 'VIBER',
532
+ fieldKey: 'viberSenderId',
533
+ messageKey: 'senderIdLabel',
534
+ type: FIELD_TYPE.SELECT,
535
+ getValue: (entity) => getSenderIdFromContactInfo(entity, 'VIBER'),
536
+ getOptions: (entity) => getSenderIdOptionsFromContactInfo(entity, 'VIBER'),
537
+ },
538
+ // ZALO
539
+ {
540
+ channel: 'ZALO',
541
+ fieldKey: 'zaloSenderId',
542
+ messageKey: 'senderIdLabel',
543
+ type: FIELD_TYPE.SELECT,
544
+ getValue: (entity) => getSenderIdFromContactInfo(entity, 'ZALO'),
545
+ getOptions: (entity) => getSenderIdOptionsFromContactInfo(entity, 'ZALO'),
546
+ },
547
+ // LINE
548
+ {
549
+ channel: 'LINE',
550
+ fieldKey: 'lineSenderId',
551
+ messageKey: 'senderIdLabel',
552
+ type: FIELD_TYPE.SELECT,
553
+ getValue: (entity) => getSenderIdFromContactInfo(entity, 'LINE'),
554
+ getOptions: (entity) => getSenderIdOptionsFromContactInfo(entity, 'LINE'),
555
+ },
556
+ ];
557
+
558
+ /**
559
+ * Get field configs for the given channels
560
+ */
561
+ export const getFieldsForChannels = (channels = []) => {
562
+ const channelSet = new Set(channels.map((channelName) => (channelName)?.toUpperCase())?.filter(Boolean));
563
+ return DELIVERY_SETTINGS_FIELDS.filter((fieldConfig) => channelSet.has(fieldConfig.channel));
564
+ };
565
+
566
+ /**
567
+ * Parse entity for display in DeliverySettingsSection summary
568
+ * Returns per-channel sender IDs and sender number
569
+ */
570
+ export const parseEntityForDisplay = (entity, context = {}) => {
571
+ if (!entity) return null;
572
+ const whatsappSourceAccountId = context?.whatsappSourceAccountId || '';
573
+ return {
574
+ senderDetails: entity,
575
+ smsSenderId: getSmsSenderIdValue(entity),
576
+ smsDomain: getSmsDomainValue(entity),
577
+ emailDomain: getEmailDomainNameForDisplay(entity),
578
+ emailSenderId: getEmailSenderIdValue(entity),
579
+ viberSenderId: getSenderIdFromContactInfo(entity, 'VIBER'),
580
+ zaloSenderId: getSenderIdFromContactInfo(entity, 'ZALO'),
581
+ lineSenderId: getSenderIdFromContactInfo(entity, 'LINE'),
582
+ whatsappSenderNumber: getWhatsappSenderNumberValue(entity, { whatsappSourceAccountId }),
583
+ whatsappAccountName: getWhatsappAccountName(entity, whatsappSourceAccountId),
584
+ rcsSenderNumber: getRcsSenderNumberValue(entity),
585
+ rcsAccountName: getRcsAccountName(entity),
586
+ senderNumber: getWhatsappSenderNumberValue(entity, { whatsappSourceAccountId }) || getRcsSenderNumberValue(entity),
587
+ };
588
+ };
589
+
590
+ /**
591
+ * Build channelSetting (adiona/campaigns format) from SenderDetails fieldValues for persistence
592
+ */
593
+ export const buildChannelSettingFromFieldValues = (fieldValues = {}) => {
594
+ const channelSetting = {};
595
+ if (fieldValues.smsDomain != null || fieldValues.smsSenderId != null) {
596
+ channelSetting.SMS = {
597
+ domainId: fieldValues.smsDomain,
598
+ gsmSenderId: fieldValues.smsSenderId,
599
+ };
600
+ }
601
+ if (fieldValues.emailDomain != null || fieldValues.emailSenderId != null || fieldValues.emailSenderName != null || fieldValues.emailReplyToId != null) {
602
+ channelSetting.EMAIL = {
603
+ domainId: fieldValues.emailDomain,
604
+ senderEmail: fieldValues.emailSenderId,
605
+ senderLabel: fieldValues.emailSenderName,
606
+ senderReplyTo: fieldValues.emailReplyToId,
607
+ };
608
+ }
609
+ if (fieldValues.whatsappSenderNumber != null || fieldValues.whatsappAccount != null) {
610
+ channelSetting.WHATSAPP = {
611
+ domainId: fieldValues.whatsappDomainId,
612
+ senderMobNum: fieldValues.whatsappSenderNumber,
613
+ };
614
+ }
615
+ if (fieldValues.rcsSenderNumber != null || fieldValues.rcsAccount != null) {
616
+ channelSetting.RCS = {
617
+ senderMobNum: fieldValues.rcsSenderNumber,
618
+ rcsSender: fieldValues.rcsSenderNumber,
619
+ };
620
+ }
621
+ // VIBER: aligned with adiona-ui (domainId + sender), campaigns-v2 (gsmSenderId/senderViber)
622
+ if (fieldValues.viberSenderId != null || fieldValues.viberAccount != null) {
623
+ channelSetting.VIBER = {
624
+ domainId: fieldValues.viberAccount,
625
+ sender: fieldValues.viberSenderId,
626
+ // Include gsmSenderId for campaigns API compatibility
627
+ gsmSenderId: fieldValues.viberSenderId,
628
+ };
629
+ }
630
+ if (fieldValues.zaloSenderId != null) {
631
+ channelSetting.ZALO = { zaloSenderId: fieldValues.zaloSenderId };
632
+ }
633
+ if (fieldValues.lineSenderId != null) {
634
+ channelSetting.LINE = { lineSenderId: fieldValues.lineSenderId };
635
+ }
636
+ return channelSetting;
637
+ };
638
+
639
+ /**
640
+ * Channels that require a domain gateway to be present in the API response.
641
+ * Aligned with cap-campaigns-v2 and adiona-ui: ZALO and LINE are excluded from this check.
642
+ */
643
+ const GATEWAY_CHECKED_CHANNELS = [SMS, EMAIL, WHATSAPP, VIBER, RCS];
644
+
645
+ /**
646
+ * Returns true when the entity contains a valid domain gateway for the channel.
647
+ * For WHATSAPP: also verifies the WABA-matched domain has at least one gsm_sender_id.
648
+ * For all others: requires a non-empty array in the entity.
649
+ */
650
+ export const hasDomainGateway = (entity, channel, context = {}) => {
651
+ if (!entity) return false;
652
+ if (!GATEWAY_CHECKED_CHANNELS.includes(channel)) return true;
653
+ const domainList = toArray(entity[channel]);
654
+ if (domainList?.length === 0) return false;
655
+ if (channel === WHATSAPP) {
656
+ const sourceAccountId = context?.whatsappSourceAccountId || '';
657
+ const domain = getWhatsappDomainBySourceAccount(entity, sourceAccountId);
658
+ if (!domain) return false;
659
+ const options = getOptionsFromContactInfo(domain, 'gsm_sender_id', ['cdma_sender_id', 'whatsapp_sender_id']);
660
+ return options?.length > 0;
661
+ }
662
+ return true;
663
+ };
664
+
665
+ /**
666
+ * Extract display values from channelSetting for summary (reverse of buildChannelSettingFromFieldValues)
667
+ */
668
+ export const parseChannelSettingForDisplay = (channelSetting = {}) => {
669
+ const out = {};
670
+ if (channelSetting.SMS) {
671
+ out.smsSenderId = channelSetting.SMS.gsmSenderId;
672
+ out.smsDomain = channelSetting.SMS.domainId;
673
+ }
674
+ if (channelSetting.EMAIL) {
675
+ out.emailDomain = channelSetting.EMAIL.domainId;
676
+ out.emailSenderId = channelSetting.EMAIL.senderEmail;
677
+ out.emailSenderName = channelSetting.EMAIL.senderLabel;
678
+ out.emailReplyToId = channelSetting.EMAIL.senderReplyTo;
679
+ }
680
+ if (channelSetting.WHATSAPP) {
681
+ out.whatsappSenderNumber = channelSetting.WHATSAPP.senderMobNum;
682
+ out.whatsappDomainId = channelSetting.WHATSAPP.domainId;
683
+ }
684
+ if (channelSetting.RCS) {
685
+ out.rcsSenderNumber = channelSetting.RCS.senderMobNum || channelSetting.RCS.rcsSender;
686
+ }
687
+ // VIBER: read sender (adiona) or viberSenderId/gsmSenderId (campaigns compat)
688
+ if (channelSetting.VIBER) {
689
+ out.viberAccount = channelSetting.VIBER.domainId;
690
+ const viberSetting = channelSetting.VIBER;
691
+ out.viberSenderId = viberSetting.sender ?? viberSetting.viberSenderId ?? viberSetting.gsmSenderId;
692
+ }
693
+ if (channelSetting.ZALO) out.zaloSenderId = channelSetting.ZALO.zaloSenderId;
694
+ if (channelSetting.LINE) out.lineSenderId = channelSetting.LINE.lineSenderId;
695
+ return out;
696
+ };