feat: Add APIs for Campaigns (#2175)

This commit is contained in:
Sojan Jose 2021-04-29 22:23:32 +05:30 committed by GitHub
parent 3afc9b5f5b
commit b89cc9cf57
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 452 additions and 1 deletions

View file

@ -0,0 +1,28 @@
class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
before_action :campaign, except: [:index, :create]
before_action :check_authorization
def index
@campaigns = Current.account.campaigns
end
def create
@campaign = Current.account.campaigns.create!(campaign_params)
end
def show; end
def update
@campaign.update(campaign_params)
end
private
def campaign
@campaign ||= Current.account.campaigns.find_by(display_id: params[:id])
end
def campaign_params
params.require(:campaign).permit(:title, :description, :content, :enabled, :inbox_id, :sender_id, trigger_rules: {})
end
end

View file

@ -11,6 +11,10 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
@assignable_agents = (Current.account.users.where(id: @inbox.members.select(:user_id)) + Current.account.administrators).uniq @assignable_agents = (Current.account.users.where(id: @inbox.members.select(:user_id)) + Current.account.administrators).uniq
end end
def campaigns
@campaigns = @inbox.campaigns
end
def create def create
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
channel = create_channel channel = create_channel

View file

@ -36,6 +36,7 @@ class Account < ApplicationRecord
has_many :data_imports, dependent: :destroy has_many :data_imports, dependent: :destroy
has_many :users, through: :account_users has_many :users, through: :account_users
has_many :inboxes, dependent: :destroy has_many :inboxes, dependent: :destroy
has_many :campaigns, dependent: :destroy
has_many :conversations, dependent: :destroy has_many :conversations, dependent: :destroy
has_many :messages, dependent: :destroy has_many :messages, dependent: :destroy
has_many :contacts, dependent: :destroy has_many :contacts, dependent: :destroy
@ -104,4 +105,8 @@ class Account < ApplicationRecord
trigger.after(:insert).for_each(:row) do trigger.after(:insert).for_each(:row) do
"execute format('create sequence IF NOT EXISTS conv_dpid_seq_%s', NEW.id);" "execute format('create sequence IF NOT EXISTS conv_dpid_seq_%s', NEW.id);"
end end
trigger.name('camp_dpid_before_insert').after(:insert).for_each(:row) do
"execute format('create sequence IF NOT EXISTS camp_dpid_seq_%s', NEW.id);"
end
end end

49
app/models/campaign.rb Normal file
View file

@ -0,0 +1,49 @@
# == Schema Information
#
# Table name: campaigns
#
# id :bigint not null, primary key
# content :text not null
# description :text
# enabled :boolean default(TRUE)
# 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
#
# Indexes
#
# index_campaigns_on_account_id (account_id)
# index_campaigns_on_inbox_id (inbox_id)
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# fk_rails_... (inbox_id => inboxes.id)
#
class Campaign < ApplicationRecord
validates :account_id, presence: true
validates :inbox_id, presence: true
belongs_to :account
belongs_to :inbox
belongs_to :sender, class_name: 'User', optional: true
has_many :conversations, dependent: :nullify, autosave: true
after_commit :set_display_id, unless: :display_id?
private
def set_display_id
reload
end
# creating db triggers
trigger.before(:insert).for_each(:row) do
"NEW.display_id := nextval('camp_dpid_seq_' || NEW.account_id);"
end
end

View file

@ -14,6 +14,7 @@
# updated_at :datetime not null # updated_at :datetime not null
# account_id :integer not null # account_id :integer not null
# assignee_id :integer # assignee_id :integer
# campaign_id :bigint
# contact_id :bigint # contact_id :bigint
# contact_inbox_id :bigint # contact_inbox_id :bigint
# display_id :integer not null # display_id :integer not null
@ -24,11 +25,13 @@
# #
# index_conversations_on_account_id (account_id) # index_conversations_on_account_id (account_id)
# index_conversations_on_account_id_and_display_id (account_id,display_id) UNIQUE # index_conversations_on_account_id_and_display_id (account_id,display_id) UNIQUE
# index_conversations_on_campaign_id (campaign_id)
# index_conversations_on_contact_inbox_id (contact_inbox_id) # index_conversations_on_contact_inbox_id (contact_inbox_id)
# index_conversations_on_team_id (team_id) # index_conversations_on_team_id (team_id)
# #
# Foreign Keys # Foreign Keys
# #
# fk_rails_... (campaign_id => campaigns.id)
# fk_rails_... (contact_inbox_id => contact_inboxes.id) # fk_rails_... (contact_inbox_id => contact_inboxes.id)
# fk_rails_... (team_id => teams.id) # fk_rails_... (team_id => teams.id)
# #
@ -54,6 +57,7 @@ class Conversation < ApplicationRecord
belongs_to :contact belongs_to :contact
belongs_to :contact_inbox belongs_to :contact_inbox
belongs_to :team, optional: true belongs_to :team, optional: true
belongs_to :campaign, optional: true
has_many :messages, dependent: :destroy, autosave: true has_many :messages, dependent: :destroy, autosave: true

View file

@ -36,6 +36,7 @@ class Inbox < ApplicationRecord
belongs_to :channel, polymorphic: true, dependent: :destroy belongs_to :channel, polymorphic: true, dependent: :destroy
has_many :campaigns, dependent: :destroy
has_many :contact_inboxes, dependent: :destroy has_many :contact_inboxes, dependent: :destroy
has_many :contacts, through: :contact_inboxes has_many :contacts, through: :contact_inboxes

View file

@ -0,0 +1,21 @@
class CampaignPolicy < ApplicationPolicy
def index?
@account_user.administrator?
end
def update?
@account_user.administrator?
end
def show?
@account_user.administrator?
end
def create?
@account_user.administrator?
end
def destroy?
@account_user.administrator?
end
end

View file

@ -27,6 +27,10 @@ class InboxPolicy < ApplicationPolicy
true true
end end
def campaigns?
@account_user.administrator?
end
def create? def create?
@account_user.administrator? @account_user.administrator?
end end

View file

@ -0,0 +1 @@
json.partial! 'api/v1/models/campaign.json.jbuilder', resource: @campaign

View file

@ -0,0 +1,3 @@
json.array! @campaigns do |campaign|
json.partial! 'api/v1/models/campaign.json.jbuilder', resource: campaign
end

View file

@ -0,0 +1 @@
json.partial! 'api/v1/models/campaign.json.jbuilder', resource: @campaign

View file

@ -0,0 +1 @@
json.partial! 'api/v1/models/campaign.json.jbuilder', resource: @campaign

View file

@ -0,0 +1,3 @@
json.array! @campaigns do |campaign|
json.partial! 'api/v1/models/campaign.json.jbuilder', resource: campaign
end

View file

@ -0,0 +1,15 @@
json.id resource.display_id
json.content resource.content
json.description resource.description
json.enabled resource.enabled
json.title resource.title
json.trigger_rules resource.trigger_rules
json.account_id resource.account_id
json.inbox do
json.partial! 'api/v1/models/inbox.json.jbuilder', resource: resource.inbox
end
json.sender do
json.partial! 'api/v1/models/agent.json.jbuilder', resource: resource.sender if resource.sender.present?
end
json.created_at resource.created_at
json.updated_at resource.updated_at

View file

@ -42,6 +42,8 @@ Rails.application.routes.draw do
end end
end end
resources :canned_responses, except: [:show, :edit, :new] resources :canned_responses, except: [:show, :edit, :new]
resources :campaigns, only: [:index, :create, :show, :update]
namespace :channels do namespace :channels do
resource :twilio_channel, only: [:create] resource :twilio_channel, only: [:create]
end end
@ -89,6 +91,7 @@ Rails.application.routes.draw do
resources :inboxes, only: [:index, :create, :update, :destroy] do resources :inboxes, only: [:index, :create, :update, :destroy] do
get :assignable_agents, on: :member get :assignable_agents, on: :member
get :campaigns, on: :member
post :set_agent_bot, on: :member post :set_agent_bot, on: :member
end end
resources :inbox_members, only: [:create, :show], param: :inbox_id resources :inbox_members, only: [:create, :show], param: :inbox_id

View file

@ -0,0 +1,18 @@
class Campaigns < ActiveRecord::Migration[6.0]
def change
create_table :campaigns do |t|
t.integer :display_id, null: false
t.string :title, null: false
t.text :description
t.text :content, null: false
t.integer :sender_id
t.boolean :enabled, default: true
t.references :account, null: false, foreign_key: true
t.references :inbox, null: false, foreign_key: true
t.column :trigger_rules, :jsonb, default: {}
t.timestamps
end
add_reference :conversations, :campaign, foreign_key: true
end
end

View file

@ -0,0 +1,28 @@
# This migration was auto-generated via `rake db:generate_trigger_migration'.
# While you can edit this file, any changes you make to the definitions here
# will be undone by the next auto-generated trigger migration.
class CreateTriggersAccountsInsertOrCampaignsInsert < ActiveRecord::Migration[6.0]
def up
create_trigger('camp_dpid_before_insert', generated: true, compatibility: 1)
.on('accounts')
.name('camp_dpid_before_insert')
.after(:insert)
.for_each(:row) do
"execute format('create sequence IF NOT EXISTS camp_dpid_seq_%s', NEW.id);"
end
create_trigger('campaigns_before_insert_row_tr', generated: true, compatibility: 1)
.on('campaigns')
.before(:insert)
.for_each(:row) do
"NEW.display_id := nextval('camp_dpid_seq_' || NEW.account_id);"
end
end
def down
drop_trigger('camp_dpid_before_insert', 'accounts', generated: true)
drop_trigger('campaigns_before_insert_row_tr', 'campaigns', generated: true)
end
end

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.define(version: 2021_04_26_191914) do ActiveRecord::Schema.define(version: 2021_04_28_151147) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements" enable_extension "pg_stat_statements"
@ -113,6 +113,22 @@ ActiveRecord::Schema.define(version: 2021_04_26_191914) do
t.string "extension" t.string "extension"
end end
create_table "campaigns", force: :cascade do |t|
t.integer "display_id", null: false
t.string "title", null: false
t.text "description"
t.text "content", null: false
t.integer "sender_id"
t.boolean "enabled", default: true
t.bigint "account_id", null: false
t.bigint "inbox_id", null: false
t.jsonb "trigger_rules", default: {}
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["account_id"], name: "index_campaigns_on_account_id"
t.index ["inbox_id"], name: "index_campaigns_on_inbox_id"
end
create_table "canned_responses", id: :serial, force: :cascade do |t| create_table "canned_responses", id: :serial, force: :cascade do |t|
t.integer "account_id", null: false t.integer "account_id", null: false
t.string "short_code" t.string "short_code"
@ -235,8 +251,10 @@ ActiveRecord::Schema.define(version: 2021_04_26_191914) do
t.string "identifier" t.string "identifier"
t.datetime "last_activity_at", default: -> { "CURRENT_TIMESTAMP" }, null: false t.datetime "last_activity_at", default: -> { "CURRENT_TIMESTAMP" }, null: false
t.bigint "team_id" t.bigint "team_id"
t.bigint "campaign_id"
t.index ["account_id", "display_id"], name: "index_conversations_on_account_id_and_display_id", unique: true t.index ["account_id", "display_id"], name: "index_conversations_on_account_id_and_display_id", unique: true
t.index ["account_id"], name: "index_conversations_on_account_id" t.index ["account_id"], name: "index_conversations_on_account_id"
t.index ["campaign_id"], name: "index_conversations_on_campaign_id"
t.index ["contact_inbox_id"], name: "index_conversations_on_contact_inbox_id" t.index ["contact_inbox_id"], name: "index_conversations_on_contact_inbox_id"
t.index ["team_id"], name: "index_conversations_on_team_id" t.index ["team_id"], name: "index_conversations_on_team_id"
end end
@ -592,8 +610,11 @@ ActiveRecord::Schema.define(version: 2021_04_26_191914) do
add_foreign_key "account_users", "accounts" add_foreign_key "account_users", "accounts"
add_foreign_key "account_users", "users" add_foreign_key "account_users", "users"
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "campaigns", "accounts"
add_foreign_key "campaigns", "inboxes"
add_foreign_key "contact_inboxes", "contacts" add_foreign_key "contact_inboxes", "contacts"
add_foreign_key "contact_inboxes", "inboxes" add_foreign_key "contact_inboxes", "inboxes"
add_foreign_key "conversations", "campaigns"
add_foreign_key "conversations", "contact_inboxes" add_foreign_key "conversations", "contact_inboxes"
add_foreign_key "conversations", "teams" add_foreign_key "conversations", "teams"
add_foreign_key "data_imports", "accounts" add_foreign_key "data_imports", "accounts"
@ -614,4 +635,19 @@ ActiveRecord::Schema.define(version: 2021_04_26_191914) do
"NEW.display_id := nextval('conv_dpid_seq_' || NEW.account_id);" "NEW.display_id := nextval('conv_dpid_seq_' || NEW.account_id);"
end end
create_trigger("camp_dpid_before_insert", :generated => true, :compatibility => 1).
on("accounts").
name("camp_dpid_before_insert").
after(:insert).
for_each(:row) do
"execute format('create sequence IF NOT EXISTS camp_dpid_seq_%s', NEW.id);"
end
create_trigger("campaigns_before_insert_row_tr", :generated => true, :compatibility => 1).
on("campaigns").
before(:insert).
for_each(:row) do
"NEW.display_id := nextval('camp_dpid_seq_' || NEW.account_id);"
end
end end

View file

@ -0,0 +1,148 @@
require 'rails_helper'
RSpec.describe 'Campaigns API', type: :request do
let(:account) { create(:account) }
describe 'GET /api/v1/accounts/{account.id}/campaigns' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/campaigns"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
let!(:campaign) { create(:campaign, account: account) }
it 'returns unauthorized for agents' do
get "/api/v1/accounts/#{account.id}/campaigns"
expect(response).to have_http_status(:unauthorized)
end
it 'returns all campaigns to administrators' do
get "/api/v1/accounts/#{account.id}/campaigns",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
body = JSON.parse(response.body, symbolize_names: true)
expect(body.first[:id]).to eq(campaign.display_id)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/campaigns/:id' do
let(:campaign) { create(:campaign, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/campaigns/#{campaign.display_id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
it 'returns unauthorized for agents' do
get "/api/v1/accounts/#{account.id}/campaigns/#{campaign.display_id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'shows the campaign for administrators' do
get "/api/v1/accounts/#{account.id}/campaigns/#{campaign.display_id}",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body, symbolize_names: true)[:id]).to eq(campaign.display_id)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/campaigns' do
let(:inbox) { create(:inbox, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/campaigns",
params: { inbox_id: inbox.id, title: 'test', content: 'test message' },
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
it 'returns unauthorized for agents' do
post "/api/v1/accounts/#{account.id}/campaigns",
params: { inbox_id: inbox.id, title: 'test', content: 'test message' },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'creates a new campaign' do
post "/api/v1/accounts/#{account.id}/campaigns",
params: { inbox_id: inbox.id, title: 'test', content: 'test message' },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body, symbolize_names: true)[:title]).to eq('test')
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/campaigns/:id' do
let(:inbox) { create(:inbox, account: account) }
let!(:campaign) { create(:campaign, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
patch "/api/v1/accounts/#{account.id}/campaigns/#{campaign.display_id}",
params: { inbox_id: inbox.id, title: 'test', content: 'test message' },
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
it 'returns unauthorized for agents' do
patch "/api/v1/accounts/#{account.id}/campaigns/#{campaign.display_id}",
params: { inbox_id: inbox.id, title: 'test', content: 'test message' },
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'updates the campaign' do
patch "/api/v1/accounts/#{account.id}/campaigns/#{campaign.display_id}",
params: { inbox_id: inbox.id, title: 'test', content: 'test message' },
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(JSON.parse(response.body, symbolize_names: true)[:title]).to eq('test')
end
end
end
end

View file

@ -74,6 +74,44 @@ RSpec.describe 'Inboxes API', type: :request do
end end
end end
describe 'GET /api/v1/accounts/{account.id}/inboxes/{inbox.id}/campaigns' do
let(:inbox) { create(:inbox, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/campaigns"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) }
let(:administrator) { create(:user, account: account, role: :administrator) }
let!(:campaign) { create(:campaign, account: account, inbox: inbox) }
it 'returns unauthorized for agents' do
get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/campaigns"
expect(response).to have_http_status(:unauthorized)
end
it 'returns all campaigns belonging to the inbox to administrators' do
# create a random campaign
create(:campaign, account: account)
get "/api/v1/accounts/#{account.id}/inboxes/#{inbox.id}/campaigns",
headers: administrator.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
body = JSON.parse(response.body, symbolize_names: true)
expect(body.first[:id]).to eq(campaign.display_id)
expect(body.length).to eq(1)
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/inboxes/:id' do describe 'DELETE /api/v1/accounts/{account.id}/inboxes/:id' do
let(:inbox) { create(:inbox, account: account) } let(:inbox) { create(:inbox, account: account) }

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
FactoryBot.define do
factory :campaign do
sequence(:title) { |n| "Campaign #{n}" }
sequence(:content) { |n| "Campaign content #{n}" }
after(:build) do |campaign|
campaign.account ||= create(:account)
campaign.inbox ||= create(
:inbox,
account: campaign.account,
channel: create(:channel_widget, account: campaign.account)
)
end
end
end

View file

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Campaign, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:account) }
it { is_expected.to belong_to(:inbox) }
end
describe '.before_create' do
let(:campaign) { build(:campaign, display_id: nil) }
before do
campaign.save
campaign.reload
end
it 'runs before_create callbacks' do
expect(campaign.display_id).to eq(1)
end
end
end

View file

@ -7,6 +7,7 @@ require Rails.root.join 'spec/models/concerns/round_robin_handler_spec.rb'
RSpec.describe Conversation, type: :model do RSpec.describe Conversation, type: :model do
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:account) } it { is_expected.to belong_to(:account) }
it { is_expected.to belong_to(:inbox) }
end end
describe 'concerns' do describe 'concerns' do