feat: Contactable Inboxes API (#2101)

- Add endpoint which lists inboxes through which a contact can be contacted
- Conversation creation API auto-creates contact_inbox for specific channels [ Twilio, email, api]
- Ability to send the initial message payload along with the conversation creation
- Fixes #1678 ( issue saving additional attributes for conversation )
This commit is contained in:
Sojan Jose
2021-04-15 15:13:01 +05:30
committed by GitHub
parent ba41a10609
commit 45e43b0b89
14 changed files with 494 additions and 10 deletions

View File

@@ -0,0 +1,231 @@
require 'rails_helper'
describe ::ContactInboxBuilder do
let(:account) { create(:account) }
let(:contact) { create(:contact, account: account) }
describe '#perform' do
describe 'twilio sms inbox' do
let!(:twilio_sms) { create(:channel_twilio_sms, account: account) }
let!(:twilio_inbox) { create(:inbox, channel: twilio_sms, account: account) }
it 'does not create contact inbox when contact inbox already exists with the source id provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number)
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: twilio_inbox.id,
source_id: contact.phone_number
).perform
expect(contact_inbox.id).to be(existing_contact_inbox.id)
end
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number)
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: twilio_inbox.id
).perform
expect(contact_inbox.id).to be(existing_contact_inbox.id)
end
it 'creates a new contact inbox when different source id is provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: contact.phone_number)
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: twilio_inbox.id,
source_id: '+224213223422'
).perform
expect(contact_inbox.id).not_to be(existing_contact_inbox.id)
expect(contact_inbox.source_id).not_to be('+224213223422')
end
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: twilio_inbox.id
).perform
expect(contact_inbox.source_id).not_to be(contact.phone_number)
end
end
describe 'twilio whatsapp inbox' do
let!(:twilio_whatsapp) { create(:channel_twilio_sms, medium: :whatsapp, account: account) }
let!(:twilio_inbox) { create(:inbox, channel: twilio_whatsapp, account: account) }
it 'does not create contact inbox when contact inbox already exists with the source id provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}")
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: twilio_inbox.id,
source_id: "whatsapp:#{contact.phone_number}"
).perform
expect(contact_inbox.id).to be(existing_contact_inbox.id)
end
it 'does not create contact inbox when contact inbox already exists with phone number and source id is not provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}")
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: twilio_inbox.id
).perform
expect(contact_inbox.id).to be(existing_contact_inbox.id)
end
it 'creates a new contact inbox when different source id is provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: twilio_inbox, source_id: "whatsapp:#{contact.phone_number}")
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: twilio_inbox.id,
source_id: 'whatsapp:+555555'
).perform
expect(contact_inbox.id).not_to be(existing_contact_inbox.id)
expect(contact_inbox.source_id).not_to be('whatsapp:+55555')
end
it 'creates a contact inbox with contact phone number when source id not provided and no contact inbox exists' do
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: twilio_inbox.id
).perform
expect(contact_inbox.source_id).not_to be("whatsapp:#{contact.phone_number}")
end
end
describe 'email inbox' do
let!(:email_channel) { create(:channel_email, account: account) }
let!(:email_inbox) { create(:inbox, channel: email_channel, account: account) }
it 'does not create contact inbox when contact inbox already exists with the source id provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email)
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: email_inbox.id,
source_id: contact.email
).perform
expect(contact_inbox.id).to be(existing_contact_inbox.id)
end
it 'does not create contact inbox when contact inbox already exists with email and source id is not provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email)
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: email_inbox.id
).perform
expect(contact_inbox.id).to be(existing_contact_inbox.id)
end
it 'creates a new contact inbox when different source id is provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: email_inbox, source_id: contact.email)
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: email_inbox.id,
source_id: 'xyc@xyc.com'
).perform
expect(contact_inbox.id).not_to be(existing_contact_inbox.id)
expect(contact_inbox.source_id).not_to be('xyc@xyc.com')
end
it 'creates a contact inbox with contact email when source id not provided and no contact inbox exists' do
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: email_inbox.id
).perform
expect(contact_inbox.source_id).not_to be(contact.email)
end
end
describe 'api inbox' do
let!(:api_channel) { create(:channel_api, account: account) }
let!(:api_inbox) { create(:inbox, channel: api_channel, account: account) }
it 'does not create contact inbox when contact inbox already exists with the source id provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: api_inbox, source_id: 'test')
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: api_inbox.id,
source_id: 'test'
).perform
expect(contact_inbox.id).to be(existing_contact_inbox.id)
end
it 'creates a new contact inbox when different source id is provided' do
existing_contact_inbox = create(:contact_inbox, contact: contact, inbox: api_inbox, source_id: SecureRandom.uuid)
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: api_inbox.id,
source_id: 'test'
).perform
expect(contact_inbox.id).not_to be(existing_contact_inbox.id)
expect(contact_inbox.source_id).not_to be('test')
end
it 'creates a contact inbox with SecureRandom.uuid when source id not provided and no contact inbox exists' do
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: api_inbox.id
).perform
expect(contact_inbox.source_id).not_to be(nil)
end
end
describe 'web widget' do
let!(:website_channel) { create(:channel_widget, account: account) }
let!(:website_inbox) { create(:inbox, channel: website_channel, account: account) }
it 'does not create contact inbox' do
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: website_inbox.id,
source_id: 'test'
).perform
expect(contact_inbox).to be(nil)
end
end
describe 'facebook inbox' do
let!(:facebook_channel) { create(:channel_facebook_page, account: account) }
let!(:facebook_inbox) { create(:inbox, channel: facebook_channel, account: account) }
it 'does not create contact inbox' do
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: facebook_inbox.id,
source_id: 'test'
).perform
expect(contact_inbox).to be(nil)
end
end
describe 'twitter inbox' do
let!(:twitter_channel) { create(:channel_twitter_profile, account: account) }
let!(:twitter_inbox) { create(:inbox, channel: twitter_channel, account: account) }
it 'does not create contact inbox' do
contact_inbox = described_class.new(
contact_id: contact.id,
inbox_id: twitter_inbox.id,
source_id: 'test'
).perform
expect(contact_inbox).to be(nil)
end
end
end
end

