Compare commits

...
10 changed files with 83 additions and 8 deletions
@@ -29,6 +29,6 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
def campaign_params def campaign_params
params.require(:campaign).permit(:title, :description, :message, :enabled, :trigger_only_during_business_hours, :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: {}, template_params: {}) :scheduled_at, :allow_bots, audience: [:type, :id], trigger_rules: {}, template_params: {})
end end
end end
@@ -47,6 +47,7 @@ const initialState = {
senderId: 0, senderId: 0,
enabled: true, enabled: true,
triggerOnlyDuringBusinessHours: false, triggerOnlyDuringBusinessHours: false,
allowBots: false,
endPoint: '', endPoint: '',
timeOnPage: 10, timeOnPage: 10,
}; };
@@ -139,6 +140,7 @@ const prepareCampaignDetails = () => ({
sender_id: state.senderId || null, sender_id: state.senderId || null,
enabled: state.enabled, enabled: state.enabled,
trigger_only_during_business_hours: state.triggerOnlyDuringBusinessHours, trigger_only_during_business_hours: state.triggerOnlyDuringBusinessHours,
allow_bots: state.allowBots,
trigger_rules: { trigger_rules: {
url: state.endPoint, url: state.endPoint,
time_on_page: state.timeOnPage, time_on_page: state.timeOnPage,
@@ -166,6 +168,7 @@ const updateStateFromCampaign = campaign => {
sender, sender,
enabled, enabled,
trigger_only_during_business_hours: triggerOnlyDuringBusinessHours, trigger_only_during_business_hours: triggerOnlyDuringBusinessHours,
allow_bots: allowBots,
trigger_rules: { url: endPoint, time_on_page: timeOnPage }, trigger_rules: { url: endPoint, time_on_page: timeOnPage },
} = campaign; } = campaign;
@@ -176,6 +179,7 @@ const updateStateFromCampaign = campaign => {
senderId: sender?.id ?? 0, senderId: sender?.id ?? 0,
enabled, enabled,
triggerOnlyDuringBusinessHours, triggerOnlyDuringBusinessHours,
allowBots,
endPoint, endPoint,
timeOnPage, timeOnPage,
}); });
@@ -295,6 +299,13 @@ defineExpose({ prepareCampaignDetails, isSubmitDisabled });
}} }}
</span> </span>
</label> </label>
<label class="flex items-center gap-2">
<input v-model="state.allowBots" type="checkbox" />
<span class="text-sm font-medium text-n-slate-12">
{{ t('CAMPAIGN.LIVE_CHAT.CREATE.FORM.OTHER_PREFERENCES.ALLOW_BOTS') }}
</span>
</label>
</fieldset> </fieldset>
<div <div
@@ -57,7 +57,8 @@
"OTHER_PREFERENCES": { "OTHER_PREFERENCES": {
"TITLE": "Other preferences", "TITLE": "Other preferences",
"ENABLED": "Enable campaign", "ENABLED": "Enable campaign",
"TRIGGER_ONLY_BUSINESS_HOURS": "Trigger only during business hours" "TRIGGER_ONLY_BUSINESS_HOURS": "Trigger only during business hours",
"ALLOW_BOTS": "Allow bots to handle conversations"
}, },
"BUTTONS": { "BUTTONS": {
"CREATE": "Create", "CREATE": "Create",
+1
View File
@@ -3,6 +3,7 @@
# Table name: campaigns # Table name: campaigns
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# allow_bots :boolean default(FALSE), not null
# audience :jsonb # audience :jsonb
# campaign_status :integer default("active"), not null # campaign_status :integer default("active"), not null
# campaign_type :integer default("ongoing"), not null # campaign_type :integer default("ongoing"), not null
+7 -3
View File
@@ -228,9 +228,13 @@ class Conversation < ApplicationRecord
def determine_conversation_status def determine_conversation_status
self.status = :resolved and return if contact.blocked? self.status = :resolved and return if contact.blocked?
# Message template hooks aren't executed for conversations from campaigns # Handle campaign conversations
# So making these conversations open for agent visibility if campaign.present?
return if campaign.present? # If campaign allows bots and inbox has an active bot, start as pending
# Otherwise, start as open (default) for immediate agent visibility
self.status = :pending if campaign.allow_bots && inbox.active_bot?
return
end
# TODO: make this an inbox config instead of assuming bot conversations should start as pending # TODO: make this an inbox config instead of assuming bot conversations should start as pending
self.status = :pending if inbox.active_bot? self.status = :pending if inbox.active_bot?
@@ -19,5 +19,6 @@ if resource.campaign_type == 'one_off'
end end
json.trigger_rules resource.trigger_rules json.trigger_rules resource.trigger_rules
json.trigger_only_during_business_hours resource.trigger_only_during_business_hours json.trigger_only_during_business_hours resource.trigger_only_during_business_hours
json.allow_bots resource.allow_bots
json.created_at resource.created_at json.created_at resource.created_at
json.updated_at resource.updated_at json.updated_at resource.updated_at
@@ -0,0 +1,5 @@
class AddAllowBotsToCampaigns < ActiveRecord::Migration[7.1]
def change
add_column :campaigns, :allow_bots, :boolean, default: false, null: false
end
end
+2 -1
View File
@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2025_10_22_152158) do ActiveRecord::Schema[7.1].define(version: 2025_11_05_114654) do
# These extensions should be enabled to support this database # These extensions should be enabled to support this database
enable_extension "pg_stat_statements" enable_extension "pg_stat_statements"
enable_extension "pg_trgm" enable_extension "pg_trgm"
@@ -278,6 +278,7 @@ ActiveRecord::Schema[7.1].define(version: 2025_10_22_152158) do
t.datetime "scheduled_at", precision: nil t.datetime "scheduled_at", precision: nil
t.boolean "trigger_only_during_business_hours", default: false t.boolean "trigger_only_during_business_hours", default: false
t.jsonb "template_params" t.jsonb "template_params"
t.boolean "allow_bots", default: false, null: false
t.index ["account_id"], name: "index_campaigns_on_account_id" t.index ["account_id"], name: "index_campaigns_on_account_id"
t.index ["campaign_status"], name: "index_campaigns_on_campaign_status" t.index ["campaign_status"], name: "index_campaigns_on_campaign_status"
t.index ["campaign_type"], name: "index_campaigns_on_campaign_type" t.index ["campaign_type"], name: "index_campaigns_on_campaign_type"
@@ -108,6 +108,29 @@ RSpec.describe 'Campaigns API', type: :request do
expect(JSON.parse(response.body, symbolize_names: true)[:title]).to eq('test') expect(JSON.parse(response.body, symbolize_names: true)[:title]).to eq('test')
end end
it 'creates a new campaign with allow_bots enabled' do
post "/api/v1/accounts/#{account.id}/campaigns",
params: { inbox_id: inbox.id, title: 'test', message: 'test message', allow_bots: true },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data[:title]).to eq('test')
expect(response_data[:allow_bots]).to be true
end
it 'creates a new campaign with allow_bots disabled by default' do
post "/api/v1/accounts/#{account.id}/campaigns",
params: { inbox_id: inbox.id, title: 'test', message: 'test message' },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data[:allow_bots]).to be false
end
it 'creates a new ongoing campaign' do it 'creates a new ongoing campaign' do
post "/api/v1/accounts/#{account.id}/campaigns", post "/api/v1/accounts/#{account.id}/campaigns",
params: { inbox_id: inbox.id, title: 'test', message: 'test message', trigger_rules: { url: 'https://test.com' } }, params: { inbox_id: inbox.id, title: 'test', message: 'test message', trigger_rules: { url: 'https://test.com' } },
@@ -189,6 +212,16 @@ RSpec.describe 'Campaigns API', type: :request do
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
expect(JSON.parse(response.body, symbolize_names: true)[:title]).to eq('test') expect(JSON.parse(response.body, symbolize_names: true)[:title]).to eq('test')
end end
it 'updates the campaign allow_bots setting' do
patch "/api/v1/accounts/#{account.id}/campaigns/#{campaign.display_id}",
params: { allow_bots: true },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body, symbolize_names: true)[:allow_bots]).to be true
end
end end
end end
+20 -2
View File
@@ -576,10 +576,28 @@ RSpec.describe Conversation do
expect(conversation.status).to eq('pending') expect(conversation.status).to eq('pending')
end end
it 'returns conversation as open if campaign is present' do it 'returns conversation as open if campaign is present without allow_bots' do
conversation = create(:conversation, inbox: bot_inbox.inbox, campaign: create(:campaign)) conversation = create(:conversation, inbox: bot_inbox.inbox, campaign: create(:campaign, allow_bots: false))
expect(conversation.status).to eq('open') expect(conversation.status).to eq('open')
end end
it 'returns conversation as pending if campaign has allow_bots enabled' do
conversation = create(:conversation, inbox: bot_inbox.inbox, campaign: create(:campaign, allow_bots: true))
expect(conversation.status).to eq('pending')
end
it 'returns conversation as open if campaign has allow_bots but inbox has no bot' do
regular_inbox = create(:inbox, account: bot_inbox.inbox.account)
conversation = create(:conversation, inbox: regular_inbox, campaign: create(:campaign, allow_bots: true))
expect(conversation.status).to eq('open')
end
it 'does not start as pending when allow_bots is explicitly disabled even with bot inbox' do
campaign = create(:campaign, allow_bots: false)
conversation = create(:conversation, inbox: bot_inbox.inbox, campaign: campaign)
expect(conversation.status).to eq('open')
expect(conversation.status).not_to eq('pending')
end
end end
describe '#botintegration: when conversation created in inbox with dialogflow integration' do describe '#botintegration: when conversation created in inbox with dialogflow integration' do