fix: Trigger campaigns only during business hours (#3085)

Fixes #2433
This commit is contained in:
Muhsin Keloth 2021-10-12 17:58:33 +05:30 committed by GitHub
parent 7c21cf2255
commit 6bfa551c85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 142 additions and 38 deletions

View file

@ -28,7 +28,7 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
end
def campaign_params
params.require(:campaign).permit(:title, :description, :message, :enabled, :inbox_id, :sender_id,
params.require(:campaign).permit(:title, :description, :message, :enabled, :trigger_only_during_business_hours, :inbox_id, :sender_id,
:scheduled_at, audience: [:type, :id], trigger_rules: {})
end
end

View file

@ -54,6 +54,7 @@
"ERROR": "Time on page is required"
},
"ENABLED": "Enable campaign",
"TRIGGER_ONLY_BUSINESS_HOURS": "Trigger only during business hours",
"SUBMIT": "Add Campaign"
},
"API": {

View file

@ -146,6 +146,15 @@
/>
{{ $t('CAMPAIGN.ADD.FORM.ENABLED') }}
</label>
<label v-if="isOngoingType">
<input
v-model="triggerOnlyDuringBusinessHours"
type="checkbox"
value="triggerOnlyDuringBusinessHours"
name="triggerOnlyDuringBusinessHours"
/>
{{ $t('CAMPAIGN.ADD.FORM.TRIGGER_ONLY_BUSINESS_HOURS') }}
</label>
</div>
<div class="modal-footer">
@ -185,6 +194,7 @@ export default {
timeOnPage: 10,
show: true,
enabled: true,
triggerOnlyDuringBusinessHours: false,
scheduledAt: null,
selectedAudience: [],
senderList: [],
@ -280,6 +290,9 @@ export default {
inbox_id: this.selectedInbox,
sender_id: this.selectedSender || null,
enabled: this.enabled,
trigger_only_during_business_hours:
// eslint-disable-next-line prettier/prettier
this.triggerOnlyDuringBusinessHours,
trigger_rules: {
url: this.endPoint,
time_on_page: this.timeOnPage,

View file

@ -87,6 +87,15 @@
/>
{{ $t('CAMPAIGN.ADD.FORM.ENABLED') }}
</label>
<label v-if="isOngoingType">
<input
v-model="triggerOnlyDuringBusinessHours"
type="checkbox"
value="triggerOnlyDuringBusinessHours"
name="triggerOnlyDuringBusinessHours"
/>
{{ $t('CAMPAIGN.ADD.FORM.TRIGGER_ONLY_BUSINESS_HOURS') }}
</label>
</div>
<div class="modal-footer">
<woot-button :is-loading="uiFlags.isCreating">
@ -125,6 +134,7 @@ export default {
selectedInbox: null,
endPoint: '',
timeOnPage: 10,
triggerOnlyDuringBusinessHours: false,
show: true,
enabled: true,
senderList: [],
@ -209,6 +219,7 @@ export default {
title,
message,
enabled,
trigger_only_during_business_hours: triggerOnlyDuringBusinessHours,
inbox: { id: inboxId },
trigger_rules: { url: endPoint, time_on_page: timeOnPage },
sender,
@ -218,6 +229,7 @@ export default {
this.endPoint = endPoint;
this.timeOnPage = timeOnPage;
this.selectedInbox = inboxId;
this.triggerOnlyDuringBusinessHours = triggerOnlyDuringBusinessHours;
this.selectedSender = (sender && sender.id) || 0;
this.enabled = enabled;
this.loadInboxMembers();
@ -233,6 +245,9 @@ export default {
title: this.title,
message: this.message,
inbox_id: this.$route.params.inboxId,
trigger_only_during_business_hours:
// eslint-disable-next-line prettier/prettier
this.triggerOnlyDuringBusinessHours,
sender_id: this.selectedSender || null,
enabled: this.enabled,
trigger_rules: {

View file

@ -19,12 +19,14 @@ import Router from './views/Router';
import { getLocale } from './helpers/urlParamsHelper';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { isEmptyObject } from 'widget/helpers/utils';
import availabilityMixin from 'widget/mixins/availability';
export default {
name: 'App',
components: {
Router,
},
mixins: [availabilityMixin],
data() {
return {
showUnreadView: false,
@ -219,7 +221,11 @@ export default {
this.scrollConversationToBottom();
} else if (message.event === 'change-url') {
const { referrerURL, referrerHost } = message;
this.initCampaigns({ currentURL: referrerURL, websiteToken });
this.initCampaigns({
currentURL: referrerURL,
websiteToken,
isInBusinessHours: this.isInBusinessHours,
});
window.referrerURL = referrerURL;
bus.$emit(BUS_EVENTS.SET_REFERRER_HOST, referrerHost);
} else if (message.event === 'toggle-close-button') {

View file

@ -7,17 +7,24 @@ export const formatCampaigns = ({ campaigns }) => {
return campaigns.map(item => {
return {
id: item.id,
triggerOnlyDuringBusinessHours:
item.trigger_only_during_business_hours || false,
timeOnPage: item?.trigger_rules?.time_on_page,
url: item?.trigger_rules?.url,
};
});
};
// Find all campaigns that matches the current URL
export const filterCampaigns = ({ campaigns, currentURL }) => {
return campaigns.filter(
item =>
stripTrailingSlash({ URL: item.url }) ===
stripTrailingSlash({ URL: currentURL })
// Filter all campaigns based on current URL and business availability time
export const filterCampaigns = ({
campaigns,
currentURL,
isInBusinessHours,
}) => {
return campaigns.filter(item =>
item.triggerOnlyDuringBusinessHours
? isInBusinessHours
: stripTrailingSlash({ URL: item.url }) ===
stripTrailingSlash({ URL: currentURL })
);
};

View file

@ -1,6 +1,7 @@
export default [
{
id: 1,
trigger_only_during_business_hours: false,
trigger_rules: {
time_on_page: 3,
url: 'https://www.chatwoot.com/pricing',
@ -8,6 +9,7 @@ export default [
},
{
id: 2,
trigger_only_during_business_hours: false,
trigger_rules: {
time_on_page: 6,
url: 'https://www.chatwoot.com/about',

View file

@ -3,8 +3,12 @@ import {
formatCampaigns,
filterCampaigns,
} from '../campaignHelper';
import campaigns from './camapginFixtures';
describe('#Campagin Helper', () => {
import campaigns from './campaignFixtures';
global.chatwootWebChannel = {
workingHoursEnabled: false,
};
describe('#Campaigns Helper', () => {
describe('stripTrailingSlash', () => {
it('should return striped trailing slash if url with trailing slash is passed', () => {
expect(
@ -14,15 +18,17 @@ describe('#Campagin Helper', () => {
});
describe('formatCampaigns', () => {
it('should return formated campaigns if camapgins are passed', () => {
it('should return formatted campaigns if campaigns are passed', () => {
expect(formatCampaigns({ campaigns })).toStrictEqual([
{
id: 1,
timeOnPage: 3,
triggerOnlyDuringBusinessHours: false,
url: 'https://www.chatwoot.com/pricing',
},
{
id: 2,
triggerOnlyDuringBusinessHours: false,
timeOnPage: 6,
url: 'https://www.chatwoot.com/about',
},
@ -30,7 +36,7 @@ describe('#Campagin Helper', () => {
});
});
describe('filterCampaigns', () => {
it('should return filtered campaigns if formatted camapgins are passed', () => {
it('should return filtered campaigns if formatted campaigns are passed', () => {
expect(
filterCampaigns({
campaigns: [

View file

@ -58,5 +58,9 @@ export default {
closeMinute: workingHourConfig.close_minutes,
};
},
isInBusinessHours() {
const { workingHoursEnabled } = window.chatwootWebChannel;
return workingHoursEnabled ? this.isInBetweenTheWorkingHours : true;
},
},
};

View file

@ -14,12 +14,18 @@ const state = {
activeCampaign: {},
};
const resetCampaignTimers = (campaigns, currentURL, websiteToken) => {
const resetCampaignTimers = (
campaigns,
currentURL,
websiteToken,
isInBusinessHours
) => {
const formattedCampaigns = formatCampaigns({ campaigns });
// Find all campaigns that matches the current URL
const filteredCampaigns = filterCampaigns({
campaigns: formattedCampaigns,
currentURL,
isInBusinessHours,
});
campaignTimer.initTimers({ campaigns: filteredCampaigns }, websiteToken);
};
@ -31,13 +37,21 @@ export const getters = {
};
export const actions = {
fetchCampaigns: async ({ commit }, { websiteToken, currentURL }) => {
fetchCampaigns: async (
{ commit },
{ websiteToken, currentURL, isInBusinessHours }
) => {
try {
const { data: campaigns } = await getCampaigns(websiteToken);
commit('setCampaigns', campaigns);
commit('setError', false);
commit('setHasFetched', true);
resetCampaignTimers(campaigns, currentURL, websiteToken);
resetCampaignTimers(
campaigns,
currentURL,
websiteToken,
isInBusinessHours
);
} catch (error) {
commit('setError', true);
commit('setHasFetched', true);
@ -45,12 +59,21 @@ export const actions = {
},
initCampaigns: async (
{ getters: { getCampaigns: campaigns }, dispatch },
{ currentURL, websiteToken }
{ currentURL, websiteToken, isInBusinessHours }
) => {
if (!campaigns.length) {
dispatch('fetchCampaigns', { websiteToken, currentURL });
dispatch('fetchCampaigns', {
websiteToken,
currentURL,
isInBusinessHours,
});
} else {
resetCampaignTimers(campaigns, currentURL, websiteToken);
resetCampaignTimers(
campaigns,
currentURL,
websiteToken,
isInBusinessHours
);
}
},
startCampaign: async ({ commit }, { websiteToken, campaignId }) => {

View file

@ -15,7 +15,11 @@ describe('#actions', () => {
API.get.mockResolvedValue({ data: campaigns });
await actions.fetchCampaigns(
{ commit },
{ websiteToken: 'XDsafmADasd', currentURL: 'https://chatwoot.com' }
{
websiteToken: 'XDsafmADasd',
currentURL: 'https://chatwoot.com',
isInBusinessHours: true,
}
);
expect(commit.mock.calls).toEqual([
['setCampaigns', campaigns],
@ -25,7 +29,12 @@ describe('#actions', () => {
expect(campaignTimer.initTimers).toHaveBeenCalledWith(
{
campaigns: [
{ id: 11, timeOnPage: '20', url: 'https://chatwoot.com' },
{
id: 11,
timeOnPage: '20',
url: 'https://chatwoot.com',
triggerOnlyDuringBusinessHours: false,
},
],
},
'XDsafmADasd'
@ -35,7 +44,11 @@ describe('#actions', () => {
API.get.mockRejectedValue({ message: 'Authentication required' });
await actions.fetchCampaigns(
{ commit },
{ websiteToken: 'XDsafmADasd', currentURL: 'https://www.chatwoot.com' }
{
websiteToken: 'XDsafmADasd',
currentURL: 'https://www.chatwoot.com',
isInBusinessHours: true,
}
);
expect(commit.mock.calls).toEqual([
['setError', true],
@ -65,7 +78,12 @@ describe('#actions', () => {
expect(campaignTimer.initTimers).toHaveBeenCalledWith(
{
campaigns: [
{ id: 11, timeOnPage: '20', url: 'https://chatwoot.com' },
{
id: 11,
timeOnPage: '20',
url: 'https://chatwoot.com',
triggerOnlyDuringBusinessHours: false,
},
],
},
'XDsafmADasd'

View file

@ -2,22 +2,23 @@
#
# Table name: campaigns
#
# id :bigint not null, primary key
# audience :jsonb
# campaign_status :integer default("active"), not null
# campaign_type :integer default("ongoing"), not null
# description :text
# enabled :boolean default(TRUE)
# message :text not null
# scheduled_at :datetime
# title :string not null
# trigger_rules :jsonb
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# display_id :integer not null
# inbox_id :bigint not null
# sender_id :integer
# id :bigint not null, primary key
# audience :jsonb
# campaign_status :integer default("active"), not null
# campaign_type :integer default("ongoing"), not null
# description :text
# enabled :boolean default(TRUE)
# message :text not null
# scheduled_at :datetime
# title :string not null
# trigger_only_during_business_hours :boolean default(FALSE)
# trigger_rules :jsonb
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# display_id :integer not null
# inbox_id :bigint not null
# sender_id :integer
#
# Indexes
#

View file

@ -17,5 +17,6 @@ if resource.campaign_type == 'one_off'
json.audience resource.audience
end
json.trigger_rules resource.trigger_rules
json.trigger_only_during_business_hours resource.trigger_only_during_business_hours
json.created_at resource.created_at
json.updated_at resource.updated_at

View file

@ -1,6 +1,7 @@
json.array! @campaigns do |campaign|
json.id campaign.display_id
json.trigger_rules campaign.trigger_rules
json.trigger_only_during_business_hours campaign.trigger_only_during_business_hours
json.message campaign.message
json.sender campaign.sender&.slice(:name, :avatar_url)
end

View file

@ -0,0 +1,5 @@
class AddTriggerOnlyDuringBusinessHoursCollectToCampaigns < ActiveRecord::Migration[6.1]
def change
add_column :campaigns, :trigger_only_during_business_hours, :boolean, default: false
end
end

View file

@ -147,6 +147,7 @@ ActiveRecord::Schema.define(version: 2021_09_29_150415) do
t.integer "campaign_status", default: 0, null: false
t.jsonb "audience", default: []
t.datetime "scheduled_at"
t.boolean "trigger_only_during_business_hours", default: false
t.index ["account_id"], name: "index_campaigns_on_account_id"
t.index ["campaign_status"], name: "index_campaigns_on_campaign_status"
t.index ["campaign_type"], name: "index_campaigns_on_campaign_type"