mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-30 18:47:51 +00:00 
			
		
		
		
	feat: hide CSAT survey URLs from agents in dashboard (#11622)
This commit is contained in:
		| @@ -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) | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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? | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								app/presenters/message_content_presenter.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								app/presenters/message_content_presenter.rb
									
									
									
									
									
										Normal 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 | ||||||
| @@ -66,7 +66,7 @@ class Facebook::SendOnFacebookService < Base::SendOnChannelService | |||||||
|         end |         end | ||||||
|       } |       } | ||||||
|     else |     else | ||||||
|       { text: message.content } |       { text: message.outgoing_content } | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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( | ||||||
|   | |||||||
| @@ -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", | ||||||
|   | |||||||
| @@ -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) | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
							
								
								
									
										65
									
								
								spec/presenters/message_content_presenter_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								spec/presenters/message_content_presenter_spec.rb
									
									
									
									
									
										Normal 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 | ||||||
		Reference in New Issue
	
	Block a user
	 Muhsin Keloth
					Muhsin Keloth