diff --git a/app/javascript/dashboard/api/inboxes.js b/app/javascript/dashboard/api/inboxes.js
index 361b9472f..83ba3e9ba 100644
--- a/app/javascript/dashboard/api/inboxes.js
+++ b/app/javascript/dashboard/api/inboxes.js
@@ -32,6 +32,16 @@ class Inboxes extends CacheEnabledApiClient {
syncTemplates(inboxId) {
return axios.post(`${this.url}/${inboxId}/sync_templates`);
}
+
+ createCSATTemplate(inboxId, template) {
+ return axios.post(`${this.url}/${inboxId}/csat_template`, {
+ template,
+ });
+ }
+
+ getCSATTemplateStatus(inboxId) {
+ return axios.get(`${this.url}/${inboxId}/csat_template`);
+ }
}
export default new Inboxes();
diff --git a/app/javascript/dashboard/components-next/message/bubbles/Template/CSAT.vue b/app/javascript/dashboard/components-next/message/bubbles/Template/CSAT.vue
new file mode 100644
index 000000000..d1c9ebb37
--- /dev/null
+++ b/app/javascript/dashboard/components-next/message/bubbles/Template/CSAT.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue
index 4b83556de..d26192bcd 100644
--- a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue
+++ b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue
@@ -859,6 +859,7 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
max-height: none !important;
min-height: 0 !important;
padding: 0 !important;
+ display: none !important;
}
> .ProseMirror {
diff --git a/app/javascript/dashboard/constants/editor.js b/app/javascript/dashboard/constants/editor.js
index a1e4da28d..95ac94b69 100644
--- a/app/javascript/dashboard/constants/editor.js
+++ b/app/javascript/dashboard/constants/editor.js
@@ -140,6 +140,11 @@ export const FORMATTING = {
nodes: [],
menu: ['strong', 'em', 'link', 'undo', 'redo'],
},
+ 'Context::Plain': {
+ marks: [],
+ nodes: [],
+ menu: [],
+ },
};
// Editor menu options for Full Editor
diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json
index fa15f5f4d..51cae8679 100644
--- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json
+++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json
@@ -808,6 +808,35 @@
"LABEL": "Message",
"PLACEHOLDER": "Please enter a message to show users with the form"
},
+ "BUTTON_TEXT": {
+ "LABEL": "Button text",
+ "PLACEHOLDER": "Please rate us"
+ },
+ "LANGUAGE": {
+ "LABEL": "Language",
+ "PLACEHOLDER": "Select template language"
+ },
+ "MESSAGE_PREVIEW": {
+ "LABEL": "Message preview",
+ "TOOLTIP": "This may vary slightly when rendered on WhatsApp's platform."
+ },
+ "TEMPLATE_STATUS": {
+ "APPROVED": "Approved by WhatsApp",
+ "PENDING": "Pending WhatsApp approval",
+ "REJECTED": "Meta rejected the template",
+ "DEFAULT": "Needs WhatsApp approval",
+ "NOT_FOUND": "The template does not exist in the Meta platform."
+ },
+ "TEMPLATE_CREATION": {
+ "SUCCESS_MESSAGE": "WhatsApp template created successfully and sent for approval",
+ "ERROR_MESSAGE": "Failed to create WhatsApp template"
+ },
+ "TEMPLATE_UPDATE_DIALOG": {
+ "TITLE": "Edit survey details",
+ "DESCRIPTION": "We will delete the previous template and make a new one which will be sent again for WhatsApp approval",
+ "CONFIRM": "Create new template",
+ "CANCEL": "Go back"
+ },
"SURVEY_RULE": {
"LABEL": "Survey rule",
"DESCRIPTION_PREFIX": "Send the survey if the conversation",
@@ -819,6 +848,7 @@
"SELECT_PLACEHOLDER": "select labels"
},
"NOTE": "Note: CSAT surveys are sent only once per conversation",
+ "WHATSAPP_NOTE": "Note: We will create a template and send it for WhatsApp approval. After being approved, surveys will be sent only once per conversation as per the survey rule.",
"API": {
"SUCCESS_MESSAGE": "CSAT settings updated successfully",
"ERROR_MESSAGE": "We couldn't update CSAT settings. Please try again later."
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/CustomerSatisfactionPage.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/CustomerSatisfactionPage.vue
index 807f1ad0f..9cf9c688f 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/CustomerSatisfactionPage.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/CustomerSatisfactionPage.vue
@@ -3,15 +3,22 @@ import { reactive, onMounted, ref, defineProps, watch, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useAlert } from 'dashboard/composables';
import { useStore, useMapGetter } from 'dashboard/composables/store';
+import { useInbox } from 'dashboard/composables/useInbox';
import { CSAT_DISPLAY_TYPES } from 'shared/constants/messages';
+import Icon from 'dashboard/components-next/icon/Icon.vue';
import WithLabel from 'v3/components/Form/WithLabel.vue';
import SectionLayout from 'dashboard/routes/dashboard/settings/account/components/SectionLayout.vue';
import CSATDisplayTypeSelector from './components/CSATDisplayTypeSelector.vue';
+import CSATTemplate from 'dashboard/components-next/message/bubbles/Template/CSAT.vue';
import Editor from 'dashboard/components-next/Editor/Editor.vue';
import FilterSelect from 'dashboard/components-next/filter/inputs/FilterSelect.vue';
import NextButton from 'dashboard/components-next/button/Button.vue';
import Switch from 'next/switch/Switch.vue';
+import Input from 'dashboard/components-next/input/Input.vue';
+import ComboBox from 'dashboard/components-next/combobox/ComboBox.vue';
+import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages.js';
+import ConfirmTemplateUpdateDialog from './components/ConfirmTemplateUpdateDialog.vue';
const props = defineProps({
inbox: { type: Object, required: true },
@@ -21,6 +28,10 @@ const { t } = useI18n();
const store = useStore();
const labels = useMapGetter('labels/getLabels');
+const { isAWhatsAppCloudChannel: isWhatsAppChannel } = useInbox(
+ props.inbox?.id
+);
+
const isUpdating = ref(false);
const selectedLabelValues = ref([]);
const currentLabel = ref('');
@@ -29,7 +40,19 @@ const state = reactive({
csatSurveyEnabled: false,
displayType: 'emoji',
message: '',
+ templateButtonText: 'Please rate us',
surveyRuleOperator: 'contains',
+ templateLanguage: '',
+});
+
+const templateStatus = ref(null);
+const templateLoading = ref(false);
+const confirmDialog = ref(null);
+
+const originalTemplateValues = ref({
+ message: '',
+ templateButtonText: '',
+ templateLanguage: '',
});
const filterTypes = [
@@ -51,6 +74,59 @@ const labelOptions = computed(() =>
: []
);
+const languageOptions = computed(() =>
+ languages.map(({ name, id }) => ({ label: `${name} (${id})`, value: id }))
+);
+
+const messagePreviewData = computed(() => ({
+ content: state.message || t('INBOX_MGMT.CSAT.MESSAGE.PLACEHOLDER'),
+}));
+
+const shouldShowTemplateStatus = computed(
+ () => templateStatus.value && !templateLoading.value
+);
+
+const templateApprovalStatus = computed(() => {
+ const statusMap = {
+ APPROVED: {
+ text: t('INBOX_MGMT.CSAT.TEMPLATE_STATUS.APPROVED'),
+ icon: 'i-lucide-circle-check',
+ color: 'text-n-teal-11',
+ },
+ PENDING: {
+ text: t('INBOX_MGMT.CSAT.TEMPLATE_STATUS.PENDING'),
+ icon: 'i-lucide-clock',
+ color: 'text-n-amber-11',
+ },
+ REJECTED: {
+ text: t('INBOX_MGMT.CSAT.TEMPLATE_STATUS.REJECTED'),
+ icon: 'i-lucide-circle-x',
+ color: 'text-n-ruby-10',
+ },
+ };
+
+ // Handle template not found case
+ if (templateStatus.value?.error === 'TEMPLATE_NOT_FOUND') {
+ return {
+ text: t('INBOX_MGMT.CSAT.TEMPLATE_STATUS.NOT_FOUND'),
+ icon: 'i-lucide-alert-triangle',
+ color: 'text-n-ruby-10',
+ };
+ }
+
+ // Handle existing template with status
+ if (templateStatus.value?.template_exists && templateStatus.value.status) {
+ return statusMap[templateStatus.value.status] || statusMap.PENDING;
+ }
+
+ // Default case - no template exists
+ return {
+ text: t('INBOX_MGMT.CSAT.TEMPLATE_STATUS.DEFAULT'),
+ icon: 'i-lucide-stamp',
+ color: 'text-n-slate-11',
+ };
+});
+
const initializeState = () => {
if (!props.inbox) return;
@@ -63,21 +139,63 @@ const initializeState = () => {
const {
display_type: displayType = CSAT_DISPLAY_TYPES.EMOJI,
message = '',
+ button_text: buttonText = 'Please rate us',
+ language = 'en',
survey_rules: surveyRules = {},
} = csat_config;
state.displayType = displayType;
state.message = message;
+ state.templateButtonText = buttonText;
+ state.templateLanguage = language;
state.surveyRuleOperator = surveyRules.operator || 'contains';
selectedLabelValues.value = Array.isArray(surveyRules.values)
? [...surveyRules.values]
: [];
+
+ // Store original template values for change detection
+ if (isWhatsAppChannel.value) {
+ originalTemplateValues.value = {
+ message: state.message,
+ templateButtonText: state.templateButtonText,
+ templateLanguage: state.templateLanguage,
+ };
+ }
+};
+
+const checkTemplateStatus = async () => {
+ if (!isWhatsAppChannel.value) return;
+
+ try {
+ templateLoading.value = true;
+ const response = await store.dispatch('inboxes/getCSATTemplateStatus', {
+ inboxId: props.inbox.id,
+ });
+
+ // Handle case where template doesn't exist
+ if (!response.template_exists && response.error === 'Template not found') {
+ templateStatus.value = {
+ template_exists: false,
+ error: 'TEMPLATE_NOT_FOUND',
+ };
+ } else {
+ templateStatus.value = response;
+ }
+ } catch (error) {
+ templateStatus.value = {
+ template_exists: false,
+ error: 'API_ERROR',
+ };
+ } finally {
+ templateLoading.value = false;
+ }
};
onMounted(() => {
initializeState();
if (!labels.value?.length) store.dispatch('labels/get');
+ if (isWhatsAppChannel.value) checkTemplateStatus();
});
watch(() => props.inbox, initializeState, { immediate: true });
@@ -105,6 +223,49 @@ const removeLabel = label => {
}
};
+// Check if template-related fields have changed
+const hasTemplateChanges = () => {
+ if (!isWhatsAppChannel.value) return false;
+
+ const original = originalTemplateValues.value;
+ return (
+ original.message !== state.message ||
+ original.templateButtonText !== state.templateButtonText ||
+ original.templateLanguage !== state.templateLanguage
+ );
+};
+
+// Check if there's an existing template
+const hasExistingTemplate = () => {
+ const { template_exists, error } = templateStatus.value || {};
+ return template_exists && !error;
+};
+
+// Check if we should create a template
+const shouldCreateTemplate = () => {
+ // Create template if no existing template
+ if (!hasExistingTemplate()) {
+ return true;
+ }
+
+ // Create template if there are changes to template fields
+ return hasTemplateChanges();
+};
+
+// Build template config for saving
+const buildTemplateConfig = () => {
+ if (!hasExistingTemplate()) return null;
+
+ const { template_name, template_id, template, status } =
+ templateStatus.value || {};
+ return {
+ name: template_name,
+ template_id,
+ language: template?.language || state.templateLanguage,
+ status,
+ };
+};
+
const updateInbox = async attributes => {
const payload = {
id: props.inbox.id,
@@ -115,31 +276,103 @@ const updateInbox = async attributes => {
return store.dispatch('inboxes/updateInbox', payload);
};
-const saveSettings = async () => {
+const createTemplate = async () => {
+ if (!isWhatsAppChannel.value) return null;
+
+ const response = await store.dispatch('inboxes/createCSATTemplate', {
+ inboxId: props.inbox.id,
+ template: {
+ message: state.message,
+ button_text: state.templateButtonText,
+ language: state.templateLanguage,
+ },
+ });
+ useAlert(t('INBOX_MGMT.CSAT.TEMPLATE_CREATION.SUCCESS_MESSAGE'));
+ return response.template;
+};
+
+const performSave = async () => {
try {
isUpdating.value = true;
+ let newTemplateData = null;
+
+ // For WhatsApp channels, create template first if needed
+ if (
+ isWhatsAppChannel.value &&
+ state.csatSurveyEnabled &&
+ shouldCreateTemplate()
+ ) {
+ try {
+ newTemplateData = await createTemplate();
+ } catch (error) {
+ const errorMessage =
+ error.response?.data?.error ||
+ t('INBOX_MGMT.CSAT.TEMPLATE_CREATION.ERROR_MESSAGE');
+ useAlert(errorMessage);
+ return;
+ }
+ }
const csatConfig = {
display_type: state.displayType,
message: state.message,
+ button_text: state.templateButtonText,
+ language: state.templateLanguage,
survey_rules: {
operator: state.surveyRuleOperator,
values: selectedLabelValues.value,
},
};
+ // Use new template data if created, otherwise preserve existing template information
+ if (newTemplateData) {
+ csatConfig.template = {
+ name: newTemplateData.name,
+ template_id: newTemplateData.template_id,
+ language: newTemplateData.language,
+ status: newTemplateData.status,
+ created_at: new Date().toISOString(),
+ };
+ } else {
+ const templateConfig = buildTemplateConfig();
+ if (templateConfig) {
+ csatConfig.template = templateConfig;
+ }
+ }
+
await updateInbox({
csat_survey_enabled: state.csatSurveyEnabled,
csat_config: csatConfig,
});
useAlert(t('INBOX_MGMT.CSAT.API.SUCCESS_MESSAGE'));
+ checkTemplateStatus();
} catch (error) {
useAlert(t('INBOX_MGMT.CSAT.API.ERROR_MESSAGE'));
} finally {
isUpdating.value = false;
}
};
+
+const saveSettings = async () => {
+ // Check if we need to show confirmation dialog for WhatsApp template changes
+ if (
+ isWhatsAppChannel.value &&
+ state.csatSurveyEnabled &&
+ hasExistingTemplate() &&
+ hasTemplateChanges()
+ ) {
+ confirmDialog.value?.open();
+ return;
+ }
+
+ await performSave();
+};
+
+const handleConfirmTemplateUpdate = async () => {
+ // We will delete the template before creating the template
+ await performSave();
+};
@@ -155,7 +388,9 @@ const saveSettings = async () => {
+
@@ -165,14 +400,97 @@ const saveSettings = async () => {
/>
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ templateApprovalStatus.text }}
+
+
+
+
+
+
+ {{ $t('INBOX_MGMT.CSAT.MESSAGE_PREVIEW.LABEL') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
{
>
{{ $t('INBOX_MGMT.CSAT.SURVEY_RULE.DESCRIPTION_PREFIX') }}
{
- {{ $t('INBOX_MGMT.CSAT.NOTE') }}
+ {{
+ isWhatsAppChannel
+ ? $t('INBOX_MGMT.CSAT.WHATSAPP_NOTE')
+ : $t('INBOX_MGMT.CSAT.NOTE')
+ }}
{
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/components/ConfirmTemplateUpdateDialog.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/components/ConfirmTemplateUpdateDialog.vue
new file mode 100644
index 000000000..6df5c2918
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/components/ConfirmTemplateUpdateDialog.vue
@@ -0,0 +1,34 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/store/modules/inboxes.js b/app/javascript/dashboard/store/modules/inboxes.js
index ac7d1483e..c24a90599 100644
--- a/app/javascript/dashboard/store/modules/inboxes.js
+++ b/app/javascript/dashboard/store/modules/inboxes.js
@@ -83,6 +83,14 @@ export const getters = {
return false;
}
+ // Filter out CSAT templates (customer_satisfaction_survey and its versions)
+ if (
+ template.name &&
+ template.name.startsWith('customer_satisfaction_survey')
+ ) {
+ return false;
+ }
+
// Filter out interactive templates (LIST, PRODUCT, CATALOG), location templates, and call permission templates
const hasUnsupportedComponents = template.components.some(
component =>
@@ -344,6 +352,14 @@ export const actions = {
throw new Error(error);
}
},
+ createCSATTemplate: async (_, { inboxId, template }) => {
+ const response = await InboxesAPI.createCSATTemplate(inboxId, template);
+ return response.data;
+ },
+ getCSATTemplateStatus: async (_, { inboxId }) => {
+ const response = await InboxesAPI.getCSATTemplateStatus(inboxId);
+ return response.data;
+ },
};
export const mutations = {
diff --git a/app/services/csat_survey_service.rb b/app/services/csat_survey_service.rb
index 15d63722c..9afddc6d3 100644
--- a/app/services/csat_survey_service.rb
+++ b/app/services/csat_survey_service.rb
@@ -4,7 +4,9 @@ class CsatSurveyService
def perform
return unless should_send_csat_survey?
- if within_messaging_window?
+ if whatsapp_channel? && template_available_and_approved?
+ send_whatsapp_template_survey
+ elsif within_messaging_window?
::MessageTemplates::Template::CsatSurvey.new(conversation: conversation).perform
else
create_csat_not_sent_activity_message
@@ -35,6 +37,64 @@ class CsatSurveyService
conversation.can_reply?
end
+ def whatsapp_channel?
+ inbox.channel_type == 'Channel::Whatsapp'
+ end
+
+ def template_available_and_approved?
+ template_config = inbox.csat_config&.dig('template')
+ return false unless template_config
+
+ template_name = template_config['name'] || Whatsapp::CsatTemplateNameService.csat_template_name(inbox.id)
+
+ status_result = inbox.channel.provider_service.get_template_status(template_name)
+
+ status_result[:success] && status_result[:template][:status] == 'APPROVED'
+ rescue StandardError => e
+ Rails.logger.error "Error checking CSAT template status: #{e.message}"
+ false
+ end
+
+ def send_whatsapp_template_survey
+ template_config = inbox.csat_config&.dig('template')
+ template_name = template_config['name'] || Whatsapp::CsatTemplateNameService.csat_template_name(inbox.id)
+
+ phone_number = conversation.contact_inbox.source_id
+ template_info = build_template_info(template_name, template_config)
+ message = build_csat_message
+
+ message_id = inbox.channel.provider_service.send_template(phone_number, template_info, message)
+
+ message.update!(source_id: message_id) if message_id.present?
+ rescue StandardError => e
+ Rails.logger.error "Error sending WhatsApp CSAT template for conversation #{conversation.id}: #{e.message}"
+ end
+
+ def build_template_info(template_name, template_config)
+ {
+ name: template_name,
+ lang_code: template_config['language'] || 'en',
+ parameters: [
+ {
+ type: 'button',
+ sub_type: 'url',
+ index: '0',
+ parameters: [{ type: 'text', text: conversation.uuid }]
+ }
+ ]
+ }
+ end
+
+ def build_csat_message
+ conversation.messages.build(
+ account: conversation.account,
+ inbox: inbox,
+ message_type: :outgoing,
+ content: inbox.csat_config&.dig('message') || 'Please rate this conversation',
+ content_type: :input_csat
+ )
+ end
+
def create_csat_not_sent_activity_message
content = I18n.t('conversations.activity.csat.not_sent_due_to_messaging_window')
activity_message_params = {
diff --git a/spec/services/csat_survey_service_spec.rb b/spec/services/csat_survey_service_spec.rb
index ecb5a75c6..6359fbda1 100644
--- a/spec/services/csat_survey_service_spec.rb
+++ b/spec/services/csat_survey_service_spec.rb
@@ -3,7 +3,9 @@ require 'rails_helper'
describe CsatSurveyService do
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account, csat_survey_enabled: true) }
- let(:conversation) { create(:conversation, inbox: inbox, account: account, status: :resolved) }
+ let(:contact) { create(:contact, account: account) }
+ let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: inbox, source_id: '+1234567890') }
+ let(:conversation) { create(:conversation, contact_inbox: contact_inbox, inbox: inbox, account: account, status: :resolved) }
let(:service) { described_class.new(conversation: conversation) }
describe '#perform' do
@@ -87,5 +89,269 @@ describe CsatSurveyService do
expect(Conversations::ActivityMessageJob).not_to have_received(:perform_later)
end
end
+
+ context 'when it is a WhatsApp channel' do
+ let(:whatsapp_channel) do
+ create(:channel_whatsapp, account: account, provider: 'whatsapp_cloud',
+ sync_templates: false, validate_provider_config: false)
+ end
+ let(:whatsapp_inbox) { create(:inbox, channel: whatsapp_channel, account: account, csat_survey_enabled: true) }
+ let(:whatsapp_contact) { create(:contact, account: account) }
+ let(:whatsapp_contact_inbox) { create(:contact_inbox, contact: whatsapp_contact, inbox: whatsapp_inbox, source_id: '1234567890') }
+ let(:whatsapp_conversation) do
+ create(:conversation, contact_inbox: whatsapp_contact_inbox, inbox: whatsapp_inbox, account: account, status: :resolved)
+ end
+ let(:whatsapp_service) { described_class.new(conversation: whatsapp_conversation) }
+ let(:mock_provider_service) { instance_double(Whatsapp::Providers::WhatsappCloudService) }
+
+ before do
+ allow(Whatsapp::Providers::WhatsappCloudService).to receive(:new).and_return(mock_provider_service)
+ allow(whatsapp_conversation).to receive(:can_reply?).and_return(true)
+ end
+
+ context 'when template is available and approved' do
+ before do
+ setup_approved_template('customer_survey_template')
+ end
+
+ it 'sends WhatsApp template survey instead of regular survey' do
+ mock_successful_template_send('template_message_id_123')
+
+ whatsapp_service.perform
+
+ expect(mock_provider_service).to have_received(:send_template).with(
+ '1234567890',
+ hash_including(
+ name: 'customer_survey_template',
+ lang_code: 'en',
+ parameters: array_including(
+ hash_including(
+ type: 'button',
+ sub_type: 'url',
+ index: '0',
+ parameters: array_including(
+ hash_including(type: 'text', text: whatsapp_conversation.uuid)
+ )
+ )
+ )
+ ),
+ instance_of(Message)
+ )
+ expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new)
+ end
+
+ it 'updates message with returned message ID' do
+ mock_successful_template_send('template_message_id_123')
+
+ whatsapp_service.perform
+
+ csat_message = whatsapp_conversation.messages.where(content_type: :input_csat).last
+ expect(csat_message).to be_present
+ expect(csat_message.source_id).to eq('template_message_id_123')
+ end
+
+ it 'builds correct template info with default template name' do
+ expected_template_name = "customer_satisfaction_survey_#{whatsapp_inbox.id}"
+ whatsapp_inbox.update(csat_config: { 'template' => {}, 'message' => 'Rate us' })
+ allow(mock_provider_service).to receive(:get_template_status)
+ .with(expected_template_name)
+ .and_return({ success: true, template: { status: 'APPROVED' } })
+ allow(mock_provider_service).to receive(:send_template) do |_phone, _template, message|
+ message.save!
+ 'msg_id'
+ end
+
+ whatsapp_service.perform
+
+ expect(mock_provider_service).to have_received(:send_template).with(
+ '1234567890',
+ hash_including(
+ name: expected_template_name,
+ lang_code: 'en'
+ ),
+ anything
+ )
+ end
+
+ it 'builds CSAT message with correct attributes' do
+ allow(mock_provider_service).to receive(:send_template) do |_phone, _template, message|
+ message.save!
+ 'msg_id'
+ end
+
+ whatsapp_service.perform
+
+ csat_message = whatsapp_conversation.messages.where(content_type: :input_csat).last
+ expect(csat_message.account).to eq(account)
+ expect(csat_message.inbox).to eq(whatsapp_inbox)
+ expect(csat_message.message_type).to eq('outgoing')
+ expect(csat_message.content).to eq('Please rate your experience')
+ expect(csat_message.content_type).to eq('input_csat')
+ end
+
+ it 'uses default message when not configured' do
+ setup_approved_template('test', { 'template' => { 'name' => 'test' } })
+ mock_successful_template_send('msg_id')
+
+ whatsapp_service.perform
+
+ csat_message = whatsapp_conversation.messages.where(content_type: :input_csat).last
+ expect(csat_message.content).to eq('Please rate this conversation')
+ end
+ end
+
+ context 'when template is not available or not approved' do
+ it 'falls back to regular survey when template is pending' do
+ setup_template_with_status('pending_template', 'PENDING')
+
+ whatsapp_service.perform
+
+ expect(MessageTemplates::Template::CsatSurvey).to have_received(:new).with(conversation: whatsapp_conversation)
+ expect(csat_template).to have_received(:perform)
+ end
+
+ it 'falls back to regular survey when template is rejected' do
+ setup_template_with_status('pending_template', 'REJECTED')
+
+ whatsapp_service.perform
+
+ expect(MessageTemplates::Template::CsatSurvey).to have_received(:new).with(conversation: whatsapp_conversation)
+ expect(csat_template).to have_received(:perform)
+ end
+
+ it 'falls back to regular survey when template API call fails' do
+ allow(mock_provider_service).to receive(:get_template_status)
+ .with('pending_template')
+ .and_return({ success: false, error: 'Template not found' })
+
+ whatsapp_service.perform
+
+ expect(MessageTemplates::Template::CsatSurvey).to have_received(:new).with(conversation: whatsapp_conversation)
+ expect(csat_template).to have_received(:perform)
+ end
+
+ it 'falls back to regular survey when template status check raises error' do
+ allow(mock_provider_service).to receive(:get_template_status)
+ .and_raise(StandardError, 'API connection failed')
+
+ whatsapp_service.perform
+
+ expect(MessageTemplates::Template::CsatSurvey).to have_received(:new).with(conversation: whatsapp_conversation)
+ expect(csat_template).to have_received(:perform)
+ end
+ end
+
+ context 'when no template is configured' do
+ it 'falls back to regular survey' do
+ whatsapp_service.perform
+
+ expect(MessageTemplates::Template::CsatSurvey).to have_received(:new).with(conversation: whatsapp_conversation)
+ expect(csat_template).to have_received(:perform)
+ end
+ end
+
+ context 'when template sending fails' do
+ before do
+ setup_approved_template('working_template', {
+ 'template' => { 'name' => 'working_template' },
+ 'message' => 'Rate us'
+ })
+ end
+
+ it 'handles template sending errors gracefully' do
+ mock_template_send_failure('Template send failed')
+
+ expect { whatsapp_service.perform }.not_to raise_error
+
+ # Should still create the CSAT message even if sending fails
+ csat_message = whatsapp_conversation.messages.where(content_type: :input_csat).last
+ expect(csat_message).to be_present
+ expect(csat_message.source_id).to be_nil
+ end
+
+ it 'does not update message when send_template returns nil' do
+ mock_template_send_with_no_id
+
+ whatsapp_service.perform
+
+ csat_message = whatsapp_conversation.messages.where(content_type: :input_csat).last
+ expect(csat_message).to be_present
+ expect(csat_message.source_id).to be_nil
+ end
+ end
+
+ context 'when outside messaging window' do
+ before do
+ allow(whatsapp_conversation).to receive(:can_reply?).and_return(false)
+ end
+
+ it 'sends template survey even when outside messaging window if template is approved' do
+ setup_approved_template('approved_template', { 'template' => { 'name' => 'approved_template' } })
+ mock_successful_template_send('msg_id')
+
+ whatsapp_service.perform
+
+ expect(mock_provider_service).to have_received(:send_template)
+ expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new)
+ # No activity message should be created when template is successfully sent
+ end
+
+ it 'creates activity message when template is not available and outside window' do
+ whatsapp_service.perform
+
+ expect(Conversations::ActivityMessageJob).to have_received(:perform_later).with(
+ whatsapp_conversation,
+ hash_including(content: I18n.t('conversations.activity.csat.not_sent_due_to_messaging_window'))
+ )
+ expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new)
+ end
+ end
+ end
+ end
+
+ private
+
+ def setup_approved_template(template_name, config = nil)
+ template_config = config || {
+ 'template' => {
+ 'name' => template_name,
+ 'language' => 'en'
+ },
+ 'message' => 'Please rate your experience'
+ }
+ whatsapp_inbox.update(csat_config: template_config)
+ allow(mock_provider_service).to receive(:get_template_status)
+ .with(template_name)
+ .and_return({ success: true, template: { status: 'APPROVED' } })
+ end
+
+ def setup_template_with_status(template_name, status)
+ whatsapp_inbox.update(csat_config: {
+ 'template' => { 'name' => template_name }
+ })
+ allow(mock_provider_service).to receive(:get_template_status)
+ .with(template_name)
+ .and_return({ success: true, template: { status: status } })
+ end
+
+ def mock_successful_template_send(message_id)
+ allow(mock_provider_service).to receive(:send_template) do |_phone, _template, message|
+ message.save!
+ message_id
+ end
+ end
+
+ def mock_template_send_failure(error_message = 'Template send failed')
+ allow(mock_provider_service).to receive(:send_template) do |_phone, _template, message|
+ message.save!
+ raise StandardError, error_message
+ end
+ end
+
+ def mock_template_send_with_no_id
+ allow(mock_provider_service).to receive(:send_template) do |_phone, _template, message|
+ message.save!
+ nil
+ end
end
end