chore: add more specs

This commit is contained in:
Muhsin Keloth
2025-07-31 12:55:09 +04:00
parent 6d86c5739c
commit 356946a413
10 changed files with 1115 additions and 484 deletions
@@ -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(
</div>
<!-- Template Parser -->
<WhatsAppTemplateParserCore
<WhatsAppTemplateParser
v-if="selectedTemplate"
ref="templateParserRef"
:template="selectedTemplate"
@@ -1,5 +1,5 @@
<script setup>
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"
>
<div class="w-full">
<WhatsAppTemplateParserCore
<WhatsAppTemplateParser
:template="template"
@send-message="handleSendMessage"
@back="handleBack"
@@ -57,7 +57,7 @@ const handleBack = () => {
/>
</div>
</template>
</WhatsAppTemplateParserCore>
</WhatsAppTemplateParser>
</div>
</div>
</template>
@@ -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({
<template>
<div>
<div
v-if="template"
class="flex flex-col gap-4 p-4 mb-4 rounded-lg bg-n-alpha-black2"
>
<div class="flex flex-col gap-4 p-4 mb-4 rounded-lg bg-n-alpha-black2">
<div class="flex justify-between items-center">
<h3 class="text-sm font-medium text-n-slate-12">
{{ template.name }}
</h3>
<span class="text-xs text-n-slate-11">
{{ t('WHATSAPP_TEMPLATES.PARSER.LANGUAGE') }}:
{{ template.language || 'en' }}
{{ languageLabel }}
</span>
</div>
<div class="flex flex-col gap-2">
<div class="rounded-md bg-n-alpha-black3">
<div class="text-sm whitespace-pre-wrap text-n-slate-12">
{{ processedString }}
{{ renderedTemplate }}
</div>
</div>
</div>
<div class="text-xs text-n-slate-11">
{{ t('WHATSAPP_TEMPLATES.PARSER.CATEGORY') }}:
{{ template.category || 'UTILITY' }}
{{ categoryLabel }}
</div>
</div>
<div v-if="variables || hasMediaHeader">
<div v-if="hasVariables || hasMediaHeader">
<div v-if="hasMediaHeader" class="mb-4">
<p class="mb-2.5 text-sm font-semibold">
{{
@@ -8,7 +8,7 @@
* 4. Replaces placeholders with user-provided values.
* 5. Emits events to send the processed message or reset the template.
*/
import WhatsAppTemplateParserCore from 'dashboard/components-next/shared/WhatsAppTemplateParserCore.vue';
import WhatsAppTemplateParser from 'dashboard/components-next/whatsapp/WhatsAppTemplateParser.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
defineProps({
@@ -31,7 +31,7 @@ const handleResetTemplate = () => {
<template>
<div class="w-full">
<WhatsAppTemplateParserCore
<WhatsAppTemplateParser
:template="template"
@send-message="handleSendMessage"
@reset-template="handleResetTemplate"
@@ -52,6 +52,6 @@ const handleResetTemplate = () => {
/>
</footer>
</template>
</WhatsAppTemplateParserCore>
</WhatsAppTemplateParser>
</div>
</template>
@@ -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);
});
});
});
@@ -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;
};
@@ -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', () => {
@@ -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',
},
];
@@ -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',
},
];
@@ -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: '<button />' },
WootInput: { template: '<input />' },
},
},
};
describe('#WhatsAppTemplates', () => {
it('returns all variables from a template string', async () => {
const wrapper = shallowMount(TemplateParser, {
...config,
props: { template: templates[0] },
});
await nextTick();
expect(wrapper.vm.variables).toEqual(['{{1}}', '{{2}}', '{{3}}']);
});
it('returns no variables from a template string if it does not contain variables', async () => {
const wrapper = shallowMount(TemplateParser, {
...config,
props: { template: templates[12] },
});
await nextTick();
expect(wrapper.vm.variables).toBeNull();
});
it('returns the body of a template', async () => {
const wrapper = shallowMount(TemplateParser, {
...config,
props: { template: templates[1] },
});
await nextTick();
const expectedOutput =
templates[1].components.find(i => i.type === 'BODY')?.text || '';
expect(wrapper.vm.templateString).toEqual(expectedOutput);
});
it('generates the templates from variable input', async () => {
const wrapper = shallowMount(TemplateParser, {
...config,
props: { template: templates[0] },
});
await nextTick();
// Instead of using `setData`, directly modify the `processedParams` using the component's logic
await wrapper.vm.$nextTick();
wrapper.vm.processedParams = { 1: 'abc', 2: 'xyz', 3: 'qwerty' };
await wrapper.vm.$nextTick();
const expectedOutput =
'Esta é a sua confirmação de voo para abc-xyz em qwerty.';
expect(wrapper.vm.processedString).toEqual(expectedOutput);
});
});