-
- {{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.TEMPLATE_BODY') }}
+
+
+
+ {{ t('WHATSAPP_TEMPLATES.PICKER.HEADER') || 'HEADER' }}
-
{{ getTemplatebody(template) }}
+
+ {{ getTemplateHeader(template).text }}
+
+
+ {{
+ t('WHATSAPP_TEMPLATES.PICKER.MEDIA_CONTENT', {
+ format: getTemplateHeader(template).format,
+ }) ||
+ `${getTemplateHeader(template).format} ${t('WHATSAPP_TEMPLATES.PICKER.MEDIA_CONTENT_FALLBACK')}`
+ }}
+
-
-
- {{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.CATEGORY') }}
+
+
+
+
+ {{ t('WHATSAPP_TEMPLATES.PICKER.BODY') || 'BODY' }}
-
{{ template.category }}
+
{{ getTemplateBody(template) }}
+
+
+
+
+
+ {{ t('WHATSAPP_TEMPLATES.PICKER.FOOTER') || 'FOOTER' }}
+
+
+ {{ getTemplateFooter(template).text }}
+
+
+
+
+
+
+ {{ t('WHATSAPP_TEMPLATES.PICKER.BUTTONS') || 'BUTTONS' }}
+
+
+
+ {{ button.text }}
+
+
+
+
+
+
+ {{ t('WHATSAPP_TEMPLATES.PICKER.CATEGORY') || 'CATEGORY' }}
+
+
{{ template.category }}
@@ -128,13 +191,13 @@ export default {
- {{ $t('WHATSAPP_TEMPLATES.PICKER.NO_TEMPLATES_FOUND') }}
+ {{ t('WHATSAPP_TEMPLATES.PICKER.NO_TEMPLATES_FOUND') }}
{{ query }}
- {{ $t('WHATSAPP_TEMPLATES.PICKER.NO_TEMPLATES_AVAILABLE') }}
+ {{ t('WHATSAPP_TEMPLATES.PICKER.NO_TEMPLATES_AVAILABLE') }}
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..6e0661152
--- /dev/null
+++ b/app/javascript/dashboard/helper/specs/templateHelper.spec.js
@@ -0,0 +1,368 @@
+import {
+ replaceTemplateVariables,
+ buildTemplateParameters,
+ processVariable,
+ allKeysRequired,
+} from '../templateHelper';
+import { templates } from '../../store/modules/specs/inboxes/templateFixtures';
+
+describe('templateHelper', () => {
+ const technicianTemplate = templates.find(t => t.name === 'technician_visit');
+
+ 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 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 handle templates with no variables', () => {
+ const emptyTemplate = templates.find(
+ t => t.name === 'no_variable_template'
+ );
+ const result = buildTemplateParameters(emptyTemplate, false);
+ expect(result).toEqual({});
+ });
+
+ it('should build parameters for templates with multiple component types', () => {
+ const complexTemplate = {
+ components: [
+ { type: 'HEADER', format: 'IMAGE' },
+ { type: 'BODY', text: 'Hi {{1}}, your order {{2}} is ready!' },
+ { type: 'FOOTER', text: 'Thank you for your business' },
+ {
+ type: 'BUTTONS',
+ buttons: [{ type: 'URL', url: 'https://example.com/{{3}}' }],
+ },
+ ],
+ };
+
+ const result = buildTemplateParameters(complexTemplate, true);
+
+ expect(result.header).toEqual({
+ media_url: '',
+ media_type: 'image',
+ });
+ expect(result.body).toEqual({ 1: '', 2: '' });
+ expect(result.buttons).toEqual([
+ {
+ type: 'url',
+ parameter: '',
+ url: 'https://example.com/{{3}}',
+ variables: ['3'],
+ },
+ ]);
+ });
+
+ it('should handle copy code buttons correctly', () => {
+ const copyCodeTemplate = templates.find(
+ t => t.name === 'discount_coupon'
+ );
+ const result = buildTemplateParameters(copyCodeTemplate, false);
+
+ expect(result.body).toBeDefined();
+ expect(result.buttons).toEqual([
+ {
+ type: 'copy_code',
+ parameter: '',
+ },
+ ]);
+ });
+
+ it('should handle templates with document headers', () => {
+ const documentTemplate = templates.find(
+ t => t.name === 'purchase_receipt'
+ );
+ const result = buildTemplateParameters(documentTemplate, true);
+
+ expect(result.header).toEqual({
+ media_url: '',
+ media_type: 'document',
+ });
+ expect(result.body).toEqual({
+ 1: '',
+ 2: '',
+ 3: '',
+ });
+ });
+
+ it('should handle video header templates', () => {
+ const videoTemplate = templates.find(t => t.name === 'training_video');
+ const result = buildTemplateParameters(videoTemplate, true);
+
+ expect(result.header).toEqual({
+ media_url: '',
+ media_type: 'video',
+ });
+ expect(result.body).toEqual({
+ name: '',
+ date: '',
+ });
+ });
+ });
+
+ describe('enhanced format validation', () => {
+ it('should validate enhanced format structure', () => {
+ const processedParams = {
+ body: { 1: 'John', 2: 'Order123' },
+ header: {
+ media_url: 'https://example.com/image.jpg',
+ media_type: 'image',
+ },
+ buttons: [{ type: 'copy_code', parameter: 'SAVE20' }],
+ };
+
+ // Test that structure is properly formed
+ expect(processedParams.body).toBeDefined();
+ expect(typeof processedParams.body).toBe('object');
+ expect(processedParams.header).toBeDefined();
+ expect(Array.isArray(processedParams.buttons)).toBe(true);
+ });
+
+ it('should handle empty component sections', () => {
+ const processedParams = {
+ body: {},
+ header: {},
+ buttons: [],
+ };
+
+ expect(allKeysRequired(processedParams.body)).toBe(true);
+ expect(allKeysRequired(processedParams.header)).toBe(true);
+ expect(processedParams.buttons.length).toBe(0);
+ });
+
+ it('should validate parameter completeness', () => {
+ const incompleteParams = {
+ body: { 1: 'John', 2: '' },
+ };
+
+ expect(allKeysRequired(incompleteParams.body)).toBe(false);
+ });
+
+ it('should handle edge cases in processVariable', () => {
+ expect(processVariable('{{')).toBe('');
+ expect(processVariable('}}')).toBe('');
+ expect(processVariable('')).toBe('');
+ expect(processVariable('{{nested{{variable}}}}')).toBe('nestedvariable');
+ });
+
+ it('should handle special characters in template variables', () => {
+ /* eslint-disable no-template-curly-in-string */
+ const templateText =
+ 'Welcome {{user_name}}, your order #{{order_id}} costs ${{amount}}';
+ /* eslint-enable no-template-curly-in-string */
+ const processedParams = {
+ body: {
+ user_name: 'John & Jane',
+ order_id: '12345',
+ amount: '99.99',
+ },
+ };
+
+ const result = replaceTemplateVariables(templateText, processedParams);
+ expect(result).toBe(
+ 'Welcome John & Jane, your order #12345 costs $99.99'
+ );
+ });
+
+ it('should handle templates with mixed parameter types', () => {
+ const mixedTemplate = {
+ components: [
+ { type: 'HEADER', format: 'VIDEO' },
+ { type: 'BODY', text: 'Order {{order_id}} status: {{status}}' },
+ { type: 'FOOTER', text: 'Thank you' },
+ {
+ type: 'BUTTONS',
+ buttons: [
+ { type: 'URL', url: 'https://track.com/{{order_id}}' },
+ { type: 'COPY_CODE' },
+ { type: 'PHONE_NUMBER', phone_number: '+1234567890' },
+ ],
+ },
+ ],
+ };
+
+ const result = buildTemplateParameters(mixedTemplate, true);
+
+ expect(result.header).toEqual({
+ media_url: '',
+ media_type: 'video',
+ });
+ expect(result.body).toEqual({
+ order_id: '',
+ status: '',
+ });
+ expect(result.buttons).toHaveLength(2); // URL and COPY_CODE (PHONE_NUMBER doesn't need parameters)
+ expect(result.buttons[0].type).toBe('url');
+ expect(result.buttons[1].type).toBe('copy_code');
+ });
+
+ it('should handle templates with no processable components', () => {
+ const emptyTemplate = {
+ components: [
+ { type: 'HEADER', format: 'TEXT', text: 'Static Header' },
+ { type: 'BODY', text: 'Static body with no variables' },
+ { type: 'FOOTER', text: 'Static footer' },
+ ],
+ };
+
+ const result = buildTemplateParameters(emptyTemplate, false);
+ expect(result).toEqual({});
+ });
+
+ it('should validate that replaceTemplateVariables preserves unreplaced variables', () => {
+ const templateText = 'Hi {{name}}, order {{order_id}} is {{status}}';
+ const partialParams = {
+ body: {
+ name: 'John',
+ // order_id missing
+ status: 'ready',
+ },
+ };
+
+ const result = replaceTemplateVariables(templateText, partialParams);
+ expect(result).toBe('Hi John, order {{order_id}} is ready');
+ expect(result).toContain('{{order_id}}'); // Unreplaced variable preserved
+ });
+ });
+});
diff --git a/app/javascript/dashboard/helper/templateHelper.js b/app/javascript/dashboard/helper/templateHelper.js
new file mode 100644
index 000000000..5c9bbff05
--- /dev/null
+++ b/app/javascript/dashboard/helper/templateHelper.js
@@ -0,0 +1,91 @@
+// Constants
+export const DEFAULT_LANGUAGE = 'en';
+export const DEFAULT_CATEGORY = 'UTILITY';
+export const COMPONENT_TYPES = {
+ HEADER: 'HEADER',
+ BODY: 'BODY',
+ BUTTONS: 'BUTTONS',
+};
+export const MEDIA_FORMATS = ['IMAGE', 'VIDEO', 'DOCUMENT'];
+
+export const findComponentByType = (template, type) =>
+ template.components?.find(component => component.type === type);
+
+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 = findComponentByType(template, COMPONENT_TYPES.BODY);
+ const headerComponent = findComponentByType(template, COMPONENT_TYPES.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);
+ 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 === COMPONENT_TYPES.BUTTONS
+ );
+
+ buttonComponents.forEach(buttonComponent => {
+ if (buttonComponent.buttons) {
+ buttonComponent.buttons.forEach((button, index) => {
+ // 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;
+};
diff --git a/app/javascript/dashboard/i18n/locale/en/whatsappTemplates.json b/app/javascript/dashboard/i18n/locale/en/whatsappTemplates.json
index 4887d07b6..5f53faaa8 100644
--- a/app/javascript/dashboard/i18n/locale/en/whatsappTemplates.json
+++ b/app/javascript/dashboard/i18n/locale/en/whatsappTemplates.json
@@ -1,29 +1,46 @@
{
- "WHATSAPP_TEMPLATES": {
- "MODAL": {
- "TITLE": "Whatsapp Templates",
- "SUBTITLE": "Select the whatsapp template you want to send",
- "TEMPLATE_SELECTED_SUBTITLE": "Process {templateName}"
- },
- "PICKER": {
- "SEARCH_PLACEHOLDER": "Search Templates",
- "NO_TEMPLATES_FOUND": "No templates found for",
- "NO_TEMPLATES_AVAILABLE": "No WhatsApp templates available. Click refresh to sync templates from WhatsApp.",
- "REFRESH_BUTTON": "Refresh templates",
- "REFRESH_SUCCESS": "Templates refresh initiated. It may take a couple of minutes to update.",
- "REFRESH_ERROR": "Failed to refresh templates. Please try again.",
- "LABELS": {
- "LANGUAGE": "Language",
- "TEMPLATE_BODY": "Template Body",
- "CATEGORY": "Category"
- }
- },
- "PARSER": {
- "VARIABLES_LABEL": "Variables",
- "VARIABLE_PLACEHOLDER": "Enter {variable} value",
- "GO_BACK_LABEL": "Go Back",
- "SEND_MESSAGE_LABEL": "Send Message",
- "FORM_ERROR_MESSAGE": "Please fill all variables before sending"
- }
+ "WHATSAPP_TEMPLATES": {
+ "MODAL": {
+ "TITLE": "Whatsapp Templates",
+ "SUBTITLE": "Select the whatsapp template you want to send",
+ "TEMPLATE_SELECTED_SUBTITLE": "Configure template: {templateName}"
+ },
+ "PICKER": {
+ "SEARCH_PLACEHOLDER": "Search Templates",
+ "NO_TEMPLATES_FOUND": "No templates found for",
+ "HEADER": "Header",
+ "BODY": "Body",
+ "FOOTER": "Footer",
+ "BUTTONS": "Buttons",
+ "CATEGORY": "Category",
+ "MEDIA_CONTENT": "Media Content",
+ "MEDIA_CONTENT_FALLBACK": "media content",
+ "NO_TEMPLATES_AVAILABLE": "No WhatsApp templates available. Click refresh to sync templates from WhatsApp.",
+ "REFRESH_BUTTON": "Refresh templates",
+ "REFRESH_SUCCESS": "Templates refresh initiated. It may take a couple of minutes to update.",
+ "REFRESH_ERROR": "Failed to refresh templates. Please try again.",
+ "LABELS": {
+ "LANGUAGE": "Language",
+ "TEMPLATE_BODY": "Template Body",
+ "CATEGORY": "Category"
+ }
+ },
+ "PARSER": {
+ "VARIABLES_LABEL": "Variables",
+ "LANGUAGE": "Language",
+ "CATEGORY": "Category",
+ "VARIABLE_PLACEHOLDER": "Enter {variable} value",
+ "GO_BACK_LABEL": "Go Back",
+ "SEND_MESSAGE_LABEL": "Send Message",
+ "FORM_ERROR_MESSAGE": "Please fill all variables before sending",
+ "MEDIA_HEADER_LABEL": "{type} Header",
+ "OTP_CODE": "Enter 4-8 digit OTP",
+ "EXPIRY_MINUTES": "Enter expiry minutes",
+ "BUTTON_PARAMETERS": "Button Parameters",
+ "BUTTON_LABEL": "Button {index}",
+ "COUPON_CODE": "Enter coupon code (max 15 chars)",
+ "MEDIA_URL_LABEL": "Enter {type} URL",
+ "BUTTON_PARAMETER": "Enter button parameter"
}
+ }
}
diff --git a/app/javascript/dashboard/store/modules/inboxes.js b/app/javascript/dashboard/store/modules/inboxes.js
index a6b93c923..c4789a7a9 100644
--- a/app/javascript/dashboard/store/modules/inboxes.js
+++ b/app/javascript/dashboard/store/modules/inboxes.js
@@ -44,15 +44,52 @@ export const getters = {
const messagesTemplates =
whatsAppMessageTemplates || apiInboxMessageTemplates;
- // filtering out the whatsapp templates with media
- if (messagesTemplates instanceof Array) {
- return messagesTemplates.filter(template => {
- return !template.components.some(
- i => i.format === 'IMAGE' || i.format === 'VIDEO'
- );
- });
+ return messagesTemplates;
+ },
+ getFilteredWhatsAppTemplates: $state => inboxId => {
+ const [inbox] = $state.records.filter(
+ record => record.id === Number(inboxId)
+ );
+
+ const {
+ message_templates: whatsAppMessageTemplates,
+ additional_attributes: additionalAttributes,
+ } = inbox || {};
+
+ const { message_templates: apiInboxMessageTemplates } =
+ additionalAttributes || {};
+ const templates = whatsAppMessageTemplates || apiInboxMessageTemplates;
+
+ if (!templates || !Array.isArray(templates)) {
+ return [];
}
- return [];
+
+ return templates.filter(template => {
+ // Ensure template has required properties
+ if (!template || !template.status || !template.components) {
+ return false;
+ }
+
+ // Only show approved templates
+ if (template.status.toLowerCase() !== 'approved') {
+ return false;
+ }
+
+ // Filter out interactive templates (LIST, PRODUCT, CATALOG), location templates, and call permission templates
+ const hasUnsupportedComponents = template.components.some(
+ component =>
+ ['LIST', 'PRODUCT', 'CATALOG', 'CALL_PERMISSION_REQUEST'].includes(
+ component.type
+ ) ||
+ (component.type === 'HEADER' && component.format === 'LOCATION')
+ );
+
+ if (hasUnsupportedComponents) {
+ return false;
+ }
+
+ return true;
+ });
},
getNewConversationInboxes($state) {
return $state.records.filter(inbox => {
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 f9ed57d63..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,5 +1,6 @@
import { getters } from '../../inboxes';
import inboxList from './fixtures';
+import { templates } from './templateFixtures';
describe('#getters', () => {
it('getInboxes', () => {
@@ -93,4 +94,269 @@ describe('#getters', () => {
provider: 'default',
});
});
+
+ describe('getFilteredWhatsAppTemplates', () => {
+ it('returns empty array when inbox not found', () => {
+ const state = { records: [] };
+ expect(getters.getFilteredWhatsAppTemplates(state)(999)).toEqual([]);
+ });
+
+ it('returns empty array when templates is null or undefined', () => {
+ const state = {
+ records: [
+ {
+ id: 1,
+ channel_type: 'Channel::Whatsapp',
+ message_templates: null,
+ additional_attributes: { message_templates: undefined },
+ },
+ ],
+ };
+ expect(getters.getFilteredWhatsAppTemplates(state)(1)).toEqual([]);
+ });
+
+ it('returns empty array when templates is not an array', () => {
+ const state = {
+ records: [
+ {
+ id: 1,
+ channel_type: 'Channel::Whatsapp',
+ message_templates: 'invalid',
+ additional_attributes: {},
+ },
+ ],
+ };
+ expect(getters.getFilteredWhatsAppTemplates(state)(1)).toEqual([]);
+ });
+
+ it('filters out templates without required properties', () => {
+ const invalidTemplates = [
+ { name: 'incomplete_template' }, // missing status and components
+ { status: 'approved' }, // missing name and components
+ { name: 'another_incomplete', status: 'approved' }, // missing components
+ ];
+
+ const state = {
+ records: [
+ {
+ id: 1,
+ channel_type: 'Channel::Whatsapp',
+ message_templates: invalidTemplates,
+ },
+ ],
+ };
+ expect(getters.getFilteredWhatsAppTemplates(state)(1)).toEqual([]);
+ });
+
+ it('filters out non-approved templates', () => {
+ const mixedStatusTemplates = [
+ {
+ name: 'pending_template',
+ status: 'pending',
+ components: [{ type: 'BODY', text: 'Test' }],
+ },
+ {
+ name: 'rejected_template',
+ status: 'rejected',
+ components: [{ type: 'BODY', text: 'Test' }],
+ },
+ {
+ name: 'approved_template',
+ status: 'approved',
+ components: [{ type: 'BODY', text: 'Test' }],
+ },
+ ];
+
+ const state = {
+ records: [
+ {
+ id: 1,
+ channel_type: 'Channel::Whatsapp',
+ message_templates: mixedStatusTemplates,
+ },
+ ],
+ };
+
+ const result = getters.getFilteredWhatsAppTemplates(state)(1);
+ expect(result).toHaveLength(1);
+ expect(result[0].name).toBe('approved_template');
+ });
+
+ it('filters out interactive templates (LIST, PRODUCT, CATALOG)', () => {
+ const interactiveTemplates = [
+ {
+ name: 'list_template',
+ status: 'approved',
+ components: [
+ { type: 'BODY', text: 'Choose an option' },
+ { type: 'LIST', sections: [] },
+ ],
+ },
+ {
+ name: 'product_template',
+ status: 'approved',
+ components: [
+ { type: 'BODY', text: 'Product info' },
+ { type: 'PRODUCT', catalog_id: '123' },
+ ],
+ },
+ {
+ name: 'catalog_template',
+ status: 'approved',
+ components: [
+ { type: 'BODY', text: 'Catalog' },
+ { type: 'CATALOG', thumbnail_product_retailer_id: '123' },
+ ],
+ },
+ {
+ name: 'regular_template',
+ status: 'approved',
+ components: [{ type: 'BODY', text: 'Regular message' }],
+ },
+ ];
+
+ const state = {
+ records: [
+ {
+ id: 1,
+ channel_type: 'Channel::Whatsapp',
+ message_templates: interactiveTemplates,
+ },
+ ],
+ };
+
+ const result = getters.getFilteredWhatsAppTemplates(state)(1);
+ expect(result).toHaveLength(1);
+ expect(result[0].name).toBe('regular_template');
+ });
+
+ it('filters out location templates', () => {
+ const locationTemplates = [
+ {
+ name: 'location_template',
+ status: 'approved',
+ components: [
+ { type: 'HEADER', format: 'LOCATION' },
+ { type: 'BODY', text: 'Location message' },
+ ],
+ },
+ {
+ name: 'regular_template',
+ status: 'approved',
+ components: [
+ { type: 'HEADER', format: 'TEXT', text: 'Header' },
+ { type: 'BODY', text: 'Regular message' },
+ ],
+ },
+ ];
+
+ const state = {
+ records: [
+ {
+ id: 1,
+ channel_type: 'Channel::Whatsapp',
+ message_templates: locationTemplates,
+ },
+ ],
+ };
+
+ const result = getters.getFilteredWhatsAppTemplates(state)(1);
+ expect(result).toHaveLength(1);
+ expect(result[0].name).toBe('regular_template');
+ });
+
+ it('returns valid templates from fixture data', () => {
+ const state = {
+ records: [
+ {
+ id: 1,
+ channel_type: 'Channel::Whatsapp',
+ message_templates: templates,
+ },
+ ],
+ };
+
+ const result = getters.getFilteredWhatsAppTemplates(state)(1);
+
+ // All templates in fixtures should be approved and valid
+ expect(result.length).toBeGreaterThan(0);
+
+ // Verify all returned templates are approved
+ result.forEach(template => {
+ expect(template.status).toBe('approved');
+ expect(template.components).toBeDefined();
+ expect(Array.isArray(template.components)).toBe(true);
+ });
+
+ // Verify specific templates from fixtures are included
+ const templateNames = result.map(t => t.name);
+ expect(templateNames).toContain('sample_flight_confirmation');
+ expect(templateNames).toContain('sample_issue_resolution');
+ expect(templateNames).toContain('sample_shipping_confirmation');
+ expect(templateNames).toContain('no_variable_template');
+ expect(templateNames).toContain('order_confirmation');
+ });
+
+ it('prioritizes message_templates over additional_attributes.message_templates', () => {
+ const primaryTemplates = [
+ {
+ name: 'primary_template',
+ status: 'approved',
+ components: [{ type: 'BODY', text: 'Primary' }],
+ },
+ ];
+
+ const fallbackTemplates = [
+ {
+ name: 'fallback_template',
+ status: 'approved',
+ components: [{ type: 'BODY', text: 'Fallback' }],
+ },
+ ];
+
+ const state = {
+ records: [
+ {
+ id: 1,
+ channel_type: 'Channel::Whatsapp',
+ message_templates: primaryTemplates,
+ additional_attributes: {
+ message_templates: fallbackTemplates,
+ },
+ },
+ ],
+ };
+
+ const result = getters.getFilteredWhatsAppTemplates(state)(1);
+ expect(result).toHaveLength(1);
+ expect(result[0].name).toBe('primary_template');
+ });
+
+ it('falls back to additional_attributes.message_templates when message_templates is null', () => {
+ const fallbackTemplates = [
+ {
+ name: 'fallback_template',
+ status: 'approved',
+ components: [{ type: 'BODY', text: 'Fallback' }],
+ },
+ ];
+
+ const state = {
+ records: [
+ {
+ id: 1,
+ channel_type: 'Channel::Whatsapp',
+ message_templates: null,
+ additional_attributes: {
+ message_templates: fallbackTemplates,
+ },
+ },
+ ],
+ };
+
+ const result = getters.getFilteredWhatsAppTemplates(state)(1);
+ expect(result).toHaveLength(1);
+ expect(result[0].name).toBe('fallback_template');
+ });
+ });
});
diff --git a/app/javascript/shared/mixins/specs/whatsappTemplates/fixtures.js b/app/javascript/dashboard/store/modules/specs/inboxes/templateFixtures.js
similarity index 50%
rename from app/javascript/shared/mixins/specs/whatsappTemplates/fixtures.js
rename to app/javascript/dashboard/store/modules/specs/inboxes/templateFixtures.js
index 02c24b2bb..c4c6a0b40 100644
--- a/app/javascript/shared/mixins/specs/whatsappTemplates/fixtures.js
+++ b/app/javascript/dashboard/store/modules/specs/inboxes/templateFixtures.js
@@ -260,4 +260,285 @@ export const templates = [
],
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: '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: '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: '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/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: '
' },
- WootInput: { template: '
' },
- },
- },
-};
-
-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);
- });
-});