@capillarytech/creatives-library 8.0.328 → 8.0.330-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/constants/unified.js +4 -0
  2. package/package.json +1 -1
  3. package/services/api.js +17 -0
  4. package/services/tests/api.test.js +85 -0
  5. package/utils/commonUtils.js +28 -0
  6. package/utils/tagValidations.js +2 -3
  7. package/utils/templateVarUtils.js +35 -6
  8. package/utils/tests/commonUtil.test.js +169 -0
  9. package/utils/tests/tagValidations.test.js +1 -1
  10. package/utils/tests/templateVarUtils.test.js +44 -0
  11. package/v2Components/CommonTestAndPreview/AddTestCustomer.js +42 -0
  12. package/v2Components/CommonTestAndPreview/CustomerCreationModal.js +155 -0
  13. package/v2Components/CommonTestAndPreview/ExistingCustomerModal.js +93 -0
  14. package/v2Components/CommonTestAndPreview/SendTestMessage.js +79 -51
  15. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +134 -34
  16. package/v2Components/CommonTestAndPreview/actions.js +10 -0
  17. package/v2Components/CommonTestAndPreview/constants.js +15 -1
  18. package/v2Components/CommonTestAndPreview/index.js +364 -72
  19. package/v2Components/CommonTestAndPreview/messages.js +106 -0
  20. package/v2Components/CommonTestAndPreview/reducer.js +10 -0
  21. package/v2Components/CommonTestAndPreview/tests/AddTestCustomer.test.js +66 -0
  22. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +648 -0
  23. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +24 -0
  24. package/v2Components/CommonTestAndPreview/tests/CustomerCreationModal.test.js +174 -0
  25. package/v2Components/CommonTestAndPreview/tests/ExistingCustomerModal.test.js +114 -0
  26. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +52 -0
  27. package/v2Components/CommonTestAndPreview/tests/constants.test.js +31 -1
  28. package/v2Components/CommonTestAndPreview/tests/index.test.js +36 -0
  29. package/v2Components/CommonTestAndPreview/tests/reducer.test.js +71 -0
  30. package/v2Components/CommonTestAndPreview/tests/selectors.test.js +17 -0
  31. package/v2Components/SmsFallback/smsFallbackUtils.js +14 -3
  32. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +16 -0
  33. package/v2Components/TestAndPreviewSlidebox/index.js +5 -0
  34. package/v2Containers/CreativesContainer/index.js +15 -10
  35. package/v2Containers/Rcs/constants.js +6 -2
  36. package/v2Containers/Rcs/index.js +219 -91
  37. package/v2Containers/Rcs/messages.js +2 -1
  38. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +20 -0
  39. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +2370 -1758
  40. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +67 -0
  41. package/v2Containers/Rcs/tests/utils.test.js +56 -0
  42. package/v2Containers/Rcs/utils.js +53 -6
  43. package/v2Containers/SmsTrai/Edit/index.js +27 -0
  44. package/v2Containers/SmsTrai/Edit/messages.js +5 -0
  45. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +357 -324
  46. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +5586 -5212
@@ -17,6 +17,9 @@ import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
17
17
  import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
18
18
  import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
19
19
  import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
20
+ import CapSpin from '@capillarytech/cap-ui-library/CapSpin';
21
+ import CustomerCreationModal from './CustomerCreationModal';
22
+ import { createTestCustomer } from '../../services/api';
20
23
 
21
24
  // Import messages and styles
22
25
  import messages from './messages';
@@ -30,8 +33,16 @@ import PreviewSection from './PreviewSection';
30
33
 
31
34
  import * as Api from '../../services/api';
32
35
  import { extractTemplateVariables } from '../../utils/templateVarUtils';
