Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fee31912e | ||
|
|
10fe518b39 | ||
|
|
70c8496af8 | ||
|
|
ffb432dfa0 | ||
|
|
d3feffa199 | ||
|
|
742c1aad47 | ||
|
|
2c9208c6a2 | ||
|
|
44ff579c5a |
@@ -45,6 +45,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
def update
|
||||
inbox_params = permitted_params.except(:channel, :csat_config)
|
||||
inbox_params[:csat_config] = format_csat_config(permitted_params[:csat_config]) if permitted_params[:csat_config].present?
|
||||
|
||||
@inbox.update!(inbox_params)
|
||||
update_inbox_working_hours
|
||||
update_channel if channel_update_required?
|
||||
@@ -87,8 +88,95 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def csat_template_status
|
||||
return render json: { error: 'CSAT template status only available for WhatsApp channels' }, status: :bad_request unless @inbox.whatsapp?
|
||||
|
||||
template_config = @inbox.csat_config&.dig('template')
|
||||
return render json: { template_exists: false } unless template_config
|
||||
|
||||
template_name = template_config['name'] || 'customer_satisfaction_survey'
|
||||
status_result = @inbox.channel.provider_service.get_template_status(template_name)
|
||||
|
||||
if status_result[:success]
|
||||
render json: {
|
||||
template_exists: true,
|
||||
template_name: template_name,
|
||||
status: status_result[:template][:status],
|
||||
template_id: status_result[:template][:id]
|
||||
}
|
||||
else
|
||||
render json: { template_exists: false, error: status_result[:error] }
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Error fetching CSAT template status: #{e.message}"
|
||||
render json: { error: e.message }, status: :internal_server_error
|
||||
end
|
||||
|
||||
def create_csat_template
|
||||
unless @inbox.whatsapp?
|
||||
return render json: { error: 'CSAT template creation only available for WhatsApp channels' },
|
||||
status: :unprocessable_content
|
||||
end
|
||||
|
||||
template_params = params.require(:template).permit(:message, :button_text)
|
||||
return render json: { error: 'Message is required' }, status: :unprocessable_content if template_params[:message].blank?
|
||||
|
||||
template_config = {
|
||||
message: template_params[:message],
|
||||
button_text: template_params[:button_text] || 'Please rate us',
|
||||
base_url: ENV.fetch('FRONTEND_URL', 'http://localhost:3000'),
|
||||
language: 'en'
|
||||
}
|
||||
|
||||
result = @inbox.channel.provider_service.create_csat_template(template_config)
|
||||
|
||||
if result[:success]
|
||||
render json: {
|
||||
template: {
|
||||
name: 'customer_satisfaction_survey',
|
||||
template_id: result[:template_id],
|
||||
status: 'PENDING',
|
||||
language: 'en'
|
||||
}
|
||||
}, status: :created
|
||||
else
|
||||
# Parse WhatsApp error details for user-friendly message
|
||||
whatsapp_error = parse_whatsapp_error(result[:response_body])
|
||||
render json: {
|
||||
error: whatsapp_error[:user_message] || result[:error],
|
||||
details: whatsapp_error[:technical_details]
|
||||
}, status: :unprocessable_content
|
||||
end
|
||||
rescue ActionController::ParameterMissing
|
||||
render json: { error: 'Template parameters are required' }, status: :unprocessable_content
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Error creating CSAT template: #{e.message}"
|
||||
render json: { error: 'Template creation failed' }, status: :internal_server_error
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_whatsapp_error(response_body)
|
||||
return { user_message: nil, technical_details: nil } if response_body.blank?
|
||||
|
||||
begin
|
||||
error_data = JSON.parse(response_body)
|
||||
whatsapp_error = error_data['error'] || {}
|
||||
|
||||
user_message = whatsapp_error['error_user_msg'] || whatsapp_error['message']
|
||||
technical_details = {
|
||||
code: whatsapp_error['code'],
|
||||
subcode: whatsapp_error['error_subcode'],
|
||||
type: whatsapp_error['type'],
|
||||
title: whatsapp_error['error_user_title']
|
||||
}.compact
|
||||
|
||||
{ user_message: user_message, technical_details: technical_details }
|
||||
rescue JSON::ParserError
|
||||
{ user_message: nil, technical_details: response_body }
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_inbox
|
||||
@inbox = Current.account.inboxes.find(params[:id])
|
||||
authorize @inbox, :show?
|
||||
@@ -152,21 +240,30 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def format_csat_config(config)
|
||||
{
|
||||
display_type: config['display_type'] || 'emoji',
|
||||
message: config['message'] || '',
|
||||
survey_rules: {
|
||||
operator: config.dig('survey_rules', 'operator') || 'contains',
|
||||
values: config.dig('survey_rules', 'values') || []
|
||||
formatted = {
|
||||
'display_type' => config['display_type'] || 'emoji',
|
||||
'message' => config['message'] || '',
|
||||
'button_text' => config['button_text'] || 'Please rate us',
|
||||
'language' => config['language'] || 'en',
|
||||
'survey_rules' => {
|
||||
'operator' => config.dig('survey_rules', 'operator') || 'contains',
|
||||
'values' => config.dig('survey_rules', 'values') || []
|
||||
}
|
||||
}
|
||||
|
||||
# Preserve existing template config if present
|
||||
formatted['template'] = config['template'] if config['template'].present?
|
||||
|
||||
formatted
|
||||
end
|
||||
|
||||
def inbox_attributes
|
||||
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
||||
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
|
||||
:lock_to_single_conversation, :portal_id, :sender_name_type, :business_name,
|
||||
{ csat_config: [:display_type, :message, { survey_rules: [:operator, { values: [] }] }] }]
|
||||
{ csat_config: [:display_type, :message, :button_text, :language,
|
||||
{ survey_rules: [:operator, { values: [] }],
|
||||
template: [:name, :template_id, :created_at, :language] }] }]
|
||||
end
|
||||
|
||||
def permitted_params(channel_attributes = [])
|
||||
|
||||
@@ -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}/create_csat_template`, {
|
||||
template,
|
||||
});
|
||||
}
|
||||
|
||||
getCSATTemplateStatus(inboxId) {
|
||||
return axios.get(`${this.url}/${inboxId}/csat_template_status`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new Inboxes();
|
||||
|
||||
@@ -792,6 +792,28 @@
|
||||
"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"
|
||||
},
|
||||
"TEMPLATE_CREATION": {
|
||||
"SUCCESS_MESSAGE": "WhatsApp template created successfully and sent for approval",
|
||||
"ERROR_MESSAGE": "Failed to create WhatsApp template"
|
||||
},
|
||||
"SURVEY_RULE": {
|
||||
"LABEL": "Survey rule",
|
||||
"DESCRIPTION_PREFIX": "Send the survey if the conversation",
|
||||
@@ -803,6 +825,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."
|
||||
|
||||
+212
-10
@@ -5,13 +5,18 @@ import { useAlert } from 'dashboard/composables';
|
||||
import { useStore, useMapGetter } from 'dashboard/composables/store';
|
||||
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 './components/CSATTemplate.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';
|
||||
|
||||
const props = defineProps({
|
||||
inbox: { type: Object, required: true },
|
||||
@@ -29,9 +34,14 @@ const state = reactive({
|
||||
csatSurveyEnabled: false,
|
||||
displayType: 'emoji',
|
||||
message: '',
|
||||
buttonText: 'Please rate us',
|
||||
surveyRuleOperator: 'contains',
|
||||
language: '',
|
||||
});
|
||||
|
||||
const templateStatus = ref(null);
|
||||
const templateLoading = ref(false);
|
||||
|
||||
const filterTypes = [
|
||||
{
|
||||
label: t('INBOX_MGMT.CSAT.SURVEY_RULE.OPERATOR.CONTAINS'),
|
||||
@@ -51,6 +61,55 @@ const labelOptions = computed(() =>
|
||||
: []
|
||||
);
|
||||
|
||||
const languageOptions = computed(() =>
|
||||
languages.map(({ name, id }) => ({ label: `${name} (${id})`, value: id }))
|
||||
);
|
||||
|
||||
const isWhatsAppChannel = computed(
|
||||
() => props.inbox?.channel_type === 'Channel::Whatsapp'
|
||||
);
|
||||
|
||||
const messagePreviewData = computed(() => ({
|
||||
content: state.message || t('INBOX_MGMT.CSAT.MESSAGE.PLACEHOLDER'),
|
||||
}));
|
||||
|
||||
const templateApprovalStatus = computed(() => {
|
||||
if (!templateStatus.value || !templateStatus.value.template_exists) {
|
||||
return {
|
||||
text: t('INBOX_MGMT.CSAT.TEMPLATE_STATUS.DEFAULT'),
|
||||
icon: 'i-lucide-stamp',
|
||||
color: 'text-gray-600',
|
||||
};
|
||||
}
|
||||
|
||||
switch (templateStatus.value.status) {
|
||||
case 'APPROVED':
|
||||
return {
|
||||
text: t('INBOX_MGMT.CSAT.TEMPLATE_STATUS.APPROVED'),
|
||||
icon: 'i-lucide-circle-check',
|
||||
color: 'text-green-600',
|
||||
};
|
||||
case 'PENDING':
|
||||
return {
|
||||
text: t('INBOX_MGMT.CSAT.TEMPLATE_STATUS.PENDING'),
|
||||
icon: 'i-lucide-clock',
|
||||
color: 'text-yellow-600',
|
||||
};
|
||||
case 'REJECTED':
|
||||
return {
|
||||
text: t('INBOX_MGMT.CSAT.TEMPLATE_STATUS.REJECTED'),
|
||||
icon: 'i-lucide-circle-x',
|
||||
color: 'text-red-600',
|
||||
};
|
||||
default:
|
||||
return {
|
||||
text: t('INBOX_MGMT.CSAT.TEMPLATE_STATUS.DEFAULT'),
|
||||
icon: 'i-lucide-stamp',
|
||||
color: 'text-gray-600',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const initializeState = () => {
|
||||
if (!props.inbox) return;
|
||||
|
||||
@@ -63,11 +122,15 @@ 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.buttonText = buttonText;
|
||||
state.language = language;
|
||||
state.surveyRuleOperator = surveyRules.operator || 'contains';
|
||||
|
||||
selectedLabelValues.value = Array.isArray(surveyRules.values)
|
||||
@@ -75,9 +138,26 @@ const initializeState = () => {
|
||||
: [];
|
||||
};
|
||||
|
||||
const checkTemplateStatus = async () => {
|
||||
if (!isWhatsAppChannel.value) return;
|
||||
|
||||
try {
|
||||
templateLoading.value = true;
|
||||
const response = await store.dispatch('inboxes/getCSATTemplateStatus', {
|
||||
inboxId: props.inbox.id,
|
||||
});
|
||||
templateStatus.value = response;
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (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 });
|
||||
@@ -115,13 +195,47 @@ const updateInbox = async attributes => {
|
||||
return store.dispatch('inboxes/updateInbox', payload);
|
||||
};
|
||||
|
||||
const createTemplate = async () => {
|
||||
if (!isWhatsAppChannel.value) return;
|
||||
|
||||
try {
|
||||
isUpdating.value = true;
|
||||
await store.dispatch('inboxes/createCSATTemplate', {
|
||||
inboxId: props.inbox.id,
|
||||
template: {
|
||||
message: state.message,
|
||||
button_text: state.buttonText,
|
||||
language: state.language,
|
||||
},
|
||||
});
|
||||
|
||||
// Check status after creation
|
||||
await checkTemplateStatus();
|
||||
useAlert(t('INBOX_MGMT.CSAT.TEMPLATE_CREATION.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error.response?.data?.error ||
|
||||
t('INBOX_MGMT.CSAT.TEMPLATE_CREATION.ERROR_MESSAGE');
|
||||
useAlert(errorMessage);
|
||||
} finally {
|
||||
isUpdating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveSettings = async () => {
|
||||
try {
|
||||
isUpdating.value = true;
|
||||
|
||||
// For WhatsApp channels, create template first if needed
|
||||
if (isWhatsAppChannel.value && state.csatSurveyEnabled) {
|
||||
await createTemplate();
|
||||
}
|
||||
|
||||
const csatConfig = {
|
||||
display_type: state.displayType,
|
||||
message: state.message,
|
||||
button_text: state.buttonText,
|
||||
language: state.language,
|
||||
survey_rules: {
|
||||
operator: state.surveyRuleOperator,
|
||||
values: selectedLabelValues.value,
|
||||
@@ -155,7 +269,9 @@ const saveSettings = async () => {
|
||||
</template>
|
||||
|
||||
<div class="grid gap-5">
|
||||
<!-- Show display type only for non-WhatsApp channels -->
|
||||
<WithLabel
|
||||
v-if="!isWhatsAppChannel"
|
||||
:label="$t('INBOX_MGMT.CSAT.DISPLAY_TYPE.LABEL')"
|
||||
name="display_type"
|
||||
>
|
||||
@@ -165,14 +281,96 @@ const saveSettings = async () => {
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
<WithLabel :label="$t('INBOX_MGMT.CSAT.MESSAGE.LABEL')" name="message">
|
||||
<Editor
|
||||
v-model="state.message"
|
||||
:placeholder="$t('INBOX_MGMT.CSAT.MESSAGE.PLACEHOLDER')"
|
||||
:max-length="200"
|
||||
class="w-full"
|
||||
/>
|
||||
</WithLabel>
|
||||
<template v-if="isWhatsAppChannel">
|
||||
<div
|
||||
class="flex flex-col gap-4 justify-between w-full md:flex-row md:gap-6"
|
||||
>
|
||||
<div class="flex flex-col flex-1 gap-3">
|
||||
<WithLabel
|
||||
:label="$t('INBOX_MGMT.CSAT.MESSAGE.LABEL')"
|
||||
name="message"
|
||||
>
|
||||
<Editor
|
||||
v-model="state.message"
|
||||
:placeholder="$t('INBOX_MGMT.CSAT.MESSAGE.PLACEHOLDER')"
|
||||
:max-length="200"
|
||||
class="w-full"
|
||||
/>
|
||||
</WithLabel>
|
||||
<Input
|
||||
v-model="state.buttonText"
|
||||
:label="$t('INBOX_MGMT.CSAT.BUTTON_TEXT.LABEL')"
|
||||
:placeholder="$t('INBOX_MGMT.CSAT.BUTTON_TEXT.PLACEHOLDER')"
|
||||
class="w-full"
|
||||
/>
|
||||
|
||||
<WithLabel
|
||||
:label="$t('INBOX_MGMT.CSAT.LANGUAGE.LABEL')"
|
||||
name="language"
|
||||
>
|
||||
<ComboBox
|
||||
v-model="state.language"
|
||||
:options="languageOptions"
|
||||
:placeholder="$t('INBOX_MGMT.CSAT.LANGUAGE.PLACEHOLDER')"
|
||||
/>
|
||||
</WithLabel>
|
||||
|
||||
<div
|
||||
v-if="templateApprovalStatus"
|
||||
class="flex gap-2 items-center mt-4"
|
||||
>
|
||||
<Icon
|
||||
:icon="templateApprovalStatus.icon"
|
||||
:class="templateApprovalStatus.color"
|
||||
class="size-4"
|
||||
/>
|
||||
<span
|
||||
:class="templateApprovalStatus.color"
|
||||
class="text-sm font-medium"
|
||||
>
|
||||
{{ templateApprovalStatus.text }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col justify-start items-center p-6 mt-1 rounded-xl bg-n-slate-2 outline outline-1 outline-n-weak"
|
||||
>
|
||||
<p
|
||||
class="inline-flex items-center text-sm font-medium text-n-slate-11"
|
||||
>
|
||||
{{ $t('INBOX_MGMT.CSAT.MESSAGE_PREVIEW.LABEL') }}
|
||||
<Icon
|
||||
v-tooltip.top-end="
|
||||
$t('INBOX_MGMT.CSAT.MESSAGE_PREVIEW.TOOLTIP')
|
||||
"
|
||||
icon="i-lucide-info"
|
||||
class="flex-shrink-0 mx-1 size-4"
|
||||
/>
|
||||
</p>
|
||||
<CSATTemplate
|
||||
:message="messagePreviewData"
|
||||
:button-text="state.buttonText"
|
||||
class="pt-12"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Non-WhatsApp channels layout -->
|
||||
<template v-else>
|
||||
<WithLabel
|
||||
:label="$t('INBOX_MGMT.CSAT.MESSAGE.LABEL')"
|
||||
name="message"
|
||||
>
|
||||
<Editor
|
||||
v-model="state.message"
|
||||
:placeholder="$t('INBOX_MGMT.CSAT.MESSAGE.PLACEHOLDER')"
|
||||
:max-length="200"
|
||||
class="w-full"
|
||||
/>
|
||||
</WithLabel>
|
||||
</template>
|
||||
|
||||
<WithLabel
|
||||
:label="$t('INBOX_MGMT.CSAT.SURVEY_RULE.LABEL')"
|
||||
@@ -180,7 +378,7 @@ const saveSettings = async () => {
|
||||
>
|
||||
<div class="mb-4">
|
||||
<span
|
||||
class="inline-flex flex-wrap items-center gap-1.5 text-sm text-n-slate-12"
|
||||
class="inline-flex flex-wrap gap-1.5 items-center text-sm text-n-slate-12"
|
||||
>
|
||||
{{ $t('INBOX_MGMT.CSAT.SURVEY_RULE.DESCRIPTION_PREFIX') }}
|
||||
<FilterSelect
|
||||
@@ -217,7 +415,11 @@ const saveSettings = async () => {
|
||||
</div>
|
||||
</WithLabel>
|
||||
<p class="text-sm italic text-n-slate-11">
|
||||
{{ $t('INBOX_MGMT.CSAT.NOTE') }}
|
||||
{{
|
||||
isWhatsAppChannel
|
||||
? $t('INBOX_MGMT.CSAT.WHATSAPP_NOTE')
|
||||
: $t('INBOX_MGMT.CSAT.NOTE')
|
||||
}}
|
||||
</p>
|
||||
<div>
|
||||
<NextButton
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<script setup>
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
defineProps({
|
||||
message: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
buttonText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2.5 text-n-slate-12 max-w-80">
|
||||
<div class="p-3 rounded-xl bg-n-alpha-2">
|
||||
<span
|
||||
v-dompurify-html="message.content"
|
||||
class="text-sm font-medium prose prose-bubble"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button :label="buttonText" slate class="!text-n-blue-text w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -332,6 +332,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 = {
|
||||
|
||||
@@ -65,4 +65,12 @@ class InboxPolicy < ApplicationPolicy
|
||||
def health?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def csat_template_status?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def create_csat_template?
|
||||
@account_user.administrator?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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,72 @@ 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'] || 'customer_satisfaction_survey'
|
||||
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'] || 'customer_satisfaction_survey'
|
||||
|
||||
phone_number = conversation.contact_inbox.source_id
|
||||
template_info = build_template_info(template_name, template_config)
|
||||
message = create_csat_message
|
||||
|
||||
inbox.channel.provider_service.send_template(phone_number, template_info, message)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Error sending WhatsApp CSAT template: #{e.message}"
|
||||
handle_template_send_failure
|
||||
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 create_csat_message
|
||||
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
|
||||
)
|
||||
message.save!
|
||||
message
|
||||
end
|
||||
|
||||
def handle_template_send_failure
|
||||
if within_messaging_window?
|
||||
::MessageTemplates::Template::CsatSurvey.new(conversation: conversation).perform
|
||||
else
|
||||
create_csat_not_sent_activity_message
|
||||
end
|
||||
end
|
||||
|
||||
def create_csat_not_sent_activity_message
|
||||
content = I18n.t('conversations.activity.csat.not_sent_due_to_messaging_window')
|
||||
activity_message_params = {
|
||||
|
||||
@@ -58,6 +58,77 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
|
||||
response.success?
|
||||
end
|
||||
|
||||
def create_csat_template(template_config)
|
||||
request_body = {
|
||||
name: 'customer_satisfaction_survey',
|
||||
language: template_config[:language] || 'en',
|
||||
category: 'UTILITY',
|
||||
components: [
|
||||
{
|
||||
type: 'BODY',
|
||||
text: template_config[:message]
|
||||
},
|
||||
{
|
||||
type: 'BUTTONS',
|
||||
buttons: [
|
||||
{
|
||||
type: 'URL',
|
||||
text: template_config[:button_text] || 'Please rate us',
|
||||
url: "#{template_config[:base_url]}/survey/responses/{{1}}",
|
||||
example: ["#{template_config[:base_url]}/survey/responses/12345"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
response = HTTParty.post(
|
||||
"#{business_account_path}/message_templates",
|
||||
headers: api_headers,
|
||||
body: request_body.to_json
|
||||
)
|
||||
|
||||
if response.success?
|
||||
{
|
||||
success: true,
|
||||
template_id: response['id'],
|
||||
template_name: 'customer_satisfaction_survey',
|
||||
status: 'PENDING'
|
||||
}
|
||||
else
|
||||
Rails.logger.error "WhatsApp template creation failed: #{response.code} - #{response.body}"
|
||||
{
|
||||
success: false,
|
||||
error: error_message(response) || "Failed to create template: #{response.code}",
|
||||
response_body: response.body
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def get_template_status(template_name)
|
||||
url = "#{business_account_path}/message_templates?name=#{template_name}&access_token=#{whatsapp_channel.provider_config['api_key']}"
|
||||
response = HTTParty.get(url)
|
||||
|
||||
return { success: false, error: 'API request failed' } unless response.success?
|
||||
|
||||
templates = response['data'] || []
|
||||
template = templates.find { |t| t['name'] == template_name }
|
||||
|
||||
if template
|
||||
{
|
||||
success: true,
|
||||
template: {
|
||||
id: template['id'],
|
||||
name: template['name'],
|
||||
status: template['status'],
|
||||
language: template['language']
|
||||
}
|
||||
}
|
||||
else
|
||||
{ success: false, error: 'Template not found' }
|
||||
end
|
||||
end
|
||||
|
||||
def api_headers
|
||||
{ 'Authorization' => "Bearer #{whatsapp_channel.provider_config['api_key']}", 'Content-Type' => 'application/json' }
|
||||
end
|
||||
|
||||
@@ -196,6 +196,8 @@ Rails.application.routes.draw do
|
||||
delete :avatar, on: :member
|
||||
post :sync_templates, on: :member
|
||||
get :health, on: :member
|
||||
get :csat_template_status, on: :member
|
||||
post :create_csat_template, on: :member
|
||||
end
|
||||
resources :inbox_members, only: [:create, :show], param: :inbox_id do
|
||||
collection do
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 689 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 197 KiB |
@@ -0,0 +1,210 @@
|
||||
# CSAT Surveys via WhatsApp Message Templates
|
||||
|
||||
## Overview
|
||||
|
||||
To ensure successful delivery of CSAT (Customer Satisfaction) surveys via WhatsApp, particularly after the 24-hour customer interaction window, messages must be sent using approved WhatsApp message templates. This spec outlines support for using templates, including automation and service integration.
|
||||
|
||||
## Problem
|
||||
|
||||
Messages sent after 24 hours without an approved template result in delivery failures. Currently, CSAT surveys are completely disabled for WhatsApp channels after the 24-hour messaging window expires.
|
||||
|
||||
## Solution
|
||||
|
||||
Support free-form content input and create WhatsApp message templates automatically in the background. Users configure their CSAT message through the existing UI, and the system handles template creation and approval workflow via WhatsApp Business API.
|
||||
|
||||
**Key Design Decisions:**
|
||||
- Templates cannot be edited once submitted to WhatsApp - editing requires deleting the old template and creating a new one
|
||||
- Template lifecycle is managed automatically - new templates overwrite previous ones
|
||||
- **Template Priority**: Approved templates are always preferred over regular CSAT messages, regardless of messaging window
|
||||
- Fallback to regular CSAT only when templates are unavailable and within messaging window
|
||||
|
||||
## Data Storage
|
||||
|
||||
Template information is stored in the channel's `csat_config` JSONB field:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Hello! Can you please take this quick survey and provide us with your feedback.",
|
||||
"display_type": "emoji",
|
||||
"survey_rules": { ... },
|
||||
"template": {
|
||||
"name": "customer_satisfaction_survey",
|
||||
"template_id": "123456789",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"language": "en"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Template Naming:**
|
||||
- **Hardcoded template name**: `customer_satisfaction_survey`
|
||||
- **Single template per channel**: No multiple templates support
|
||||
- **Language handling**: Deferred to future implementation
|
||||
|
||||
**Note:** Template status is checked in real-time via API calls, not stored in the database.
|
||||
|
||||
## Template Structure
|
||||
|
||||
Template uses conversation UUID as parameter:
|
||||
- **Message**: User-configured message content
|
||||
- **Button URL**: `{{base_url}}/survey/responses/{{1}}` where `{{1}}` is the conversation UUID
|
||||
- **Button Text**: User-configured button text (default: "Please rate us")
|
||||
|
||||
#### Template Creation API
|
||||
|
||||
Send a POST request to the WhatsApp Business Account > Message Templates endpoint to create a template.
|
||||
|
||||
Request Syntax
|
||||
|
||||
POST /<WHATSAPP_BUSINESS_ACCOUNT_ID>/message_templates
|
||||
|
||||
Post Body
|
||||
|
||||
{
|
||||
"name": "customer_satisfaction_survey",
|
||||
"category": "MARKETING",
|
||||
"language": "<LANGUAGE>",
|
||||
"components": [<COMPONENTS>]
|
||||
}
|
||||
|
||||
```
|
||||
curl --location 'https://graph.facebook.com/v22.0/{{business_account_id}}/message_templates' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "customer_satisfaction_survey",
|
||||
"language": "en",
|
||||
"category": "MARKETING",
|
||||
"components": [
|
||||
{
|
||||
"type": "BODY",
|
||||
"text": "Hello! Can you please take this quick survey and provide us with your feedback."
|
||||
},
|
||||
{
|
||||
"type": "BUTTONS",
|
||||
"buttons": [
|
||||
{
|
||||
"type": "URL",
|
||||
"text": "Please rate us",
|
||||
"url": "{{base_url}}/survey/responses/{{1}}",
|
||||
"example": [
|
||||
"12345"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
## Service Integration
|
||||
|
||||
### CsatSurveyService Modifications
|
||||
|
||||
Extend `app/services/csat_survey_service.rb` to handle WhatsApp templates:
|
||||
|
||||
1. **Template Check**: Check if template exists in `csat_config` and verify status via real-time API call
|
||||
2. **Template Priority Logic**:
|
||||
- If template exists and approved: Always send template (regardless of messaging window)
|
||||
- If no template or not approved: Fall back to regular CSAT within messaging window
|
||||
- If outside window and no approved template: Create activity message
|
||||
3. **Survey Rules**: Apply existing label-based survey rules before sending
|
||||
|
||||
### WhatsApp Provider Integration
|
||||
|
||||
Modify `app/services/whatsapp/send_on_whatsapp_service.rb` to support CSAT templates:
|
||||
- Add CSAT template sending capability
|
||||
- Use conversation UUID as template parameter
|
||||
- Handle template-specific error cases
|
||||
|
||||
### Template Management
|
||||
|
||||
**Creation Workflow:**
|
||||
1. User updates CSAT configuration in settings
|
||||
2. System creates template via WhatsApp Business API
|
||||
3. Template info stored in `csat_config` (without status)
|
||||
4. Old templates are automatically replaced
|
||||
|
||||
**Send Survey Logic:**
|
||||
1. Conversation is resolved
|
||||
2. Check existing survey rules (labels, etc.)
|
||||
3. Check if template exists and get status via API call
|
||||
4. **Template Priority:**
|
||||
- If template approved: Send template (regardless of messaging window)
|
||||
- If no template or not approved:
|
||||
- Within messaging window: Send regular CSAT message
|
||||
- Outside messaging window: Create activity message
|
||||
|
||||
## Error Handling & Fallback
|
||||
|
||||
**Template Creation Failures:**
|
||||
- Display error message in frontend
|
||||
- Log error details for debugging
|
||||
- Continue using regular CSAT within messaging window
|
||||
|
||||
**Template Sending Failures:**
|
||||
- Log failure reason
|
||||
- Create activity message indicating survey couldn't be sent
|
||||
- Track failure metrics for monitoring
|
||||
|
||||
**Fallback Strategy:**
|
||||
- **Template Priority**: Always prefer approved templates over regular CSAT messages
|
||||
- **No Template Available**:
|
||||
- Within messaging window: Send regular CSAT message
|
||||
- Outside messaging window: Create activity message
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
**Scope:**
|
||||
- WhatsApp Cloud API channels only (primary focus)
|
||||
- 360Dialog provider is deprecated and not supported
|
||||
- Future extension to other WhatsApp providers like twilio can be considered
|
||||
|
||||
**Provider Support:**
|
||||
- Implement in `app/services/whatsapp/providers/whatsapp_cloud_service.rb`
|
||||
- Use existing template management methods
|
||||
|
||||
## Template Status Checking
|
||||
|
||||
**Real-time Status API:**
|
||||
|
||||
Check template status before sending surveys:
|
||||
|
||||
```bash
|
||||
curl --location 'https://graph.facebook.com/v20.0/{{business_account_id}}/message_templates?name={{template_name}}&access_token={{access_token}}'
|
||||
```
|
||||
|
||||
Example:
|
||||
```bash
|
||||
curl --location 'https://graph.facebook.com/v20.0/1189403312549467/message_templates?name=customer_satisfaction_survey&access_token={{access_token}}'
|
||||
```
|
||||
|
||||
**Response Format:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"name": "customer_satisfaction_survey",
|
||||
"status": "APPROVED|PENDING|REJECTED|DISABLED",
|
||||
"id": "123456789",
|
||||
"language": "en",
|
||||
"category": "MARKETING"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation Points:**
|
||||
|
||||
1. **Frontend Configuration Page**: Check template status when user visits CSAT settings to show approval status
|
||||
2. **Before Survey Sending**: Real-time API call to verify template is approved - if approved, always use template
|
||||
3. **Caching Strategy**: Consider short-term caching (5-10 minutes) to avoid excessive API calls during high-volume periods
|
||||
4. **Template Priority**: Approved templates bypass messaging window restrictions and are always sent
|
||||
|
||||
**Analytics:**
|
||||
- Template usage tracking not included in initial implementation
|
||||
- Regular CSAT analytics remain unchanged
|
||||
- Can be added in future iterations
|
||||
|
||||
**Enterprise Compatibility:**
|
||||
- No specific enterprise overrides required
|
||||
- Standard CSAT enterprise policies apply
|
||||
Reference in New Issue
Block a user