feat: hide CSAT survey URLs from agents in dashboard (#11622)

This commit is contained in:
Muhsin Keloth
2025-06-11 23:39:47 +05:30
committed by GitHub
parent 5745a55db5
commit f627dbe42d
15 changed files with 148 additions and 46 deletions

View File

@@ -34,7 +34,7 @@ class Channel::Sms < ApplicationRecord
end end
def send_message(contact_number, message) def send_message(contact_number, message)
body = message_body(contact_number, message.content) body = message_body(contact_number, message.outgoing_content)
body['media'] = message.attachments.map(&:download_url) if message.attachments.present? body['media'] = message.attachments.map(&:download_url) if message.attachments.present?
send_to_bandwidth(body, message) send_to_bandwidth(body, message)

View File

@@ -33,7 +33,7 @@ class Channel::Telegram < ApplicationRecord
end end
def send_message_on_telegram(message) def send_message_on_telegram(message)
message_id = send_message(message) if message.content.present? message_id = send_message(message) if message.outgoing_content.present?
message_id = Telegram::SendAttachmentsService.new(message: message).perform if message.attachments.present? message_id = Telegram::SendAttachmentsService.new(message: message).perform if message.attachments.present?
message_id message_id
end end
@@ -95,7 +95,7 @@ class Channel::Telegram < ApplicationRecord
end end
def send_message(message) def send_message(message)
response = message_request(chat_id(message), message.content, reply_markup(message), reply_to_message_id(message)) response = message_request(chat_id(message), message.outgoing_content, reply_markup(message), reply_to_message_id(message))
process_error(message, response) process_error(message, response)
response.parsed_response['result']['message_id'] if response.success? response.parsed_response['result']['message_id'] if response.success?
end end

View File

@@ -181,17 +181,9 @@ class Message < ApplicationRecord
data data
end end
def content # Method to get content with survey URL for outgoing channel delivery
# move this to a presenter def outgoing_content
return self[:content] if !input_csat? || inbox.web_widget? MessageContentPresenter.new(self).outgoing_content
survey_link = "#{ENV.fetch('FRONTEND_URL', nil)}/survey/responses/#{conversation.uuid}"
if inbox.csat_config&.dig('message').present?
"#{inbox.csat_config['message']} #{survey_link}"
else
I18n.t('conversations.survey.response', link: survey_link)
end
end end
def email_notifiable_message? def email_notifiable_message?

View File

@@ -0,0 +1,20 @@
class MessageContentPresenter < SimpleDelegator
def outgoing_content
return content unless should_append_survey_link?
survey_link = survey_url(conversation.uuid)
custom_message = inbox.csat_config&.dig('message')
custom_message.present? ? "#{custom_message} #{survey_link}" : I18n.t('conversations.survey.response', link: survey_link)
end
private
def should_append_survey_link?
input_csat? && !inbox.web_widget?
end
def survey_url(conversation_uuid)
"#{ENV.fetch('FRONTEND_URL', nil)}/survey/responses/#{conversation_uuid}"
end
end

View File

@@ -66,7 +66,7 @@ class Facebook::SendOnFacebookService < Base::SendOnChannelService
end end
} }
else else
{ text: message.content } { text: message.outgoing_content }
end end
end end

View File

@@ -30,7 +30,7 @@ class Instagram::BaseSendService < Base::SendOnChannelService
params = { params = {
recipient: { id: contact.get_source_id(inbox.id) }, recipient: { id: contact.get_source_id(inbox.id) },
message: { message: {
text: message.content text: message.outgoing_content
} }
} }

View File

@@ -56,7 +56,7 @@ class Line::SendOnLineService < Base::SendOnChannelService
def text_message def text_message
{ {
type: 'text', type: 'text',
text: message.content text: message.outgoing_content
} }
end end

View File

@@ -16,7 +16,7 @@ class Twilio::SendOnTwilioService < Base::SendOnChannelService
def message_params def message_params
{ {
body: message.content, body: message.outgoing_content,
to: contact_inbox.source_id, to: contact_inbox.source_id,
media_url: attachments media_url: attachments
} }

View File

@@ -37,7 +37,7 @@ class Twitter::SendOnTwitterService < Base::SendOnChannelService
def send_direct_message def send_direct_message
twitter_client.send_direct_message( twitter_client.send_direct_message(
recipient_id: contact_inbox.source_id, recipient_id: contact_inbox.source_id,
message: message.content message: message.outgoing_content
) )
end end
@@ -52,7 +52,7 @@ class Twitter::SendOnTwitterService < Base::SendOnChannelService
def send_tweet_reply def send_tweet_reply
response = twitter_client.send_tweet_reply( response = twitter_client.send_tweet_reply(
reply_to_tweet_id: reply_to_message.source_id, reply_to_tweet_id: reply_to_message.source_id,
tweet: "#{screen_name} #{message.content}" tweet: "#{screen_name} #{message.outgoing_content}"
) )
if response.status == '200' if response.status == '200'
tweet_data = response.body tweet_data = response.body

