Merge branch 'develop' into codex/cw-7519-intercom-stalled-retry-15m

This commit is contained in:
Sony Mathew
2026-07-21 18:09:06 +05:30
committed by GitHub
1072 changed files with 49075 additions and 1593 deletions
@@ -64,6 +64,14 @@ RSpec.describe 'Agent Bot API', type: :request do
expect(response).to have_http_status(:success)
expect(response.body).to include(agent_bot.access_token.token)
end
it 'supports API token authentication' do
get "/api/v1/accounts/#{account.id}/agent_bots",
headers: { api_access_token: admin.access_token.token },
as: :json
expect(response).to have_http_status(:success)
end
end
end
@@ -0,0 +1,120 @@
require 'rails_helper'
RSpec.describe 'Api::V1::Accounts::Captain::AgentSessions', type: :request do
let(:account) { create(:account) }
let(:agent) { create(:user, account: account, role: :agent) }
let(:inbox) { create(:inbox, account: account) }
let(:conversation) { create(:conversation, account: account, inbox: inbox) }
let(:assistant) { create(:captain_assistant, account: account) }
let(:message) do
create(:message, account: account, conversation: conversation, message_type: :outgoing, sender: assistant)
end
before { create(:inbox_member, user: agent, inbox: inbox) }
def json_response
JSON.parse(response.body, symbolize_names: true)
end
describe 'GET /api/v1/accounts/:account_id/captain/agent_sessions/:id' do
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/captain/agent_sessions/#{message.id}", as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when the message has an agent session' do
let(:document) { create(:captain_document, account: account, assistant: assistant) }
let(:documented_faq) do
create(:captain_assistant_response, account: account, assistant: assistant,
question: 'How do I reset my password?', documentable: document)
end
let(:plain_faq) do
create(:captain_assistant_response, account: account, assistant: assistant, question: 'How do I change my email?')
end
let(:pdf_document) do
create(:captain_document, account: account, assistant: assistant, external_link: nil,
pdf_file: Rack::Test::UploadedFile.new(Rails.root.join('spec/assets/sample.pdf'), 'application/pdf'))
end
let(:pdf_faq) do
create(:captain_assistant_response, account: account, assistant: assistant,
question: 'What are the pricing tiers?', documentable: pdf_document)
end
let(:scenario) { create(:captain_scenario, account: account, assistant: assistant, title: 'Refund flow') }
let(:run_context) do
[
{ 'role' => 'user', 'content' => 'I want a refund' },
{ 'role' => 'assistant', 'content' => '', 'agent_name' => 'Assistant',
'tool_calls' => [{ 'id' => 'call_1', 'name' => 'faq_lookup', 'arguments' => { 'query' => 'refund' } }] },
{ 'role' => 'tool', 'content' => 'Refunds take 5 days', 'tool_call_id' => 'call_1' },
{ 'role' => 'assistant', 'content' => 'Refunds take 5 days', 'agent_name' => "scenario_#{scenario.id}_refund_flow" }
]
end
let!(:agent_session) do
create(:captain_agent_session, account: account, assistant: assistant,
subject: conversation, result: message,
llm_model: 'openai-gpt-5.2', credits_consumed: 1.0,
faq_ids: [documented_faq.id, plain_faq.id, pdf_faq.id, documented_faq.id + 100_000],
scenario_ids: [scenario.id],
run_context: run_context)
end
it 'returns the session with hydrated citations and scenarios' do
get "/api/v1/accounts/#{account.id}/captain/agent_sessions/#{message.id}",
headers: agent.create_new_auth_token, as: :json
expect(response).to have_http_status(:success)
aggregate_failures do
expect(json_response[:id]).to eq(agent_session.id)
expect(json_response[:message_id]).to eq(message.id)
expect(json_response[:llm_model]).to eq('openai-gpt-5.2')
expect(json_response[:credits_consumed]).to eq(1.0)
expect(json_response[:run_context].length).to eq(4)
expect(json_response[:run_context].second[:tool_calls].first[:arguments][:query]).to eq('refund')
citations = json_response[:citations].index_by { |citation| citation[:id] }
expect(citations.keys).to contain_exactly(documented_faq.id, plain_faq.id, pdf_faq.id)
expect(citations[documented_faq.id][:title]).to eq('How do I reset my password?')
expect(citations[documented_faq.id][:link]).to eq(document.external_link)
expect(citations[plain_faq.id][:link]).to be_nil
expect(pdf_document.external_link).to start_with('PDF:')
expect(citations[pdf_faq.id][:link]).to eq(pdf_document.display_url)
expect(citations[pdf_faq.id][:link]).to match(%r{\Ahttps?://})
expect(json_response[:scenarios]).to eq([{ id: scenario.id, title: 'Refund flow' }])
end
end
it 'does not allow an agent without access to the conversation' do
other_agent = create(:user, account: account, role: :agent)
get "/api/v1/accounts/#{account.id}/captain/agent_sessions/#{message.id}",
headers: other_agent.create_new_auth_token, as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when the message has no agent session' do
it 'returns not found' do
get "/api/v1/accounts/#{account.id}/captain/agent_sessions/#{message.id}",
headers: agent.create_new_auth_token, as: :json
expect(response).to have_http_status(:not_found)
end
end
context 'when the message does not belong to the account' do
it 'returns not found' do
other_message = create(:message)
get "/api/v1/accounts/#{account.id}/captain/agent_sessions/#{other_message.id}",
headers: agent.create_new_auth_token, as: :json
expect(response).to have_http_status(:not_found)
end
end
end
end
@@ -257,10 +257,20 @@ RSpec.describe 'Api::V1::Accounts::Captain::Assistants', type: :request do
let(:alice) { create(:user, account: account, role: :administrator, name: 'Alice Adams') }
let(:bob) { create(:user, account: account, role: :administrator, name: 'Bob Brown') }
let(:summary_service) { instance_double(Captain::OverviewSummaryService) }
let(:summary_stats) do
{
conversations_handled: { current: 42 },
hours_saved: { current: 12 },
auto_resolution_rate: { current: 65.0, trend: 5.0 },
handoff_rate: { current: 20.0, trend: -2.0 },
reopen_rate: { current: 5.0, trend: -1.0 },
knowledge: { coverage: 80, approved: 8, documents: 3 }
}
end
def get_summary(user)
get "/api/v1/accounts/#{account.id}/captain/assistants/#{assistant.id}/summary",
params: { range: '30' },
params: { range: '30', stats: summary_stats },
headers: user.create_new_auth_token,
as: :json
end
@@ -273,6 +283,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::Assistants', type: :request do
it 'caches the summary per viewer so one user never receives another user\'s greeting' do
allow(summary_service).to receive(:perform).and_return({ message: 'Hi Alice' })
expect(Captain::AssistantStatsBuilder).not_to receive(:new)
get_summary(alice)
get_summary(alice) # served from Alice's cache, no regeneration
@@ -280,6 +291,7 @@ RSpec.describe 'Api::V1::Accounts::Captain::Assistants', type: :request do
expect(response).to have_http_status(:success)
expect(Captain::OverviewSummaryService).to have_received(:new).twice
expect(Captain::OverviewSummaryService).to have_received(:new).with(hash_including(stats: summary_stats)).twice
end
it 'does not cache failures so a transient error is retried' do
@@ -33,8 +33,8 @@ RSpec.describe 'Twilio::VoiceController', type: :request do
expect(Voice::InboundCallBuilder).to receive(:perform!).with(
inbox: inbox,
from_number: from_number,
call_sid: call_sid
call_sid: call_sid,
caller: { source_ids: [from_number], contact_attributes: { name: from_number, phone_number: from_number } }
).and_return(call)
post "/twilio/voice/call/#{digits}", params: {
@@ -561,6 +561,22 @@ RSpec.describe Captain::Conversation::ResponseBuilderJob, type: :job do
expect(account.reload.usage_limits[:captain][:responses][:consumed]).to eq(0)
end
it 'attributes the handoff session to the private reason note when the tool recorded one' do
handoff_note = create(:message, conversation: conversation, account: account, message_type: :outgoing,
private: true, sender: assistant, content: 'Needs a human')
run_context[:state][:cw_metadata][:handoff_note_id] = handoff_note.id
allow(mock_agent_runner_service).to receive(:generate_response) do
conversation.update!(status: :open)
{ 'response' => 'Let me connect you', 'handoff_tool_called' => true }
end
described_class.perform_now(conversation, assistant)
session = Captain::AgentSession.last
expect(session.credits_consumed).to eq(0.0)
expect(session.result_id).to eq(handoff_note.id)
end
it 'creates a zero-credit session when the handoff tool fired but failed to commit' do
allow(mock_agent_runner_service).to receive(:generate_response).and_return({
'response' => 'I tried to hand off',
@@ -86,6 +86,12 @@ RSpec.describe Captain::Tools::HandoffTool, type: :model do
tool.perform(tool_context, reason: reason)
end
it 'records the handoff note id in the run state for session capture' do
tool.perform(tool_context, reason: 'Customer needs specialized support')
expect(tool_context.state[:cw_metadata][:handoff_note_id]).to eq(Message.last.id)
end
end
context 'without reason provided' do
@@ -107,6 +113,12 @@ RSpec.describe Captain::Tools::HandoffTool, type: :model do
tool.perform(tool_context)
end
it 'does not record a handoff note id since the empty note never renders' do
tool.perform(tool_context)
expect(tool_context.state[:cw_metadata]).to be_nil
end
end
context 'when handoff fails' do
@@ -111,6 +111,25 @@ RSpec.describe Captain::Assistant::SessionCaptureService do
expect(history.first).to include('role' => 'user', 'content' => 'CUST001')
end
it 'stores multimodal content without cached attachment bytes' do
content = RubyLLM::Content.new('See image', ['https://example.com/image.jpg'])
content.attachments.first.instance_variable_set(:@content, "\xFF\xD8\xFF\xE0JFIF".b)
run_context[:conversation_history] = [
{ role: :user, content: content },
{ role: :assistant, content: 'I can see the image', agent_name: 'Assistant' }
]
history = service.capture!.run_context
expect(history.first).to include(
'role' => 'user',
'content' => {
'text' => 'See image',
'attachments' => [{ 'type' => 'image', 'source' => 'https://example.com/image.jpg' }]
}
)
end
it 'stores the full history when it contains no user message' do
run_context[:conversation_history] = conversation_history.reject { |message| message[:role] == :user }
@@ -17,8 +17,8 @@ RSpec.describe Voice::InboundCallBuilder do
def perform_builder
described_class.perform!(
inbox: inbox,
from_number: from_number,
call_sid: call_sid
call_sid: call_sid,
caller: { source_ids: [from_number], contact_attributes: { name: from_number, phone_number: from_number } }
)
end
@@ -100,26 +100,29 @@ RSpec.describe Voice::InboundCallBuilder do
end
end
context 'when the WhatsApp wa_id needs Brazil normalization to match an existing ContactInbox' do
context 'when a WhatsApp call shares a BSUID with an existing ContactInbox' do
let(:whatsapp_channel) do
create(:channel_whatsapp, account: account, provider: 'whatsapp_cloud',
provider_config: { 'phone_number_id' => '123', 'source' => 'embedded_signup', 'calling_enabled' => true },
validate_provider_config: false, sync_templates: false)
end
let(:whatsapp_inbox) { whatsapp_channel.inbox }
let!(:stored_contact) { create(:contact, account: account, phone_number: '+5541988887777') }
let!(:stored_contact) { create(:contact, account: account) }
let!(:stored_contact_inbox) do
create(:contact_inbox, contact: stored_contact, inbox: whatsapp_inbox, source_id: '5541988887777')
create(:contact_inbox, contact: stored_contact, inbox: whatsapp_inbox, source_id: 'IN.2081978709342942')
end
before { account.enable_features!('channel_voice') }
it 'reuses the contact via normalized wa_id rather than forking a new ContactInbox' do
# Closes the gap: the contact was keyed by BSUID, but the call also carries a phone.
# Matching across every source_id reuses the contact instead of forking on the phone.
it 'reuses the contact by matching any source_id, not just the first' do
call = described_class.perform!(
inbox: whatsapp_inbox,
from_number: '+554188887777',
call_sid: 'wacall_br_1',
provider: :whatsapp
call_sid: 'wacall_bsuid_1',
provider: :whatsapp,
caller: { source_ids: ['5541988887777', 'IN.2081978709342942'],
contact_attributes: { name: 'Ada Lovelace', phone_number: '+5541988887777' } }
)
expect(call.contact).to eq(stored_contact)
@@ -74,6 +74,120 @@ describe Whatsapp::IncomingCallService do
end
end
describe 'inbound connect from a username (BSUID) caller' do
let(:sdp_offer) { "v=0\r\n...sdp..." }
let(:bsuid) { 'IN.2081978709342942' }
let!(:agent) { create(:user, account: account) }
before { create(:inbox_member, inbox: inbox, user: agent) }
it 'keys a phone caller by the phone (matching messaging) even when a BSUID is also present' do
allow(ActionCable.server).to receive(:broadcast)
params = {
calls: [{ id: provider_call_id, from: from_number, from_user_id: bsuid, event: 'connect',
session: { sdp: sdp_offer, sdp_type: 'offer' } }],
contacts: [{ wa_id: from_number, user_id: bsuid, profile: { name: 'Ada Lovelace' } }]
}
expect { described_class.new(inbox: inbox, params: params).perform }
.to change(Call, :count).by(1).and change(Conversation, :count).by(1)
contact_inbox = Call.last.conversation.contact_inbox
expect(contact_inbox.source_id).to eq(from_number)
expect(contact_inbox.contact.name).to eq('Ada Lovelace')
end
it 'keys a username-only caller by the BSUID when no phone `from` is present' do
allow(ActionCable.server).to receive(:broadcast)
params = {
calls: [{ id: provider_call_id, from_user_id: bsuid, event: 'connect',
session: { sdp: sdp_offer, sdp_type: 'offer' } }],
contacts: [{ user_id: bsuid, profile: { name: 'Ada Lovelace' } }]
}
expect { described_class.new(inbox: inbox, params: params).perform }
.to change(Call, :count).by(1)
contact_inbox = Call.last.conversation.contact_inbox
expect(contact_inbox.source_id).to eq(bsuid)
expect(contact_inbox.contact.name).to eq('Ada Lovelace')
end
it 'reuses the phone-keyed ContactInbox messaging created for a phone caller and backfills the BSUID alias' do
allow(ActionCable.server).to receive(:broadcast)
contact = create(:contact, account: account)
existing = create(:contact_inbox, inbox: inbox, contact: contact, source_id: from_number)
params = {
calls: [{ id: provider_call_id, from: from_number, from_user_id: bsuid, event: 'connect',
session: { sdp: sdp_offer, sdp_type: 'offer' } }],
contacts: [{ wa_id: from_number, user_id: bsuid }]
}
# The conversation reuses the existing phone thread; the BSUID alias is backfilled onto the same contact.
expect { described_class.new(inbox: inbox, params: params).perform }
.to change(Call, :count).by(1).and change(ContactInbox, :count).by(1)
expect(Call.last.contact).to eq(contact)
expect(Call.last.conversation.contact_inbox).to eq(existing)
expect(inbox.contact_inboxes.find_by(source_id: bsuid).contact).to eq(contact)
end
it 'reuses a phone ContactInbox via the same wa_id normalization messaging uses' do
allow(ActionCable.server).to receive(:broadcast)
contact = create(:contact, account: account, phone_number: '+5541988887777')
existing = create(:contact_inbox, inbox: inbox, contact: contact, source_id: '5541988887777')
params = {
calls: [{ id: provider_call_id, from: '554188887777', event: 'connect',
session: { sdp: sdp_offer, sdp_type: 'offer' } }],
contacts: [{ wa_id: '554188887777' }]
}
expect { described_class.new(inbox: inbox, params: params).perform }
.to change(Call, :count).by(1).and not_change(ContactInbox, :count)
expect(Call.last.conversation.contact_inbox).to eq(existing)
end
it 'reuses the BSUID-keyed ContactInbox messaging created for a username-only caller' do
allow(ActionCable.server).to receive(:broadcast)
contact = create(:contact, account: account)
existing = create(:contact_inbox, inbox: inbox, contact: contact, source_id: bsuid)
params = {
calls: [{ id: provider_call_id, from_user_id: bsuid, event: 'connect',
session: { sdp: sdp_offer, sdp_type: 'offer' } }],
contacts: [{ user_id: bsuid }]
}
expect { described_class.new(inbox: inbox, params: params).perform }
.to change(Call, :count).by(1).and not_change(ContactInbox, :count)
expect(Call.last.contact).to eq(contact)
expect(Call.last.conversation.contact_inbox).to eq(existing)
end
# The gap: messaging created the contact username-only (BSUID-keyed), and the call now
# also exposes a phone. Matching across every source_id reuses the BSUID thread instead
# of forking a new phone-keyed contact.
it 'reuses a BSUID-keyed ContactInbox even when the call also carries a phone and backfills the phone alias' do
allow(ActionCable.server).to receive(:broadcast)
contact = create(:contact, account: account)
existing = create(:contact_inbox, inbox: inbox, contact: contact, source_id: bsuid)
params = {
calls: [{ id: provider_call_id, from: from_number, from_user_id: bsuid, event: 'connect',
session: { sdp: sdp_offer, sdp_type: 'offer' } }],
contacts: [{ wa_id: from_number, user_id: bsuid }]
}
# The conversation reuses the existing BSUID thread; the phone alias is backfilled onto the same contact.
expect { described_class.new(inbox: inbox, params: params).perform }
.to change(Call, :count).by(1).and change(ContactInbox, :count).by(1)
expect(Call.last.contact).to eq(contact)
expect(Call.last.conversation.contact_inbox).to eq(existing)
expect(inbox.contact_inboxes.find_by(source_id: from_number).contact).to eq(contact)
end
end
describe 'outbound connect (existing call)' do
let!(:call) do
conversation = create(:conversation, account: account, inbox: inbox)
+8 -2
View File
@@ -117,10 +117,16 @@ RSpec.describe Account do
it 'configures the account feature flag extension column' do
expect(described_class.flag_columns).to include('feature_flags', 'feature_flags_ext_1')
expect(described_class.flag_mapping['feature_flags_ext_1']).to eq(feature_whatsapp_manual_transfer: 1, feature_data_import: 1 << 1,
feature_api_and_webhooks: 1 << 2, feature_whatsapp_reconfigure: 1 << 3)
expect(described_class.flag_mapping['feature_flags_ext_1']).to eq(
feature_whatsapp_manual_transfer: 1,
feature_data_import: 1 << 1,
feature_api_and_webhooks: 1 << 2,
feature_whatsapp_reconfigure: 1 << 3,
feature_whatsapp_embedded_signup_inbox_creation: 1 << 4
)
expect(described_class.flag_mapping['feature_flags_ext_1'][:feature_whatsapp_manual_transfer]).to eq(1)
expect(described_class.flag_mapping['feature_flags_ext_1'][:feature_data_import]).to eq(2)
expect(described_class.flag_mapping['feature_flags_ext_1'][:feature_whatsapp_embedded_signup_inbox_creation]).to eq(16)
end
it 'keeps existing feature flags on the original column' do
@@ -34,8 +34,8 @@ describe Whatsapp::IncomingMessageService do
it 'appends to last conversation when if conversation already exists' do
contact_inbox = create(:contact_inbox, inbox: whatsapp_channel.inbox, source_id: params[:messages].first[:from])
2.times.each { create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox) }
last_conversation = create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox)
2.times.each { create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox, contact: contact_inbox.contact) }
last_conversation = create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox, contact: contact_inbox.contact)
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
# no new conversation should be created
expect(whatsapp_channel.inbox.conversations.count).to eq(3)
@@ -46,7 +46,7 @@ describe Whatsapp::IncomingMessageService do
it 'reopen last conversation if last conversation is resolved and lock to single conversation is enabled' do
whatsapp_channel.inbox.update(lock_to_single_conversation: true)
contact_inbox = create(:contact_inbox, inbox: whatsapp_channel.inbox, source_id: params[:messages].first[:from])
last_conversation = create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox)
last_conversation = create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox, contact: contact_inbox.contact)
last_conversation.update(status: 'resolved')
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
# no new conversation should be created
@@ -59,7 +59,7 @@ describe Whatsapp::IncomingMessageService do
it 'creates a new conversation if last conversation is resolved and lock to single conversation is disabled' do
whatsapp_channel.inbox.update(lock_to_single_conversation: false)
contact_inbox = create(:contact_inbox, inbox: whatsapp_channel.inbox, source_id: params[:messages].first[:from])
last_conversation = create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox)
last_conversation = create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox, contact: contact_inbox.contact)
last_conversation.update(status: 'resolved')
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
# new conversation should be created
@@ -70,7 +70,7 @@ describe Whatsapp::IncomingMessageService do
it 'will not create a new conversation if last conversation is not resolved and lock to single conversation is disabled' do
whatsapp_channel.inbox.update(lock_to_single_conversation: false)
contact_inbox = create(:contact_inbox, inbox: whatsapp_channel.inbox, source_id: params[:messages].first[:from])
last_conversation = create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox)
last_conversation = create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox, contact: contact_inbox.contact)
last_conversation.update(status: Conversation.statuses.except('resolved').keys.sample)
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
# new conversation should be created
@@ -238,7 +238,7 @@ describe Whatsapp::IncomingMessageService do
end
before do
create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox)
create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox, contact: contact_inbox.contact)
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
end
@@ -453,7 +453,7 @@ describe Whatsapp::IncomingMessageService do
it 'appends to existing contact if contact inbox exists' do
contact_inbox = create(:contact_inbox, inbox: whatsapp_channel.inbox, source_id: wa_id)
last_conversation = create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox)
last_conversation = create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox, contact: contact_inbox.contact)
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
# no new conversation should be created
expect(whatsapp_channel.inbox.conversations.count).to eq(1)
@@ -468,7 +468,7 @@ describe Whatsapp::IncomingMessageService do
context 'when a contact inbox exists in the old format without 9 included' do
it 'appends to existing contact' do
contact_inbox = create(:contact_inbox, inbox: whatsapp_channel.inbox, source_id: wa_id)
last_conversation = create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox)
last_conversation = create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox, contact: contact_inbox.contact)
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
# no new conversation should be created
expect(whatsapp_channel.inbox.conversations.count).to eq(1)
@@ -480,7 +480,7 @@ describe Whatsapp::IncomingMessageService do
context 'when a contact inbox exists in the new format with 9 included' do
it 'appends to existing contact' do
contact_inbox = create(:contact_inbox, inbox: whatsapp_channel.inbox, source_id: '5541988887777')
last_conversation = create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox)
last_conversation = create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox, contact: contact_inbox.contact)
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
# no new conversation should be created
expect(whatsapp_channel.inbox.conversations.count).to eq(1)
@@ -515,7 +515,7 @@ describe Whatsapp::IncomingMessageService do
# Normalized format removes the 9 after country code
normalized_wa_id = '541123456789'
contact_inbox = create(:contact_inbox, inbox: whatsapp_channel.inbox, source_id: normalized_wa_id)
last_conversation = create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox)
last_conversation = create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox, contact: contact_inbox.contact)
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
# no new conversation should be created
expect(whatsapp_channel.inbox.conversations.count).to eq(1)
@@ -532,7 +532,7 @@ describe Whatsapp::IncomingMessageService do
context 'when a contact inbox exists with the same format' do
it 'appends to existing contact' do
contact_inbox = create(:contact_inbox, inbox: whatsapp_channel.inbox, source_id: wa_id)
last_conversation = create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox)
last_conversation = create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox, contact: contact_inbox.contact)
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
# no new conversation should be created
expect(whatsapp_channel.inbox.conversations.count).to eq(1)