View File

@@ -189,6 +189,34 @@ RSpec.describe 'Contacts API', type: :request do
end
end
describe 'GET /api/v1/accounts/{account.id}/contacts/:id/contactable_inboxes' do
let!(:contact) { create(:contact, account: account) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/contactable_inboxes"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated user' do
let(:admin) { create(:user, account: account, role: :administrator) }
it 'shows the contact' do
inbox_service = double
allow(Contacts::ContactableInboxesService).to receive(:new).and_return(inbox_service)
allow(inbox_service).to receive(:get).and_return({})
expect(inbox_service).to receive(:get).and_return({})
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/contactable_inboxes",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/contacts' do
let(:custom_attributes) { { test: 'test', test1: 'test1' } }
let(:valid_params) { { contact: { name: 'test', custom_attributes: custom_attributes } } }

View File

@@ -151,14 +151,43 @@ RSpec.describe 'Conversations API', type: :request do
it 'creates a new conversation' do
allow(Rails.configuration.dispatcher).to receive(:dispatch)
additional_attributes = { test: 'test' }
post "/api/v1/accounts/#{account.id}/conversations",
headers: agent.create_new_auth_token,
params: { source_id: contact_inbox.source_id },
params: { source_id: contact_inbox.source_id, additional_attributes: additional_attributes },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data[:additional_attributes]).to eq(additional_attributes)
end
it 'creates a new conversation with message when message is passed' do
allow(Rails.configuration.dispatcher).to receive(:dispatch)
post "/api/v1/accounts/#{account.id}/conversations",
headers: agent.create_new_auth_token,
params: { source_id: contact_inbox.source_id, message: { content: 'hi' } },
as: :json
expect(response).to have_http_status(:success)
response_data = JSON.parse(response.body, symbolize_names: true)
expect(response_data[:additional_attributes]).to eq({})
expect(account.conversations.find_by(display_id: response_data[:id]).messages.first.content).to eq 'hi'
end
it 'calls contact inbox builder if contact_id and inbox_id is present' do
builder = double
contact = create(:contact, account: account)
inbox = create(:inbox, account: account)
allow(Rails.configuration.dispatcher).to receive(:dispatch)
allow(ContactInboxBuilder).to receive(:new).and_return(builder)
allow(builder).to receive(:perform)
expect(builder).to receive(:perform)
post "/api/v1/accounts/#{account.id}/conversations",
headers: agent.create_new_auth_token,
params: { contact_id: contact.id, inbox_id: inbox.id },
as: :json
end
end
end

