@capillarytech/creatives-library 8.0.318 → 8.0.320

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 (139) hide show
  1. package/constants/unified.js +15 -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/utils/templateVarUtils.js +172 -0
  7. package/utils/tests/templateVarUtils.test.js +160 -0
  8. package/v2Components/CapTagList/index.js +10 -0
  9. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  10. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  11. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  12. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  13. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  14. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  15. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  16. package/v2Components/CommonTestAndPreview/SendTestMessage.js +11 -5
  17. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +20 -1
  18. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  19. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +12 -0
  20. package/v2Components/CommonTestAndPreview/constants.js +38 -0
  21. package/v2Components/CommonTestAndPreview/index.js +693 -155
  22. package/v2Components/CommonTestAndPreview/messages.js +41 -3
  23. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  24. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  25. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +352 -0
  26. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +269 -1
  27. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  28. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  29. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +25 -4
  30. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  31. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
  32. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  33. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  34. package/v2Components/FormBuilder/index.js +7 -1
  35. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  36. package/v2Components/SmsFallback/constants.js +73 -0
  37. package/v2Components/SmsFallback/index.js +956 -0
  38. package/v2Components/SmsFallback/index.scss +265 -0
  39. package/v2Components/SmsFallback/messages.js +78 -0
  40. package/v2Components/SmsFallback/smsFallbackUtils.js +107 -0
  41. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  42. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  43. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  44. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  45. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +261 -0
  46. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  47. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  48. package/v2Components/TestAndPreviewSlidebox/index.js +8 -1
  49. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  50. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  51. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  52. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  53. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  54. package/v2Containers/CommunicationFlow/CommunicationFlow.js +291 -0
  55. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +25 -0
  56. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +255 -0
  57. package/v2Containers/CommunicationFlow/constants.js +200 -0
  58. package/v2Containers/CommunicationFlow/index.js +102 -0
  59. package/v2Containers/CommunicationFlow/messages.js +346 -0
  60. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +522 -0
  61. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +170 -0
  62. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +796 -0
  63. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/index.js +5 -0
  64. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +95 -0
  65. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/Tests/CommunicationStrategyStep.test.js +133 -0
  66. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/index.js +5 -0
  67. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +289 -0
  68. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.scss +70 -0
  69. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +319 -0
  70. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.scss +69 -0
  71. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +616 -0
  72. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +577 -0
  73. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +1111 -0
  74. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +696 -0
  75. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/index.js +7 -0
  76. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.js +102 -0
  77. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.scss +36 -0
  78. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/Tests/DynamicControlsStep.test.js +91 -0
  79. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/index.js +5 -0
  80. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/MessageTypeStep.js +86 -0
  81. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/Tests/MessageTypeStep.test.js +100 -0
  82. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/index.js +5 -0
  83. package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +30 -0
  84. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  85. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  86. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  87. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  88. package/v2Containers/CreativesContainer/constants.js +12 -0
  89. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  90. package/v2Containers/CreativesContainer/index.js +289 -99
  91. package/v2Containers/CreativesContainer/index.scss +51 -1
  92. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  93. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +104 -0
  94. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +110 -0
  95. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  96. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +363 -0
  97. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -10
  98. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  99. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  100. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  101. package/v2Containers/Rcs/constants.js +32 -1
  102. package/v2Containers/Rcs/index.js +950 -873
  103. package/v2Containers/Rcs/index.scss +85 -6
  104. package/v2Containers/Rcs/messages.js +10 -1
  105. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +205 -0
  106. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +40834 -1963
  107. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  108. package/v2Containers/Rcs/tests/index.test.js +41 -38
  109. package/v2Containers/Rcs/tests/mockData.js +38 -0
  110. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +251 -0
  111. package/v2Containers/Rcs/tests/utils.test.js +379 -1
  112. package/v2Containers/Rcs/utils.js +358 -10
  113. package/v2Containers/Sms/Create/index.js +81 -36
  114. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  115. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  116. package/v2Containers/SmsTrai/Create/index.js +9 -4
  117. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  118. package/v2Containers/SmsTrai/Edit/index.js +609 -128
  119. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  120. package/v2Containers/SmsTrai/Edit/messages.js +9 -4
  121. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4327 -2374
  122. package/v2Containers/SmsWrapper/index.js +37 -8
  123. package/v2Containers/TagList/index.js +6 -0
  124. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  125. package/v2Containers/Templates/_templates.scss +61 -2
  126. package/v2Containers/Templates/actions.js +11 -0
  127. package/v2Containers/Templates/constants.js +2 -0
  128. package/v2Containers/Templates/index.js +90 -40
  129. package/v2Containers/Templates/sagas.js +57 -12
  130. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  131. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
  132. package/v2Containers/Templates/tests/sagas.test.js +193 -12
  133. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  134. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  135. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  136. package/v2Containers/TemplatesV2/index.js +86 -23
  137. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  138. package/v2Containers/Whatsapp/index.js +3 -20
  139. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -0,0 +1,1111 @@