View File

@@ -93,7 +93,7 @@ class Whatsapp::Providers::BaseService
def create_button_payload(message) def create_button_payload(message)
buttons = create_buttons(message.content_attributes['items']) buttons = create_buttons(message.content_attributes['items'])
json_hash = { 'buttons' => buttons } json_hash = { 'buttons' => buttons }
create_payload('button', message.content, JSON.generate(json_hash)) create_payload('button', message.outgoing_content, JSON.generate(json_hash))
end end
def create_list_payload(message) def create_list_payload(message)
@@ -101,6 +101,6 @@ class Whatsapp::Providers::BaseService
section1 = { 'rows' => rows } section1 = { 'rows' => rows }
sections = [section1] sections = [section1]
json_hash = { :button => 'Choose an item', 'sections' => sections } json_hash = { :button => 'Choose an item', 'sections' => sections }
create_payload('list', message.content, JSON.generate(json_hash)) create_payload('list', message.outgoing_content, JSON.generate(json_hash))
end end
end end

View File

@@ -63,7 +63,7 @@ class Whatsapp::Providers::Whatsapp360DialogService < Whatsapp::Providers::BaseS
headers: api_headers, headers: api_headers,
body: { body: {
to: phone_number, to: phone_number,
text: { body: message.content }, text: { body: message.outgoing_content },
type: 'text' type: 'text'
}.to_json }.to_json
) )
@@ -77,7 +77,7 @@ class Whatsapp::Providers::Whatsapp360DialogService < Whatsapp::Providers::BaseS
type_content = { type_content = {
'link': attachment.download_url 'link': attachment.download_url
} }
type_content['caption'] = message.content unless %w[audio sticker].include?(type) type_content['caption'] = message.outgoing_content unless %w[audio sticker].include?(type)
type_content['filename'] = attachment.file.filename if type == 'document' type_content['filename'] = attachment.file.filename if type == 'document'
response = HTTParty.post( response = HTTParty.post(

View File

@@ -82,7 +82,7 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
messaging_product: 'whatsapp', messaging_product: 'whatsapp',
context: whatsapp_reply_context(message), context: whatsapp_reply_context(message),
to: phone_number, to: phone_number,
text: { body: message.content }, text: { body: message.outgoing_content },
type: 'text' type: 'text'
}.to_json }.to_json
) )
@@ -96,7 +96,7 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
type_content = { type_content = {
'link': attachment.download_url 'link': attachment.download_url
} }
type_content['caption'] = message.content unless %w[audio sticker].include?(type) type_content['caption'] = message.outgoing_content unless %w[audio sticker].include?(type)
type_content['filename'] = attachment.file.filename if type == 'document' type_content['filename'] = attachment.file.filename if type == 'document'
response = HTTParty.post( response = HTTParty.post(
"#{phone_id_path}/messages", "#{phone_id_path}/messages",

View File

@@ -61,7 +61,7 @@ class Whatsapp::SendOnWhatsappService < Base::SendOnChannelService
return if body_object.blank? return if body_object.blank?
template_match_regex = build_template_match_regex(body_object['text']) template_match_regex = build_template_match_regex(body_object['text'])
message.content.match(template_match_regex) message.outgoing_content.match(template_match_regex)
end end
def build_template_match_regex(template_text) def build_template_match_regex(template_text)

View File

@@ -478,32 +478,57 @@ RSpec.describe Message do
describe '#content' do describe '#content' do
let(:conversation) { create(:conversation) } let(:conversation) { create(:conversation) }
let(:message) { create(:message, conversation: conversation, content_type: 'input_csat', content: 'Original content') }
it 'returns original content for web widget inbox' do context 'when message is not input_csat' do
allow(message.inbox).to receive(:web_widget?).and_return(true) let(:message) { create(:message, conversation: conversation, content_type: 'text', content: 'Regular message') }
expect(message.content).to eq('Original content')
it 'returns original content' do
expect(message.content).to eq('Regular message')
end
end end
context 'when inbox is not a web widget' do context 'when message is input_csat' do
before do let(:message) { create(:message, conversation: conversation, content_type: 'input_csat', content: 'Rate your experience') }
allow(message.inbox).to receive(:web_widget?).and_return(false)
allow(ENV).to receive(:fetch).with('FRONTEND_URL', nil).and_return('https://app.chatwoot.com') context 'when inbox is web widget' do
before do
allow(message.inbox).to receive(:web_widget?).and_return(true)
end
it 'returns original content without survey URL' do
expect(message.content).to eq('Rate your experience')
end
end end
it 'returns custom message with survey link when csat message is configured' do context 'when inbox is not web widget' do
allow(message.inbox).to receive(:csat_config).and_return({ 'message' => 'Custom survey message:' }) before do
expected_content = "Custom survey message: https://app.chatwoot.com/survey/responses/#{conversation.uuid}" allow(message.inbox).to receive(:web_widget?).and_return(false)
expect(message.content).to eq(expected_content) end
end
it 'returns default message with survey link when no custom csat message' do it 'returns only the stored content (clean for dashboard)' do
allow(message.inbox).to receive(:csat_config).and_return(nil) expect(message.content).to eq('Rate your experience')
allow(I18n).to receive(:t).with('conversations.survey.response', link: "https://app.chatwoot.com/survey/responses/#{conversation.uuid}") end
.and_return("Please rate your conversation: https://app.chatwoot.com/survey/responses/#{conversation.uuid}")
expected_content = "Please rate your conversation: https://app.chatwoot.com/survey/responses/#{conversation.uuid}" it 'returns only the base content without URL when survey_url stored separately' do
expect(message.content).to eq(expected_content) message.content_attributes = { 'survey_url' => 'https://app.chatwoot.com/survey/responses/12345' }
expect(message.content).to eq('Rate your experience')
end
end end
end end
end end
describe '#outgoing_content' do
let(:conversation) { create(:conversation) }
let(:message) { create(:message, conversation: conversation, content_type: 'text', content: 'Regular message') }
it 'delegates to MessageContentPresenter' do
presenter = instance_double(MessageContentPresenter)
allow(MessageContentPresenter).to receive(:new).with(message).and_return(presenter)
allow(presenter).to receive(:outgoing_content).and_return('Presented content')
expect(message.outgoing_content).to eq('Presented content')
expect(MessageContentPresenter).to have_received(:new).with(message)
expect(presenter).to have_received(:outgoing_content)
end
end
end end

View File

@@ -0,0 +1,65 @@
require 'rails_helper'
RSpec.describe MessageContentPresenter do
let(:conversation) { create(:conversation) }
let(:message) { create(:message, conversation: conversation, content_type: content_type, content: content) }
let(:presenter) { described_class.new(message) }
describe '#outgoing_content' do
context 'when message is not input_csat' do
let(:content_type) { 'text' }
let(:content) { 'Regular message' }
it 'returns regular content' do
expect(presenter.outgoing_content).to eq('Regular message')
end
end
context 'when message is input_csat and inbox is web widget' do
let(:content_type) { 'input_csat' }
let(:content) { 'Rate your experience' }
before do
allow(message.inbox).to receive(:web_widget?).and_return(true)
end
it 'returns regular content without survey URL' do
expect(presenter.outgoing_content).to eq('Rate your experience')
end
end
context 'when message is input_csat and inbox is not web widget' do
let(:content_type) { 'input_csat' }
let(:content) { 'Rate your experience' }
before do
allow(message.inbox).to receive(:web_widget?).and_return(false)
allow(ENV).to receive(:fetch).with('FRONTEND_URL', nil).and_return('https://app.chatwoot.com')
end
it 'returns I18n default message when no CSAT config and dynamically generates survey URL' do
expected_url = "https://app.chatwoot.com/survey/responses/#{conversation.uuid}"
allow(I18n).to receive(:t).with('conversations.survey.response', link: expected_url)
.and_return("Please rate this conversation, #{expected_url}")
expect(presenter.outgoing_content).to eq("Please rate this conversation, #{expected_url}")
end
it 'returns CSAT config message when config exists and dynamically generates survey URL' do
allow(message.inbox).to receive(:csat_config).and_return({ 'message' => 'Custom CSAT message' })
expected_url = "https://app.chatwoot.com/survey/responses/#{conversation.uuid}"
expect(presenter.outgoing_content).to eq("Custom CSAT message #{expected_url}")
end
end
end
describe 'delegation' do
let(:content_type) { 'text' }
let(:content) { 'Test message' }
it 'delegates model methods to the wrapped message' do
expect(presenter.content).to eq('Test message')
expect(presenter.content_type).to eq('text')
expect(presenter.conversation).to eq(conversation)
end
end
end