feat: Add campaigns in web widget (#2227)
* add campaign store(getter, actions and mutations) * add campaign store module * add get campaigns api * add fetch campaign action widget load * add specs * code cleanup * trigger campaig api fixes * integrate campaign trigger action * code cleanup * revert changes * trigger api fixes * review fixes * code beautification * chore: Fix multiple campaigns being send because of race condition * chore: rubocop * chore: Fix specs * disable campaigns Co-authored-by: Nithin David Thomas <webofnithin@gmail.com> Co-authored-by: Sojan <sojan@pepalo.com>
This commit is contained in:
@@ -5,10 +5,12 @@ class Campaigns::CampaignConversationBuilder
|
|||||||
@contact_inbox = ContactInbox.find(@contact_inbox_id)
|
@contact_inbox = ContactInbox.find(@contact_inbox_id)
|
||||||
@campaign = @contact_inbox.inbox.campaigns.find_by!(display_id: campaign_display_id)
|
@campaign = @contact_inbox.inbox.campaigns.find_by!(display_id: campaign_display_id)
|
||||||
|
|
||||||
# We won't send campaigns if a conversation is already present
|
|
||||||
return if @contact_inbox.conversations.present?
|
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
|
@contact_inbox.lock!
|
||||||
|
|
||||||
|
# We won't send campaigns if a conversation is already present
|
||||||
|
return if @contact_inbox.reload.conversations.present?
|
||||||
|
|
||||||
@conversation = ::Conversation.create!(conversation_params)
|
@conversation = ::Conversation.create!(conversation_params)
|
||||||
Messages::MessageBuilder.new(@campaign.sender, @conversation, message_params).perform
|
Messages::MessageBuilder.new(@campaign.sender, @conversation, message_params).perform
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
...mapActions('appConfig', ['setWidgetColor']),
|
...mapActions('appConfig', ['setWidgetColor']),
|
||||||
...mapActions('conversation', ['fetchOldConversations', 'setUserLastSeen']),
|
...mapActions('conversation', ['fetchOldConversations', 'setUserLastSeen']),
|
||||||
|
...mapActions('campaign', ['fetchCampaigns']),
|
||||||
...mapActions('agent', ['fetchAvailableAgents']),
|
...mapActions('agent', ['fetchAvailableAgents']),
|
||||||
scrollConversationToBottom() {
|
scrollConversationToBottom() {
|
||||||
const container = this.$el.querySelector('.conversation-wrap');
|
const container = this.$el.querySelector('.conversation-wrap');
|
||||||
@@ -149,6 +150,7 @@ export default {
|
|||||||
this.fetchOldConversations().then(() => this.setUnreadView());
|
this.fetchOldConversations().then(() => this.setUnreadView());
|
||||||
this.setPopoutDisplay(message.showPopoutButton);
|
this.setPopoutDisplay(message.showPopoutButton);
|
||||||
this.fetchAvailableAgents(websiteToken);
|
this.fetchAvailableAgents(websiteToken);
|
||||||
|
this.fetchCampaigns(websiteToken);
|
||||||
this.setHideMessageBubble(message.hideMessageBubble);
|
this.setHideMessageBubble(message.hideMessageBubble);
|
||||||
this.$store.dispatch('contacts/get');
|
this.$store.dispatch('contacts/get');
|
||||||
} else if (message.event === 'widget-visible') {
|
} else if (message.event === 'widget-visible') {
|
||||||
|
|||||||
23
app/javascript/widget/api/campaign.js
Normal file
23
app/javascript/widget/api/campaign.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import endPoints from 'widget/api/endPoints';
|
||||||
|
import { API } from 'widget/helpers/axios';
|
||||||
|
|
||||||
|
const getCampaigns = async websiteToken => {
|
||||||
|
const urlData = endPoints.getCampaigns(websiteToken);
|
||||||
|
const result = await API.get(urlData.url, { params: urlData.params });
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerCampaign = async ({ campaignId }) => {
|
||||||
|
const { websiteToken } = window.chatwootWebChannel;
|
||||||
|
const urlData = endPoints.triggerCampaign(websiteToken, campaignId);
|
||||||
|
|
||||||
|
await API.post(
|
||||||
|
urlData.url,
|
||||||
|
{ ...urlData.data },
|
||||||
|
{
|
||||||
|
params: urlData.params,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getCampaigns, triggerCampaign };
|
||||||
@@ -64,6 +64,24 @@ const getAvailableAgents = token => ({
|
|||||||
website_token: token,
|
website_token: token,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const getCampaigns = token => ({
|
||||||
|
url: '/api/v1/widget/campaigns',
|
||||||
|
params: {
|
||||||
|
website_token: token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const triggerCampaign = (token, campaignId) => ({
|
||||||
|
url: '/api/v1/widget/events',
|
||||||
|
data: {
|
||||||
|
name: 'campaign.triggered',
|
||||||
|
event_info: {
|
||||||
|
campaign_id: campaignId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
website_token: token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
createConversation,
|
createConversation,
|
||||||
@@ -72,4 +90,6 @@ export default {
|
|||||||
getConversation,
|
getConversation,
|
||||||
updateMessage,
|
updateMessage,
|
||||||
getAvailableAgents,
|
getAvailableAgents,
|
||||||
|
getCampaigns,
|
||||||
|
triggerCampaign,
|
||||||
};
|
};
|
||||||
|
|||||||
14
app/javascript/widget/helpers/campaignTimer.js
Normal file
14
app/javascript/widget/helpers/campaignTimer.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { triggerCampaign } from 'widget/api/campaign';
|
||||||
|
const startTimer = async ({ allCampaigns }) => {
|
||||||
|
allCampaigns.forEach(campaign => {
|
||||||
|
const {
|
||||||
|
trigger_rules: { time_on_page: timeOnPage },
|
||||||
|
id: campaignId,
|
||||||
|
} = campaign;
|
||||||
|
setTimeout(async () => {
|
||||||
|
await triggerCampaign({ campaignId });
|
||||||
|
}, timeOnPage * 1000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { startTimer };
|
||||||
@@ -9,9 +9,9 @@ import conversationLabels from 'widget/store/modules/conversationLabels';
|
|||||||
import events from 'widget/store/modules/events';
|
import events from 'widget/store/modules/events';
|
||||||
import globalConfig from 'shared/store/globalConfig';
|
import globalConfig from 'shared/store/globalConfig';
|
||||||
import message from 'widget/store/modules/message';
|
import message from 'widget/store/modules/message';
|
||||||
|
import campaign from 'widget/store/modules/campaign';
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
|
||||||
export default new Vuex.Store({
|
export default new Vuex.Store({
|
||||||
modules: {
|
modules: {
|
||||||
agent,
|
agent,
|
||||||
@@ -23,5 +23,6 @@ export default new Vuex.Store({
|
|||||||
events,
|
events,
|
||||||
globalConfig,
|
globalConfig,
|
||||||
message,
|
message,
|
||||||
|
campaign,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
51
app/javascript/widget/store/modules/campaign.js
Normal file
51
app/javascript/widget/store/modules/campaign.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import { getCampaigns } from 'widget/api/campaign';
|
||||||
|
import { startTimer } from 'widget/helpers/campaignTimer';
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
records: [],
|
||||||
|
uiFlags: {
|
||||||
|
isError: false,
|
||||||
|
hasFetched: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getters = {
|
||||||
|
getHasFetched: $state => $state.uiFlags.hasFetched,
|
||||||
|
fetchCampaigns: $state => $state.records,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
fetchCampaigns: async ({ commit }, websiteToken) => {
|
||||||
|
try {
|
||||||
|
const { data } = await getCampaigns(websiteToken);
|
||||||
|
startTimer({ allCampaigns: data });
|
||||||
|
commit('setCampaigns', data);
|
||||||
|
commit('setError', false);
|
||||||
|
commit('setHasFetched', true);
|
||||||
|
} catch (error) {
|
||||||
|
commit('setError', true);
|
||||||
|
commit('setHasFetched', true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
setCampaigns($state, data) {
|
||||||
|
Vue.set($state, 'records', data);
|
||||||
|
},
|
||||||
|
setError($state, value) {
|
||||||
|
Vue.set($state.uiFlags, 'isError', value);
|
||||||
|
},
|
||||||
|
setHasFetched($state, value) {
|
||||||
|
Vue.set($state.uiFlags, 'hasFetched', value);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state,
|
||||||
|
getters,
|
||||||
|
actions,
|
||||||
|
mutations,
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { API } from 'widget/helpers/axios';
|
||||||
|
import { actions } from '../../campaign';
|
||||||
|
import { campaigns } from './data';
|
||||||
|
|
||||||
|
const commit = jest.fn();
|
||||||
|
jest.mock('widget/helpers/axios');
|
||||||
|
|
||||||
|
describe('#actions', () => {
|
||||||
|
describe('#fetchCampaigns', () => {
|
||||||
|
it('sends correct actions if API is success', async () => {
|
||||||
|
API.get.mockResolvedValue({ data: campaigns });
|
||||||
|
await actions.fetchCampaigns({ commit }, 'XDsafmADasd');
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
['setCampaigns', campaigns],
|
||||||
|
['setError', false],
|
||||||
|
['setHasFetched', true],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('sends correct actions if API is error', async () => {
|
||||||
|
API.get.mockRejectedValue({ message: 'Authentication required' });
|
||||||
|
await actions.fetchCampaigns({ commit }, 'XDsafmADasd');
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
['setError', true],
|
||||||
|
['setHasFetched', true],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
86
app/javascript/widget/store/modules/specs/campaign/data.js
Normal file
86
app/javascript/widget/store/modules/specs/campaign/data.js
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
export const campaigns = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Welcome',
|
||||||
|
description: null,
|
||||||
|
account_id: 1,
|
||||||
|
inbox: {
|
||||||
|
id: 37,
|
||||||
|
channel_id: 1,
|
||||||
|
name: 'Chatwoot',
|
||||||
|
channel_type: 'Channel::WebWidget',
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
|
account_id: 1,
|
||||||
|
availability_status: 'offline',
|
||||||
|
confirmed: true,
|
||||||
|
email: 'sojan@chatwoot.com',
|
||||||
|
available_name: 'Sojan',
|
||||||
|
id: 10,
|
||||||
|
name: 'Sojan',
|
||||||
|
},
|
||||||
|
message: 'Hey, What brings you today',
|
||||||
|
enabled: true,
|
||||||
|
trigger_rules: {
|
||||||
|
url: 'https://github.com',
|
||||||
|
time_on_page: 10,
|
||||||
|
},
|
||||||
|
created_at: '2021-05-03T04:53:36.354Z',
|
||||||
|
updated_at: '2021-05-03T04:53:36.354Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
title: 'Onboarding Campaign',
|
||||||
|
description: null,
|
||||||
|
account_id: 1,
|
||||||
|
inbox: {
|
||||||
|
id: 37,
|
||||||
|
channel_id: 1,
|
||||||
|
name: 'GitX',
|
||||||
|
channel_type: 'Channel::WebWidget',
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
|
account_id: 1,
|
||||||
|
availability_status: 'offline',
|
||||||
|
confirmed: true,
|
||||||
|
email: 'sojan@chatwoot.com',
|
||||||
|
available_name: 'Sojan',
|
||||||
|
id: 10,
|
||||||
|
},
|
||||||
|
message: 'Begin your onboarding campaign with a welcome message',
|
||||||
|
enabled: true,
|
||||||
|
trigger_rules: {
|
||||||
|
url: 'https://chatwoot.com',
|
||||||
|
time_on_page: '20',
|
||||||
|
},
|
||||||
|
created_at: '2021-05-03T08:15:35.828Z',
|
||||||
|
updated_at: '2021-05-03T08:15:35.828Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
title: 'Thanks',
|
||||||
|
description: null,
|
||||||
|
account_id: 1,
|
||||||
|
inbox: {
|
||||||
|
id: 37,
|
||||||
|
channel_id: 1,
|
||||||
|
name: 'Chatwoot',
|
||||||
|
channel_type: 'Channel::WebWidget',
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
|
account_id: 1,
|
||||||
|
availability_status: 'offline',
|
||||||
|
confirmed: true,
|
||||||
|
email: 'nithin@chatwoot.com',
|
||||||
|
available_name: 'Nithin',
|
||||||
|
},
|
||||||
|
message: 'Thanks for coming to the show. How may I help you?',
|
||||||
|
enabled: false,
|
||||||
|
trigger_rules: {
|
||||||
|
url: 'https://noshow.com',
|
||||||
|
time_on_page: 10,
|
||||||
|
},
|
||||||
|
created_at: '2021-05-03T10:22:51.025Z',
|
||||||
|
updated_at: '2021-05-03T10:22:51.025Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { getters } from '../../campaign';
|
||||||
|
import { campaigns } from './data';
|
||||||
|
|
||||||
|
describe('#getters', () => {
|
||||||
|
it('fetchCampaigns', () => {
|
||||||
|
const state = {
|
||||||
|
records: campaigns,
|
||||||
|
};
|
||||||
|
expect(getters.fetchCampaigns(state)).toEqual([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Welcome',
|
||||||
|
description: null,
|
||||||
|
account_id: 1,
|
||||||
|
inbox: {
|
||||||
|
id: 37,
|
||||||
|
channel_id: 1,
|
||||||
|
name: 'Chatwoot',
|
||||||
|
channel_type: 'Channel::WebWidget',
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
|
account_id: 1,
|
||||||
|
availability_status: 'offline',
|
||||||
|
confirmed: true,
|
||||||
|
email: 'sojan@chatwoot.com',
|
||||||
|
available_name: 'Sojan',
|
||||||
|
id: 10,
|
||||||
|
name: 'Sojan',
|
||||||
|
},
|
||||||
|
message: 'Hey, What brings you today',
|
||||||
|
enabled: true,
|
||||||
|
trigger_rules: {
|
||||||
|
url: 'https://github.com',
|
||||||
|
time_on_page: 10,
|
||||||
|
},
|
||||||
|
created_at: '2021-05-03T04:53:36.354Z',
|
||||||
|
updated_at: '2021-05-03T04:53:36.354Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
title: 'Onboarding Campaign',
|
||||||
|
description: null,
|
||||||
|
account_id: 1,
|
||||||
|
inbox: {
|
||||||
|
id: 37,
|
||||||
|
channel_id: 1,
|
||||||
|
name: 'GitX',
|
||||||
|
channel_type: 'Channel::WebWidget',
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
|
account_id: 1,
|
||||||
|
availability_status: 'offline',
|
||||||
|
confirmed: true,
|
||||||
|
email: 'sojan@chatwoot.com',
|
||||||
|
available_name: 'Sojan',
|
||||||
|
id: 10,
|
||||||
|
},
|
||||||
|
message: 'Begin your onboarding campaign with a welcome message',
|
||||||
|
enabled: true,
|
||||||
|
trigger_rules: {
|
||||||
|
url: 'https://chatwoot.com',
|
||||||
|
time_on_page: '20',
|
||||||
|
},
|
||||||
|
created_at: '2021-05-03T08:15:35.828Z',
|
||||||
|
updated_at: '2021-05-03T08:15:35.828Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
title: 'Thanks',
|
||||||
|
description: null,
|
||||||
|
account_id: 1,
|
||||||
|
inbox: {
|
||||||
|
id: 37,
|
||||||
|
channel_id: 1,
|
||||||
|
name: 'Chatwoot',
|
||||||
|
channel_type: 'Channel::WebWidget',
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
|
account_id: 1,
|
||||||
|
availability_status: 'offline',
|
||||||
|
confirmed: true,
|
||||||
|
email: 'nithin@chatwoot.com',
|
||||||
|
available_name: 'Nithin',
|
||||||
|
},
|
||||||
|
message: 'Thanks for coming to the show. How may I help you?',
|
||||||
|
enabled: false,
|
||||||
|
trigger_rules: {
|
||||||
|
url: 'https://noshow.com',
|
||||||
|
time_on_page: 10,
|
||||||
|
},
|
||||||
|
created_at: '2021-05-03T10:22:51.025Z',
|
||||||
|
updated_at: '2021-05-03T10:22:51.025Z',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { mutations } from '../../campaign';
|
||||||
|
import { campaigns } from './data';
|
||||||
|
|
||||||
|
describe('#mutations', () => {
|
||||||
|
describe('#setCampagins', () => {
|
||||||
|
it('set campaign records', () => {
|
||||||
|
const state = { records: [] };
|
||||||
|
mutations.setCampaigns(state, campaigns);
|
||||||
|
expect(state.records).toEqual(campaigns);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#setError', () => {
|
||||||
|
it('set error flag', () => {
|
||||||
|
const state = { records: [], uiFlags: {} };
|
||||||
|
mutations.setError(state, true);
|
||||||
|
expect(state.uiFlags.isError).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#setHasFetched', () => {
|
||||||
|
it('set fetched flag', () => {
|
||||||
|
const state = { records: [], uiFlags: {} };
|
||||||
|
mutations.setHasFetched(state, true);
|
||||||
|
expect(state.uiFlags.hasFetched).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,7 +6,7 @@ class CampaignListener < BaseListener
|
|||||||
return if campaign_display_id.blank?
|
return if campaign_display_id.blank?
|
||||||
|
|
||||||
::Campaigns::CampaignConversationBuilder.new(
|
::Campaigns::CampaignConversationBuilder.new(
|
||||||
contact_inbox: contact_inbox.id,
|
contact_inbox_id: contact_inbox.id,
|
||||||
campaign_display_id: campaign_display_id,
|
campaign_display_id: campaign_display_id,
|
||||||
conversation_additional_attributes: event.data[:event_info].except(:campaign_id)
|
conversation_additional_attributes: event.data[:event_info].except(:campaign_id)
|
||||||
).perform
|
).perform
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ class MessageTemplates::HookExecutionService
|
|||||||
|
|
||||||
def perform
|
def perform
|
||||||
return if inbox.agent_bot_inbox&.active?
|
return if inbox.agent_bot_inbox&.active?
|
||||||
|
return if conversation.campaign.present?
|
||||||
|
|
||||||
# TODO: let's see whether this is needed and remove this and related logic if not
|
# TODO: let's see whether this is needed and remove this and related logic if not
|
||||||
# ::MessageTemplates::Template::OutOfOffice.new(conversation: conversation).perform if should_send_out_of_office_message?
|
# ::MessageTemplates::Template::OutOfOffice.new(conversation: conversation).perform if should_send_out_of_office_message?
|
||||||
|
|
||||||
::MessageTemplates::Template::Greeting.new(conversation: conversation).perform if should_send_greeting?
|
::MessageTemplates::Template::Greeting.new(conversation: conversation).perform if should_send_greeting?
|
||||||
::MessageTemplates::Template::EmailCollect.new(conversation: conversation).perform if should_send_email_collect?
|
::MessageTemplates::Template::EmailCollect.new(conversation: conversation).perform if should_send_email_collect?
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ describe CampaignListener do
|
|||||||
context 'when params contain campaign id' do
|
context 'when params contain campaign id' do
|
||||||
it 'triggers campaign conversation builder' do
|
it 'triggers campaign conversation builder' do
|
||||||
expect(Campaigns::CampaignConversationBuilder).to receive(:new)
|
expect(Campaigns::CampaignConversationBuilder).to receive(:new)
|
||||||
.with({ contact_inbox: contact_inbox.id, campaign_display_id: campaign.display_id, conversation_additional_attributes: {} }).once
|
.with({ contact_inbox_id: contact_inbox.id, campaign_display_id: campaign.display_id, conversation_additional_attributes: {} }).once
|
||||||
listener.campaign_triggered(event)
|
listener.campaign_triggered(event)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -39,6 +39,18 @@ describe ::MessageTemplates::HookExecutionService do
|
|||||||
expect(::MessageTemplates::Template::EmailCollect).not_to have_received(:new).with(conversation: message.conversation)
|
expect(::MessageTemplates::Template::EmailCollect).not_to have_received(:new).with(conversation: message.conversation)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'doesnot calls ::MessageTemplates::Template::EmailCollect on campaign conversations' do
|
||||||
|
contact = create(:contact, email: nil)
|
||||||
|
conversation = create(:conversation, contact: contact, campaign: create(:campaign))
|
||||||
|
|
||||||
|
allow(::MessageTemplates::Template::EmailCollect).to receive(:new).and_return(true)
|
||||||
|
|
||||||
|
# described class gets called in message after commit
|
||||||
|
message = create(:message, conversation: conversation)
|
||||||
|
|
||||||
|
expect(::MessageTemplates::Template::EmailCollect).not_to have_received(:new).with(conversation: message.conversation)
|
||||||
|
end
|
||||||
|
|
||||||
it 'doesnot calls ::MessageTemplates::Template::Greeting if greeting_message is empty' do
|
it 'doesnot calls ::MessageTemplates::Template::Greeting if greeting_message is empty' do
|
||||||
contact = create(:contact, email: nil)
|
contact = create(:contact, email: nil)
|
||||||
conversation = create(:conversation, contact: contact)
|
conversation = create(:conversation, contact: contact)
|
||||||
|
|||||||
Reference in New Issue
Block a user