1
+ /**
2
+ * Tests mirror how domainProperties is used in the Communication / Campaigns flow:
3
+ * one API response drives SMS, Email, WhatsApp (WABA), RCS, Viber, Zalo, and LINE
4
+ * sender UI, summary lines, save payload, and gateway validation.
5
+ */
6
+ import {
7
+ SMS,
8
+ EMAIL,
9
+ WHATSAPP,
10
+ VIBER,
11
+ RCS,
12
+ ZALO,
13
+ LINE,
14
+ } from '../../../../CreativesContainer/constants';
15
+ import {
16
+ getSmsDefaultsForDomainId,
17
+ getEmailDefaultsForDomainId,
18
+ getDefaultValueForField,
19
+ getWhatsappAccountName,
20
+ getRcsAccountName,
21
+ FIELD_TYPE,
22
+ DELIVERY_SETTINGS_FIELDS,
23
+ getFieldsForChannels,
24
+ parseEntityForDisplay,
25
+ buildChannelSettingFromFieldValues,
26
+ hasDomainGateway,
27
+ parseChannelSettingForDisplay,
28
+ } from '../deliverySettingsConfig';
29
+
30
+ /** Typical campaigns `domainProperties` entity: channel keys → list of gateway rows */
31
+ const domainPropertiesFromApi = {
32
+ SMS: [
33
+ {
34
+ domainProperties: {
35
+ id: 's1',
36
+ domainName: 'Primary SMS gateway',
37
+ contactInfo: [
38
+ {
39
+ type: 'gsm_sender_id', value: '+1001', valid: true, default: true,
40
+ },
41
+ {
42
+ type: 'cdma_sender_id', value: '+cdma-fallback', valid: true, default: false,
43
+ },
44
+ ],
45
+ },
46
+ },
47
+ {
48
+ domainProperties: {
49
+ id: 's2',
50
+ domainName: 'Secondary SMS gateway',
51
+ contactInfo: [{
52
+ type: 'gsm_sender_id', value: '+2002', valid: true, default: true,
53
+ }],
54
+ },
55
+ },
56
+ ],
57
+ EMAIL: [
58
+ {
59
+ domainProperties: {
60
+ id: 'e1',
61
+ domainName: 'Transactional mail',
62
+ contactInfo: [
63
+ {
64
+ type: 'sender_id',
65
+ value: 'noreply@brand.com',
66
+ label: 'Brand',
67
+ valid: true,
68
+ default: true,
69
+ },
70
+ {
71
+ type: 'reply_to_id', value: 'support@brand.com', valid: true, default: true,
72
+ },
73
+ ],
74
+ },
75
+ },
76
+ {
77
+ domainProperties: {
78
+ id: 'e2',
79
+ domainName: 'Promotional mail',
80
+ contactInfo: [
81
+ {
82
+ type: 'sender_id',
83
+ value: 'promo@brand.com',
84
+ label: 'Promo desk',
85
+ valid: true,
86
+ default: true,
87
+ },
88
+ ],
89
+ },
90
+ },
91
+ ],
92
+ WHATSAPP: {
93
+ domainProperties: {
94
+ id: 'wa-1',
95
+ domainName: 'Brand WA',
96
+ connectionProperties: { wabaId: 'waba-99' },
97
+ contactInfo: [{
98
+ type: 'gsm_sender_id', value: '+wa1', valid: true, default: true,
99
+ }],
100
+ },
101
+ },
102
+ RCS: [
103
+ {
104
+ domainProperties: {
105
+ domainName: 'RCS brand',
106
+ contactInfo: [{
107
+ type: 'gsm_sender_id', value: '+rcs1', valid: true, default: true,
108
+ }],
109
+ },
110
+ },
111
+ ],
112
+ VIBER: [
113
+ {
114
+ id: 'vgw-1',
115
+ domainProperties: {
116
+ id: 0,
117
+ domainName: 'Viber gateway',
118
+ contactInfo: [
119
+ {
120
+ type: 'viber_sender_id', value: 'viber-sender-1', valid: true, default: true,
121
+ },
122
+ ],
123
+ },
124
+ },
125
+ ],
126
+ ZALO: [
127
+ {
128
+ domainProperties: {
129
+ id: 'z1',
130
+ domainName: 'Zalo OA 1',
131
+ contactInfo: [{
132
+ type: 'gsm_sender_id', value: '+z1', valid: true, default: true,
133
+ }],
134
+ },
135
+ },
136
+ {
137
+ domainProperties: {
138
+ id: 'z2',
139
+ domainName: 'Zalo OA 2',
140
+ contactInfo: [{
141
+ type: 'gsm_sender_id', value: '+z2', valid: true, default: true,
142
+ }],
143
+ },
144
+ },
145
+ ],
146
+ LINE: [
147
+ {
148
+ domainProperties: {
149
+ id: 'l1',
150
+ domainName: 'LINE channel',
151
+ contactInfo: [{
152
+ type: 'gsm_sender_id', value: '+line1', valid: true, default: true,
153
+ }],
154
+ },
155
+ },
156
+ ],
157
+ };
158
+
159
+ describe('deliverySettingsConfig — user-facing flows', () => {
160
+ describe('SMS: choosing gateway and sender', () => {
161
+ it('defaults to the first gateway’s primary sender, with GSM preferred over CDMA when both exist', () => {
162
+ const entity = domainPropertiesFromApi;
163
+ expect(parseEntityForDisplay(entity).smsSenderId).toBe('+1001');
164
+ expect(getSmsDefaultsForDomainId(entity, 's2').senderId).toBe('+2002');
165
+ });
166
+
167
+ it('when only CDMA senders exist for a gateway, still resolves a sender', () => {
168
+ const cdmaOnly = {
169
+ SMS: [
170
+ {
171
+ domainProperties: {
172
+ id: 'c1',
173
+ domainName: 'CDMA-only GW',
174
+ contactInfo: [
175
+ {
176
+ type: 'cdma_sender_id', value: '+cdma-only', valid: true, default: true,
177
+ },
178
+ ],
179
+ },
180
+ },
181
+ ],
182
+ };
183
+ expect(parseEntityForDisplay(cdmaOnly).smsSenderId).toBe('+cdma-only');
184
+ });
185
+
186
+ it('Reset / default helpers respect the gateway the user selected in the form', () => {
187
+ const entity = domainPropertiesFromApi;
188
+ expect(getDefaultValueForField('smsDomain', entity)).toBe('s1');
189
+ expect(
190
+ getDefaultValueForField('smsSenderId', entity, { fieldValues: { smsDomain: 's2' } }),
191
+ ).toBe('+2002');
192
+ });
193
+
194
+ it('returns safe empty defaults when domain properties were not loaded', () => {
195
+ expect(getSmsDefaultsForDomainId(null, 's1')).toEqual({ senderId: '' });
196
+ });
197
+ });
198
+
199
+ describe('Email: multiple domains, from-address, reply-to', () => {
200
+ const entity = domainPropertiesFromApi;
201
+
202
+ it('summary shows the active domain name and default sender for the first gateway', () => {
203
+ const summary = parseEntityForDisplay(entity);
204
+ expect(summary.emailDomain).toBe('Transactional mail');
205
+ expect(summary.emailSenderId).toBe('noreply@brand.com');
206
+ });
207
+
208
+ it('when the user switches domain in the form, defaults follow that gateway’s contactInfo', () => {
209
+ const ctx = { fieldValues: { emailDomain: 'e2' } };
210
+ expect(getDefaultValueForField('emailSenderId', entity, ctx)).toBe('promo@brand.com');
211
+ expect(getDefaultValueForField('emailSenderName', entity, ctx)).toBe('Promo desk');
212
+ expect(getEmailDefaultsForDomainId(entity, 'e2').senderId).toBe('promo@brand.com');
213
+ });
214
+
215
+ it('some tenants return domain display name on the root object — summary still shows it', () => {
216
+ const legacyShape = {
217
+ EMAIL: [
218
+ {
219
+ id: 'er1',
220
+ domainName: 'Marketing (legacy shape)',
221
+ domainProperties: { id: 'er1', contactInfo: [] },
222
+ },
223
+ ],
224
+ };
225
+ expect(parseEntityForDisplay(legacyShape).emailDomain).toBe('Marketing (legacy shape)');
226
+ });
227
+
228
+ it('when multiple sender_id / reply_to rows exist, defaults prefer rows marked default', () => {
229
+ const richDomain = {
230
+ EMAIL: [
231
+ {
232
+ domainProperties: {
233
+ id: 'edef',
234
+ domainName: 'Rich domain',
235
+ contactInfo: [
236
+ {
237
+ type: 'sender_id',
238
+ value: 'a@d.com',
239
+ label: 'Label A',
240
+ valid: true,
241
+ default: false,
242
+ },
243
+ {
244
+ type: 'sender_id',
245
+ value: 'b@d.com',
246
+ label: 'Label B',
247
+ valid: true,
248
+ default: true,
249
+ },
250
+ {
251
+ type: 'reply_to_id',
252
+ value: 'r1@d.com',
253
+ valid: true,
254
+ default: false,
255
+ },
256
+ {
257
+ type: 'reply_to_id',
258
+ value: 'r2@d.com',
259
+ valid: true,
260
+ default: true,
261
+ },
262
+ ],
263
+ },
264
+ },
265
+ ],
266
+ };
267
+ expect(parseEntityForDisplay(richDomain).emailSenderId).toBe('b@d.com');
268
+ expect(getEmailDefaultsForDomainId(richDomain, 'edef').replyToId).toBe('r2@d.com');
269
+ const emailSenderNameField = getFieldsForChannels(['EMAIL']).find(
270
+ (f) => f.fieldKey === 'emailSenderName',
271
+ );
272
+ expect(emailSenderNameField.getValue(richDomain)).toBe('Label B');
273
+ });
274
+
275
+ it('returns safe empty defaults when domain properties were not loaded', () => {
276
+ expect(getEmailDefaultsForDomainId(null, 'e1')).toEqual({
277
+ senderId: '',
278
+ senderName: '',
279
+ replyToId: '',
280
+ });
281
+ });
282
+ });
283
+
284
+ describe('WhatsApp: template WABA vs connected domains', () => {
285
+ const singleWa = domainPropertiesFromApi;
286
+
287
+ it('matches the business account name using the template’s WABA / connection id', () => {
288
+ expect(getWhatsappAccountName(singleWa, 'waba-99')).toBe('Brand WA');
289
+ expect(
290
+ parseEntityForDisplay(singleWa, { whatsappSourceAccountId: 'waba-99' }).whatsappSenderNumber,
291
+ ).toBe('+wa1');
292
+ });
293
+
294
+ it('with no WABA on the template, uses the first WhatsApp gateway (single-account setups)', () => {
295
+ expect(getWhatsappAccountName(singleWa, '')).toBe('Brand WA');
296
+ });
297
+
298
+ it('if the template WABA does not match any row, UI still binds to the first connected domain', () => {
299
+ const onlyOther = {
300
+ WHATSAPP: [
301
+ {
302
+ domainProperties: {
303
+ domainName: 'Fallback WA',
304
+ connectionProperties: { sourceAccountIdentifier: 'other-waba' },
305
+ contactInfo: [{
306
+ type: 'gsm_sender_id', value: '+x', valid: true, default: true,
307
+ }],
308
+ },
309
+ },
310
+ ],
311
+ };
312
+ expect(getWhatsappAccountName(onlyOther, 'unknown-waba')).toBe('Fallback WA');
313
+ });
314
+
315
+ it('no entity → no account label (e.g. fetch failed)', () => {
316
+ expect(getWhatsappAccountName(null, 'waba-99')).toBe('');
317
+ });
318
+ });
319
+
320
+ describe('RCS: brand line in summary', () => {
321
+ it('shows RCS brand and number; combined creative prefers WhatsApp for the shared senderNumber slot', () => {
322
+ const entity = domainPropertiesFromApi;
323
+ expect(getRcsAccountName(entity)).toBe('RCS brand');
324
+ const summary = parseEntityForDisplay(entity);
325
+ expect(summary.rcsSenderNumber).toBe('+rcs1');
326
+ expect(summary.senderNumber).toBe('+wa1');
327
+ });
328
+
329
+ it('RCS-only creative uses RCS for the shared senderNumber line', () => {
330
+ const rcsOnly = { RCS: domainPropertiesFromApi.RCS };
331
+ expect(parseEntityForDisplay(rcsOnly).senderNumber).toBe('+rcs1');
332
+ });
333
+
334
+ it('empty payload → no RCS label', () => {
335
+ expect(getRcsAccountName({})).toBe('');
336
+ });
337
+ });
338
+
339
+ describe('Viber: gateway id 0 + alternate API sender types', () => {
340
+ it('uses viber_sender_id when GSM is not present (API variance)', () => {
341
+ const entity = domainPropertiesFromApi;
342
+ expect(parseEntityForDisplay(entity).viberSenderId).toBe('viber-sender-1');
343
+ });
344
+
345
+ it('Iris-style mapping: domainProperties.id is 0, real gateway id on parent — account select still works', () => {
346
+ const entity = {
347
+ VIBER: [
348
+ {
349
+ domainProperties: {
350
+ id: 0,
351
+ domainName: 'Primary',
352
+ contactInfo: [],
353
+ },
354
+ id: 'gateway-x',
355
+ },
356
+ {
357
+ domainProperties: {
358
+ id: 5,
359
+ domainName: 'Listed',
360
+ contactInfo: [],
361
+ },
362
+ },
363
+ ],
364
+ };
365
+ const viberAccountField = getFieldsForChannels(['VIBER']).find(
366
+ (f) => f.fieldKey === 'viberAccount',
367
+ );
368
+ const opts = viberAccountField.getOptions(entity);
369
+ expect(opts.map((o) => o.value)).toEqual(expect.arrayContaining(['gateway-x', 5]));
370
+ });
371
+ });
372
+
373
+ describe('Zalo / LINE: multiple official accounts', () => {
374
+ it('aggregates sender options across all returned gateways for the channel', () => {
375
+ const zaloField = getFieldsForChannels(['ZALO'])[0];
376
+ const opts = zaloField.getOptions(domainPropertiesFromApi);
377
+ const values = opts.map((o) => o.value);
378
+ expect(values).toEqual(expect.arrayContaining(['+z1', '+z2']));
379
+ });
380
+ });
381
+
382
+ describe('Which delivery fields appear (channel enablement)', () => {
383
+ it('only includes field configs for channels in the creative / flow', () => {
384
+ expect(FIELD_TYPE.SELECT).toBe('select');
385
+ expect(DELIVERY_SETTINGS_FIELDS.length).toBeGreaterThan(5);
386
+ const keys = getFieldsForChannels(['sms', 'EMAIL', '', null]).map((f) => f.fieldKey);
387
+ expect(keys).toEqual(
388
+ expect.arrayContaining(['smsDomain', 'smsSenderId', 'emailDomain', 'emailSenderId']),
389
+ );
390
+ });
391
+
392
+ it('for a full multi-channel response, every configured getter runs without throwing', () => {
393
+ const combined = domainPropertiesFromApi;
394
+ const waCtx = { whatsappSourceAccountId: 'waba-99' };
395
+ const fields = getFieldsForChannels([
396
+ 'SMS',
397
+ 'EMAIL',
398
+ 'WHATSAPP',
399
+ 'RCS',
400
+ 'VIBER',
401
+ 'ZALO',
402
+ 'LINE',
403
+ ]);
404
+ fields.forEach((f) => {
405
+ const ctx = f.fieldKey === 'whatsappSenderNumber' ? { fieldValues: {}, ...waCtx } : { fieldValues: {} };
406
+ if (typeof f.getValue === 'function') f.getValue(combined, ctx);
407
+ if (typeof f.getOptions === 'function') f.getOptions(combined, ctx);
408
+ });
409
+ });
410
+
411
+ it('SMS label-only contact row still yields a display value', () => {
412
+ const labelOnlySms = {
413
+ SMS: [
414
+ {
415
+ domainProperties: {
416
+ id: 'lbl',
417
+ domainName: 'Lbl GW',
418
+ contactInfo: [
419
+ {
420
+ type: 'gsm_sender_id', label: 'only-label', valid: true, default: true,
421
+ },
422
+ ],
423
+ },
424
+ },
425
+ ],
426
+ };
427
+ getFieldsForChannels(['SMS']).forEach((f) => {
428
+ f.getValue(labelOnlySms, { fieldValues: {} });
429
+ f.getOptions(labelOnlySms);
430
+ });
431
+ expect(parseEntityForDisplay(labelOnlySms).smsSenderId).toBe('only-label');
432
+ });
433
+ });
434
+
435
+ describe('Save + reopen', () => {
436
+ it('buildChannelSettingFromFieldValues matches what campaigns expect on save', () => {
437
+ const saved = buildChannelSettingFromFieldValues({
438
+ smsDomain: 'd1',
439
+ smsSenderId: 'g1',
440
+ emailDomain: 'ed',
441
+ emailSenderId: 'mail@x.com',
442
+ emailSenderName: 'N',
443
+ emailReplyToId: 'r@x.com',
444
+ whatsappSenderNumber: '+w',
445
+ whatsappDomainId: 'wd',
446
+ whatsappAccount: 'acc',
447
+ rcsSenderNumber: '+r',
448
+ rcsAccount: 'rcs-acc',
449
+ viberAccount: 'va',
450
+ viberSenderId: 'vs',
451
+ zaloSenderId: 'zs',
452
+ lineSenderId: 'ls',
453
+ });
454
+ expect(saved).toEqual({
455
+ SMS: { domainId: 'd1', gsmSenderId: 'g1' },
456
+ EMAIL: {
457
+ domainId: 'ed',
458
+ senderEmail: 'mail@x.com',
459
+ senderLabel: 'N',
460
+ senderReplyTo: 'r@x.com',
461
+ },
462
+ WHATSAPP: { domainId: 'wd', senderMobNum: '+w' },
463
+ RCS: { senderMobNum: '+r', rcsSender: '+r' },
464
+ VIBER: { domainId: 'va', sender: 'vs', gsmSenderId: 'vs' },
465
+ ZALO: { zaloSenderId: 'zs' },
466
+ LINE: { lineSenderId: 'ls' },
467
+ });
468
+ });
469
+
470
+ it('parseChannelSettingForDisplay hydrates form-shaped values when editing an existing creative', () => {
471
+ const flat = parseChannelSettingForDisplay({
472
+ SMS: { domainId: 'd', gsmSenderId: 'g' },
473
+ EMAIL: {
474
+ domainId: 'ed',
475
+ senderEmail: 'e@e.com',
476
+ senderLabel: 'L',
477
+ senderReplyTo: 'r@e.com',
478
+ },
479
+ WHATSAPP: { senderMobNum: '+w', domainId: 'wid' },
480
+ RCS: { rcsSender: '+r' },
481
+ VIBER: { domainId: 'vd', viberSenderId: 'vx' },
482
+ ZALO: { zaloSenderId: 'zz' },
483
+ LINE: { lineSenderId: 'll' },
484
+ });
485
+ expect(flat).toMatchObject({
486
+ smsDomain: 'd',
487
+ smsSenderId: 'g',
488
+ emailDomain: 'ed',
489
+ whatsappDomainId: 'wid',
490
+ viberSenderId: 'vx',
491
+ });
492
+ expect(parseChannelSettingForDisplay({ VIBER: { domainId: 'd', gsmSenderId: 'gsm' } }).viberSenderId).toBe(
493
+ 'gsm',
494
+ );
495
+ expect(parseChannelSettingForDisplay({ VIBER: { domainId: 'd', sender: 's' } }).viberSenderId).toBe('s');
496
+ });
497
+
498
+ it('nothing saved yet → empty channelSetting object', () => {
499
+ expect(buildChannelSettingFromFieldValues({})).toEqual({});
500
+ expect(parseChannelSettingForDisplay()).toEqual({});
501
+ });
502
+ });
503
+
504
+ describe('Gateway checks before showing delivery errors', () => {
505
+ it('SMS / Email / WhatsApp / Viber / RCS need a non-empty gateway list when we validate', () => {
506
+ expect(hasDomainGateway(null, SMS)).toBe(false);
507
+ expect(hasDomainGateway({ SMS: [] }, SMS)).toBe(false);
508
+ expect(hasDomainGateway(domainPropertiesFromApi, SMS)).toBe(true);
509
+ expect(hasDomainGateway(domainPropertiesFromApi, EMAIL)).toBe(true);
510
+ expect(hasDomainGateway(domainPropertiesFromApi, VIBER)).toBe(true);
511
+ expect(hasDomainGateway(domainPropertiesFromApi, RCS)).toBe(true);
512
+ });
513
+
514
+ it('WhatsApp also requires at least one sendable number on the matched (or fallback) domain', () => {
515
+ expect(hasDomainGateway({ WHATSAPP: [] }, WHATSAPP)).toBe(false);
516
+ expect(
517
+ hasDomainGateway(domainPropertiesFromApi, WHATSAPP, { whatsappSourceAccountId: 'waba-99' }),
518
+ ).toBe(true);
519
+ const noSenders = {
520
+ WHATSAPP: [
521
+ {
522
+ domainProperties: {
523
+ connectionProperties: { sourceAccountIdentifier: 'x' },
524
+ contactInfo: [],
525
+ },
526
+ },
527
+ ],
528
+ };
529
+ expect(hasDomainGateway(noSenders, WHATSAPP, { whatsappSourceAccountId: 'x' })).toBe(false);
530
+ });
531
+
532
+ it('Zalo and Line are not blocked by the domain-gateway gate (product rules)', () => {
533
+ expect(hasDomainGateway({}, ZALO)).toBe(true);
534
+ expect(hasDomainGateway({}, LINE)).toBe(true);
535
+ });
536
+ });
537
+
538
+ describe('parseEntityForDisplay', () => {
539
+ it('returns null when the parent has not loaded entity yet', () => {
540
+ expect(parseEntityForDisplay(null)).toBeNull();
541
+ });
542
+ });
543
+
544
+ describe('getDefaultValueForField — edge cases', () => {
545
+ it('unknown field keys do not break reset logic', () => {
546
+ expect(getDefaultValueForField('unknown', domainPropertiesFromApi)).toBe('');
547
+ });
548
+
549
+ it('falsy entity → empty default', () => {
550
+ expect(getDefaultValueForField('smsDomain', null)).toBe('');
551
+ });
552
+
553
+ it('returns emailReplyToId default for the selected domain', () => {
554
+ // Covers line 277: if (fieldKey === 'emailReplyToId') return defs.replyToId branch
555
+ const ctx = { fieldValues: { emailDomain: 'e1' } };
556
+ expect(getDefaultValueForField('emailReplyToId', domainPropertiesFromApi, ctx)).toBe('support@brand.com');
557
+ });
558
+
559
+ it('returns emailSenderName default for the selected domain', () => {
560
+ const ctx = { fieldValues: { emailDomain: 'e1' } };
561
+ expect(getDefaultValueForField('emailSenderName', domainPropertiesFromApi, ctx)).toBe('Brand');
562
+ });
563
+
564
+ it('returns emailSenderId default for the selected domain', () => {
565
+ const ctx = { fieldValues: { emailDomain: 'e1' } };
566
+ expect(getDefaultValueForField('emailSenderId', domainPropertiesFromApi, ctx)).toBe('noreply@brand.com');
567
+ });
568
+ });
569
+
570
+ describe('internal helper branches — contactInfo, domain shape, WhatsApp', () => {
571
+ it('getValueFromContactInfo returns empty string when contactInfo is not an array', () => {
572
+ // Covers line 45: !Array.isArray(contactInfo) → return ''
573
+ const fieldWithNonArrayContactInfo = {
574
+ SMS: [{ domainProperties: { id: 'x', contactInfo: { type: 'gsm_sender_id', value: '+bad' } } }],
575
+ };
576
+ const fields = getFieldsForChannels(['SMS']);
577
+ const senderField = fields.find((f) => f.fieldKey === 'smsSenderId');
578
+ // Should not throw and should return ''
579
+ expect(senderField.getValue(fieldWithNonArrayContactInfo, {})).toBe('');
580
+ });
581
+
582
+ it('getOptionsFromContactInfo returns [] when contactInfo is a non-array object', () => {
583
+ // Covers line 60: !Array.isArray(contactInfo) → return []
584
+ const entity = {
585
+ SMS: [{ domainProperties: { id: 'y', contactInfo: { type: 'gsm_sender_id', value: '+bad' } } }],
586
+ };
587
+ const senderField = getFieldsForChannels(['SMS']).find((f) => f.fieldKey === 'smsSenderId');
588
+ expect(senderField.getOptions(entity)).toEqual([]);
589
+ });
590
+
591
+ it('getSmsDomainById returns first domain when domainId is null', () => {
592
+ // Covers line 83: domainId == null → return arr[0]
593
+ const result = getSmsDefaultsForDomainId(domainPropertiesFromApi, null);
594
+ // Should return defaults for the first SMS gateway
595
+ expect(result.senderId).toBe('+1001');
596
+ });
597
+
598
+ it('getSmsDomainById falls back to arr[0] when domainId does not match any domain', () => {
599
+ // Covers line 88: || arr[0] || null fallback
600
+ const result = getSmsDefaultsForDomainId(domainPropertiesFromApi, 'no-match-id');
601
+ expect(result.senderId).toBe('+1001');
602
+ });
603
+
604
+ it('getSmsDomainOptions uses String(index) as value when no id fields exist on domain', () => {
605
+ // Covers line 97: d.domainId ?? d.id ?? String(i) → String(i) for index fallback
606
+ const entityNoId = {
607
+ SMS: [
608
+ { domainProperties: { domainName: 'No-ID Gateway', contactInfo: [] } },
609
+ ],
610
+ };
611
+ const domainField = getFieldsForChannels(['SMS']).find((f) => f.fieldKey === 'smsDomain');
612
+ const opts = domainField.getOptions(entityNoId);
613
+ expect(opts[0].value).toBe('0'); // String(index 0)
614
+ });
615
+
616
+ it('getSmsDomainOptions uses hostName as label fallback when domainName is absent', () => {
617
+ // Covers label fallback: domainProperties?.hostName branch
618
+ const entityHostname = {
619
+ SMS: [
620
+ { domainProperties: { id: 's-h', hostName: 'smtp.brand.com', contactInfo: [] } },
621
+ ],
622
+ };
623
+ const domainField = getFieldsForChannels(['SMS']).find((f) => f.fieldKey === 'smsDomain');
624
+ const opts = domainField.getOptions(entityHostname);
625
+ expect(opts[0].label).toBe('smtp.brand.com');
626
+ });
627
+
628
+ it('getSmsDomainOptions deduplicates entries with the same label', () => {
629
+ // Covers line 99: !seen.has(label) deduplication
630
+ const entityDuplicate = {
631
+ SMS: [
632
+ { domainProperties: { id: 's-a', domainName: 'Same Name', contactInfo: [] } },
633
+ { domainProperties: { id: 's-b', domainName: 'Same Name', contactInfo: [] } },
634
+ ],
635
+ };
636
+ const domainField = getFieldsForChannels(['SMS']).find((f) => f.fieldKey === 'smsDomain');
637
+ const opts = domainField.getOptions(entityDuplicate);
638
+ expect(opts.length).toBe(1);
639
+ expect(opts[0].label).toBe('Same Name');
640
+ });
641
+
642
+ it('getDomainSourceAccountId uses sourceAccountIdentifier when wabaId is absent', () => {
643
+ // Covers line 299: connectionProperties?.wabaId || connectionProperties?.sourceAccountIdentifier
644
+ const entityWithSourceId = {
645
+ WHATSAPP: [{
646
+ domainProperties: {
647
+ domainName: 'WA Source',
648
+ connectionProperties: { sourceAccountIdentifier: 'src-acc-1' },
649
+ contactInfo: [{ type: 'gsm_sender_id', value: '+w1', valid: true, default: true }],
650
+ },
651
+ }],
652
+ };
653
+ expect(getWhatsappAccountName(entityWithSourceId, 'src-acc-1')).toBe('WA Source');
654
+ });
655
+
656
+ it('getWhatsappAccountName returns first domain when sourceAccountId is null/empty', () => {
657
+ // Covers fallback branch in getWhatsappDomainBySourceAccount
658
+ const entity = {
659
+ WHATSAPP: [{
660
+ domainProperties: {
661
+ domainName: 'First WA',
662
+ connectionProperties: {},
663
+ contactInfo: [{ type: 'gsm_sender_id', value: '+w2', valid: true, default: true }],
664
+ },
665
+ }],
666
+ };
667
+ expect(getWhatsappAccountName(entity, null)).toBe('First WA');
668
+ expect(getWhatsappAccountName(entity, '')).toBe('First WA');
669
+ });
670
+
671
+ it('parseEntityForDisplay returns all channel defaults together in a combined entity', () => {
672
+ // Covers the full parseEntityForDisplay with combined entity — all channel branches hit
673
+ const summary = parseEntityForDisplay(domainPropertiesFromApi, { whatsappSourceAccountId: 'waba-99' });
674
+ expect(summary.smsDomain).toBeTruthy();
675
+ expect(summary.emailDomain).toBeTruthy();
676
+ expect(summary.whatsappSenderNumber).toBe('+wa1');
677
+ expect(summary.rcsSenderNumber).toBe('+rcs1');
678
+ expect(summary.viberSenderId).toBe('viber-sender-1');
679
+ expect(summary.zaloSenderId).toBe('+z1');
680
+ expect(summary.lineSenderId).toBe('+line1');
681
+ });
682
+
683
+ it('email domain name display uses hostName as fallback when domainName is absent', () => {
684
+ // Covers getEmailDomainNameForDisplay hostName branch
685
+ const entity = {
686
+ EMAIL: [{
687
+ domainProperties: { id: 'e-h', hostName: 'mail.company.com', contactInfo: [] },
688
+ }],
689
+ };
690
+ const summary = parseEntityForDisplay(entity);
691
+ expect(summary.emailDomain).toBe('mail.company.com');
692
+ });
693
+
694
+ it('email sender name from options uses label-then-value fallback', () => {
695
+ // Covers getEmailSenderNameOptions: label || value and value || label chains
696
+ const entity = {
697
+ EMAIL: [{
698
+ domainProperties: {
699
+ id: 'e-ln',
700
+ contactInfo: [
701
+ // Has value but no label — label should fall back to value
702
+ { type: 'sender_id', value: 'no-label@co.com', valid: true, default: true },
703
+ ],
704
+ },
705
+ }],
706
+ };
707
+ const senderNameField = getFieldsForChannels(['EMAIL']).find((f) => f.fieldKey === 'emailSenderName');
708
+ const opts = senderNameField.getOptions(entity);
709
+ // Should still produce an option using value as label
710
+ expect(opts.length).toBeGreaterThan(0);
711
+ });
712
+
713
+ it('buildChannelSettingFromFieldValues with only ZALO data produces only ZALO key', () => {
714
+ const result = buildChannelSettingFromFieldValues({ zaloSenderId: 'zs1' });
715
+ expect(result).toEqual({ ZALO: { zaloSenderId: 'zs1' } });
716
+ });
717
+
718
+ it('buildChannelSettingFromFieldValues with only LINE data produces only LINE key', () => {
719
+ const result = buildChannelSettingFromFieldValues({ lineSenderId: 'ls1' });
720
+ expect(result).toEqual({ LINE: { lineSenderId: 'ls1' } });
721
+ });
722
+
723
+ it('buildChannelSettingFromFieldValues with only RCS data produces only RCS key', () => {
724
+ const result = buildChannelSettingFromFieldValues({ rcsSenderNumber: '+rcs-x' });
725
+ expect(result).toEqual({ RCS: { senderMobNum: '+rcs-x', rcsSender: '+rcs-x' } });
726
+ });
727
+
728
+ it('hasDomainGateway returns false for an unknown channel when entity is empty', () => {
729
+ expect(hasDomainGateway({}, SMS)).toBe(false);
730
+ expect(hasDomainGateway({}, EMAIL)).toBe(false);
731
+ expect(hasDomainGateway({}, RCS)).toBe(false);
732
+ });
733
+
734
+ it('toArray passes through arrays unchanged', () => {
735
+ // Validate array→array passthrough in toArray via getSmsDomainOptions
736
+ const entity = {
737
+ SMS: [
738
+ { domainProperties: { id: 'arr-1', domainName: 'Array GW', contactInfo: [] } },
739
+ ],
740
+ };
741
+ const domainField = getFieldsForChannels(['SMS']).find((f) => f.fieldKey === 'smsDomain');
742
+ expect(domainField.getOptions(entity).length).toBe(1);
743
+ });
744
+
745
+ it('getChannelData returns null when entity has no matching channel key', () => {
746
+ // Covers getChannelData returning null → functions returning empty defaults
747
+ const emptyEntity = {};
748
+ expect(getSmsDefaultsForDomainId(emptyEntity, 'x')).toEqual({ senderId: '' });
749
+ });
750
+
751
+ it('contactInfo entry with no value but a valid label falls back to label in options', () => {
752
+ // Covers getOptionsFromContactInfo: val = m.value || m.label || ''
753
+ const entityLabelOnly = {
754
+ SMS: [{
755
+ domainProperties: {
756
+ id: 'lbl',
757
+ contactInfo: [
758
+ { type: 'gsm_sender_id', label: 'label-only-sender', valid: true, default: true },
759
+ ],
760
+ },
761
+ }],
762
+ };
763
+ const senderField = getFieldsForChannels(['SMS']).find((f) => f.fieldKey === 'smsSenderId');
764
+ const opts = senderField.getOptions(entityLabelOnly);
765
+ expect(opts[0].value).toBe('label-only-sender');
766
+ });
767
+
768
+ it('getEmailDomainById returns first domain when domainId is null', () => {
769
+ // Covers email version of line 144: domainId == null → arr[0]
770
+ const result = getEmailDefaultsForDomainId(domainPropertiesFromApi, null);
771
+ expect(result.senderId).toBe('noreply@brand.com');
772
+ });
773
+
774
+ it('getEmailDomainById falls back to arr[0] when domainId has no match', () => {
775
+ // Covers email fallback: find() || arr[0]
776
+ const result = getEmailDefaultsForDomainId(domainPropertiesFromApi, 'nonexistent-domain-id');
777
+ expect(result.senderId).toBe('noreply@brand.com');
778
+ });
779
+
780
+ it('getEmailSenderIdOptions uses current entity email data when no domain selected in form', () => {
781
+ // Covers line 184: selectedDomainId ? ... : getEmailData(entity) branch (falsy selectedDomainId)
782
+ const emailField = getFieldsForChannels(['EMAIL']).find((f) => f.fieldKey === 'emailSenderId');
783
+ const opts = emailField.getOptions(domainPropertiesFromApi, { fieldValues: {} });
784
+ expect(opts.some((o) => o.value === 'noreply@brand.com')).toBe(true);
785
+ });
786
+
787
+ it('getEmailSenderNameOptions with contact having only value (no label) uses value as display', () => {
788
+ // Covers getEmailSenderNameOptions: contact?.label || contact?.value — label absent
789
+ const entity = {
790
+ EMAIL: [{
791
+ domainProperties: {
792
+ id: 'e-val',
793
+ contactInfo: [
794
+ { type: 'sender_id', value: 'sender@domain.com', valid: true, default: true },
795
+ ],
796
+ },
797
+ }],
798
+ };
799
+ const senderNameField = getFieldsForChannels(['EMAIL']).find((f) => f.fieldKey === 'emailSenderName');
800
+ const opts = senderNameField.getOptions(entity, { fieldValues: {} });
801
+ expect(opts[0].value).toBe('sender@domain.com');
802
+ });
803
+
804
+ it('getEmailReplyToIdOptions uses current email domain when no domain selected in form', () => {
805
+ // Covers getEmailReplyToIdOptions: falsy selectedDomainId → getEmailData
806
+ const replyField = getFieldsForChannels(['EMAIL']).find((f) => f.fieldKey === 'emailReplyToId');
807
+ const opts = replyField.getOptions(domainPropertiesFromApi, { fieldValues: {} });
808
+ expect(opts.some((o) => o.value === 'support@brand.com')).toBe(true);
809
+ });
810
+
811
+ it('getRcsAccountName uses domainName from root when domainProperties is absent', () => {
812
+ // Covers rcsData.domainName || rcsData.label fallback
813
+ const entity = {
814
+ RCS: [{ domainName: 'Root RCS Name', domainProperties: { contactInfo: [] } }],
815
+ };
816
+ expect(getRcsAccountName(entity)).toBe('Root RCS Name');
817
+ });
818
+
819
+ it('getWhatsappDomainBySourceAccount falls back to first domain when sourceAccountId matches nothing', () => {
820
+ // Covers line 306: find() || arr[0] fallback
821
+ const entity = {
822
+ WHATSAPP: [{
823
+ domainProperties: {
824
+ domainName: 'Only WA Domain',
825
+ connectionProperties: { sourceAccountIdentifier: 'different-id' },
826
+ contactInfo: [{ type: 'gsm_sender_id', value: '+wa-only', valid: true, default: true }],
827
+ },
828
+ }],
829
+ };
830
+ expect(getWhatsappAccountName(entity, 'not-matching-id')).toBe('Only WA Domain');
831
+ });
832
+
833
+ it('getDomainSourceAccountId uses userid as final fallback', () => {
834
+ // Covers line 299: userid fallback
835
+ const entity = {
836
+ WHATSAPP: [{
837
+ domainProperties: {
838
+ domainName: 'WA userid',
839
+ connectionProperties: { userid: 'user-acc-1' },
840
+ contactInfo: [{ type: 'gsm_sender_id', value: '+wuid', valid: true, default: true }],
841
+ },
842
+ }],
843
+ };
844
+ expect(getWhatsappAccountName(entity, 'user-acc-1')).toBe('WA userid');
845
+ });
846
+
847
+ it('getViberDomainId returns domainId/id root fields when domainProperties.id is 0', () => {
848
+ // Covers line 354-355: dpId === 0 → d.domainId ?? d.id
849
+ const entityViberZeroId = {
850
+ VIBER: [{
851
+ domainId: 'vdomId',
852
+ domainProperties: { id: 0, domainName: 'Viber Zero', contactInfo: [] },
853
+ }],
854
+ };
855
+ const viberAccountField = getFieldsForChannels(['VIBER']).find((f) => f.fieldKey === 'viberAccount');
856
+ const val = viberAccountField.getValue(entityViberZeroId);
857
+ expect(val).toBe('vdomId');
858
+ });
859
+
860
+ it('reduce short-circuits when preferred type is already found (no fallback iteration)', () => {
861
+ // Covers getValueFromContactInfo: `if (found) return found` short-circuit in reduce
862
+ const entityBothTypes = {
863
+ SMS: [{
864
+ domainProperties: {
865
+ id: 'both',
866
+ contactInfo: [
867
+ { type: 'gsm_sender_id', value: '+gsm', valid: true, default: true },
868
+ { type: 'cdma_sender_id', value: '+cdma', valid: true, default: false },
869
+ ],
870
+ },
871
+ }],
872
+ };
873
+ const senderField = getFieldsForChannels(['SMS']).find((f) => f.fieldKey === 'smsSenderId');
874
+ // GSM exists, so CDMA should not be used
875
+ expect(senderField.getValue(entityBothTypes, {})).toBe('+gsm');
876
+ });
877
+
878
+ it('getOptionsFromContactInfo deduplicates values across contactInfo entries', () => {
879
+ // Covers seen.has(val) deduplication branch in getOptionsFromContactInfo
880
+ const entityDupeValues = {
881
+ SMS: [{
882
+ domainProperties: {
883
+ id: 'dupe',
884
+ contactInfo: [
885
+ { type: 'gsm_sender_id', value: '+same', valid: true },
886
+ { type: 'gsm_sender_id', value: '+same', valid: true },
887
+ ],
888
+ },
889
+ }],
890
+ };
891
+ const senderField = getFieldsForChannels(['SMS']).find((f) => f.fieldKey === 'smsSenderId');
892
+ const opts = senderField.getOptions(entityDupeValues);
893
+ expect(opts.filter((o) => o.value === '+same').length).toBe(1);
894
+ });
895
+
896
+ it('getEmailDefaultsForDomain returns empty object when domainData is null', () => {
897
+ // Covers line 240: if (!domainData) return empty defaults
898
+ const result = getEmailDefaultsForDomainId({ EMAIL: [] }, 'any-id');
899
+ expect(result).toEqual({ senderId: '', senderName: '', replyToId: '' });
900
+ });
901
+
902
+ it('getEmailDefaultsForDomain returns empty strings when domain has no sender_id or reply_to_id entries', () => {
903
+ // Covers the ternary false branches: senderMatch ? ... : '' and replyMatch ? ... : ''
904
+ const result = getEmailDefaultsForDomainId({
905
+ EMAIL: [{
906
+ domainProperties: {
907
+ id: 'e-no-sender',
908
+ contactInfo: [
909
+ { type: 'gsm_sender_id', value: '+111', valid: true },
910
+ ],
911
+ },
912
+ }],
913
+ }, 'e-no-sender');
914
+ expect(result).toEqual({ senderId: '', senderName: '', replyToId: '' });
915
+ });
916
+
917
+ it('getEmailDefaultsForDomain uses label fallback for senderId and replyToId when value is absent', () => {
918
+ // Covers: (senderMatch.value || senderMatch.label) and (replyMatch.value || replyMatch.label)
919
+ // when .value is null/falsy and .label provides the fallback
920
+ const result = getEmailDefaultsForDomainId({
921
+ EMAIL: [{
922
+ domainProperties: {
923
+ id: 'e-label-only',
924
+ contactInfo: [
925
+ {
926
+ type: 'sender_id', label: 'Brand Mailer', value: null, valid: true, default: true,
927
+ },
928
+ {
929
+ type: 'reply_to_id', label: 'support@brand.com', value: null, valid: true, default: true,
930
+ },
931
+ ],
932
+ },
933
+ }],
934
+ }, 'e-label-only');
935
+ expect(result.senderId).toBe('Brand Mailer');
936
+ expect(result.replyToId).toBe('support@brand.com');
937
+ });
938
+
939
+ it('getEmailDefaultsForDomain uses value fallback for senderName when label is absent', () => {
940
+ // Covers: (senderMatch.label || senderMatch.value) when .label is null/falsy
941
+ const result = getEmailDefaultsForDomainId({
942
+ EMAIL: [{
943
+ domainProperties: {
944
+ id: 'e-value-only',
945
+ contactInfo: [
946
+ {
947
+ type: 'sender_id', value: 'noreply@brand.com', label: null, valid: true, default: true,
948
+ },
949
+ ],
950
+ },
951
+ }],
952
+ }, 'e-value-only');
953
+ expect(result.senderName).toBe('noreply@brand.com');
954
+ });
955
+
956
+ it('getEmailDomainOptions uses index as value when all id fields are absent', () => {
957
+ // Covers line 158: d.domainId ?? d.id ?? String(i) → String(i)
958
+ const entity = {
959
+ EMAIL: [
960
+ { domainProperties: { domainName: 'Email No ID', contactInfo: [] } },
961
+ ],
962
+ };
963
+ const domainField = getFieldsForChannels(['EMAIL']).find((f) => f.fieldKey === 'emailDomain');
964
+ const opts = domainField.getOptions(entity);
965
+ expect(opts[0].value).toBe('0');
966
+ });
967
+
968
+ it('parseEntityForDisplay returns RCS-only summary correctly', () => {
969
+ // Covers RCS-only branch in parseEntityForDisplay
970
+ const rcsOnly = {
971
+ RCS: [{
972
+ domainProperties: {
973
+ domainName: 'RCS-only',
974
+ contactInfo: [{ type: 'gsm_sender_id', value: '+rcs-o', valid: true, default: true }],
975
+ },
976
+ }],
977
+ };
978
+ const summary = parseEntityForDisplay(rcsOnly);
979
+ expect(summary.rcsSenderNumber).toBe('+rcs-o');
980
+ expect(summary.senderNumber).toBe('+rcs-o');
981
+ });
982
+
983
+ it('buildChannelSettingFromFieldValues with only VIBER data produces only VIBER key', () => {
984
+ const result = buildChannelSettingFromFieldValues({ viberAccount: 'va1', viberSenderId: 'vs1' });
985
+ expect(result).toEqual({ VIBER: { domainId: 'va1', sender: 'vs1', gsmSenderId: 'vs1' } });
986
+ });
987
+
988
+ it('parseChannelSettingForDisplay handles missing VIBER sub-fields gracefully', () => {
989
+ // Covers fallback branches in parseChannelSettingForDisplay VIBER section
990
+ const flat = parseChannelSettingForDisplay({ VIBER: { domainId: 'v1' } });
991
+ expect(flat.viberAccount).toBe('v1');
992
+ // viberSenderId is undefined when no sender fields present (not in the mapping)
993
+ expect(flat.viberSenderId).toBeFalsy();
994
+ });
995
+ });
996
+
997
+ describe('getViberAccountOptions — synthetic fallback prepend', () => {
998
+ it('prepends a synthetic entry when the saved viberAccount is absent from API options (stale saved domain)', () => {
999
+ // Triggers the prepend path: context.fieldValues.viberAccount is not present in API options
1000
+ const entityWithActiveDomain = {
1001
+ VIBER: [{
1002
+ domainProperties: {
1003
+ id: 5,
1004
+ domainName: 'Current Domain',
1005
+ contactInfo: [],
1006
+ },
1007
+ }],
1008
+ };
1009
+ const contextWithStaleSave = { fieldValues: { viberAccount: 'old-deactivated-domain' } };
1010
+ const viberAccountField = getFieldsForChannels(['VIBER']).find((fieldConfig) => fieldConfig.fieldKey === 'viberAccount');
1011
+ const opts = viberAccountField.getOptions(entityWithActiveDomain, contextWithStaleSave);
1012
+ expect(opts[0].value).toBe('old-deactivated-domain');
1013
+ expect(opts.some((option) => option.value === 5)).toBe(true);
1014
+ });
1015
+
1016
+ it('uses the first domain name as the prepended entry label when available', () => {
1017
+ const entityWithActiveDomain = {
1018
+ VIBER: [{
1019
+ domainProperties: {
1020
+ id: 5,
1021
+ domainName: 'Active Domain',
1022
+ contactInfo: [],
1023
+ },
1024
+ }],
1025
+ };
1026
+ const contextWithStaleSave = { fieldValues: { viberAccount: 'stale-account-123' } };
1027
+ const viberAccountField = getFieldsForChannels(['VIBER']).find((fieldConfig) => fieldConfig.fieldKey === 'viberAccount');
1028
+ const opts = viberAccountField.getOptions(entityWithActiveDomain, contextWithStaleSave);
1029
+ expect(opts[0]).toEqual({ label: 'Active Domain', value: 'stale-account-123' });
1030
+ });
1031
+
1032
+ it('falls back to String(currentVal) as label when firstDomain has no domainName', () => {
1033
+ const entityNoDomainName = {
1034
+ VIBER: [{
1035
+ domainProperties: { id: 5, contactInfo: [] },
1036
+ }],
1037
+ };
1038
+ const contextWithStaleSave = { fieldValues: { viberAccount: 'stale-456' } };
1039
+ const viberAccountField = getFieldsForChannels(['VIBER']).find((fieldConfig) => fieldConfig.fieldKey === 'viberAccount');
1040
+ const opts = viberAccountField.getOptions(entityNoDomainName, contextWithStaleSave);
1041
+ expect(opts[0]).toEqual({ label: 'stale-456', value: 'stale-456' });
1042
+ });
1043
+
1044
+ it('does not prepend when the saved account is already present in the API options', () => {
1045
+ const entityWithSingleDomain = {
1046
+ VIBER: [{
1047
+ domainProperties: {
1048
+ id: 99,
1049
+ domainName: 'Viber 99',
1050
+ contactInfo: [],
1051
+ },
1052
+ }],
1053
+ };
1054
+ const viberAccountField = getFieldsForChannels(['VIBER']).find((fieldConfig) => fieldConfig.fieldKey === 'viberAccount');
1055
+ const opts = viberAccountField.getOptions(entityWithSingleDomain, {});
1056
+ expect(opts.some((option) => option.value === 99)).toBe(true);
1057
+ expect(opts.length).toBe(1);
1058
+ });
1059
+ });
1060
+
1061
+ describe('getSenderIdOptionsFromContactInfo — synthetic fallback prepend', () => {
1062
+ it('prepends a synthetic option when the saved sender ID is absent from derived contact list', () => {
1063
+ // Covers line 409-411: getSenderIdOptionsFromContactInfo synthetic prepend
1064
+ // This happens when the saved currentVal from getChannelData is not in the options list
1065
+ // For VIBER with viber_sender_id in contactInfo but getChannelData returns a different one
1066
+ const entityViberMismatch = {
1067
+ VIBER: [
1068
+ {
1069
+ id: 'vgw-1',
1070
+ domainProperties: {
1071
+ id: 0,
1072
+ domainName: 'Viber GW',
1073
+ contactInfo: [
1074
+ // Only viber_sender_id — gsm_sender_id is absent
1075
+ // getSenderIdFromContactInfo prefers gsm, falls back to cdma, then viber_sender_id
1076
+ // If there's a saved value as viber_sender_id but not in options...
1077
+ { type: 'viber_sender_id', value: 'viber-acc', valid: true, default: true },
1078
+ ],
1079
+ },
1080
+ },
1081
+ ],
1082
+ };
1083
+
1084
+ const senderIdField = getFieldsForChannels(['VIBER']).find((f) => f.fieldKey === 'viberSenderId');
1085
+ const opts = senderIdField.getOptions(entityViberMismatch);
1086
+ // viber-acc should be in options via fallback
1087
+ expect(opts.some((o) => o.value === 'viber-acc')).toBe(true);
1088
+ });
1089
+
1090
+ it('aggregates ZALO senders and does NOT prepend when current value is in the list', () => {
1091
+ // Covers the else branch of the synthetic prepend: currentVal IS in options → returns as-is
1092
+ const entityZalo = {
1093
+ ZALO: [
1094
+ {
1095
+ domainProperties: {
1096
+ id: 'z1',
1097
+ contactInfo: [
1098
+ { type: 'gsm_sender_id', value: 'zalo-sender', valid: true, default: true },
1099
+ ],
1100
+ },
1101
+ },
1102
+ ],
1103
+ };
1104
+ const senderIdField = getFieldsForChannels(['ZALO']).find((f) => f.fieldKey === 'zaloSenderId');
1105
+ const opts = senderIdField.getOptions(entityZalo);
1106
+ expect(opts.map((o) => o.value)).toContain('zalo-sender');
1107
+ // Should not have duplicates
1108
+ expect(opts.filter((o) => o.value === 'zalo-sender').length).toBe(1);
1109
+ });
1110
+ });
1111
+ });