feat: Add backend changes for whatsapp csat template (#12984)
This PR add the backend changes for the feature [sending CSAT surveys via WhatsApp message templates ](https://github.com/chatwoot/chatwoot/pull/12787) --------- Co-authored-by: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Api::V1::Accounts::InboxCsatTemplatesController, type: :request do
|
||||
let(:account) { create(:account) }
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
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) }
|
||||
let(:web_widget_inbox) { create(:inbox, account: account) }
|
||||
let(:mock_service) { instance_double(Whatsapp::Providers::WhatsappCloudService) }
|
||||
|
||||
before do
|
||||
create(:inbox_member, user: agent, inbox: whatsapp_inbox)
|
||||
allow(Whatsapp::Providers::WhatsappCloudService).to receive(:new).and_return(mock_service)
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/accounts/{account.id}/inboxes/{inbox.id}/csat_template' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is not a WhatsApp channel' do
|
||||
it 'returns bad request' do
|
||||
get "/api/v1/accounts/#{account.id}/inboxes/#{web_widget_inbox.id}/csat_template",
|
||||
headers: admin.create_new_auth_token,
|
||||
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')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is a WhatsApp channel' do
|
||||
it 'returns template not found when no configuration exists' do
|
||||
get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['template_exists']).to be false
|
||||
end
|
||||
|
||||
it 'returns template status when template exists on WhatsApp' do
|
||||
template_config = {
|
||||
'template' => {
|
||||
'name' => 'custom_survey_template',
|
||||
'template_id' => '123456789',
|
||||
'language' => 'en'
|
||||
}
|
||||
}
|
||||
whatsapp_inbox.update!(csat_config: template_config)
|
||||
|
||||
allow(mock_service).to receive(:get_template_status)
|
||||
.with('custom_survey_template')
|
||||
.and_return({
|
||||
success: true,
|
||||
template: { id: '123456789', status: 'APPROVED' }
|
||||
})
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = response.parsed_body
|
||||
expect(response_data['template_exists']).to be true
|
||||
expect(response_data['template_name']).to eq('custom_survey_template')
|
||||
expect(response_data['status']).to eq('APPROVED')
|
||||
expect(response_data['template_id']).to eq('123456789')
|
||||
end
|
||||
|
||||
it 'returns template not found when template does not exist on WhatsApp' do
|
||||
template_config = { 'template' => { 'name' => 'custom_survey_template' } }
|
||||
whatsapp_inbox.update!(csat_config: template_config)
|
||||
|
||||
allow(mock_service).to receive(:get_template_status)
|
||||
.with('custom_survey_template')
|
||||
.and_return({ success: false, error: 'Template not found' })
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = response.parsed_body
|
||||
expect(response_data['template_exists']).to be false
|
||||
expect(response_data['error']).to eq('Template not found')
|
||||
end
|
||||
|
||||
it 'handles service errors gracefully' do
|
||||
template_config = { 'template' => { 'name' => 'custom_survey_template' } }
|
||||
whatsapp_inbox.update!(csat_config: template_config)
|
||||
|
||||
allow(mock_service).to receive(:get_template_status)
|
||||
.and_raise(StandardError, 'API connection failed')
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:internal_server_error)
|
||||
expect(response.parsed_body['error']).to eq('API connection failed')
|
||||
end
|
||||
|
||||
it 'returns unauthorized when agent is not assigned to inbox' do
|
||||
other_agent = create(:user, account: account, role: :agent)
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
headers: other_agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
it 'allows access when agent is assigned to inbox' do
|
||||
whatsapp_inbox.update!(csat_config: { 'template' => { 'name' => 'test' } })
|
||||
allow(mock_service).to receive(:get_template_status)
|
||||
.and_return({ success: true, template: { id: '123', status: 'APPROVED' } })
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/inboxes/{inbox.id}/csat_template' do
|
||||
let(:valid_template_params) do
|
||||
{
|
||||
template: {
|
||||
message: 'How would you rate your experience?',
|
||||
button_text: 'Rate Us',
|
||||
language: 'en'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
params: valid_template_params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is not a WhatsApp channel' do
|
||||
it 'returns bad request' do
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{web_widget_inbox.id}/csat_template",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: valid_template_params,
|
||||
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')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is a WhatsApp channel' do
|
||||
it 'returns error when message is missing' do
|
||||
invalid_params = {
|
||||
template: {
|
||||
button_text: 'Rate Us',
|
||||
language: 'en'
|
||||
}
|
||||
}
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: invalid_params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Message is required')
|
||||
end
|
||||
|
||||
it 'returns error when template parameters are completely missing' do
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: {},
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Template parameters are required')
|
||||
end
|
||||
|
||||
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'
|
||||
})
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: valid_template_params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:created)
|
||||
response_data = response.parsed_body
|
||||
expect(response_data['template']['name']).to eq("customer_satisfaction_survey_#{whatsapp_inbox.id}")
|
||||
expect(response_data['template']['template_id']).to eq('987654321')
|
||||
expect(response_data['template']['status']).to eq('PENDING')
|
||||
expect(response_data['template']['language']).to eq('en')
|
||||
end
|
||||
|
||||
it 'uses default values for optional parameters' do
|
||||
minimal_params = {
|
||||
template: {
|
||||
message: 'How would you rate your experience?'
|
||||
}
|
||||
}
|
||||
|
||||
allow(mock_service).to receive(:get_template_status).and_return({ success: false })
|
||||
expect(mock_service).to receive(:create_csat_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}")
|
||||
{ success: true, template_name: "customer_satisfaction_survey_#{whatsapp_inbox.id}", template_id: '123' }
|
||||
end
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: minimal_params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:created)
|
||||
end
|
||||
|
||||
it 'handles WhatsApp API errors with user-friendly messages' do
|
||||
whatsapp_error_response = {
|
||||
'error' => {
|
||||
'code' => 100,
|
||||
'error_subcode' => 2_388_092,
|
||||
'message' => 'Invalid parameter',
|
||||
'error_user_title' => 'Template Creation Failed',
|
||||
'error_user_msg' => 'The template message contains invalid content. Please review your message and try again.'
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: valid_template_params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
response_data = response.parsed_body
|
||||
expect(response_data['error']).to eq('The template message contains invalid content. Please review your message and try again.')
|
||||
expect(response_data['details']).to include({
|
||||
'code' => 100,
|
||||
'subcode' => 2_388_092,
|
||||
'title' => 'Template Creation Failed'
|
||||
})
|
||||
end
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: valid_template_params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.parsed_body['error']).to eq('Network timeout')
|
||||
end
|
||||
|
||||
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)
|
||||
.and_raise(StandardError, 'Unexpected error')
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: valid_template_params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:internal_server_error)
|
||||
expect(response.parsed_body['error']).to eq('Template creation failed')
|
||||
end
|
||||
|
||||
it 'deletes existing template before creating new one' do
|
||||
whatsapp_inbox.update!(csat_config: {
|
||||
'template' => {
|
||||
'name' => 'existing_template',
|
||||
'template_id' => '111111111'
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
.with('existing_template')
|
||||
.and_return({ success: true })
|
||||
expect(mock_service).to receive(:create_csat_template)
|
||||
.and_return({
|
||||
success: true,
|
||||
template_name: "customer_satisfaction_survey_#{whatsapp_inbox.id}",
|
||||
template_id: '222222222'
|
||||
})
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: valid_template_params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:created)
|
||||
end
|
||||
|
||||
it 'continues with creation even if deletion fails' do
|
||||
whatsapp_inbox.update!(csat_config: {
|
||||
'template' => { 'name' => 'existing_template' }
|
||||
})
|
||||
|
||||
allow(mock_service).to receive(:get_template_status).and_return({ success: true })
|
||||
allow(mock_service).to receive(:delete_csat_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'
|
||||
})
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
headers: admin.create_new_auth_token,
|
||||
params: valid_template_params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:created)
|
||||
end
|
||||
|
||||
it 'returns unauthorized when agent is not assigned to inbox' do
|
||||
other_agent = create(:user, account: account, role: :agent)
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
headers: other_agent.create_new_auth_token,
|
||||
params: valid_template_params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
|
||||
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'
|
||||
})
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/inboxes/#{whatsapp_inbox.id}/csat_template",
|
||||
headers: agent.create_new_auth_token,
|
||||
params: valid_template_params,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:created)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -804,6 +804,101 @@ RSpec.describe 'Inboxes API', type: :request do
|
||||
expect(found_inbox['csat_config']['display_type']).to eq('emoji')
|
||||
end
|
||||
end
|
||||
|
||||
it 'successfully updates inbox with template configuration' do
|
||||
csat_config_with_template = csat_config.merge({
|
||||
'template' => {
|
||||
'name' => 'custom_survey_template',
|
||||
'template_id' => '123456789',
|
||||
'language' => 'en',
|
||||
'created_at' => Time.current.iso8601
|
||||
}
|
||||
})
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}",
|
||||
params: {
|
||||
csat_survey_enabled: true,
|
||||
csat_config: csat_config_with_template
|
||||
},
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
inbox.reload
|
||||
template_config = inbox.csat_config['template']
|
||||
expect(template_config).to be_present
|
||||
expect(template_config['name']).to eq('custom_survey_template')
|
||||
expect(template_config['template_id']).to eq('123456789')
|
||||
expect(template_config['language']).to eq('en')
|
||||
end
|
||||
|
||||
it 'returns template configuration in inbox details' do
|
||||
csat_config_with_template = csat_config.merge({
|
||||
'template' => {
|
||||
'name' => 'custom_survey_template',
|
||||
'template_id' => '123456789',
|
||||
'language' => 'en',
|
||||
'created_at' => Time.current.iso8601
|
||||
}
|
||||
})
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}",
|
||||
params: {
|
||||
csat_survey_enabled: true,
|
||||
csat_config: csat_config_with_template
|
||||
},
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}",
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
json_response = response.parsed_body
|
||||
template_config = json_response['csat_config']['template']
|
||||
|
||||
expect(template_config).to be_present
|
||||
expect(template_config['name']).to eq('custom_survey_template')
|
||||
expect(template_config['template_id']).to eq('123456789')
|
||||
expect(template_config['language']).to eq('en')
|
||||
expect(template_config['created_at']).to be_present
|
||||
end
|
||||
|
||||
it 'removes template configuration when not provided in update' do
|
||||
# First set up template configuration
|
||||
csat_config_with_template = csat_config.merge({
|
||||
'template' => {
|
||||
'name' => 'custom_survey_template',
|
||||
'template_id' => '123456789'
|
||||
}
|
||||
})
|
||||
|
||||
patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}",
|
||||
params: {
|
||||
csat_survey_enabled: true,
|
||||
csat_config: csat_config_with_template
|
||||
},
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
# Then update without template
|
||||
patch "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}",
|
||||
params: {
|
||||
csat_survey_enabled: true,
|
||||
csat_config: csat_config.merge({ 'message' => 'Updated message' })
|
||||
},
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
inbox.reload
|
||||
config = inbox.csat_config
|
||||
expect(config['message']).to eq('Updated message')
|
||||
expect(config['template']).to be_nil # Template should be removed when not provided
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
376
spec/services/whatsapp/csat_template_service_spec.rb
Normal file
376
spec/services/whatsapp/csat_template_service_spec.rb
Normal file
@@ -0,0 +1,376 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Whatsapp::CsatTemplateService do
|
||||
let(:account) { create(:account) }
|
||||
let(:whatsapp_channel) do
|
||||
create(:channel_whatsapp, account: account, provider: 'whatsapp_cloud', sync_templates: false, validate_provider_config: false)
|
||||
end
|
||||
let(:inbox) { create(:inbox, channel: whatsapp_channel, account: account) }
|
||||
let(:service) { described_class.new(whatsapp_channel) }
|
||||
|
||||
let(:expected_template_name) { "customer_satisfaction_survey_#{whatsapp_channel.inbox.id}" }
|
||||
let(:template_config) do
|
||||
{
|
||||
message: 'How would you rate your experience?',
|
||||
button_text: 'Rate Us',
|
||||
language: 'en',
|
||||
base_url: 'https://example.com',
|
||||
template_name: expected_template_name
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
allow(ENV).to receive(:fetch).and_call_original
|
||||
allow(ENV).to receive(:fetch).with('WHATSAPP_CLOUD_BASE_URL', anything).and_return('https://graph.facebook.com')
|
||||
end
|
||||
|
||||
describe '#generate_template_name' do
|
||||
context 'when no existing template' do
|
||||
it 'returns base name as-is' do
|
||||
whatsapp_channel.inbox.update!(csat_config: {})
|
||||
result = service.send(:generate_template_name, 'new_template_name')
|
||||
expect(result).to eq('new_template_name')
|
||||
end
|
||||
|
||||
it 'returns base name when template key is missing' do
|
||||
whatsapp_channel.inbox.update!(csat_config: { 'other_config' => 'value' })
|
||||
result = service.send(:generate_template_name, 'new_template_name')
|
||||
expect(result).to eq('new_template_name')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when existing template has no versioned name' do
|
||||
it 'starts versioning from 1' do
|
||||
whatsapp_channel.inbox.update!(csat_config: {
|
||||
'template' => { 'name' => expected_template_name }
|
||||
})
|
||||
result = service.send(:generate_template_name, 'new_template_name')
|
||||
expect(result).to eq("#{expected_template_name}_1")
|
||||
end
|
||||
|
||||
it 'starts versioning from 1 for custom name' do
|
||||
whatsapp_channel.inbox.update!(csat_config: {
|
||||
'template' => { 'name' => 'custom_survey' }
|
||||
})
|
||||
result = service.send(:generate_template_name, 'new_template_name')
|
||||
expect(result).to eq("#{expected_template_name}_1")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when existing template has versioned name' do
|
||||
it 'increments version number' do
|
||||
whatsapp_channel.inbox.update!(csat_config: {
|
||||
'template' => { 'name' => "#{expected_template_name}_1" }
|
||||
})
|
||||
result = service.send(:generate_template_name, 'new_template_name')
|
||||
expect(result).to eq("#{expected_template_name}_2")
|
||||
end
|
||||
|
||||
it 'increments higher version numbers' do
|
||||
whatsapp_channel.inbox.update!(csat_config: {
|
||||
'template' => { 'name' => "#{expected_template_name}_5" }
|
||||
})
|
||||
result = service.send(:generate_template_name, 'new_template_name')
|
||||
expect(result).to eq("#{expected_template_name}_6")
|
||||
end
|
||||
|
||||
it 'handles double digit version numbers' do
|
||||
whatsapp_channel.inbox.update!(csat_config: {
|
||||
'template' => { 'name' => "#{expected_template_name}_12" }
|
||||
})
|
||||
result = service.send(:generate_template_name, 'new_template_name')
|
||||
expect(result).to eq("#{expected_template_name}_13")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when existing template has non-matching versioned name' do
|
||||
it 'starts versioning from 1' do
|
||||
whatsapp_channel.inbox.update!(csat_config: {
|
||||
'template' => { 'name' => 'different_survey_3' }
|
||||
})
|
||||
result = service.send(:generate_template_name, 'new_template_name')
|
||||
expect(result).to eq("#{expected_template_name}_1")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when template name is blank' do
|
||||
it 'returns base name' do
|
||||
whatsapp_channel.inbox.update!(csat_config: {
|
||||
'template' => { 'name' => '' }
|
||||
})
|
||||
result = service.send(:generate_template_name, 'new_template_name')
|
||||
expect(result).to eq('new_template_name')
|
||||
end
|
||||
|
||||
it 'returns base name when template name is nil' do
|
||||
whatsapp_channel.inbox.update!(csat_config: {
|
||||
'template' => { 'name' => nil }
|
||||
})
|
||||
result = service.send(:generate_template_name, 'new_template_name')
|
||||
expect(result).to eq('new_template_name')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#build_template_request_body' do
|
||||
it 'builds correct request structure' do
|
||||
result = service.send(:build_template_request_body, template_config)
|
||||
|
||||
expect(result).to eq({
|
||||
name: expected_template_name,
|
||||
language: 'en',
|
||||
category: 'MARKETING',
|
||||
components: [
|
||||
{
|
||||
type: 'BODY',
|
||||
text: 'How would you rate your experience?'
|
||||
},
|
||||
{
|
||||
type: 'BUTTONS',
|
||||
buttons: [
|
||||
{
|
||||
type: 'URL',
|
||||
text: 'Rate Us',
|
||||
url: 'https://example.com/survey/responses/{{1}}',
|
||||
example: ['12345']
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
end
|
||||
|
||||
it 'uses default language when not provided' do
|
||||
config_without_language = template_config.except(:language)
|
||||
result = service.send(:build_template_request_body, config_without_language)
|
||||
expect(result[:language]).to eq('en')
|
||||
end
|
||||
|
||||
it 'uses default button text when not provided' do
|
||||
config_without_button = template_config.except(:button_text)
|
||||
result = service.send(:build_template_request_body, config_without_button)
|
||||
expect(result[:components][1][:buttons][0][:text]).to eq('Please rate us')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#create_template' do
|
||||
let(:mock_response) do
|
||||
# rubocop:disable RSpec/VerifiedDoubles
|
||||
double('response', :success? => true, :body => '{}', '[]' => { 'id' => '123', 'name' => 'template_name' })
|
||||
# rubocop:enable RSpec/VerifiedDoubles
|
||||
end
|
||||
|
||||
before do
|
||||
allow(HTTParty).to receive(:post).and_return(mock_response)
|
||||
inbox.update!(csat_config: {})
|
||||
end
|
||||
|
||||
it 'creates template with generated name' do
|
||||
expected_body = {
|
||||
name: expected_template_name,
|
||||
language: 'en',
|
||||
category: 'MARKETING',
|
||||
components: [
|
||||
{
|
||||
type: 'BODY',
|
||||
text: 'How would you rate your experience?'
|
||||
},
|
||||
{
|
||||
type: 'BUTTONS',
|
||||
buttons: [
|
||||
{
|
||||
type: 'URL',
|
||||
text: 'Rate Us',
|
||||
url: 'https://example.com/survey/responses/{{1}}',
|
||||
example: ['12345']
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
expect(HTTParty).to receive(:post).with(
|
||||
"https://graph.facebook.com/v14.0/#{whatsapp_channel.provider_config['business_account_id']}/message_templates",
|
||||
headers: {
|
||||
'Authorization' => "Bearer #{whatsapp_channel.provider_config['api_key']}",
|
||||
'Content-Type' => 'application/json'
|
||||
},
|
||||
body: expected_body.to_json
|
||||
)
|
||||
|
||||
service.create_template(template_config)
|
||||
end
|
||||
|
||||
it 'returns success response on successful creation' do
|
||||
allow(mock_response).to receive(:[]).with('id').and_return('template_123')
|
||||
allow(mock_response).to receive(:[]).with('name').and_return(expected_template_name)
|
||||
|
||||
result = service.create_template(template_config)
|
||||
|
||||
expect(result).to eq({
|
||||
success: true,
|
||||
template_id: 'template_123',
|
||||
template_name: expected_template_name,
|
||||
language: 'en',
|
||||
status: 'PENDING'
|
||||
})
|
||||
end
|
||||
|
||||
context 'when API call fails' do
|
||||
let(:error_response) do
|
||||
# rubocop:disable RSpec/VerifiedDoubles
|
||||
double('response', success?: false, code: 400, body: '{"error": "Invalid template"}')
|
||||
# rubocop:enable RSpec/VerifiedDoubles
|
||||
end
|
||||
|
||||
before do
|
||||
allow(HTTParty).to receive(:post).and_return(error_response)
|
||||
allow(Rails.logger).to receive(:error)
|
||||
end
|
||||
|
||||
it 'returns error response' do
|
||||
result = service.create_template(template_config)
|
||||
|
||||
expect(result).to eq({
|
||||
success: false,
|
||||
error: 'Template creation failed',
|
||||
response_body: '{"error": "Invalid template"}'
|
||||
})
|
||||
end
|
||||
|
||||
it 'logs the error' do
|
||||
expect(Rails.logger).to receive(:error).with('WhatsApp template creation failed: 400 - {"error": "Invalid template"}')
|
||||
service.create_template(template_config)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#delete_template' do
|
||||
it 'makes DELETE request to correct endpoint' do
|
||||
# rubocop:disable RSpec/VerifiedDoubles
|
||||
mock_response = double('response', success?: true, body: '{}')
|
||||
# rubocop:enable RSpec/VerifiedDoubles
|
||||
|
||||
expect(HTTParty).to receive(:delete).with(
|
||||
"https://graph.facebook.com/v14.0/#{whatsapp_channel.provider_config['business_account_id']}/message_templates?name=test_template",
|
||||
headers: {
|
||||
'Authorization' => "Bearer #{whatsapp_channel.provider_config['api_key']}",
|
||||
'Content-Type' => 'application/json'
|
||||
}
|
||||
).and_return(mock_response)
|
||||
|
||||
result = service.delete_template('test_template')
|
||||
expect(result).to eq({ success: true, response_body: '{}' })
|
||||
end
|
||||
|
||||
it 'uses default template name when none provided' do
|
||||
# rubocop:disable RSpec/VerifiedDoubles
|
||||
mock_response = double('response', success?: true, body: '{}')
|
||||
# rubocop:enable RSpec/VerifiedDoubles
|
||||
|
||||
expect(HTTParty).to receive(:delete).with(
|
||||
"https://graph.facebook.com/v14.0/#{whatsapp_channel.provider_config['business_account_id']}/message_templates?name=#{expected_template_name}",
|
||||
anything
|
||||
).and_return(mock_response)
|
||||
|
||||
service.delete_template
|
||||
end
|
||||
|
||||
it 'returns failure response when API call fails' do
|
||||
# rubocop:disable RSpec/VerifiedDoubles
|
||||
mock_response = double('response', success?: false, body: '{"error": "Template not found"}')
|
||||
# rubocop:enable RSpec/VerifiedDoubles
|
||||
allow(HTTParty).to receive(:delete).and_return(mock_response)
|
||||
|
||||
result = service.delete_template('test_template')
|
||||
expect(result).to eq({ success: false, response_body: '{"error": "Template not found"}' })
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_template_status' do
|
||||
it 'makes GET request to correct endpoint' do
|
||||
# rubocop:disable RSpec/VerifiedDoubles
|
||||
mock_response = double('response', success?: true, body: '{}')
|
||||
# rubocop:enable RSpec/VerifiedDoubles
|
||||
allow(mock_response).to receive(:[]).with('data').and_return([{
|
||||
'id' => '123',
|
||||
'name' => 'test_template',
|
||||
'status' => 'APPROVED',
|
||||
'language' => 'en'
|
||||
}])
|
||||
|
||||
expect(HTTParty).to receive(:get).with(
|
||||
"https://graph.facebook.com/v14.0/#{whatsapp_channel.provider_config['business_account_id']}/message_templates?name=test_template",
|
||||
headers: {
|
||||
'Authorization' => "Bearer #{whatsapp_channel.provider_config['api_key']}",
|
||||
'Content-Type' => 'application/json'
|
||||
}
|
||||
).and_return(mock_response)
|
||||
|
||||
service.get_template_status('test_template')
|
||||
end
|
||||
|
||||
it 'returns success response when template exists' do
|
||||
# rubocop:disable RSpec/VerifiedDoubles
|
||||
mock_response = double('response', success?: true, body: '{}')
|
||||
# rubocop:enable RSpec/VerifiedDoubles
|
||||
allow(mock_response).to receive(:[]).with('data').and_return([{
|
||||
'id' => '123',
|
||||
'name' => 'test_template',
|
||||
'status' => 'APPROVED',
|
||||
'language' => 'en'
|
||||
}])
|
||||
allow(HTTParty).to receive(:get).and_return(mock_response)
|
||||
|
||||
result = service.get_template_status('test_template')
|
||||
|
||||
expect(result).to eq({
|
||||
success: true,
|
||||
template: {
|
||||
id: '123',
|
||||
name: 'test_template',
|
||||
status: 'APPROVED',
|
||||
language: 'en'
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
it 'returns failure response when template not found' do
|
||||
# rubocop:disable RSpec/VerifiedDoubles
|
||||
mock_response = double('response', success?: true, body: '{}')
|
||||
# rubocop:enable RSpec/VerifiedDoubles
|
||||
allow(mock_response).to receive(:[]).with('data').and_return([])
|
||||
allow(HTTParty).to receive(:get).and_return(mock_response)
|
||||
|
||||
result = service.get_template_status('test_template')
|
||||
expect(result).to eq({ success: false, error: 'Template not found' })
|
||||
end
|
||||
|
||||
it 'returns failure response when API call fails' do
|
||||
# rubocop:disable RSpec/VerifiedDoubles
|
||||
mock_response = double('response', success?: false, body: '{}')
|
||||
# rubocop:enable RSpec/VerifiedDoubles
|
||||
allow(HTTParty).to receive(:get).and_return(mock_response)
|
||||
|
||||
result = service.get_template_status('test_template')
|
||||
expect(result).to eq({ success: false, error: 'Template not found' })
|
||||
end
|
||||
|
||||
context 'when API raises an exception' do
|
||||
before do
|
||||
allow(HTTParty).to receive(:get).and_raise(StandardError, 'Network error')
|
||||
allow(Rails.logger).to receive(:error)
|
||||
end
|
||||
|
||||
it 'handles exceptions gracefully' do
|
||||
result = service.get_template_status('test_template')
|
||||
expect(result).to eq({ success: false, error: 'Network error' })
|
||||
end
|
||||
|
||||
it 'logs the error' do
|
||||
expect(Rails.logger).to receive(:error).with('Error fetching template status: Network error')
|
||||
service.get_template_status('test_template')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -312,4 +312,92 @@ describe Whatsapp::Providers::WhatsappCloudService do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'CSAT template methods' do
|
||||
let(:mock_csat_template_service) { instance_double(Whatsapp::CsatTemplateService) }
|
||||
let(:expected_template_name) { "customer_satisfaction_survey_#{whatsapp_channel.inbox.id}" }
|
||||
let(:template_config) do
|
||||
{
|
||||
name: expected_template_name,
|
||||
language: 'en',
|
||||
category: 'UTILITY'
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
allow(Whatsapp::CsatTemplateService).to receive(:new)
|
||||
.with(whatsapp_channel)
|
||||
.and_return(mock_csat_template_service)
|
||||
end
|
||||
|
||||
describe '#create_csat_template' do
|
||||
it 'delegates to csat_template_service with correct config' do
|
||||
allow(mock_csat_template_service).to receive(:create_template)
|
||||
.with(template_config)
|
||||
.and_return({ success: true, template_id: '123' })
|
||||
|
||||
result = service.create_csat_template(template_config)
|
||||
|
||||
expect(mock_csat_template_service).to have_received(:create_template).with(template_config)
|
||||
expect(result).to eq({ success: true, template_id: '123' })
|
||||
end
|
||||
end
|
||||
|
||||
describe '#delete_csat_template' do
|
||||
it 'delegates to csat_template_service with default template name' do
|
||||
allow(mock_csat_template_service).to receive(:delete_template)
|
||||
.with(expected_template_name)
|
||||
.and_return({ success: true })
|
||||
|
||||
result = service.delete_csat_template
|
||||
|
||||
expect(mock_csat_template_service).to have_received(:delete_template).with(expected_template_name)
|
||||
expect(result).to eq({ success: true })
|
||||
end
|
||||
|
||||
it 'delegates to csat_template_service with custom template name' do
|
||||
custom_template_name = 'custom_csat_template'
|
||||
allow(mock_csat_template_service).to receive(:delete_template)
|
||||
.with(custom_template_name)
|
||||
.and_return({ success: true })
|
||||
|
||||
result = service.delete_csat_template(custom_template_name)
|
||||
|
||||
expect(mock_csat_template_service).to have_received(:delete_template).with(custom_template_name)
|
||||
expect(result).to eq({ success: true })
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_template_status' do
|
||||
it 'delegates to csat_template_service with template name' do
|
||||
template_name = 'customer_survey_template'
|
||||
expected_response = { success: true, template: { status: 'APPROVED' } }
|
||||
allow(mock_csat_template_service).to receive(:get_template_status)
|
||||
.with(template_name)
|
||||
.and_return(expected_response)
|
||||
|
||||
result = service.get_template_status(template_name)
|
||||
|
||||
expect(mock_csat_template_service).to have_received(:get_template_status).with(template_name)
|
||||
expect(result).to eq(expected_response)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'csat_template_service memoization' do
|
||||
it 'creates and memoizes the csat_template_service instance' do
|
||||
allow(Whatsapp::CsatTemplateService).to receive(:new)
|
||||
.with(whatsapp_channel)
|
||||
.and_return(mock_csat_template_service)
|
||||
allow(mock_csat_template_service).to receive(:get_template_status)
|
||||
.and_return({ success: true })
|
||||
|
||||
# Call multiple methods that use the service
|
||||
service.get_template_status('test1')
|
||||
service.get_template_status('test2')
|
||||
|
||||
# Verify the service was only instantiated once
|
||||
expect(Whatsapp::CsatTemplateService).to have_received(:new).once
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user