36
+ import AddTestCustomerButton from './AddTestCustomer';
37
+ import ExistingCustomerModal from './ExistingCustomerModal';
38
+ // Import constants
33
39
  import {
34
40
  CHANNELS,
41
+ CUSTOMER_MODAL_NEW,
42
+ CUSTOMER_MODAL_EXISTING,
43
+ IDENTIFIER_TYPE_EMAIL,
44
+ IDENTIFIER_TYPE_MOBILE,
45
+ IDENTIFIER_TYPE_PHONE,
35
46
  TEST,
36
47
  DESKTOP,
37
48
  ANDROID,
@@ -86,6 +97,10 @@ import {
86
97
  extractPreviewFromLiquidResponse,
87
98
  getSmsFallbackTextForTagExtraction,
88
99
  } from './previewApiUtils';
100
+ import { pickFirstSmsFallbackTemplateString } from '../../v2Containers/Rcs/rcsLibraryHydrationUtils';
101
+
102
+ import { isValidEmail, isValidMobile, formatPhoneNumber } from '../../utils/commonUtils';
103
+ import { getMembersLookup } from '../../services/api';
89
104
 
90
105
  /**
91
106
  * Drop empty GSM rows. RCS/DLT responses often set gsm_sender_id equal to domainName — keep those rows
@@ -186,6 +201,20 @@ const mapRcsSuggestionForTestMeta = (suggestionRow, index) => ({
186
201
  : '',
187
202
  });
188
203
 
204
+ /**
205
+ * CapTreeSelect and group resolution use strict equality; API data may mix numeric and string ids.
206
+ */
207
+ const normalizeTestEntityId = (id) => {
208
+ if (id === undefined || id === null) return id;
209
+ return String(id);
210
+ };
211
+
212
+ const testEntityIdsEqual = (a, b) => {
213
+ if (a == null && b == null) return true;
214
+ if (a == null || b == null) return false;
215
+ return String(a) === String(b);
216
+ };
217
+
189
218
  /**
190
219
  * Preview Component Factory - REMOVED IN PHASE 5
191
220
  * Now using UnifiedPreview component for all channels
@@ -252,6 +281,11 @@ const CommonTestAndPreview = (props) => {
252
281
  const [tagsExtracted, setTagsExtracted] = useState(false);
253
282
 
254
283
  const initialDevice = CHANNELS_USING_ANDROID_PREVIEW_DEVICE.includes(channel) ? ANDROID : DESKTOP;
284
+ const [searchValue, setSearchValue] = useState("");
285
+ const [customerModal, setCustomerModal] = useState([false, ""]);
286
+ const [isCustomerDataLoading, setIsCustomerDataLoading] = useState(false);
287
+ const [customerData, setCustomerData] = useState({ name: '', email: '', mobile: '', customerId: '' });
288
+
255
289
  const [previewDevice, setPreviewDevice] = useState(initialDevice);
256
290
  const [activePreviewTab, setActivePreviewTab] = useState(PREVIEW_TAB_RCS);
257
291
  const [smsFallbackPreviewText, setSmsFallbackPreviewText] = useState(undefined);
@@ -634,19 +668,26 @@ const CommonTestAndPreview = (props) => {
634
668
  }, [channel, smsFallbackContent, smsFallbackPreviewText]);
635
669
 
636
670
  // Build test entities tree data from testCustomers prop
671
+ // Build test entities tree data from testCustomers prop (includes customers added via addTestCustomer action)
637
672
  const testEntitiesTreeData = useMemo(() => {
638
673
  const groupsNode = {
639
674
  title: 'Groups',
640
675
  value: 'groups-node',
641
676
  selectable: false,
642
- children: testGroups?.map((group) => ({ title: group?.groupName, value: group?.groupId })),
677
+ children: testGroups?.map((group) => ({
678
+ title: group?.groupName,
679
+ value: normalizeTestEntityId(group?.groupId),
680
+ })),
643
681
  };
644
682
 
645
683
  const customersNode = {
646
684
  title: 'Individuals',
647
685
  value: 'customers-node',
648
686
  selectable: false,
649
- children: testCustomers?.map((customer) => ({ title: customer.name, value: customer?.userId })),
687
+ children: testCustomers?.map((customer) => ({
688
+ title: customer?.name?.trim() || customer?.email?.trim() || customer?.mobile?.trim() || customer?.userId || customer?.customerId,
689
+ value: normalizeTestEntityId(customer?.userId ?? customer?.customerId),
690
+ })) || [],
650
691
  };
651
692
 
652
693
  return [groupsNode, customersNode];
@@ -673,6 +714,93 @@ const CommonTestAndPreview = (props) => {
673
714
  return resolvedText;
674
715
  };
675
716
 
717
+ /**
718
+ * Common handler for saving test customers (both new and existing)
719
+ */
720
+ const handleSaveTestCustomer = async (validationErrors = {}, setIsLoading = () => {}) => {
721
+ // Check for validation errors before saving (for new customers)
722
+ if (customerModal[1] === CUSTOMER_MODAL_NEW && (validationErrors.email || validationErrors.mobile)) {
723
+ return;
724
+ }
725
+
726
+ setIsLoading(true);
727
+
728
+ try {
729
+ let payload;
730
+
731
+ if (customerModal[1] === CUSTOMER_MODAL_EXISTING) {
732
+ // For existing customers, use customerId
733
+ payload = {
734
+ campaignUserId: customerData.customerId
735
+ };
736
+ } else {
737
+ // For new customers, use customer object
738
+ payload = {
739
+ customer: {
740
+ firstName: customerData.name || "",
741
+ mobile: customerData.mobile || "",
742
+ email: customerData.email || ""
743
+ }
744
+ };
745
+ }
746
+
747
+ const response = await createTestCustomer(payload);
748
+
749
+
750
+ // Handle success: add to test customers list and selection (existing and new)
751
+ if (response && response.success) {
752
+ CapNotification.success({
753
+ message: formatMessage(messages.newTestCustomerAddedSuccess),
754
+ });
755
+ // API may return customerId in response.response (e.g. { response: { customerId: 438845651 } })
756
+ const res = response?.response || response;
757
+ const addedId = customerModal[1] === CUSTOMER_MODAL_EXISTING
758
+ ? customerData?.customerId || customerData?.campaignUserId
759
+ : res?.customerId || res?.campaignUserId;
760
+ if (addedId) {
761
+ const normalizedAddedId = normalizeTestEntityId(addedId);
762
+ actions.addTestCustomer({
763
+ userId: normalizedAddedId,
764
+ customerId: normalizedAddedId,
765
+ name: customerData?.name?.trim() || '',
766
+ email: customerData?.email || '',
767
+ mobile: customerData?.mobile || '',
768
+ });
769
+ setSelectedTestEntities((prev) => (
770
+ prev.some((id) => testEntityIdsEqual(id, normalizedAddedId))
771
+ ? prev
772
+ : [...prev, normalizedAddedId]
773
+ ));
774
+ }
775
+ handleCloseCustomerModal();
776
+ } else {
777
+ // Show error notification for unsuccessful response
778
+ CapNotification.error({
779
+ message: formatMessage(messages.errorTitle),
780
+ description: response?.message || formatMessage(messages.failedToAddTestCustomer),
781
+ });
782
+ }
783
+ } catch (error) {
784
+ CapNotification.error({
785
+ message: formatMessage(messages.errorTitle),
786
+ description: error?.message || formatMessage(messages.errorAddingTestCustomer),
787
+ });
788
+ } finally {
789
+ setIsLoading(false);
790
+ }
791
+ };
792
+
793
+ const handleCloseCustomerModal = () => {
794
+ setCustomerModal([false, ""]);
795
+ setSearchValue('');
796
+ setCustomerData({
797
+ name: '',
798
+ email: '',
799
+ mobile: '',
800
+ customerId: '',
801
+ });
802
+ };
803
+
676
804
  /**
677
805
  * Prepare payload for preview API based on channel
678
806
  */
@@ -912,78 +1040,77 @@ const CommonTestAndPreview = (props) => {
912
1040
  * rcsMessageContent: { channel, accountId?, rcsRichCardContent: { contentType, cardType, cardSettings, cardContent }, smsFallBackContent? }
913
1041
  * Then rcsDeliverySettings, executionParams, clientName last.
914
1042
  */
915
- const buildRcsTestMessagePayload = (formDataObj, _contentStr, customValuesObj, deliverySettingsOverride, basePayload, rcsExtra = {}) => {
916
- const plainCustom =
917
- customValuesObj != null && typeof customValuesObj.toJS === 'function'
918
- ? customValuesObj.toJS()
919
- : (customValuesObj || {});
920
- const userVarMap = Object.fromEntries(
921
- Object.entries(plainCustom).filter(([, v]) => v != null && String(v).trim() !== '')
922
- );
923
- const rcsData = formDataObj?.versions?.base?.content?.RCS ?? formDataObj?.content?.RCS ?? {};
924
- const rcsContent = rcsData?.rcsContent || {};
925
- const smsFallback = rcsData?.smsFallBackContent || {};
926
- let cardContentList = [];
927
- if (Array.isArray(rcsContent?.cardContent)) {
928
- cardContentList = rcsContent.cardContent;
929
- } else if (rcsContent?.cardContent) {
930
- cardContentList = [rcsContent.cardContent];
1043
+ const buildRcsTestMessagePayload = (
1044
+ creativeFormData,
1045
+ _unusedEditorContentString,
1046
+ _customValuesObj,
1047
+ deliverySettingsOverride,
1048
+ basePayload,
1049
+ _rcsTestMetaExtras = {},
1050
+ ) => {
1051
+ const rcsSectionFromForm =
1052
+ creativeFormData?.versions?.base?.content?.RCS ?? creativeFormData?.content?.RCS ?? {};
1053
+ const rcsContentFromForm = rcsSectionFromForm?.rcsContent || {};
1054
+ const smsFallbackFromCreativeForm = rcsSectionFromForm?.smsFallBackContent || {};
1055
+ let rcsCardPayloadList = [];
1056
+ if (Array.isArray(rcsContentFromForm?.cardContent)) {
1057
+ rcsCardPayloadList = rcsContentFromForm.cardContent;
1058
+ } else if (rcsContentFromForm?.cardContent) {
1059
+ rcsCardPayloadList = [rcsContentFromForm.cardContent];
931
1060
  }
932
- // Merge test customValues into cardVarMapped (template snapshot + user-entered RCS + fallback SMS tags).
933
- const cardContent = cardContentList.map((rcsCard) => {
934
- const baseMap = rcsCard.cardVarMapped || {};
935
- const mergedCardVarMapped =
936
- Object.keys(userVarMap).length > 0 ? { ...baseMap, ...userVarMap } : baseMap;
937
- const mediaNorm = rcsCard?.media ? normalizeRcsTestCardMedia(rcsCard.media) : undefined;
938
- const suggestionsRaw = Array.isArray(rcsCard?.suggestions) ? rcsCard.suggestions : [];
939
- const suggestionsMapped = suggestionsRaw.map((suggestionItem, i) =>
940
- mapRcsSuggestionForTestMeta(suggestionItem, i));
1061
+ // Raw title/description with template tags; SMS fallback uses tagged template fields (pickFirst…).
1062
+ const cardContentForTestMetaApi = rcsCardPayloadList.map((singleRcsCardPayload) => {
1063
+ const normalizedCardMediaForTestApi = singleRcsCardPayload?.media
1064
+ ? normalizeRcsTestCardMedia(singleRcsCardPayload.media)
1065
+ : undefined;
1066
+ const suggestionsFromCard = Array.isArray(singleRcsCardPayload?.suggestions)
1067
+ ? singleRcsCardPayload.suggestions
1068
+ : [];
1069
+ const suggestionsFormattedForTestMeta = suggestionsFromCard.map((suggestionItem, index) =>
1070
+ mapRcsSuggestionForTestMeta(suggestionItem, index));
941
1071
  return {
942
- title: rcsCard?.title ?? '',
943
- description: rcsCard?.description ?? '',
944
- mediaType: rcsCard?.mediaType ?? MEDIA_TYPE_TEXT,
945
- ...(mediaNorm && { media: mediaNorm }),
946
- ...(Object.keys(mergedCardVarMapped).length > 0 && { cardVarMapped: mergedCardVarMapped }),
947
- ...(suggestionsMapped.length > 0 && { suggestions: suggestionsMapped }),
1072
+ title: singleRcsCardPayload?.title ?? '',
1073
+ description: singleRcsCardPayload?.description ?? '',
1074
+ mediaType: singleRcsCardPayload?.mediaType ?? MEDIA_TYPE_TEXT,
1075
+ ...(normalizedCardMediaForTestApi && { media: normalizedCardMediaForTestApi }),
1076
+ ...(suggestionsFormattedForTestMeta.length > 0 && {
1077
+ suggestions: suggestionsFormattedForTestMeta,
1078
+ }),
948
1079
  };
949
1080
  });
950
- // Prefer parent `smsFallbackContent` snapshot (rcsExtra) — includes VarSegment-resolved body via
951
- // getSmsFallbackTextForTagExtraction. Nested formData.smsFallBackContent is often stale vs live editor.
952
- const rcsExtraFallbackTemplate = rcsExtra?.smsFallbackTemplateContent;
953
- const hasResolvedFallbackBodyFromRcsExtra =
954
- rcsExtraFallbackTemplate != null
955
- && String(rcsExtraFallbackTemplate).trim() !== '';
956
- const smsMessageRaw = hasResolvedFallbackBodyFromRcsExtra
957
- ? String(rcsExtraFallbackTemplate)
958
- : (smsFallback?.smsContent ?? smsFallback?.message ?? '');
1081
+ const smsFallbackTaggedTemplateBody = pickFirstSmsFallbackTemplateString(
1082
+ smsFallbackFromCreativeForm,
1083
+ ) || '';
959
1084
  const smsSenderFromDelivery = deliverySettingsOverride?.cdmaSenderId?.includes('|')
960
1085
  ? deliverySettingsOverride.cdmaSenderId.split('|')[1]
961
1086
  : deliverySettingsOverride?.cdmaSenderId;
962
1087
  const deliveryFallbackSmsId =
963
1088
  typeof smsSenderFromDelivery === 'string' ? smsSenderFromDelivery.trim() : '';
964
1089
  const creativeFallbackSmsId =
965
- smsFallback?.senderId != null ? String(smsFallback.senderId).trim() : '';
1090
+ smsFallbackFromCreativeForm?.senderId != null
1091
+ ? String(smsFallbackFromCreativeForm.senderId).trim()
1092
+ : '';
966
1093
  const fallbackSmsSenderIdForChannel = deliveryFallbackSmsId || creativeFallbackSmsId || '';
967
1094
 
968
1095
  const smsFallBackContent =
969
- smsMessageRaw.trim() !== ''
970
- ? { message: smsMessageRaw }
1096
+ smsFallbackTaggedTemplateBody.trim() !== ''
1097
+ ? { message: smsFallbackTaggedTemplateBody }
971
1098
  : undefined;
972
1099
 
973
1100
  // accountId: WeCRM account id (not sourceAccountIdentifier) for createMessageMeta
974
1101
  const accountIdForMeta =
975
- rcsContent?.accountId != null && String(rcsContent.accountId).trim() !== ''
976
- ? String(rcsContent.accountId)
1102
+ rcsContentFromForm?.accountId != null && String(rcsContentFromForm.accountId).trim() !== ''
1103
+ ? String(rcsContentFromForm.accountId)
977
1104
  : undefined;
978
1105
 
979
1106
  const rcsRichCardContent = {
980
1107
  contentType: RCS_TEST_META_CONTENT_TYPE_RICHCARD,
981
- cardType: rcsContent?.cardType ?? RCS_TEST_META_CARD_TYPE_STANDALONE,
982
- cardSettings: rcsContent?.cardSettings ?? {
1108
+ cardType: rcsContentFromForm?.cardType ?? RCS_TEST_META_CARD_TYPE_STANDALONE,
1109
+ cardSettings: rcsContentFromForm?.cardSettings ?? {
983
1110
  cardOrientation: RCS_TEST_META_CARD_ORIENTATION_VERTICAL,
984
1111
  cardWidth: RCS_TEST_META_CARD_WIDTH_SMALL,
985
1112
  },
986
- ...(cardContent.length > 0 && { cardContent }),
1113
+ ...(cardContentForTestMetaApi.length > 0 && { cardContent: cardContentForTestMetaApi }),
987
1114
  };
988
1115
 
989
1116
  const rcsMessageContent = {
@@ -2763,7 +2890,8 @@ const CommonTestAndPreview = (props) => {
2763
2890
  * Handle slidebox close
2764
2891
  */
2765
2892
  const handleClose = () => {
2766
- // Reset state when closing
2893
+ // Reset state when closing (includes add-customer modal + tree search state)
2894
+ handleCloseCustomerModal();
2767
2895
  setSelectedCustomer(null);
2768
2896
  setRequiredTags([]);
2769
2897
  setOptionalTags([]);
@@ -3023,16 +3151,132 @@ const CommonTestAndPreview = (props) => {
3023
3151
  * Handle test entities change
3024
3152
  */
3025
3153
  const handleTestEntitiesChange = (value) => {
3026
- setSelectedTestEntities(value);
3154
+ if (!Array.isArray(value)) {
3155
+ if (value == null || value === '') {
3156
+ setSelectedTestEntities([]);
3157
+ return;
3158
+ }
3159
+ setSelectedTestEntities([normalizeTestEntityId(value)]);
3160
+ return;
3161
+ }
3162
+ setSelectedTestEntities(value.map((v) => normalizeTestEntityId(v)));
3027
3163
  };
3028
3164
 
3165
+ /**
3166
+ * Map API customerDetails item to our customerData shape
3167
+ */
3168
+ const mapCustomerDetailsToCustomerData = (detail, identifierValue) => {
3169
+ const firstName = detail.firstName || '';
3170
+ const lastName = detail.lastName || '';
3171
+ const name = [firstName, lastName].filter(Boolean).join(' ').trim() || '';
3172
+ const getIdentifierValue = (type) => {
3173
+ const fromIdentifiers = detail.identifiers?.find((i) => i.type === type)?.value;
3174
+ if (fromIdentifiers) return fromIdentifiers;
3175
+ const fromCommChannels = detail.commChannels?.find((c) => c.type === type)?.value;
3176
+ return fromCommChannels || (channel === CHANNELS.EMAIL && type === IDENTIFIER_TYPE_EMAIL ? identifierValue : channel === CHANNELS.SMS && type === IDENTIFIER_TYPE_MOBILE ? identifierValue : '');
3177
+ };
3178
+ return {
3179
+ name,
3180
+ email: channel === CHANNELS.EMAIL ? (getIdentifierValue(IDENTIFIER_TYPE_EMAIL) || identifierValue) : (getIdentifierValue(IDENTIFIER_TYPE_EMAIL) || ''),
3181
+ mobile: channel === CHANNELS.SMS ? (getIdentifierValue(IDENTIFIER_TYPE_MOBILE) || getIdentifierValue(IDENTIFIER_TYPE_PHONE) || identifierValue) : (getIdentifierValue(IDENTIFIER_TYPE_MOBILE) || getIdentifierValue(IDENTIFIER_TYPE_PHONE) || ''),
3182
+ customerId: detail.userId != null ? String(detail.userId) : '',
3183
+ };
3184
+ };
3185
+
3186
+ const handleAddTestCustomer = async () => {
3187
+ const identifierType = channel === CHANNELS.EMAIL ? IDENTIFIER_TYPE_EMAIL : IDENTIFIER_TYPE_MOBILE;
3188
+ const searchValueToCheck = channel === CHANNELS.SMS
3189
+ ? formatPhoneNumber((searchValue || '').trim())
3190
+ : (searchValue || '').trim();
3191
+
3192
+ // Check if this customer is already in the test customers list
3193
+ const existingTestCustomer = testCustomers?.find(customer => {
3194
+ if (channel === CHANNELS.EMAIL) {
3195
+ return customer.email === searchValueToCheck;
3196
+ } else if (channel === CHANNELS.SMS) {
3197
+ return customer.mobile === searchValueToCheck;
3198
+ }
3199
+ return false;
3200
+ });
3201
+
3202
+ if (existingTestCustomer) {
3203
+ const entityId = existingTestCustomer.userId ?? existingTestCustomer.customerId;
3204
+ if (entityId != null) {
3205
+ const id = normalizeTestEntityId(entityId);
3206
+ setSelectedTestEntities((prev) => (
3207
+ prev.some((existing) => testEntityIdsEqual(existing, id)) ? prev : [...prev, id]
3208
+ ));
3209
+ }
3210
+ setSearchValue('');
3211
+ CapNotification.success({
3212
+ message: formatMessage(messages.customerAlreadyInTestList),
3213
+ });
3214
+ return;
3215
+ }
3216
+
3217
+ setIsCustomerDataLoading(true);
3218
+
3219
+ try {
3220
+ const response = await getMembersLookup(identifierType, searchValueToCheck);
3221
+ const success = response?.success && !response?.status?.isError;
3222
+ const res = response?.response || {};
3223
+ const exists = res.exists || false;
3224
+ const details = res.customerDetails || [];
3225
+
3226
+ if (!success) {
3227
+ const errorMessage = response?.message || response?.status?.message || formatMessage(messages.memberLookupError);
3228
+ CapNotification.error({
3229
+ message: formatMessage(messages.memberLookupError),
3230
+ description: errorMessage,
3231
+ });
3232
+ return;
3233
+ }
3234
+
3235
+ if (exists && details.length > 0) {
3236
+ const mapped = mapCustomerDetailsToCustomerData(details[0], searchValueToCheck);
3237
+ const customerIdFromLookup = mapped.customerId;
3238
+ const alreadyInTestListByCustomerId = customerIdFromLookup && testCustomers?.some(
3239
+ (c) => String(c?.customerId) === customerIdFromLookup || String(c?.userId) === customerIdFromLookup
3240
+ );
3241
+ if (alreadyInTestListByCustomerId) {
3242
+ const id = normalizeTestEntityId(customerIdFromLookup);
3243
+ setSelectedTestEntities((prev) => (
3244
+ prev.some((existing) => testEntityIdsEqual(existing, id)) ? prev : [...prev, id]
3245
+ ));
3246
+ setSearchValue('');
3247
+ CapNotification.success({
3248
+ message: formatMessage(messages.customerAlreadyInTestList),
3249
+ });
3250
+ return;
3251
+ }
3252
+ setCustomerData(mapped);
3253
+ setCustomerModal([true, CUSTOMER_MODAL_EXISTING]);
3254
+ } else {
3255
+ setCustomerData({
3256
+ name: '',
3257
+ email: channel === CHANNELS.EMAIL ? searchValueToCheck : '',
3258
+ mobile: channel === CHANNELS.SMS ? searchValueToCheck : '',
3259
+ customerId: '',
3260
+ });
3261
+ setCustomerModal([true, CUSTOMER_MODAL_NEW]);
3262
+ }
3263
+ } catch {
3264
+ CapNotification.error({
3265
+ message: formatMessage(messages.memberLookupError),
3266
+ description: formatMessage(messages.memberLookupError),
3267
+ });
3268
+ } finally {
3269
+ setIsCustomerDataLoading(false);
3270
+ }
3271
+ };
3272
+
3029
3273
  /**
3030
3274
  * Handle send test message
3031
3275
  */
3032
3276
  const handleSendTestMessage = () => {
3033
3277
  const allUserIds = [];
3034
3278
  selectedTestEntities.forEach((entityId) => {
3035
- const group = testGroups.find((testGroup) => testGroup.groupId === entityId);
3279
+ const group = testGroups.find((g) => testEntityIdsEqual(g.groupId, entityId));
3036
3280
  if (group) {
3037
3281
  allUserIds.push(...group.userIds);
3038
3282
  } else {
@@ -3055,15 +3299,7 @@ const CommonTestAndPreview = (props) => {
3055
3299
  uniqueUserIds,
3056
3300
  previewData,
3057
3301
  deliveryOverride,
3058
- channel === CHANNELS.RCS
3059
- ? {
3060
- smsFallbackTemplateContent:
3061
- smsFallbackTextForTagExtraction
3062
- || smsFallbackContent?.templateContent
3063
- || smsFallbackContent?.content
3064
- || '',
3065
- }
3066
- : {},
3302
+ {},
3067
3303
  );
3068
3304
 
3069
3305
  actions.createMessageMetaRequested(
@@ -3155,6 +3391,20 @@ const CommonTestAndPreview = (props) => {
3155
3391
  }));
3156
3392
  };
3157
3393
 
3394
+ /** Trim pasted emails (trailing CR/LF). SMS: strip non-digits so pasted formatted numbers match isValidMobile / API. */
3395
+ const handleTestCustomersSearch = useCallback((value) => {
3396
+ if (value == null || value === '') {
3397
+ setSearchValue('');
3398
+ return;
3399
+ }
3400
+ const raw = String(value).trim();
3401
+ if (channel === CHANNELS.SMS) {
3402
+ setSearchValue(formatPhoneNumber(raw));
3403
+ } else {
3404
+ setSearchValue(raw);
3405
+ }
3406
+ }, [channel]);
3407
+
3158
3408
  const renderSendTestMessage = () => (
3159
3409
  <SendTestMessage
3160
3410
  isFetchingTestCustomers={isFetchingTestCustomers}
@@ -3167,6 +3417,7 @@ const CommonTestAndPreview = (props) => {
3167
3417
  content={getCurrentContent}
3168
3418
  channel={channel}
3169
3419
  isSendingTestMessage={isSendingTestMessage}
3420
+ renderAddTestCustomerButton={renderAddTestCustomerButton}
3170
3421
  formatMessage={formatMessage}
3171
3422
  deliverySettings={testPreviewDeliverySettings[channel]}
3172
3423
  senderDetailsByChannel={senderDetailsByChannel}
@@ -3176,6 +3427,8 @@ const CommonTestAndPreview = (props) => {
3176
3427
  smsTraiDltEnabled={smsTraiDltEnabled}
3177
3428
  registeredSenderIds={registeredSenderIds}
3178
3429
  isChannelSmsFallbackPreviewEnabled={channel === CHANNELS.RCS && !!smsFallbackContent?.templateContent}
3430
+ searchValue={searchValue}
3431
+ setSearchValue={handleTestCustomersSearch}
3179
3432
  />
3180
3433
  );
3181
3434
 
@@ -3185,6 +3438,21 @@ const CommonTestAndPreview = (props) => {
3185
3438
  />
3186
3439
  );
3187
3440
 
3441
+ const renderAddTestCustomerButton = () => {
3442
+ const raw = (searchValue || '').trim();
3443
+ const value = channel === CHANNELS.SMS ? formatPhoneNumber(raw) : raw;
3444
+ const showAddButton =
3445
+ [CHANNELS.EMAIL, CHANNELS.SMS].includes(channel) &&
3446
+ (channel === CHANNELS.EMAIL ? isValidEmail(value) : isValidMobile(value));
3447
+ if (!showAddButton) return null;
3448
+ return (
3449
+ <AddTestCustomerButton
3450
+ searchValue={value}
3451
+ handleAddTestCustomer={handleAddTestCustomer}
3452
+ />
3453
+ );
3454
+ };
3455
+
3188
3456
  // Header content for the slidebox
3189
3457
  const slideboxHeader = (
3190
3458
  <CapRow className="test-preview-header">
@@ -3204,14 +3472,18 @@ const CommonTestAndPreview = (props) => {
3204
3472
  show={show}
3205
3473
  size="size-xl"
3206
3474
  content={(
3207
- <CapRow className="test-preview-container">
3208
- <CapRow className="test-and-preview-panels">
3209
- {/* Left Panel */}
3210
- <CapRow className="left-panel">
3211
- {channel === CHANNELS.ZALO ? null : renderLeftPanelContent()}
3212
- <CapDivider className="panel-divider" />
3213
-
3214
- {/* Send Test Message Section */}
3475
+ <CapSpin
3476
+ spinning={isCustomerDataLoading}
3477
+ className={`common-test-preview-lookup-spin ${isCustomerDataLoading ? 'common-test-preview-customer-loading' : ''}`}
3478
+ >
3479
+ <CapRow className="test-preview-container">
3480
+ <CapRow className="test-and-preview-panels">
3481
+ {/* Left Panel */}
3482
+ <CapRow className="left-panel">
3483
+ {channel === CHANNELS.ZALO ? null : renderLeftPanelContent()}
3484
+ <CapDivider className="panel-divider" />
3485
+
3486
+ {/* Send Test Message Section */}
3215
3487
  {config.enableTestMessage !== false && (
3216
3488
  <CapRow className="panel-section send-test-section">
3217
3489
  {renderSendTestMessage()}
@@ -3225,7 +3497,27 @@ const CommonTestAndPreview = (props) => {
3225
3497
  {renderPreview()}
3226
3498
  </CapRow>
3227
3499
  </CapRow>
3228
- </CapRow>
3500
+ {customerModal[0] && customerModal[1] === CUSTOMER_MODAL_EXISTING && (
3501
+ <ExistingCustomerModal
3502
+ customerData={customerData}
3503
+ onCloseCustomerModal={handleCloseCustomerModal}
3504
+ customerModal={customerModal}
3505
+ channel={channel}
3506
+ onSave={handleSaveTestCustomer}
3507
+ />
3508
+ )}
3509
+ {customerModal[0] && customerModal[1] === CUSTOMER_MODAL_NEW && (
3510
+ <CustomerCreationModal
3511
+ customerData={customerData}
3512
+ setCustomerData={setCustomerData}
3513
+ onCloseCustomerModal={handleCloseCustomerModal}
3514
+ customerModal={customerModal}
3515
+ onSave={handleSaveTestCustomer}
3516
+ channel={channel}
3517
+ />
3518
+ )}
3519
+ </CapRow>
3520
+ </CapSpin>
3229
3521
  )}
3230
3522
  />
3231
3523
  );
@@ -3327,4 +3619,4 @@ CommonTestAndPreview.defaultProps = {
3327
3619
  // Note: Redux connection is handled by the wrapper components (e.g., TestAndPreviewSlidebox)
3328
3620
  // This component receives all Redux props (including intl) from its parent
3329
3621
 
3330
- export default CommonTestAndPreview;
3622
+ export default CommonTestAndPreview;