diff --git a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignForm.vue b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignForm.vue index ab059e11c..0babd11ec 100644 --- a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignForm.vue +++ b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignForm.vue @@ -9,7 +9,7 @@ import Input from 'dashboard/components-next/input/Input.vue'; import Button from 'dashboard/components-next/button/Button.vue'; import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue'; import TagMultiSelectComboBox from 'dashboard/components-next/combobox/TagMultiSelectComboBox.vue'; -import WhatsAppTemplateParserCore from 'dashboard/components-next/shared/WhatsAppTemplateParserCore.vue'; +import WhatsAppTemplateParser from 'dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue'; const emit = defineEmits(['submit', 'cancel']); @@ -128,7 +128,7 @@ const prepareCampaignDetails = () => { const parserData = templateParserRef.value; // Extract template content - this should be the template message body - const templateContent = parserData?.processedString || ''; + const templateContent = parserData?.renderedTemplate || ''; // Prepare template_params object with the same structure as used in contacts const templateParams = { @@ -214,7 +214,7 @@ watch( - -import WhatsAppTemplateParserCore from 'dashboard/components-next/shared/WhatsAppTemplateParserCore.vue'; +import WhatsAppTemplateParser from 'dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue'; import Button from 'dashboard/components-next/button/Button.vue'; import { useI18n } from 'vue-i18n'; @@ -28,7 +28,7 @@ const handleBack = () => { class="absolute top-full mt-1.5 max-h-[30rem] overflow-y-auto left-0 flex flex-col gap-4 px-4 pt-6 pb-5 items-start w-[28.75rem] h-auto bg-n-solid-2 border border-n-strong shadow-sm rounded-lg" >
- { />
-
+ diff --git a/app/javascript/dashboard/components-next/shared/WhatsAppTemplateParserCore.vue b/app/javascript/dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue similarity index 56% rename from app/javascript/dashboard/components-next/shared/WhatsAppTemplateParserCore.vue rename to app/javascript/dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue index acc29ace3..a49a8df6e 100644 --- a/app/javascript/dashboard/components-next/shared/WhatsAppTemplateParserCore.vue +++ b/app/javascript/dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue @@ -5,6 +5,12 @@ import { requiredIf } from '@vuelidate/validators'; import { useI18n } from 'vue-i18n'; import Input from 'dashboard/components-next/input/Input.vue'; +import { + buildTemplateParameters, + allKeysRequired, + replaceTemplateVariables, + populateAuthenticationButtonParameters, +} from 'dashboard/helper/templateHelper'; const props = defineProps({ template: { @@ -17,20 +23,14 @@ const emit = defineEmits(['sendMessage', 'resetTemplate', 'back']); const { t } = useI18n(); -const processVariable = str => { - return str.replace(/{{|}}/g, ''); -}; - -const allKeysRequired = value => { - const keys = Object.keys(value); - return keys.every(key => value[key]); -}; - const processedParams = ref({}); -const templateString = computed(() => { - return props.template.components.find(component => component.type === 'BODY') - .text; +const languageLabel = computed(() => { + return `${t('WHATSAPP_TEMPLATES.PARSER.LANGUAGE')}: ${props.template.language || 'en'}`; +}); + +const categoryLabel = computed(() => { + return `${t('WHATSAPP_TEMPLATES.PARSER.CATEGORY')}: ${props.template.category || 'UTILITY'}`; }); const headerComponent = computed(() => { @@ -39,6 +39,14 @@ const headerComponent = computed(() => { ); }); +const bodyComponent = computed(() => { + return props.template.components.find(component => component.type === 'BODY'); +}); + +const bodyText = computed(() => { + return bodyComponent.value.text; +}); + const hasMediaHeader = computed(() => { return ( headerComponent.value && @@ -47,109 +55,30 @@ const hasMediaHeader = computed(() => { ); }); -const variables = computed(() => { - return templateString.value.match(/{{([^}]+)}}/g); +const hasVariables = computed(() => { + return bodyText.value.match(/{{([^}]+)}}/g); }); -const processedString = computed(() => { - return templateString.value.replace(/{{([^}]+)}}/g, (match, variable) => { - const variableKey = processVariable(variable); - return processedParams.value.body?.[variableKey] || `{{${variable}}}`; - }); +const renderedTemplate = computed(() => { + return replaceTemplateVariables(bodyText.value, processedParams.value); }); const v$ = useVuelidate( { processedParams: { - requiredIfKeysPresent: requiredIf(variables), + requiredIfKeysPresent: requiredIf(hasVariables), allKeysRequired, }, }, { processedParams } ); -const generateVariables = () => { - const allVariables = {}; - - // Process body variables - const matchedVariables = templateString.value.match(/{{([^}]+)}}/g); - if (matchedVariables) { - allVariables.body = {}; - matchedVariables.forEach(variable => { - const key = processVariable(variable); - // Special handling for authentication templates - if (props.template?.category === 'AUTHENTICATION') { - if ( - key === '1' || - key.toLowerCase().includes('otp') || - key.toLowerCase().includes('code') - ) { - allVariables.body.otp_code = ''; - } else if ( - key === '2' || - key.toLowerCase().includes('expiry') || - key.toLowerCase().includes('minute') - ) { - allVariables.body.expiry_minutes = ''; - } else { - allVariables.body[key] = ''; - } - } else { - allVariables.body[key] = ''; - } - }); - } - - // Add media URL field if template has media header - if (hasMediaHeader.value) { - if (!allVariables.header) allVariables.header = {}; - allVariables.header.media_url = ''; - allVariables.header.media_type = headerComponent.value.format.toLowerCase(); - } - - // Process button variables - const buttonComponents = props.template.components.filter( - component => component.type === 'BUTTONS' +const initializeTemplateParameters = () => { + const templateParameters = buildTemplateParameters( + props.template, + hasMediaHeader.value ); - - buttonComponents.forEach(buttonComponent => { - if (buttonComponent.buttons) { - buttonComponent.buttons.forEach((button, index) => { - // Skip button parameter inputs for authentication templates - // as they are auto-populated with OTP codes - if (props.template?.category !== 'AUTHENTICATION') { - // Handle URL buttons with variables - if ( - button.type === 'URL' && - button.url && - button.url.includes('{{') - ) { - const buttonVars = button.url.match(/{{([^}]+)}}/g) || []; - if (buttonVars.length > 0) { - if (!allVariables.buttons) allVariables.buttons = []; - allVariables.buttons[index] = { - type: 'url', - parameter: '', - url: button.url, - variables: buttonVars.map(v => processVariable(v)), - }; - } - } - - // Handle copy code buttons - if (button.type === 'COPY_CODE') { - if (!allVariables.buttons) allVariables.buttons = []; - allVariables.buttons[index] = { - type: 'copy_code', - parameter: '', - }; - } - } - }); - } - }); - - processedParams.value = allVariables; + processedParams.value = templateParameters; }; const updateMediaUrl = value => { @@ -163,23 +92,13 @@ const sendMessage = () => { v$.value.$touch(); if (v$.value.$invalid) return; - // Auto-populate button parameters for authentication templates - const finalParams = { ...processedParams.value }; - if (props.template?.category === 'AUTHENTICATION' && finalParams.buttons) { - finalParams.buttons.forEach((button, index) => { - if (button.type === 'url') { - // For authentication templates, auto-populate URL button parameter with OTP - if (finalParams.body?.['1']) { - finalParams.buttons[index].parameter = finalParams.body['1']; - } else if (finalParams.body?.otp_code) { - finalParams.buttons[index].parameter = finalParams.body.otp_code; - } - } - }); - } + const finalParams = populateAuthenticationButtonParameters( + props.template, + processedParams.value + ); const payload = { - message: processedString.value, + message: renderedTemplate.value, templateParams: { name: props.template.name, category: props.template.category, @@ -199,14 +118,14 @@ const goBack = () => { emit('back'); }; -onMounted(generateVariables); +onMounted(initializeTemplateParameters); defineExpose({ processedParams, - variables, + hasVariables, hasMediaHeader, headerComponent, - processedString, + renderedTemplate, v$, updateMediaUrl, sendMessage, @@ -217,35 +136,30 @@ defineExpose({ diff --git a/app/javascript/dashboard/helper/specs/templateHelper.spec.js b/app/javascript/dashboard/helper/specs/templateHelper.spec.js new file mode 100644 index 000000000..16b669e15 --- /dev/null +++ b/app/javascript/dashboard/helper/specs/templateHelper.spec.js @@ -0,0 +1,308 @@ +import { + replaceTemplateVariables, + buildTemplateParameters, + processVariable, + allKeysRequired, + populateAuthenticationButtonParameters, +} from '../templateHelper'; +import { templates } from '../../store/modules/specs/inboxes/templateFixtures'; + +describe('templateHelper', () => { + const technicianTemplate = templates.find(t => t.name === 'technician_visit'); + const otpTemplate = templates.find(t => t.name === 'basic_otp'); + + describe('processVariable', () => { + it('should remove curly braces from variables', () => { + expect(processVariable('{{name}}')).toBe('name'); + expect(processVariable('{{1}}')).toBe('1'); + expect(processVariable('{{customer_id}}')).toBe('customer_id'); + }); + }); + + describe('allKeysRequired', () => { + it('should return true when all keys have values', () => { + const obj = { name: 'John', age: '30' }; + expect(allKeysRequired(obj)).toBe(true); + }); + + it('should return false when some keys are empty', () => { + const obj = { name: 'John', age: '' }; + expect(allKeysRequired(obj)).toBe(false); + }); + + it('should return true for empty object', () => { + expect(allKeysRequired({})).toBe(true); + }); + }); + + describe('replaceTemplateVariables', () => { + const templateText = + "Hi {{1}}, we're scheduling a technician visit to {{2}} on {{3}} between {{4}} and {{5}}. Please confirm if this time slot works for you."; + + it('should replace all variables with provided values', () => { + const processedParams = { + body: { + 1: 'John', + 2: '123 Main St', + 3: '2025-01-15', + 4: '10:00 AM', + 5: '2:00 PM', + }, + }; + + const result = replaceTemplateVariables(templateText, processedParams); + expect(result).toBe( + "Hi John, we're scheduling a technician visit to 123 Main St on 2025-01-15 between 10:00 AM and 2:00 PM. Please confirm if this time slot works for you." + ); + }); + + it('should keep original variable format when no replacement value provided', () => { + const processedParams = { + body: { + 1: 'John', + 3: '2025-01-15', + }, + }; + + const result = replaceTemplateVariables(templateText, processedParams); + expect(result).toContain('John'); + expect(result).toContain('2025-01-15'); + expect(result).toContain('{{2}}'); + expect(result).toContain('{{4}}'); + expect(result).toContain('{{5}}'); + }); + + it('should handle empty processedParams', () => { + const result = replaceTemplateVariables(templateText, {}); + expect(result).toBe(templateText); + }); + }); + + describe('buildTemplateParameters', () => { + it('should build parameters for template with body variables', () => { + const result = buildTemplateParameters(technicianTemplate, false); + + expect(result.body).toEqual({ + 1: '', + 2: '', + 3: '', + 4: '', + 5: '', + }); + }); + + it('should include header parameters when hasMediaHeader is true', () => { + const imageTemplate = templates.find( + t => t.name === 'order_confirmation' + ); + const result = buildTemplateParameters(imageTemplate, true); + + expect(result.header).toEqual({ + media_url: '', + media_type: 'image', + }); + }); + + it('should not include header parameters when hasMediaHeader is false', () => { + const result = buildTemplateParameters(technicianTemplate, false); + expect(result.header).toBeUndefined(); + }); + + it('should handle template with no body component', () => { + const templateWithoutBody = { + components: [{ type: 'HEADER', format: 'TEXT' }], + }; + + const result = buildTemplateParameters(templateWithoutBody, false); + expect(result).toEqual({}); + }); + + it('should handle template with no variables', () => { + const templateWithoutVars = templates.find( + t => t.name === 'no_variable_template' + ); + const result = buildTemplateParameters(templateWithoutVars, false); + + expect(result.body).toBeUndefined(); + }); + + it('should handle AUTHENTICATION category templates with special OTP handling', () => { + const result = buildTemplateParameters(otpTemplate, false); + expect(result.body).toEqual({ + otp_code: '', + }); + }); + + it('should handle URL buttons with variables for non-authentication templates', () => { + const templateWithUrlButton = { + category: 'MARKETING', + components: [ + { + type: 'BODY', + text: 'Check out our website at {{site_url}}', + }, + { + type: 'BUTTONS', + buttons: [ + { + type: 'URL', + url: 'https://example.com/{{campaign_id}}', + text: 'Visit Site', + }, + ], + }, + ], + }; + + const result = buildTemplateParameters(templateWithUrlButton, false); + expect(result.buttons).toEqual([ + { + type: 'url', + parameter: '', + url: 'https://example.com/{{campaign_id}}', + variables: ['campaign_id'], + }, + ]); + }); + + it('should skip button variables for AUTHENTICATION templates', () => { + const result = buildTemplateParameters(otpTemplate, false); + expect(result.buttons).toBeUndefined(); + }); + }); + + describe('populateAuthenticationButtonParameters', () => { + it('should auto-populate URL button parameters with OTP code for authentication templates', () => { + const processedParams = { + body: { + otp_code: '123456', + }, + buttons: [ + { + type: 'url', + parameter: '', + url: 'https://www.whatsapp.com/otp/code/?code=otp{{1}}', + }, + ], + }; + + const result = populateAuthenticationButtonParameters( + otpTemplate, + processedParams + ); + + expect(result.buttons[0].parameter).toBe('123456'); + }); + + it('should use positional parameter "1" if available for authentication templates', () => { + const processedParams = { + body: { + 1: '654321', + otp_code: '123456', + }, + buttons: [ + { + type: 'url', + parameter: '', + url: 'https://www.whatsapp.com/otp/code/?code=otp{{1}}', + }, + ], + }; + + const result = populateAuthenticationButtonParameters( + otpTemplate, + processedParams + ); + + expect(result.buttons[0].parameter).toBe('654321'); + }); + + it('should not modify non-authentication templates', () => { + const processedParams = { + body: { + name: 'John', + }, + buttons: [ + { + type: 'url', + parameter: '', + url: 'https://example.com/{{name}}', + }, + ], + }; + + const result = populateAuthenticationButtonParameters( + technicianTemplate, + processedParams + ); + + expect(result.buttons[0].parameter).toBe(''); + }); + + it('should not modify non-URL buttons in authentication templates', () => { + const authTemplateWithQuickReply = { + category: 'AUTHENTICATION', + components: [ + { + type: 'BODY', + text: 'Your code is {{1}}', + }, + ], + }; + + const processedParams = { + body: { + otp_code: '123456', + }, + buttons: [ + { + type: 'quick_reply', + parameter: 'original_value', + }, + ], + }; + + const result = populateAuthenticationButtonParameters( + authTemplateWithQuickReply, + processedParams + ); + + expect(result.buttons[0].parameter).toBe('original_value'); + }); + + it('should handle templates without buttons', () => { + const processedParams = { + body: { + otp_code: '123456', + }, + }; + + const result = populateAuthenticationButtonParameters( + otpTemplate, + processedParams + ); + + expect(result).toEqual(processedParams); + }); + + it('should not mutate the original processedParams object', () => { + const processedParams = { + body: { + otp_code: '123456', + }, + buttons: [ + { + type: 'url', + parameter: '', + url: 'https://www.whatsapp.com/otp/code/?code=otp{{1}}', + }, + ], + }; + + const originalParams = JSON.parse(JSON.stringify(processedParams)); + populateAuthenticationButtonParameters(otpTemplate, processedParams); + + expect(processedParams).toEqual(originalParams); + }); + }); +}); diff --git a/app/javascript/dashboard/helper/templateHelper.js b/app/javascript/dashboard/helper/templateHelper.js new file mode 100644 index 000000000..ce05bd783 --- /dev/null +++ b/app/javascript/dashboard/helper/templateHelper.js @@ -0,0 +1,134 @@ +export const processVariable = str => { + return str.replace(/{{|}}/g, ''); +}; + +export const allKeysRequired = value => { + const keys = Object.keys(value); + return keys.every(key => value[key]); +}; + +export const replaceTemplateVariables = (templateText, processedParams) => { + return templateText.replace(/{{([^}]+)}}/g, (match, variable) => { + const variableKey = processVariable(variable); + return processedParams.body?.[variableKey] || `{{${variable}}}`; + }); +}; + +export const buildTemplateParameters = (template, hasMediaHeaderValue) => { + const allVariables = {}; + + const bodyComponent = template.components.find( + component => component.type === 'BODY' + ); + const headerComponent = template.components.find( + component => component.type === 'HEADER' + ); + + if (!bodyComponent) return allVariables; + + const templateString = bodyComponent.text; + + // Process body variables + const matchedVariables = templateString.match(/{{([^}]+)}}/g); + if (matchedVariables) { + allVariables.body = {}; + matchedVariables.forEach(variable => { + const key = processVariable(variable); + // Special handling for authentication templates + if (template?.category === 'AUTHENTICATION') { + if ( + key === '1' || + key.toLowerCase().includes('otp') || + key.toLowerCase().includes('code') + ) { + allVariables.body.otp_code = ''; + } else if ( + key === '2' || + key.toLowerCase().includes('expiry') || + key.toLowerCase().includes('minute') + ) { + allVariables.body.expiry_minutes = ''; + } else { + allVariables.body[key] = ''; + } + } else { + allVariables.body[key] = ''; + } + }); + } + + if (hasMediaHeaderValue) { + if (!allVariables.header) allVariables.header = {}; + allVariables.header.media_url = ''; + allVariables.header.media_type = headerComponent.format.toLowerCase(); + } + + // Process button variables + const buttonComponents = template.components.filter( + component => component.type === 'BUTTONS' + ); + + buttonComponents.forEach(buttonComponent => { + if (buttonComponent.buttons) { + buttonComponent.buttons.forEach((button, index) => { + // Skip button parameter inputs for authentication templates + // as they are auto-populated with OTP codes + if (template?.category !== 'AUTHENTICATION') { + // Handle URL buttons with variables + if ( + button.type === 'URL' && + button.url && + button.url.includes('{{') + ) { + const buttonVars = button.url.match(/{{([^}]+)}}/g) || []; + if (buttonVars.length > 0) { + if (!allVariables.buttons) allVariables.buttons = []; + allVariables.buttons[index] = { + type: 'url', + parameter: '', + url: button.url, + variables: buttonVars.map(v => processVariable(v)), + }; + } + } + + // Handle copy code buttons + if (button.type === 'COPY_CODE') { + if (!allVariables.buttons) allVariables.buttons = []; + allVariables.buttons[index] = { + type: 'copy_code', + parameter: '', + }; + } + } + }); + } + }); + + return allVariables; +}; + +export const populateAuthenticationButtonParameters = ( + template, + processedParams +) => { + const finalParams = { ...processedParams }; + + if (template?.category === 'AUTHENTICATION' && finalParams.buttons) { + // Deep copy the buttons array to avoid mutating the original + finalParams.buttons = finalParams.buttons.map(button => ({ ...button })); + + finalParams.buttons.forEach((button, index) => { + if (button.type === 'url') { + // For authentication templates, auto-populate URL button parameter with OTP + if (finalParams.body?.['1']) { + finalParams.buttons[index].parameter = finalParams.body['1']; + } else if (finalParams.body?.otp_code) { + finalParams.buttons[index].parameter = finalParams.body.otp_code; + } + } + }); + } + + return finalParams; +}; diff --git a/app/javascript/dashboard/store/modules/specs/inboxes/getters.spec.js b/app/javascript/dashboard/store/modules/specs/inboxes/getters.spec.js index c834a4f85..eeb52b1dc 100644 --- a/app/javascript/dashboard/store/modules/specs/inboxes/getters.spec.js +++ b/app/javascript/dashboard/store/modules/specs/inboxes/getters.spec.js @@ -1,6 +1,6 @@ import { getters } from '../../inboxes'; import inboxList from './fixtures'; -import { templates } from '../../../../../shared/mixins/specs/whatsappTemplates/fixtures'; +import { templates } from './templateFixtures'; describe('#getters', () => { it('getInboxes', () => { diff --git a/app/javascript/dashboard/store/modules/specs/inboxes/templateFixtures.js b/app/javascript/dashboard/store/modules/specs/inboxes/templateFixtures.js new file mode 100644 index 000000000..0cb1e8cc9 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/inboxes/templateFixtures.js @@ -0,0 +1,620 @@ +export const templates = [ + { + name: 'sample_flight_confirmation', + status: 'approved', + category: 'TICKET_UPDATE', + language: 'pt_BR', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { type: 'HEADER', format: 'DOCUMENT' }, + { + text: 'Esta é a sua confirmação de voo para {{1}}-{{2}} em {{3}}.', + type: 'BODY', + }, + { + text: 'Esta mensagem é de uma empresa não verificada.', + type: 'FOOTER', + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'sample_issue_resolution', + status: 'approved', + category: 'ISSUE_RESOLUTION', + language: 'pt_BR', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + text: 'Oi, {{1}}. Nós conseguimos resolver o problema que você estava enfrentando?', + type: 'BODY', + }, + { + text: 'Esta mensagem é de uma empresa não verificada.', + type: 'FOOTER', + }, + { + type: 'BUTTONS', + buttons: [ + { text: 'Sim', type: 'QUICK_REPLY' }, + { text: 'Não', type: 'QUICK_REPLY' }, + ], + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'sample_issue_resolution', + status: 'approved', + category: 'ISSUE_RESOLUTION', + language: 'es', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + text: 'Hola, {{1}}. ¿Pudiste solucionar el problema que tenías?', + type: 'BODY', + }, + { + text: 'Este mensaje proviene de un negocio no verificado.', + type: 'FOOTER', + }, + { + type: 'BUTTONS', + buttons: [ + { text: 'Sí', type: 'QUICK_REPLY' }, + { text: 'No', type: 'QUICK_REPLY' }, + ], + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'sample_issue_resolution', + status: 'approved', + category: 'ISSUE_RESOLUTION', + language: 'id', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + text: 'Halo {{1}}, apakah kami bisa mengatasi masalah yang sedang Anda hadapi?', + type: 'BODY', + }, + { + text: 'Pesan ini berasal dari bisnis yang tidak terverifikasi.', + type: 'FOOTER', + }, + { + type: 'BUTTONS', + buttons: [ + { text: 'Ya', type: 'QUICK_REPLY' }, + { text: 'Tidak', type: 'QUICK_REPLY' }, + ], + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'sample_shipping_confirmation', + status: 'approved', + category: 'SHIPPING_UPDATE', + language: 'pt_BR', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + text: 'Seu pacote foi enviado. Ele será entregue em {{1}} dias úteis.', + type: 'BODY', + }, + { + text: 'Esta mensagem é de uma empresa não verificada.', + type: 'FOOTER', + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'sample_shipping_confirmation', + status: 'approved', + category: 'SHIPPING_UPDATE', + language: 'id', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + text: 'Paket Anda sudah dikirim. Paket akan sampai dalam {{1}} hari kerja.', + type: 'BODY', + }, + { + text: 'Pesan ini berasal dari bisnis yang tidak terverifikasi.', + type: 'FOOTER', + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'sample_shipping_confirmation', + status: 'approved', + category: 'SHIPPING_UPDATE', + language: 'es', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + text: 'ó tu paquete. La entrega se realizará en {{1}} dí.', + type: 'BODY', + }, + { + text: 'Este mensaje proviene de un negocio no verificado.', + type: 'FOOTER', + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'sample_flight_confirmation', + status: 'approved', + category: 'TICKET_UPDATE', + language: 'id', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { type: 'HEADER', format: 'DOCUMENT' }, + { + text: 'Ini merupakan konfirmasi penerbangan Anda untuk {{1}}-{{2}} di {{3}}.', + type: 'BODY', + }, + { + text: 'Pesan ini berasal dari bisnis yang tidak terverifikasi.', + type: 'FOOTER', + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'sample_issue_resolution', + status: 'approved', + category: 'ISSUE_RESOLUTION', + language: 'en_US', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + text: 'Hi {{1}}, were we able to solve the issue that you were facing?', + type: 'BODY', + }, + { text: 'This message is from an unverified business.', type: 'FOOTER' }, + { + type: 'BUTTONS', + buttons: [ + { text: 'Yes', type: 'QUICK_REPLY' }, + { text: 'No', type: 'QUICK_REPLY' }, + ], + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'sample_flight_confirmation', + status: 'approved', + category: 'TICKET_UPDATE', + language: 'es', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { type: 'HEADER', format: 'DOCUMENT' }, + { + text: 'Confirmamos tu vuelo a {{1}}-{{2}} para el {{3}}.', + type: 'BODY', + }, + { + text: 'Este mensaje proviene de un negocio no verificado.', + type: 'FOOTER', + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'sample_flight_confirmation', + status: 'approved', + category: 'TICKET_UPDATE', + language: 'en_US', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { type: 'HEADER', format: 'DOCUMENT' }, + { + text: 'This is your flight confirmation for {{1}}-{{2}} on {{3}}.', + type: 'BODY', + }, + { text: 'This message is from an unverified business.', type: 'FOOTER' }, + ], + rejected_reason: 'NONE', + }, + { + name: 'sample_shipping_confirmation', + status: 'approved', + category: 'SHIPPING_UPDATE', + language: 'en_US', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + text: 'Your package has been shipped. It will be delivered in {{1}} business days.', + type: 'BODY', + }, + { text: 'This message is from an unverified business.', type: 'FOOTER' }, + ], + rejected_reason: 'NONE', + }, + { + name: 'no_variable_template', + status: 'approved', + category: 'TICKET_UPDATE', + language: 'pt_BR', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + type: 'HEADER', + format: 'DOCUMENT', + }, + { + text: 'This is a test whatsapp template', + type: 'BODY', + }, + { + text: 'Esta mensagem é de uma empresa não verificada.', + type: 'FOOTER', + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'order_confirmation', + status: 'approved', + category: 'TICKET_UPDATE', + language: 'en_US', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + type: 'HEADER', + format: 'IMAGE', + example: { + header_handle: ['https://example.com/shoes.jpg'], + }, + }, + { + text: 'Hi your order {{1}} is confirmed. Please wait for further updates', + type: 'BODY', + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'technician_visit', + status: 'approved', + category: 'UTILITY', + language: 'en_US', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + text: 'Technician visit', + type: 'HEADER', + format: 'TEXT', + }, + { + text: "Hi {{1}}, we're scheduling a technician visit to {{2}} on {{3}} between {{4}} and {{5}}. Please confirm if this time slot works for you.", + type: 'BODY', + }, + { + type: 'BUTTONS', + buttons: [ + { + text: 'Confirm', + type: 'QUICK_REPLY', + }, + { + text: 'Reschedule', + type: 'QUICK_REPLY', + }, + ], + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'basic_otp', + status: 'approved', + category: 'AUTHENTICATION', + language: 'en', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + text: '*{{1}}* is your verification code. For your security, do not share this code.', + type: 'BODY', + }, + { + type: 'BUTTONS', + buttons: [ + { + url: 'https://www.whatsapp.com/otp/code/?otp_type=COPY_CODE&code=otp{{1}}', + text: 'Copy code', + type: 'URL', + }, + ], + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'event_invitation_static', + status: 'approved', + category: 'MARKETING', + language: 'en', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + text: "You're invited to {{event_name}} at {{location}}, Join us for an amazing experience!", + type: 'BODY', + }, + { + type: 'BUTTONS', + buttons: [ + { + url: 'https://events.example.com/register', + text: 'Visit website', + type: 'URL', + }, + { + url: 'https://maps.app.goo.gl/YoWAzRj1GDuxs6qz8', + text: 'Get Directions', + type: 'URL', + }, + ], + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'purchase_receipt', + status: 'approved', + category: 'UTILITY', + language: 'en_US', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + type: 'HEADER', + format: 'DOCUMENT', + }, + { + text: 'Thank you for using your {{1}} card at {{2}}. Your {{3}} is attached as a PDF.', + type: 'BODY', + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'secure_login_otp', + status: 'approved', + category: 'AUTHENTICATION', + language: 'en', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + text: '*{{1}}* is your verification code. For your security, do not share this code.', + type: 'BODY', + }, + { + text: 'This code expires in 10 minutes.', + type: 'FOOTER', + }, + { + type: 'BUTTONS', + buttons: [ + { + url: 'https://www.whatsapp.com/otp/code/?otp_type=ZERO_TAP&cta_display_name=Autofill&package_name=com.chatwoot.app&signature_hash=12121212121&code_expiration_minutes=10&code=otp{{1}}', + text: 'Copy code', + type: 'URL', + }, + ], + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'discount_coupon', + status: 'approved', + category: 'MARKETING', + language: 'en', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + text: '🎉 Special offer for you! Get {{discount_percentage}}% off your next purchase. Use the code below at checkout', + type: 'BODY', + }, + { + type: 'BUTTONS', + buttons: [ + { + text: 'Copy offer code', + type: 'COPY_CODE', + }, + ], + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'support_callback', + status: 'approved', + category: 'UTILITY', + language: 'en', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + text: 'Hello {{name}}, our support team will call you regarding ticket # {{ticket_id}}.', + type: 'BODY', + }, + { + type: 'BUTTONS', + buttons: [ + { + text: 'Call Support', + type: 'PHONE_NUMBER', + phone_number: '+16506677566', + }, + ], + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'training_video', + status: 'approved', + category: 'MARKETING', + language: 'en', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + type: 'HEADER', + format: 'VIDEO', + }, + { + text: "Hi {{name}}, here's your training video. Please watch by{{date}}.", + type: 'BODY', + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'product_launch', + status: 'approved', + category: 'MARKETING', + language: 'en', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + type: 'HEADER', + format: 'IMAGE', + }, + { + text: 'New arrival! Our stunning coat now available in {{color}} color.', + type: 'BODY', + }, + { + text: 'Free shipping on orders over $100. Limited time offer.', + type: 'FOOTER', + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'greet', + status: 'approved', + category: 'MARKETING', + language: 'en', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + text: 'Hey {{customer_name}} how may I help you?', + type: 'BODY', + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'hello_world', + status: 'approved', + category: 'UTILITY', + language: 'en_US', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + text: 'Hello World', + type: 'HEADER', + format: 'TEXT', + }, + { + text: 'Welcome and congratulations!! This message demonstrates your ability to send a WhatsApp message notification from the Cloud API, hosted by Meta. Thank you for taking the time to test with us.', + type: 'BODY', + }, + { + text: 'WhatsApp Business Platform sample message', + type: 'FOOTER', + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'otp_verification', + status: 'approved', + category: 'AUTHENTICATION', + language: 'en_US', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + text: 'Use code *{{1}}* to verify your transaction of {{2}}.', + type: 'BODY', + }, + { + type: 'BUTTONS', + buttons: [ + { + url: 'https://www.whatsapp.com/otp/code/?otp_type=COPY_CODE&code=otp{{1}}', + text: 'Copy code', + type: 'URL', + }, + ], + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'feedback_request', + status: 'approved', + category: 'MARKETING', + language: 'en', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + text: "Hey {{name}}, how was your experience with Puma? We'd love your feedback!", + type: 'BODY', + }, + { + type: 'BUTTONS', + buttons: [ + { + url: 'https://feedback.example.com/survey', + text: 'Leave Feedback', + type: 'URL', + }, + ], + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'address_update', + status: 'approved', + category: 'UTILITY', + language: 'en_US', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + text: 'Address update', + type: 'HEADER', + format: 'TEXT', + }, + { + text: 'Hi {{1}}, your delivery address has been successfully updated to {{2}}. Contact {{3}} for any inquiries.', + type: 'BODY', + }, + ], + rejected_reason: 'NONE', + }, + { + name: 'delivery_confirmation', + status: 'approved', + category: 'UTILITY', + language: 'en_US', + namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', + components: [ + { + text: '{{1}}, your order was successfully delivered on {{2}}.\n\nThank you for your purchase.\n', + type: 'BODY', + }, + ], + rejected_reason: 'NONE', + }, +]; diff --git a/app/javascript/shared/mixins/specs/whatsappTemplates/fixtures.js b/app/javascript/shared/mixins/specs/whatsappTemplates/fixtures.js deleted file mode 100644 index 5acb42cb4..000000000 --- a/app/javascript/shared/mixins/specs/whatsappTemplates/fixtures.js +++ /dev/null @@ -1,284 +0,0 @@ -export const templates = [ - { - name: 'sample_flight_confirmation', - status: 'approved', - category: 'TICKET_UPDATE', - language: 'pt_BR', - namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', - components: [ - { type: 'HEADER', format: 'DOCUMENT' }, - { - text: 'Esta é a sua confirmação de voo para {{1}}-{{2}} em {{3}}.', - type: 'BODY', - }, - { - text: 'Esta mensagem é de uma empresa não verificada.', - type: 'FOOTER', - }, - ], - rejected_reason: 'NONE', - }, - { - name: 'sample_issue_resolution', - status: 'approved', - category: 'ISSUE_RESOLUTION', - language: 'pt_BR', - namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', - components: [ - { - text: 'Oi, {{1}}. Nós conseguimos resolver o problema que você estava enfrentando?', - type: 'BODY', - }, - { - text: 'Esta mensagem é de uma empresa não verificada.', - type: 'FOOTER', - }, - { - type: 'BUTTONS', - buttons: [ - { text: 'Sim', type: 'QUICK_REPLY' }, - { text: 'Não', type: 'QUICK_REPLY' }, - ], - }, - ], - rejected_reason: 'NONE', - }, - { - name: 'sample_issue_resolution', - status: 'approved', - category: 'ISSUE_RESOLUTION', - language: 'es', - namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', - components: [ - { - text: 'Hola, {{1}}. ¿Pudiste solucionar el problema que tenías?', - type: 'BODY', - }, - { - text: 'Este mensaje proviene de un negocio no verificado.', - type: 'FOOTER', - }, - { - type: 'BUTTONS', - buttons: [ - { text: 'Sí', type: 'QUICK_REPLY' }, - { text: 'No', type: 'QUICK_REPLY' }, - ], - }, - ], - rejected_reason: 'NONE', - }, - { - name: 'sample_issue_resolution', - status: 'approved', - category: 'ISSUE_RESOLUTION', - language: 'id', - namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', - components: [ - { - text: 'Halo {{1}}, apakah kami bisa mengatasi masalah yang sedang Anda hadapi?', - type: 'BODY', - }, - { - text: 'Pesan ini berasal dari bisnis yang tidak terverifikasi.', - type: 'FOOTER', - }, - { - type: 'BUTTONS', - buttons: [ - { text: 'Ya', type: 'QUICK_REPLY' }, - { text: 'Tidak', type: 'QUICK_REPLY' }, - ], - }, - ], - rejected_reason: 'NONE', - }, - { - name: 'sample_shipping_confirmation', - status: 'approved', - category: 'SHIPPING_UPDATE', - language: 'pt_BR', - namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', - components: [ - { - text: 'Seu pacote foi enviado. Ele será entregue em {{1}} dias úteis.', - type: 'BODY', - }, - { - text: 'Esta mensagem é de uma empresa não verificada.', - type: 'FOOTER', - }, - ], - rejected_reason: 'NONE', - }, - { - name: 'sample_shipping_confirmation', - status: 'approved', - category: 'SHIPPING_UPDATE', - language: 'id', - namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', - components: [ - { - text: 'Paket Anda sudah dikirim. Paket akan sampai dalam {{1}} hari kerja.', - type: 'BODY', - }, - { - text: 'Pesan ini berasal dari bisnis yang tidak terverifikasi.', - type: 'FOOTER', - }, - ], - rejected_reason: 'NONE', - }, - { - name: 'sample_shipping_confirmation', - status: 'approved', - category: 'SHIPPING_UPDATE', - language: 'es', - namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', - components: [ - { - text: 'ó tu paquete. La entrega se realizará en {{1}} dí.', - type: 'BODY', - }, - { - text: 'Este mensaje proviene de un negocio no verificado.', - type: 'FOOTER', - }, - ], - rejected_reason: 'NONE', - }, - { - name: 'sample_flight_confirmation', - status: 'approved', - category: 'TICKET_UPDATE', - language: 'id', - namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', - components: [ - { type: 'HEADER', format: 'DOCUMENT' }, - { - text: 'Ini merupakan konfirmasi penerbangan Anda untuk {{1}}-{{2}} di {{3}}.', - type: 'BODY', - }, - { - text: 'Pesan ini berasal dari bisnis yang tidak terverifikasi.', - type: 'FOOTER', - }, - ], - rejected_reason: 'NONE', - }, - { - name: 'sample_issue_resolution', - status: 'approved', - category: 'ISSUE_RESOLUTION', - language: 'en_US', - namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', - components: [ - { - text: 'Hi {{1}}, were we able to solve the issue that you were facing?', - type: 'BODY', - }, - { text: 'This message is from an unverified business.', type: 'FOOTER' }, - { - type: 'BUTTONS', - buttons: [ - { text: 'Yes', type: 'QUICK_REPLY' }, - { text: 'No', type: 'QUICK_REPLY' }, - ], - }, - ], - rejected_reason: 'NONE', - }, - { - name: 'sample_flight_confirmation', - status: 'approved', - category: 'TICKET_UPDATE', - language: 'es', - namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', - components: [ - { type: 'HEADER', format: 'DOCUMENT' }, - { - text: 'Confirmamos tu vuelo a {{1}}-{{2}} para el {{3}}.', - type: 'BODY', - }, - { - text: 'Este mensaje proviene de un negocio no verificado.', - type: 'FOOTER', - }, - ], - rejected_reason: 'NONE', - }, - { - name: 'sample_flight_confirmation', - status: 'approved', - category: 'TICKET_UPDATE', - language: 'en_US', - namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', - components: [ - { type: 'HEADER', format: 'DOCUMENT' }, - { - text: 'This is your flight confirmation for {{1}}-{{2}} on {{3}}.', - type: 'BODY', - }, - { text: 'This message is from an unverified business.', type: 'FOOTER' }, - ], - rejected_reason: 'NONE', - }, - { - name: 'sample_shipping_confirmation', - status: 'approved', - category: 'SHIPPING_UPDATE', - language: 'en_US', - namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', - components: [ - { - text: 'Your package has been shipped. It will be delivered in {{1}} business days.', - type: 'BODY', - }, - { text: 'This message is from an unverified business.', type: 'FOOTER' }, - ], - rejected_reason: 'NONE', - }, - { - name: 'no_variable_template', - status: 'approved', - category: 'TICKET_UPDATE', - language: 'pt_BR', - namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', - components: [ - { - type: 'HEADER', - format: 'DOCUMENT', - }, - { - text: 'This is a test whatsapp template', - type: 'BODY', - }, - { - text: 'Esta mensagem é de uma empresa não verificada.', - type: 'FOOTER', - }, - ], - rejected_reason: 'NONE', - }, - { - name: 'order_confirmation', - status: 'approved', - category: 'TICKET_UPDATE', - language: 'en_US', - namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9', - components: [ - { - type: 'HEADER', - format: 'IMAGE', - example: { - header_handle: ['https://example.com/shoes.jpg'], - }, - }, - { - text: 'Hi your order {{1}} is confirmed. Please wait for further updates', - type: 'BODY', - }, - ], - rejected_reason: 'NONE', - }, -]; diff --git a/app/javascript/shared/mixins/specs/whatsappTemplates/whatsappTemplates.spec.js b/app/javascript/shared/mixins/specs/whatsappTemplates/whatsappTemplates.spec.js deleted file mode 100644 index adad15bf7..000000000 --- a/app/javascript/shared/mixins/specs/whatsappTemplates/whatsappTemplates.spec.js +++ /dev/null @@ -1,61 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import TemplateParser from '../../../../dashboard/components/widgets/conversation/WhatsappTemplates/TemplateParser.vue'; -import { templates } from './fixtures'; -import { nextTick } from 'vue'; - -const config = { - global: { - stubs: { - NextButton: { template: '