feat: Add support for sending CSAT surveys via templates (Whatsapp Twilio) (#13143)
Fixes https://linear.app/chatwoot/issue/CW-6189/support-for-sending-csat-surveys-via-approved-whatsapp --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -9,11 +9,11 @@ RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request d
|
||||
end
|
||||
let(:whatsapp_inbox) { create(:inbox, channel: whatsapp_channel, account: account) }
|
||||
let(:web_widget_inbox) { create(:inbox, account: account) }
|
||||
let(:mock_service) { instance_double(Whatsapp::Providers::WhatsappCloudService) }
|
||||
let(:mock_service) { instance_double(Whatsapp::CsatTemplateService) }
|
||||
|
||||
before do
|
||||
create(:inbox_member, user: agent, inbox: whatsapp_inbox)
|
||||
allow(Whatsapp::Providers::WhatsappCloudService).to receive(:new).and_return(mock_service)
|
||||
allow(Whatsapp::CsatTemplateService).to receive(:new).and_return(mock_service)
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/accounts/{account.id}/inboxes/{inbox.id}/csat_template' do
|
||||
@@ -32,7 +32,7 @@ RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request d
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
expect(response.parsed_body['error']).to eq('CSAT template operations only available for WhatsApp channels')
|
||||
expect(response.parsed_body['error']).to eq('CSAT template operations only available for WhatsApp and Twilio WhatsApp channels')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -161,7 +161,7 @@ RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request d
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:bad_request)
|
||||
expect(response.parsed_body['error']).to eq('CSAT template operations only available for WhatsApp channels')
|
||||
expect(response.parsed_body['error']).to eq('CSAT template operations only available for WhatsApp and Twilio WhatsApp channels')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -195,11 +195,11 @@ RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request d
|
||||
|
||||
it 'creates template successfully' do
|
||||
allow(mock_service).to receive(:get_template_status).and_return({ success: false })
|
||||
allow(mock_service).to receive(:create_csat_template).and_return({
|
||||
success: true,
|
||||
template_name: "customer_satisfaction_survey_#{whatsapp_inbox.id}",
|
||||
template_id: '987654321'
|
||||
})
|
||||
allow(mock_service).to receive(:create_template).and_return({
|
||||
success: true,
|
||||
template_name: "customer_satisfaction_survey_#{whatsapp_inbox.id}",
|
||||
template_id: '987654321'
|
||||
})
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
headers: admin.create_new_auth_token,
|
||||
@@ -222,7 +222,7 @@ RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request d
|
||||
}
|
||||
|
||||
allow(mock_service).to receive(:get_template_status).and_return({ success: false })
|
||||
expect(mock_service).to receive(:create_csat_template) do |config|
|
||||
expect(mock_service).to receive(:create_template) do |config|
|
||||
expect(config[:button_text]).to eq('Please rate us')
|
||||
expect(config[:language]).to eq('en')
|
||||
expect(config[:template_name]).to eq("customer_satisfaction_survey_#{whatsapp_inbox.id}")
|
||||
@@ -249,11 +249,11 @@ RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request d
|
||||
}
|
||||
|
||||
allow(mock_service).to receive(:get_template_status).and_return({ success: false })
|
||||
allow(mock_service).to receive(:create_csat_template).and_return({
|
||||
success: false,
|
||||
error: 'Template creation failed',
|
||||
response_body: whatsapp_error_response.to_json
|
||||
})
|
||||
allow(mock_service).to receive(:create_template).and_return({
|
||||
success: false,
|
||||
error: 'Template creation failed',
|
||||
response_body: whatsapp_error_response.to_json
|
||||
})
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
headers: admin.create_new_auth_token,
|
||||
@@ -272,11 +272,11 @@ RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request d
|
||||
|
||||
it 'handles generic API errors' do
|
||||
allow(mock_service).to receive(:get_template_status).and_return({ success: false })
|
||||
allow(mock_service).to receive(:create_csat_template).and_return({
|
||||
success: false,
|
||||
error: 'Network timeout',
|
||||
response_body: nil
|
||||
})
|
||||
allow(mock_service).to receive(:create_template).and_return({
|
||||
success: false,
|
||||
error: 'Network timeout',
|
||||
response_body: nil
|
||||
})
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
headers: admin.create_new_auth_token,
|
||||
@@ -289,7 +289,7 @@ RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request d
|
||||
|
||||
it 'handles unexpected service errors' do
|
||||
allow(mock_service).to receive(:get_template_status).and_return({ success: false })
|
||||
allow(mock_service).to receive(:create_csat_template)
|
||||
allow(mock_service).to receive(:create_template)
|
||||
.and_raise(StandardError, 'Unexpected error')
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
@@ -312,10 +312,10 @@ RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request d
|
||||
allow(mock_service).to receive(:get_template_status)
|
||||
.with('existing_template')
|
||||
.and_return({ success: true, template: { id: '111111111' } })
|
||||
expect(mock_service).to receive(:delete_csat_template)
|
||||
expect(mock_service).to receive(:delete_template)
|
||||
.with('existing_template')
|
||||
.and_return({ success: true })
|
||||
expect(mock_service).to receive(:create_csat_template)
|
||||
expect(mock_service).to receive(:create_template)
|
||||
.and_return({
|
||||
success: true,
|
||||
template_name: "customer_satisfaction_survey_#{whatsapp_inbox.id}",
|
||||
@@ -336,13 +336,13 @@ RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request d
|
||||
})
|
||||
|
||||
allow(mock_service).to receive(:get_template_status).and_return({ success: true })
|
||||
allow(mock_service).to receive(:delete_csat_template)
|
||||
allow(mock_service).to receive(:delete_template)
|
||||
.and_return({ success: false, response_body: 'Delete failed' })
|
||||
allow(mock_service).to receive(:create_csat_template).and_return({
|
||||
success: true,
|
||||
template_name: "customer_satisfaction_survey_#{whatsapp_inbox.id}",
|
||||
template_id: '333333333'
|
||||
})
|
||||
allow(mock_service).to receive(:create_template).and_return({
|
||||
success: true,
|
||||
template_name: "customer_satisfaction_survey_#{whatsapp_inbox.id}",
|
||||
template_id: '333333333'
|
||||
})
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
headers: admin.create_new_auth_token,
|
||||
@@ -365,11 +365,11 @@ RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request d
|
||||
|
||||
it 'allows access when agent is assigned to inbox' do
|
||||
allow(mock_service).to receive(:get_template_status).and_return({ success: false })
|
||||
allow(mock_service).to receive(:create_csat_template).and_return({
|
||||
success: true,
|
||||
template_name: 'customer_satisfaction_survey',
|
||||
template_id: '444444444'
|
||||
})
|
||||
allow(mock_service).to receive(:create_template).and_return({
|
||||
success: true,
|
||||
template_name: 'customer_satisfaction_survey',
|
||||
template_id: '444444444'
|
||||
})
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
headers: agent.create_new_auth_token,
|
||||
|
||||
@@ -8,11 +8,8 @@ describe Twilio::SendOnTwilioService do
|
||||
let(:message_record_double) { double }
|
||||
|
||||
let!(:account) { create(:account) }
|
||||
let!(:widget_inbox) { create(:inbox, account: account) }
|
||||
let!(:twilio_sms) { create(:channel_twilio_sms, account: account) }
|
||||
let!(:twilio_whatsapp) { create(:channel_twilio_sms, medium: :whatsapp, account: account) }
|
||||
let!(:twilio_inbox) { create(:inbox, channel: twilio_sms, account: account) }
|
||||
let!(:twilio_whatsapp_inbox) { create(:inbox, channel: twilio_whatsapp, account: account) }
|
||||
let!(:contact) { create(:contact, account: account) }
|
||||
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: twilio_inbox) }
|
||||
let(:conversation) { create(:conversation, contact: contact, inbox: twilio_inbox, contact_inbox: contact_inbox) }
|
||||
@@ -23,6 +20,10 @@ describe Twilio::SendOnTwilioService do
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
let!(:widget_inbox) { create(:inbox, account: account) }
|
||||
let!(:twilio_whatsapp) { create(:channel_twilio_sms, medium: :whatsapp, account: account) }
|
||||
let!(:twilio_whatsapp_inbox) { create(:inbox, channel: twilio_whatsapp, account: account) }
|
||||
|
||||
context 'without reply' do
|
||||
it 'if message is private' do
|
||||
message = create(:message, message_type: 'outgoing', private: true, inbox: twilio_inbox, account: account)
|
||||
@@ -107,4 +108,146 @@ describe Twilio::SendOnTwilioService do
|
||||
expect(outgoing_message.reload.status).to eq('failed')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#send_csat_template_message' do
|
||||
let(:test_message) { create(:message, message_type: 'outgoing', inbox: twilio_inbox, account: account, conversation: conversation) }
|
||||
let(:service) { described_class.new(message: test_message) }
|
||||
let(:mock_twilio_message) { instance_double(Twilio::REST::Api::V2010::AccountContext::MessageInstance, sid: 'SM123456789') }
|
||||
|
||||
# Test parameters defined using let statements
|
||||
let(:test_params) do
|
||||
{
|
||||
phone_number: '+1234567890',
|
||||
content_sid: 'HX123456789',
|
||||
content_variables: { '1' => 'conversation-uuid-123' }
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
allow(twilio_sms).to receive(:send_message_from).and_return({ from: '+0987654321' })
|
||||
allow(twilio_sms).to receive(:respond_to?).and_return(true)
|
||||
allow(twilio_sms).to receive(:twilio_delivery_status_index_url).and_return('http://localhost:3000/twilio/delivery_status')
|
||||
end
|
||||
|
||||
context 'when template message is sent successfully' do
|
||||
before do
|
||||
allow(messages_double).to receive(:create).and_return(mock_twilio_message)
|
||||
end
|
||||
|
||||
it 'sends template message with correct parameters' do
|
||||
expected_params = {
|
||||
to: test_params[:phone_number],
|
||||
content_sid: test_params[:content_sid],
|
||||
content_variables: test_params[:content_variables].to_json,
|
||||
status_callback: 'http://localhost:3000/twilio/delivery_status',
|
||||
from: '+0987654321'
|
||||
}
|
||||
|
||||
result = service.send_csat_template_message(**test_params)
|
||||
|
||||
expect(messages_double).to have_received(:create).with(expected_params)
|
||||
expect(result).to eq({ success: true, message_id: 'SM123456789' })
|
||||
end
|
||||
|
||||
it 'sends template message without content variables when empty' do
|
||||
expected_params = {
|
||||
to: test_params[:phone_number],
|
||||
content_sid: test_params[:content_sid],
|
||||
status_callback: 'http://localhost:3000/twilio/delivery_status',
|
||||
from: '+0987654321'
|
||||
}
|
||||
|
||||
result = service.send_csat_template_message(
|
||||
phone_number: test_params[:phone_number],
|
||||
content_sid: test_params[:content_sid]
|
||||
)
|
||||
|
||||
expect(messages_double).to have_received(:create).with(expected_params)
|
||||
expect(result).to eq({ success: true, message_id: 'SM123456789' })
|
||||
end
|
||||
|
||||
it 'includes custom status callback when channel supports it' do
|
||||
allow(twilio_sms).to receive(:respond_to?).and_return(true)
|
||||
allow(twilio_sms).to receive(:twilio_delivery_status_index_url).and_return('https://example.com/webhook')
|
||||
|
||||
expected_params = {
|
||||
to: test_params[:phone_number],
|
||||
content_sid: test_params[:content_sid],
|
||||
content_variables: test_params[:content_variables].to_json,
|
||||
status_callback: 'https://example.com/webhook',
|
||||
from: '+0987654321'
|
||||
}
|
||||
|
||||
service.send_csat_template_message(**test_params)
|
||||
|
||||
expect(messages_double).to have_received(:create).with(expected_params)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when Twilio API returns an error' do
|
||||
before do
|
||||
allow(Rails.logger).to receive(:error)
|
||||
end
|
||||
|
||||
it 'handles Twilio::REST::TwilioError' do
|
||||
allow(messages_double).to receive(:create).and_raise(Twilio::REST::TwilioError, 'Invalid phone number')
|
||||
|
||||
result = service.send_csat_template_message(**test_params)
|
||||
|
||||
expect(result).to eq({ success: false, error: 'Invalid phone number' })
|
||||
expect(Rails.logger).to have_received(:error).with('Failed to send Twilio template message: Invalid phone number')
|
||||
end
|
||||
|
||||
it 'handles Twilio API errors' do
|
||||
allow(messages_double).to receive(:create).and_raise(Twilio::REST::TwilioError, 'Content template not found')
|
||||
|
||||
result = service.send_csat_template_message(**test_params)
|
||||
|
||||
expect(result).to eq({ success: false, error: 'Content template not found' })
|
||||
expect(Rails.logger).to have_received(:error).with('Failed to send Twilio template message: Content template not found')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with parameter handling' do
|
||||
before do
|
||||
allow(messages_double).to receive(:create).and_return(mock_twilio_message)
|
||||
end
|
||||
|
||||
it 'handles empty content_variables hash' do
|
||||
expected_params = {
|
||||
to: test_params[:phone_number],
|
||||
content_sid: test_params[:content_sid],
|
||||
status_callback: 'http://localhost:3000/twilio/delivery_status',
|
||||
from: '+0987654321'
|
||||
}
|
||||
|
||||
service.send_csat_template_message(
|
||||
phone_number: test_params[:phone_number],
|
||||
content_sid: test_params[:content_sid],
|
||||
content_variables: {}
|
||||
)
|
||||
|
||||
expect(messages_double).to have_received(:create).with(expected_params)
|
||||
end
|
||||
|
||||
it 'converts content_variables to JSON when present' do
|
||||
variables = { '1' => 'test-uuid', '2' => 'another-value' }
|
||||
expected_params = {
|
||||
to: test_params[:phone_number],
|
||||
content_sid: test_params[:content_sid],
|
||||
content_variables: variables.to_json,
|
||||
status_callback: 'http://localhost:3000/twilio/delivery_status',
|
||||
from: '+0987654321'
|
||||
}
|
||||
|
||||
service.send_csat_template_message(
|
||||
phone_number: test_params[:phone_number],
|
||||
content_sid: test_params[:content_sid],
|
||||
content_variables: variables
|
||||
)
|
||||
|
||||
expect(messages_double).to have_received(:create).with(expected_params)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user