View File

@@ -0,0 +1,66 @@
require 'rails_helper'
describe Contacts::ContactableInboxesService do
let(:account) { create(:account) }
let(:contact) { create(:contact, account: account) }
let!(:twilio_sms) { create(:channel_twilio_sms, account: account) }
let!(:twilio_sms_inbox) { create(:inbox, channel: twilio_sms, account: account) }
let!(:twilio_whatsapp) { create(:channel_twilio_sms, medium: :whatsapp, account: account) }
let!(:twilio_whatsapp_inbox) { create(:inbox, channel: twilio_whatsapp, account: account) }
let!(:email_channel) { create(:channel_email, account: account) }
let!(:email_inbox) { create(:inbox, channel: email_channel, account: account) }
let!(:api_channel) { create(:channel_api, account: account) }
let!(:api_inbox) { create(:inbox, channel: api_channel, account: account) }
let!(:website_channel) { create(:channel_widget, account: account) }
let!(:website_inbox) { create(:inbox, channel: website_channel, account: account) }
describe '#get' do
it 'returns the contactable inboxes for the contact' do
contactable_inboxes = described_class.new(contact: contact).get
expect(contactable_inboxes).to include({ source_id: contact.phone_number, inbox: twilio_sms_inbox })
expect(contactable_inboxes).to include({ source_id: "whatsapp:#{contact.phone_number}", inbox: twilio_whatsapp_inbox })
expect(contactable_inboxes).to include({ source_id: contact.email, inbox: email_inbox })
expect(contactable_inboxes.pluck(:inbox)).to include(api_inbox)
end
it 'doest not return the non contactable inboxes for the contact' do
facebook_channel = create(:channel_facebook_page, account: account)
facebook_inbox = create(:inbox, channel: facebook_channel, account: account)
twitter_channel = create(:channel_twitter_profile, account: account)
twitter_inbox = create(:inbox, channel: twitter_channel, account: account)
contactable_inboxes = described_class.new(contact: contact).get
expect(contactable_inboxes.pluck(:inbox)).not_to include(website_inbox)
expect(contactable_inboxes.pluck(:inbox)).not_to include(facebook_inbox)
expect(contactable_inboxes.pluck(:inbox)).not_to include(twitter_inbox)
end
context 'when api inbox is available' do
it 'returns existing source id if contact inbox exists' do
contact_inbox = create(:contact_inbox, inbox: api_inbox, contact: contact)
contactable_inboxes = described_class.new(contact: contact).get
expect(contactable_inboxes).to include({ source_id: contact_inbox.source_id, inbox: api_inbox })
end
end
context 'when website inbox is available' do
it 'returns existing source id if contact inbox exists without any conversations' do
contact_inbox = create(:contact_inbox, inbox: website_inbox, contact: contact)
contactable_inboxes = described_class.new(contact: contact).get
expect(contactable_inboxes).to include({ source_id: contact_inbox.source_id, inbox: website_inbox })
end
it 'does not return existing source id if contact inbox exists with conversations' do
contact_inbox = create(:contact_inbox, inbox: website_inbox, contact: contact)
create(:conversation, contact: contact, inbox: website_inbox, contact_inbox: contact_inbox)
contactable_inboxes = described_class.new(contact: contact).get
expect(contactable_inboxes.pluck(:inbox)).not_to include(website_inbox)
end
end
end
end