Compare commits

...
Author SHA1 Message Date
Muhsin 8fee31912e chore: add language option 2025-11-02 14:29:27 +05:30
Muhsin 10fe518b39 chore: csat text 2025-11-02 14:24:22 +05:30
Muhsin 70c8496af8 chore: add status 2025-11-02 14:18:00 +05:30
iamsivin ffb432dfa0 chore: Update styles 2025-10-30 17:19:47 +05:30
Muhsin d3feffa199 Update CustomerSatisfactionPage.vue 2025-10-30 16:46:55 +05:30
Muhsin 742c1aad47 feat: update the design 2025-10-28 18:16:04 +05:30
Muhsin 2c9208c6a2 feat: add create_csat_template api 2025-10-28 15:50:23 +05:30
Muhsin 44ff579c5a feat: make the changes 2025-10-27 23:51:59 +05:30
14 changed files with 745 additions and 18 deletions
@@ -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 = [])
+10
View File
@@ -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."
@@ -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
@@ -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 = {
+8
View File
@@ -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
+69 -1
View File
@@ -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
+2
View File
@@ -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
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

+210
View File
@@ -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