diff --git a/app/builders/campaigns/campaign_conversation_builder.rb b/app/builders/campaigns/campaign_conversation_builder.rb new file mode 100644 index 000000000..3a9291a80 --- /dev/null +++ b/app/builders/campaigns/campaign_conversation_builder.rb @@ -0,0 +1,36 @@ +class Campaigns::CampaignConversationBuilder + pattr_initialize [:contact_inbox_id!, :campaign_display_id!, :conversation_additional_attributes] + + def perform + @contact_inbox = ContactInbox.find(@contact_inbox_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 + @conversation = ::Conversation.create!(conversation_params) + Messages::MessageBuilder.new(@campaign.sender, @conversation, message_params).perform + end + @conversation + end + + private + + def message_params + ActionController::Parameters.new({ + content: @campaign.message + }) + end + + def conversation_params + { + account_id: @campaign.account_id, + inbox_id: @contact_inbox.inbox_id, + contact_id: @contact_inbox.contact_id, + contact_inbox_id: @contact_inbox.id, + campaign_id: @campaign.id, + additional_attributes: conversation_additional_attributes + } + end +end diff --git a/app/controllers/api/v1/widget/campaigns_controller.rb b/app/controllers/api/v1/widget/campaigns_controller.rb new file mode 100644 index 000000000..fc26b18c8 --- /dev/null +++ b/app/controllers/api/v1/widget/campaigns_controller.rb @@ -0,0 +1,13 @@ +class Api::V1::Widget::CampaignsController < Api::V1::Widget::BaseController + skip_before_action :set_contact + + def index + @campaigns = @web_widget.inbox.campaigns.where(enabled: true) + end + + private + + def permitted_params + params.permit(:website_token) + end +end diff --git a/app/controllers/api/v1/widget/events_controller.rb b/app/controllers/api/v1/widget/events_controller.rb index 374541a10..2cf17c964 100644 --- a/app/controllers/api/v1/widget/events_controller.rb +++ b/app/controllers/api/v1/widget/events_controller.rb @@ -2,7 +2,8 @@ class Api::V1::Widget::EventsController < Api::V1::Widget::BaseController include Events::Types def create - Rails.configuration.dispatcher.dispatch(permitted_params[:name], Time.zone.now, contact_inbox: @contact_inbox, event_info: event_info) + Rails.configuration.dispatcher.dispatch(permitted_params[:name], Time.zone.now, contact_inbox: @contact_inbox, + event_info: permitted_params[:event_info].to_h.merge(event_info)) head :no_content end @@ -17,6 +18,6 @@ class Api::V1::Widget::EventsController < Api::V1::Widget::BaseController end def permitted_params - params.permit(:name, :website_token) + params.permit(:name, :website_token, event_info: {}) end end diff --git a/app/dispatchers/async_dispatcher.rb b/app/dispatchers/async_dispatcher.rb index e5fbdd7f7..0e238e3a0 100644 --- a/app/dispatchers/async_dispatcher.rb +++ b/app/dispatchers/async_dispatcher.rb @@ -12,7 +12,8 @@ class AsyncDispatcher < BaseDispatcher [ EventListener.instance, WebhookListener.instance, - InstallationWebhookListener.instance, HookListener.instance + InstallationWebhookListener.instance, HookListener.instance, + CampaignListener.instance ] end end diff --git a/app/listeners/campaign_listener.rb b/app/listeners/campaign_listener.rb new file mode 100644 index 000000000..ed082357f --- /dev/null +++ b/app/listeners/campaign_listener.rb @@ -0,0 +1,14 @@ +class CampaignListener < BaseListener + def campaign_triggered(event) + contact_inbox = event.data[:contact_inbox] + campaign_display_id = event.data[:event_info][:campaign_id] + + return if campaign_display_id.blank? + + ::Campaigns::CampaignConversationBuilder.new( + contact_inbox: contact_inbox.id, + campaign_display_id: campaign_display_id, + conversation_additional_attributes: event.data[:event_info].except(:campaign_id) + ).perform + end +end diff --git a/app/views/api/v1/widget/campaigns/index.json.jbuilder b/app/views/api/v1/widget/campaigns/index.json.jbuilder new file mode 100644 index 000000000..0d7aa24e3 --- /dev/null +++ b/app/views/api/v1/widget/campaigns/index.json.jbuilder @@ -0,0 +1,4 @@ +json.array! @campaigns do |campaign| + json.id campaign.display_id + json.trigger_rules campaign.trigger_rules +end diff --git a/config/routes.rb b/config/routes.rb index 288dae9ae..e558f0905 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -148,6 +148,7 @@ Rails.application.routes.draw do resources :agent_bots, only: [:index] namespace :widget do + resources :campaigns, only: [:index] resources :events, only: [:create] resources :messages, only: [:index, :create, :update] resources :conversations, only: [:index, :create] do diff --git a/lib/events/types.rb b/lib/events/types.rb index c8c420ffe..4d027f6f8 100644 --- a/lib/events/types.rb +++ b/lib/events/types.rb @@ -6,6 +6,9 @@ module Events::Types ACCOUNT_CREATED = 'account.created' #### Account Events ### + # campaign events + CAMPAIGN_TRIGGERED = 'campaign.triggered' + # channel events WEBWIDGET_TRIGGERED = 'webwidget.triggered' @@ -15,6 +18,7 @@ module Events::Types # FIXME: deprecate the opened and resolved events in future in favor of status changed event. CONVERSATION_OPENED = 'conversation.opened' CONVERSATION_RESOLVED = 'conversation.resolved' + CONVERSATION_STATUS_CHANGED = 'conversation.status_changed' CONVERSATION_CONTACT_CHANGED = 'conversation.contact_changed' ASSIGNEE_CHANGED = 'assignee.changed' diff --git a/spec/builders/campaigns/campaign_conversation_builder_spec.rb b/spec/builders/campaigns/campaign_conversation_builder_spec.rb new file mode 100644 index 000000000..076aeab67 --- /dev/null +++ b/spec/builders/campaigns/campaign_conversation_builder_spec.rb @@ -0,0 +1,31 @@ +require 'rails_helper' + +describe ::Campaigns::CampaignConversationBuilder do + let(:account) { create(:account) } + let(:inbox) { create(:inbox, account: account) } + let(:contact) { create(:contact, account: account, identifier: '123') } + let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: inbox) } + let(:campaign) { create(:campaign, inbox: inbox, account: account) } + + describe '#perform' do + it 'creates a conversation with campaign id and message with campaign message' do + campaign_conversation = described_class.new( + contact_inbox_id: contact_inbox.id, + campaign_display_id: campaign.display_id + ).perform + + expect(campaign_conversation.campaign_id).to eq(campaign.id) + expect(campaign_conversation.messages.first.content).to eq(campaign.message) + end + + it 'will not create a conversation with campaign id if another conversation exists' do + create(:conversation, contact_inbox_id: contact_inbox.id, inbox: inbox, account: account) + campaign_conversation = described_class.new( + contact_inbox_id: contact_inbox.id, + campaign_display_id: campaign.display_id + ).perform + + expect(campaign_conversation).to eq(nil) + end + end +end diff --git a/spec/controllers/api/v1/widget/campaigns_controller_spec.rb b/spec/controllers/api/v1/widget/campaigns_controller_spec.rb new file mode 100644 index 000000000..8a938b667 --- /dev/null +++ b/spec/controllers/api/v1/widget/campaigns_controller_spec.rb @@ -0,0 +1,31 @@ +require 'rails_helper' + +RSpec.describe '/api/v1/widget/campaigns', type: :request do + let(:account) { create(:account) } + let(:web_widget) { create(:channel_widget, account: account) } + let!(:campaign_1) { create(:campaign, inbox: web_widget.inbox, enabled: true, account: account) } + let!(:campaign_2) { create(:campaign, inbox: web_widget.inbox, enabled: false, account: account) } + + describe 'GET /api/v1/widget/campaigns' do + let(:params) { { website_token: web_widget.website_token } } + + context 'with correct website token' do + it 'returns the list of enabled campaigns' do + get '/api/v1/widget/campaigns', params: params + + expect(response).to have_http_status(:success) + json_response = JSON.parse(response.body) + expect(json_response.length).to eq 1 + expect(json_response.pluck('id')).to include(campaign_1.display_id) + expect(json_response.pluck('id')).not_to include(campaign_2.display_id) + end + end + + context 'with invalid website token' do + it 'returns the list of agents' do + get '/api/v1/widget/campaigns', params: { website_token: '' } + expect(response).to have_http_status(:not_found) + end + end + end +end diff --git a/spec/controllers/api/v1/widget/events_controller_spec.rb b/spec/controllers/api/v1/widget/events_controller_spec.rb index e2c45ab6e..da4136680 100644 --- a/spec/controllers/api/v1/widget/events_controller_spec.rb +++ b/spec/controllers/api/v1/widget/events_controller_spec.rb @@ -9,7 +9,7 @@ RSpec.describe '/api/v1/widget/events', type: :request do let(:token) { ::Widget::TokenService.new(payload: payload).generate_token } describe 'POST /api/v1/widget/events' do - let(:params) { { website_token: web_widget.website_token, name: 'webwidget.triggered' } } + let(:params) { { website_token: web_widget.website_token, name: 'webwidget.triggered', event_info: { test_id: 'test' } } } context 'with invalid website token' do it 'returns unauthorized' do @@ -32,7 +32,7 @@ RSpec.describe '/api/v1/widget/events', type: :request do expect(response).to have_http_status(:success) expect(Rails.configuration.dispatcher).to have_received(:dispatch) .with(params[:name], anything, contact_inbox: contact_inbox, - event_info: { browser_language: nil, widget_language: nil, browser: anything }) + event_info: { test_id: 'test', browser_language: nil, widget_language: nil, browser: anything }) end end end diff --git a/spec/controllers/api/v1/widget/inbox_members_controller_spec.rb b/spec/controllers/api/v1/widget/inbox_members_controller_spec.rb index 72b1315b8..dc7bd2ce9 100644 --- a/spec/controllers/api/v1/widget/inbox_members_controller_spec.rb +++ b/spec/controllers/api/v1/widget/inbox_members_controller_spec.rb @@ -11,7 +11,7 @@ RSpec.describe '/api/v1/widget/inbox_members', type: :request do create(:inbox_member, user: agent_2, inbox: web_widget.inbox) end - describe 'POST /api/v1/widget/inbox_members' do + describe 'GET /api/v1/widget/inbox_members' do let(:params) { { website_token: web_widget.website_token } } context 'with correct website token' do diff --git a/spec/listeners/campaign_listener_spec.rb b/spec/listeners/campaign_listener_spec.rb new file mode 100644 index 000000000..bf5ed77c0 --- /dev/null +++ b/spec/listeners/campaign_listener_spec.rb @@ -0,0 +1,40 @@ +require 'rails_helper' +describe CampaignListener do + let(:listener) { described_class.instance } + let(:account) { create(:account) } + let(:inbox) { create(:inbox, account: account) } + let(:contact) { create(:contact, account: account, identifier: '123') } + let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: inbox) } + let(:campaign) { create(:campaign, inbox: inbox, account: account) } + + let!(:event) do + Events::Base.new('campaign_triggered', Time.zone.now, + contact_inbox: contact_inbox, event_info: { campaign_id: campaign.display_id }) + end + + describe '#campaign_triggered' do + let(:builder) { double } + + before do + allow(Campaigns::CampaignConversationBuilder).to receive(:new).and_return(builder) + allow(builder).to receive(:perform) + end + + context 'when params contain campaign id' do + it 'triggers campaign conversation builder' do + expect(Campaigns::CampaignConversationBuilder).to receive(:new) + .with({ contact_inbox: contact_inbox.id, campaign_display_id: campaign.display_id, conversation_additional_attributes: {} }).once + listener.campaign_triggered(event) + end + end + + context 'when params does not contain campaign id' do + it 'does not trigger campaign conversation builder' do + event = Events::Base.new('campaign_triggered', Time.zone.now, + contact_inbox: contact_inbox, event_info: {}) + expect(Campaigns::CampaignConversationBuilder).to receive(:new).exactly(0).times + listener.campaign_triggered(event) + end + end + end +end