diff --git a/app/models/channel/sms.rb b/app/models/channel/sms.rb index ab04f55f0..db4da3237 100644 --- a/app/models/channel/sms.rb +++ b/app/models/channel/sms.rb @@ -34,7 +34,7 @@ class Channel::Sms < ApplicationRecord end 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? send_to_bandwidth(body, message) diff --git a/app/models/channel/telegram.rb b/app/models/channel/telegram.rb index 7c09fe284..9e4b95c57 100644 --- a/app/models/channel/telegram.rb +++ b/app/models/channel/telegram.rb @@ -33,7 +33,7 @@ class Channel::Telegram < ApplicationRecord end 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 end @@ -95,7 +95,7 @@ class Channel::Telegram < ApplicationRecord end 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) response.parsed_response['result']['message_id'] if response.success? end diff --git a/app/models/message.rb b/app/models/message.rb index 9381c33f6..e7c4e9b6c 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -181,17 +181,9 @@ class Message < ApplicationRecord data end - def content - # move this to a presenter - return self[:content] if !input_csat? || inbox.web_widget? - - 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 + # Method to get content with survey URL for outgoing channel delivery + def outgoing_content + MessageContentPresenter.new(self).outgoing_content end def email_notifiable_message? diff --git a/app/presenters/message_content_presenter.rb b/app/presenters/message_content_presenter.rb new file mode 100644 index 000000000..461dc8b88 --- /dev/null +++ b/app/presenters/message_content_presenter.rb @@ -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 \ No newline at end of file diff --git a/app/services/facebook/send_on_facebook_service.rb b/app/services/facebook/send_on_facebook_service.rb index f0b54a733..ed3b7e4ab 100644 --- a/app/services/facebook/send_on_facebook_service.rb +++ b/app/services/facebook/send_on_facebook_service.rb @@ -66,7 +66,7 @@ class Facebook::SendOnFacebookService < Base::SendOnChannelService end } else - { text: message.content } + { text: message.outgoing_content } end end diff --git a/app/services/instagram/base_send_service.rb b/app/services/instagram/base_send_service.rb index ff5f9216e..9b156932c 100644 --- a/app/services/instagram/base_send_service.rb +++ b/app/services/instagram/base_send_service.rb @@ -30,7 +30,7 @@ class Instagram::BaseSendService < Base::SendOnChannelService params = { recipient: { id: contact.get_source_id(inbox.id) }, message: { - text: message.content + text: message.outgoing_content } } diff --git a/app/services/line/send_on_line_service.rb b/app/services/line/send_on_line_service.rb index 057eb09e2..a5467ebbf 100644 --- a/app/services/line/send_on_line_service.rb +++ b/app/services/line/send_on_line_service.rb @@ -56,7 +56,7 @@ class Line::SendOnLineService < Base::SendOnChannelService def text_message { type: 'text', - text: message.content + text: message.outgoing_content } end diff --git a/app/services/twilio/send_on_twilio_service.rb b/app/services/twilio/send_on_twilio_service.rb index 5bd262759..950880f08 100644 --- a/app/services/twilio/send_on_twilio_service.rb +++ b/app/services/twilio/send_on_twilio_service.rb @@ -16,7 +16,7 @@ class Twilio::SendOnTwilioService < Base::SendOnChannelService def message_params { - body: message.content, + body: message.outgoing_content, to: contact_inbox.source_id, media_url: attachments } diff --git a/app/services/twitter/send_on_twitter_service.rb b/app/services/twitter/send_on_twitter_service.rb index 76ee29dbc..83086bd12 100644 --- a/app/services/twitter/send_on_twitter_service.rb +++ b/app/services/twitter/send_on_twitter_service.rb @@ -37,7 +37,7 @@ class Twitter::SendOnTwitterService < Base::SendOnChannelService def send_direct_message twitter_client.send_direct_message( recipient_id: contact_inbox.source_id, - message: message.content + message: message.outgoing_content ) end @@ -52,7 +52,7 @@ class Twitter::SendOnTwitterService < Base::SendOnChannelService def send_tweet_reply response = twitter_client.send_tweet_reply( reply_to_tweet_id: reply_to_message.source_id, - tweet: "#{screen_name} #{message.content}" + tweet: "#{screen_name} #{message.outgoing_content}" ) if response.status == '200' tweet_data = response.body diff --git a/app/services/whatsapp/providers/base_service.rb b/app/services/whatsapp/providers/base_service.rb index 55714ba4d..b60c02369 100644 --- a/app/services/whatsapp/providers/base_service.rb +++ b/app/services/whatsapp/providers/base_service.rb @@ -93,7 +93,7 @@ class Whatsapp::Providers::BaseService def create_button_payload(message) buttons = create_buttons(message.content_attributes['items']) json_hash = { 'buttons' => buttons } - create_payload('button', message.content, JSON.generate(json_hash)) + create_payload('button', message.outgoing_content, JSON.generate(json_hash)) end def create_list_payload(message) @@ -101,6 +101,6 @@ class Whatsapp::Providers::BaseService section1 = { 'rows' => rows } sections = [section1] 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 diff --git a/app/services/whatsapp/providers/whatsapp_360_dialog_service.rb b/app/services/whatsapp/providers/whatsapp_360_dialog_service.rb index 61cdeebf1..6e393f9f5 100644 --- a/app/services/whatsapp/providers/whatsapp_360_dialog_service.rb +++ b/app/services/whatsapp/providers/whatsapp_360_dialog_service.rb @@ -63,7 +63,7 @@ class Whatsapp::Providers::Whatsapp360DialogService < Whatsapp::Providers::BaseS headers: api_headers, body: { to: phone_number, - text: { body: message.content }, + text: { body: message.outgoing_content }, type: 'text' }.to_json ) @@ -77,7 +77,7 @@ class Whatsapp::Providers::Whatsapp360DialogService < Whatsapp::Providers::BaseS type_content = { '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' response = HTTParty.post( diff --git a/app/services/whatsapp/providers/whatsapp_cloud_service.rb b/app/services/whatsapp/providers/whatsapp_cloud_service.rb index ec2275920..1620e4e42 100644 --- a/app/services/whatsapp/providers/whatsapp_cloud_service.rb +++ b/app/services/whatsapp/providers/whatsapp_cloud_service.rb @@ -82,7 +82,7 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi messaging_product: 'whatsapp', context: whatsapp_reply_context(message), to: phone_number, - text: { body: message.content }, + text: { body: message.outgoing_content }, type: 'text' }.to_json ) @@ -96,7 +96,7 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi type_content = { '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' response = HTTParty.post( "#{phone_id_path}/messages", diff --git a/app/services/whatsapp/send_on_whatsapp_service.rb b/app/services/whatsapp/send_on_whatsapp_service.rb index 186c8b2ae..61b78f892 100644 --- a/app/services/whatsapp/send_on_whatsapp_service.rb +++ b/app/services/whatsapp/send_on_whatsapp_service.rb @@ -61,7 +61,7 @@ class Whatsapp::SendOnWhatsappService < Base::SendOnChannelService return if body_object.blank? template_match_regex = build_template_match_regex(body_object['text']) - message.content.match(template_match_regex) + message.outgoing_content.match(template_match_regex) end def build_template_match_regex(template_text) diff --git a/spec/models/message_spec.rb b/spec/models/message_spec.rb index b4be3ac5a..3bf64a231 100644 --- a/spec/models/message_spec.rb +++ b/spec/models/message_spec.rb @@ -478,32 +478,57 @@ RSpec.describe Message do describe '#content' do 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 - allow(message.inbox).to receive(:web_widget?).and_return(true) - expect(message.content).to eq('Original content') + context 'when message is not input_csat' do + let(:message) { create(:message, conversation: conversation, content_type: 'text', content: 'Regular message') } + + it 'returns original content' do + expect(message.content).to eq('Regular message') + end end - context 'when inbox is not a web widget' do - 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') + context 'when message is input_csat' do + let(:message) { create(:message, conversation: conversation, content_type: 'input_csat', content: 'Rate your experience') } + + 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 - it 'returns custom message with survey link when csat message is configured' do - allow(message.inbox).to receive(:csat_config).and_return({ 'message' => 'Custom survey message:' }) - expected_content = "Custom survey message: https://app.chatwoot.com/survey/responses/#{conversation.uuid}" - expect(message.content).to eq(expected_content) - end + context 'when inbox is not web widget' do + before do + allow(message.inbox).to receive(:web_widget?).and_return(false) + end - it 'returns default message with survey link when no custom csat message' do - allow(message.inbox).to receive(:csat_config).and_return(nil) - allow(I18n).to receive(:t).with('conversations.survey.response', link: "https://app.chatwoot.com/survey/responses/#{conversation.uuid}") - .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}" - expect(message.content).to eq(expected_content) + it 'returns only the stored content (clean for dashboard)' do + expect(message.content).to eq('Rate your experience') + end + + it 'returns only the base content without URL when survey_url stored separately' do + message.content_attributes = { 'survey_url' => 'https://app.chatwoot.com/survey/responses/12345' } + expect(message.content).to eq('Rate your experience') + 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 diff --git a/spec/presenters/message_content_presenter_spec.rb b/spec/presenters/message_content_presenter_spec.rb new file mode 100644 index 000000000..b1be37ef2 --- /dev/null +++ b/spec/presenters/message_content_presenter_spec.rb @@ -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 \ No newline at end of file