Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe ActionService do
|
||||
let(:account) { create(:account) }
|
||||
|
||||
describe '#add_sla' do
|
||||
let(:sla_policy) { create(:sla_policy, account: account) }
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:action_service) { described_class.new(conversation) }
|
||||
|
||||
context 'when sla_policy_id is present' do
|
||||
it 'adds the sla policy to the conversation and create applied_sla entry' do
|
||||
action_service.add_sla([sla_policy.id])
|
||||
expect(conversation.reload.sla_policy_id).to eq(sla_policy.id)
|
||||
|
||||
# check if appliedsla table entry is created with matching attributes
|
||||
applied_sla = AppliedSla.last
|
||||
expect(applied_sla.account_id).to eq(account.id)
|
||||
expect(applied_sla.sla_policy_id).to eq(sla_policy.id)
|
||||
expect(applied_sla.conversation_id).to eq(conversation.id)
|
||||
expect(applied_sla.sla_status).to eq('active')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sla_policy_id is not present' do
|
||||
it 'does not add the sla policy to the conversation' do
|
||||
action_service.add_sla(nil)
|
||||
expect(conversation.reload.sla_policy_id).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation already has a sla policy' do
|
||||
it 'does not add the new sla policy to the conversation' do
|
||||
existing_sla_policy = sla_policy
|
||||
new_sla_policy = create(:sla_policy, account: account)
|
||||
conversation.update!(sla_policy_id: existing_sla_policy.id)
|
||||
action_service.add_sla([new_sla_policy.id])
|
||||
expect(conversation.reload.sla_policy_id).to eq(existing_sla_policy.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sla_policy is not found' do
|
||||
it 'does not add the sla policy to the conversation' do
|
||||
action_service.add_sla([sla_policy.id + 1])
|
||||
expect(conversation.reload.sla_policy_id).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,186 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Enterprise::AutoAssignment::AssignmentService, type: :service do
|
||||
let(:account) { create(:account) }
|
||||
let(:assignment_policy) { create(:assignment_policy, account: account, enabled: true) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:agent1) { create(:user, account: account, name: 'Agent 1') }
|
||||
let(:agent2) { create(:user, account: account, name: 'Agent 2') }
|
||||
let(:assignment_service) { AutoAssignment::AssignmentService.new(inbox: inbox) }
|
||||
|
||||
before do
|
||||
# Create inbox members
|
||||
create(:inbox_member, inbox: inbox, user: agent1)
|
||||
create(:inbox_member, inbox: inbox, user: agent2)
|
||||
|
||||
# Link inbox to assignment policy
|
||||
create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy)
|
||||
|
||||
# Enable assignment_v2 (base) and advanced_assignment (premium) features
|
||||
account.enable_features('assignment_v2')
|
||||
account.save!
|
||||
|
||||
# Set agents as online
|
||||
OnlineStatusTracker.update_presence(account.id, 'User', agent1.id)
|
||||
OnlineStatusTracker.set_status(account.id, agent1.id, 'online')
|
||||
OnlineStatusTracker.update_presence(account.id, 'User', agent2.id)
|
||||
OnlineStatusTracker.set_status(account.id, agent2.id, 'online')
|
||||
end
|
||||
|
||||
describe 'exclusion rules' do
|
||||
let(:capacity_policy) { create(:agent_capacity_policy, account: account) }
|
||||
let(:label1) { create(:label, account: account, title: 'high-priority') }
|
||||
let(:label2) { create(:label, account: account, title: 'vip') }
|
||||
|
||||
before do
|
||||
create(:inbox_capacity_limit, inbox: inbox, agent_capacity_policy: capacity_policy, conversation_limit: 10)
|
||||
inbox.enable_auto_assignment = true
|
||||
inbox.save!
|
||||
end
|
||||
|
||||
context 'when excluding conversations by label' do
|
||||
let!(:conversation_with_label) { create(:conversation, inbox: inbox, assignee: nil) }
|
||||
let!(:conversation_without_label) { create(:conversation, inbox: inbox, assignee: nil) }
|
||||
|
||||
before do
|
||||
conversation_with_label.update_labels([label1.title])
|
||||
|
||||
capacity_policy.update!(exclusion_rules: {
|
||||
'excluded_labels' => [label1.title]
|
||||
})
|
||||
end
|
||||
|
||||
it 'excludes conversations with specified labels' do
|
||||
# First check conversations are unassigned
|
||||
expect(conversation_with_label.assignee).to be_nil
|
||||
expect(conversation_without_label.assignee).to be_nil
|
||||
|
||||
# Run bulk assignment
|
||||
assigned_count = assignment_service.perform_bulk_assignment(limit: 10)
|
||||
|
||||
# Only the conversation without label should be assigned
|
||||
expect(assigned_count).to eq(1)
|
||||
expect(conversation_with_label.reload.assignee).to be_nil
|
||||
expect(conversation_without_label.reload.assignee).to be_present
|
||||
end
|
||||
|
||||
it 'handles bulk assignment correctly' do
|
||||
assigned_count = assignment_service.perform_bulk_assignment(limit: 10)
|
||||
|
||||
# Only 1 conversation should be assigned (the one without label)
|
||||
expect(assigned_count).to eq(1)
|
||||
expect(conversation_with_label.reload.assignee).to be_nil
|
||||
expect(conversation_without_label.reload.assignee).to be_present
|
||||
end
|
||||
|
||||
it 'excludes conversations with multiple labels' do
|
||||
conversation_without_label.update_labels([label2.title])
|
||||
|
||||
capacity_policy.update!(exclusion_rules: {
|
||||
'excluded_labels' => [label1.title, label2.title]
|
||||
})
|
||||
|
||||
assigned_count = assignment_service.perform_bulk_assignment(limit: 10)
|
||||
|
||||
# Both conversations should be excluded
|
||||
expect(assigned_count).to eq(0)
|
||||
expect(conversation_with_label.reload.assignee).to be_nil
|
||||
expect(conversation_without_label.reload.assignee).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when excluding conversations by age' do
|
||||
let!(:old_conversation) { create(:conversation, inbox: inbox, assignee: nil, created_at: 25.hours.ago) }
|
||||
let!(:recent_conversation) { create(:conversation, inbox: inbox, assignee: nil, created_at: 1.hour.ago) }
|
||||
|
||||
before do
|
||||
capacity_policy.update!(exclusion_rules: {
|
||||
'exclude_older_than_hours' => 24
|
||||
})
|
||||
end
|
||||
|
||||
it 'excludes conversations older than specified hours' do
|
||||
assigned_count = assignment_service.perform_bulk_assignment(limit: 10)
|
||||
|
||||
# Only recent conversation should be assigned
|
||||
expect(assigned_count).to eq(1)
|
||||
expect(old_conversation.reload.assignee).to be_nil
|
||||
expect(recent_conversation.reload.assignee).to be_present
|
||||
end
|
||||
|
||||
it 'handles different time thresholds' do
|
||||
capacity_policy.update!(exclusion_rules: {
|
||||
'exclude_older_than_hours' => 2
|
||||
})
|
||||
|
||||
assigned_count = assignment_service.perform_bulk_assignment(limit: 10)
|
||||
|
||||
# Only conversation created within 2 hours should be assigned
|
||||
expect(assigned_count).to eq(1)
|
||||
expect(recent_conversation.reload.assignee).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'when combining exclusion rules' do
|
||||
it 'applies both exclusion rules' do
|
||||
# Create conversations
|
||||
old_conversation_with_label = create(:conversation, inbox: inbox, assignee: nil, created_at: 25.hours.ago)
|
||||
old_conversation_without_label = create(:conversation, inbox: inbox, assignee: nil, created_at: 25.hours.ago)
|
||||
recent_conversation_with_label = create(:conversation, inbox: inbox, assignee: nil, created_at: 1.hour.ago)
|
||||
recent_conversation_without_label = create(:conversation, inbox: inbox, assignee: nil, created_at: 1.hour.ago)
|
||||
|
||||
# Add labels
|
||||
old_conversation_with_label.update_labels([label1.title])
|
||||
recent_conversation_with_label.update_labels([label1.title])
|
||||
|
||||
capacity_policy.update!(exclusion_rules: {
|
||||
'excluded_labels' => [label1.title],
|
||||
'exclude_older_than_hours' => 24
|
||||
})
|
||||
|
||||
assigned_count = assignment_service.perform_bulk_assignment(limit: 10)
|
||||
|
||||
# Only recent conversation without label should be assigned
|
||||
expect(assigned_count).to eq(1)
|
||||
expect(old_conversation_with_label.reload.assignee).to be_nil
|
||||
expect(old_conversation_without_label.reload.assignee).to be_nil
|
||||
expect(recent_conversation_with_label.reload.assignee).to be_nil
|
||||
expect(recent_conversation_without_label.reload.assignee).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'when exclusion rules are empty' do
|
||||
let!(:conversation1) { create(:conversation, inbox: inbox, assignee: nil) }
|
||||
let!(:conversation2) { create(:conversation, inbox: inbox, assignee: nil) }
|
||||
|
||||
before do
|
||||
capacity_policy.update!(exclusion_rules: {})
|
||||
end
|
||||
|
||||
it 'assigns all eligible conversations' do
|
||||
assigned_count = assignment_service.perform_bulk_assignment(limit: 10)
|
||||
|
||||
expect(assigned_count).to eq(2)
|
||||
expect(conversation1.reload.assignee).to be_present
|
||||
expect(conversation2.reload.assignee).to be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no capacity policy exists' do
|
||||
let!(:conversation1) { create(:conversation, inbox: inbox, assignee: nil) }
|
||||
let!(:conversation2) { create(:conversation, inbox: inbox, assignee: nil) }
|
||||
|
||||
before do
|
||||
InboxCapacityLimit.destroy_all
|
||||
end
|
||||
|
||||
it 'assigns all eligible conversations without exclusions' do
|
||||
assigned_count = assignment_service.perform_bulk_assignment(limit: 10)
|
||||
|
||||
expect(assigned_count).to eq(2)
|
||||
expect(conversation1.reload.assignee).to be_present
|
||||
expect(conversation2.reload.assignee).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,88 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Enterprise::AutoAssignment::BalancedSelector do
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:selector) { described_class.new(inbox: inbox) }
|
||||
let(:agent1) { create(:user, account: account, role: :agent, availability: :online) }
|
||||
let(:agent2) { create(:user, account: account, role: :agent, availability: :online) }
|
||||
let(:agent3) { create(:user, account: account, role: :agent, availability: :online) }
|
||||
let(:member1) { create(:inbox_member, inbox: inbox, user: agent1) }
|
||||
let(:member2) { create(:inbox_member, inbox: inbox, user: agent2) }
|
||||
let(:member3) { create(:inbox_member, inbox: inbox, user: agent3) }
|
||||
|
||||
describe '#select_agent' do
|
||||
context 'when selecting based on workload' do
|
||||
let(:available_agents) { [member1, member2, member3] }
|
||||
|
||||
it 'selects the agent with least open conversations' do
|
||||
# Agent1 has 3 open conversations
|
||||
3.times { create(:conversation, inbox: inbox, assignee: agent1, status: 'open') }
|
||||
|
||||
# Agent2 has 1 open conversation
|
||||
create(:conversation, inbox: inbox, assignee: agent2, status: 'open')
|
||||
|
||||
# Agent3 has 2 open conversations
|
||||
2.times { create(:conversation, inbox: inbox, assignee: agent3, status: 'open') }
|
||||
|
||||
selected_agent = selector.select_agent(available_agents)
|
||||
|
||||
# Should select agent2 as they have the least conversations
|
||||
expect(selected_agent).to eq(agent2)
|
||||
end
|
||||
|
||||
it 'considers only open conversations' do
|
||||
# Agent1 has 1 open and 3 resolved conversations
|
||||
create(:conversation, inbox: inbox, assignee: agent1, status: 'open')
|
||||
3.times { create(:conversation, inbox: inbox, assignee: agent1, status: 'resolved') }
|
||||
|
||||
# Agent2 has 2 open conversations
|
||||
2.times { create(:conversation, inbox: inbox, assignee: agent2, status: 'open') }
|
||||
|
||||
selected_agent = selector.select_agent([member1, member2])
|
||||
|
||||
# Should select agent1 as they have fewer open conversations
|
||||
expect(selected_agent).to eq(agent1)
|
||||
end
|
||||
|
||||
it 'selects any agent when agents have equal workload' do
|
||||
# All agents have same number of conversations
|
||||
[member1, member2, member3].each do |member|
|
||||
create(:conversation, inbox: inbox, assignee: member.user, status: 'open')
|
||||
end
|
||||
|
||||
selected_agent = selector.select_agent(available_agents)
|
||||
|
||||
# Should select one of the agents (when equal, min_by returns the first one it finds)
|
||||
expect([agent1, agent2, agent3]).to include(selected_agent)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no agents are available' do
|
||||
it 'returns nil' do
|
||||
selected_agent = selector.select_agent([])
|
||||
expect(selected_agent).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when one agent is available' do
|
||||
it 'returns that agent' do
|
||||
selected_agent = selector.select_agent([member1])
|
||||
expect(selected_agent).to eq(agent1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with new agents (no conversations)' do
|
||||
it 'prioritizes agents with no conversations' do
|
||||
# Agent1 and 2 have conversations
|
||||
create(:conversation, inbox: inbox, assignee: agent1, status: 'open')
|
||||
create(:conversation, inbox: inbox, assignee: agent2, status: 'open')
|
||||
|
||||
# Agent3 is new with no conversations
|
||||
selected_agent = selector.select_agent([member1, member2, member3])
|
||||
|
||||
expect(selected_agent).to eq(agent3)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,120 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Enterprise::AutoAssignment::CapacityService, type: :service do
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account, enable_auto_assignment: true) }
|
||||
|
||||
# Assignment policy with rate limiting
|
||||
let(:assignment_policy) do
|
||||
create(:assignment_policy,
|
||||
account: account,
|
||||
enabled: true,
|
||||
fair_distribution_limit: 5,
|
||||
fair_distribution_window: 3600)
|
||||
end
|
||||
|
||||
# Agent capacity policy
|
||||
let(:agent_capacity_policy) do
|
||||
create(:agent_capacity_policy, account: account, name: 'Limited Capacity')
|
||||
end
|
||||
|
||||
# Agents with different capacity settings
|
||||
let(:agent_with_capacity) { create(:user, account: account, role: :agent, availability: :online) }
|
||||
let(:agent_without_capacity) { create(:user, account: account, role: :agent, availability: :online) }
|
||||
let(:agent_at_capacity) { create(:user, account: account, role: :agent, availability: :online) }
|
||||
|
||||
before do
|
||||
# Create inbox assignment policy
|
||||
create(:inbox_assignment_policy, inbox: inbox, assignment_policy: assignment_policy)
|
||||
|
||||
# Set inbox capacity limit
|
||||
create(:inbox_capacity_limit,
|
||||
agent_capacity_policy: agent_capacity_policy,
|
||||
inbox: inbox,
|
||||
conversation_limit: 3)
|
||||
|
||||
# Assign capacity policy to specific agents
|
||||
agent_with_capacity.account_users.find_by(account: account)
|
||||
.update!(agent_capacity_policy: agent_capacity_policy)
|
||||
|
||||
agent_at_capacity.account_users.find_by(account: account)
|
||||
.update!(agent_capacity_policy: agent_capacity_policy)
|
||||
|
||||
# Create inbox members
|
||||
create(:inbox_member, inbox: inbox, user: agent_with_capacity)
|
||||
create(:inbox_member, inbox: inbox, user: agent_without_capacity)
|
||||
create(:inbox_member, inbox: inbox, user: agent_at_capacity)
|
||||
|
||||
# Mock online status
|
||||
allow(OnlineStatusTracker).to receive(:get_available_users).and_return({
|
||||
agent_with_capacity.id.to_s => 'online',
|
||||
agent_without_capacity.id.to_s => 'online',
|
||||
agent_at_capacity.id.to_s => 'online'
|
||||
})
|
||||
|
||||
# Enable assignment_v2 (base) and advanced_assignment (premium) features
|
||||
account.enable_features('assignment_v2', 'advanced_assignment')
|
||||
account.save!
|
||||
|
||||
# Create existing assignments for agent_at_capacity (at limit)
|
||||
3.times do
|
||||
create(:conversation, inbox: inbox, assignee: agent_at_capacity, status: :open)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'capacity filtering' do
|
||||
it 'excludes agents at capacity' do
|
||||
# Get available agents respecting capacity
|
||||
capacity_service = described_class.new
|
||||
online_agents = inbox.available_agents
|
||||
filtered_agents = online_agents.select do |inbox_member|
|
||||
capacity_service.agent_has_capacity?(inbox_member.user, inbox)
|
||||
end
|
||||
available_users = filtered_agents.map(&:user)
|
||||
|
||||
expect(available_users).to include(agent_with_capacity)
|
||||
expect(available_users).to include(agent_without_capacity) # No capacity policy = unlimited
|
||||
expect(available_users).not_to include(agent_at_capacity) # At capacity limit
|
||||
end
|
||||
|
||||
it 'respects inbox-specific capacity limits' do
|
||||
capacity_service = described_class.new
|
||||
|
||||
expect(capacity_service.agent_has_capacity?(agent_with_capacity, inbox)).to be true
|
||||
expect(capacity_service.agent_has_capacity?(agent_without_capacity, inbox)).to be true
|
||||
expect(capacity_service.agent_has_capacity?(agent_at_capacity, inbox)).to be false
|
||||
end
|
||||
end
|
||||
|
||||
describe 'assignment with capacity' do
|
||||
let(:service) { AutoAssignment::AssignmentService.new(inbox: inbox) }
|
||||
|
||||
it 'assigns to agents with available capacity' do
|
||||
# Create conversation before assignment
|
||||
conversation = create(:conversation, inbox: inbox, assignee: nil, status: :open)
|
||||
|
||||
# Mock the selector to prefer agent_at_capacity (but should skip due to capacity)
|
||||
selector = instance_double(AutoAssignment::RoundRobinSelector)
|
||||
allow(AutoAssignment::RoundRobinSelector).to receive(:new).and_return(selector)
|
||||
allow(selector).to receive(:select_agent) do |agents|
|
||||
agents.map(&:user).find { |u| [agent_with_capacity, agent_without_capacity].include?(u) }
|
||||
end
|
||||
|
||||
assigned_count = service.perform_bulk_assignment(limit: 1)
|
||||
expect(assigned_count).to eq(1)
|
||||
expect(conversation.reload.assignee).to be_in([agent_with_capacity, agent_without_capacity])
|
||||
expect(conversation.reload.assignee).not_to eq(agent_at_capacity)
|
||||
end
|
||||
|
||||
it 'returns false when all agents are at capacity' do
|
||||
# Fill up remaining agents
|
||||
3.times { create(:conversation, inbox: inbox, assignee: agent_with_capacity, status: :open) }
|
||||
|
||||
# agent_without_capacity has no limit, so should still be available
|
||||
conversation2 = create(:conversation, inbox: inbox, assignee: nil, status: :open)
|
||||
assigned_count = service.perform_bulk_assignment(limit: 1)
|
||||
expect(assigned_count).to eq(1)
|
||||
expect(conversation2.reload.assignee).to eq(agent_without_capacity)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,51 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Enterprise::Billing::CancelCloudSubscriptionsService do
|
||||
subject(:service) { described_class.new(account: account) }
|
||||
|
||||
let(:account) { create(:account, custom_attributes: custom_attributes) }
|
||||
let(:custom_attributes) { { 'stripe_customer_id' => 'cus_123' } }
|
||||
|
||||
describe '#perform' do
|
||||
context 'when deployment is not cloud' do
|
||||
it 'does not call stripe subscriptions api' do
|
||||
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(false)
|
||||
allow(Stripe::Subscription).to receive(:list)
|
||||
|
||||
service.perform
|
||||
|
||||
expect(Stripe::Subscription).not_to have_received(:list)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when stripe customer id is missing' do
|
||||
let(:custom_attributes) { {} }
|
||||
|
||||
it 'does not call stripe subscriptions api' do
|
||||
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
|
||||
allow(Stripe::Subscription).to receive(:list)
|
||||
|
||||
service.perform
|
||||
|
||||
expect(Stripe::Subscription).not_to have_received(:list)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is cloud with active subscriptions' do
|
||||
let(:subscription_response) { Struct.new(:data).new([sub_1, sub_2]) }
|
||||
let(:sub_1) { instance_double(Stripe::Subscription, id: 'sub_1', cancel_at_period_end: false) }
|
||||
let(:sub_2) { instance_double(Stripe::Subscription, id: 'sub_2', cancel_at_period_end: true) }
|
||||
|
||||
it 'marks only active subscriptions that are not yet set to cancel at period end' do
|
||||
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
|
||||
allow(Stripe::Subscription).to receive(:list).and_return(subscription_response)
|
||||
allow(Stripe::Subscription).to receive(:update)
|
||||
|
||||
service.perform
|
||||
|
||||
expect(Stripe::Subscription).to have_received(:list).with(customer: 'cus_123', status: 'active', limit: 100)
|
||||
expect(Stripe::Subscription).to have_received(:update).with('sub_1', cancel_at_period_end: true).once
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Enterprise::Billing::CreateSessionService do
|
||||
subject(:create_session_service) { described_class }
|
||||
|
||||
describe '#perform' do
|
||||
it 'calls stripe billing portal session' do
|
||||
customer_id = 'cus_random_number'
|
||||
return_url = 'https://www.chatwoot.com'
|
||||
allow(Stripe::BillingPortal::Session).to receive(:create).with({ customer: customer_id, return_url: return_url })
|
||||
|
||||
create_session_service.new.create_session(customer_id, return_url)
|
||||
|
||||
expect(Stripe::BillingPortal::Session).to have_received(:create).with(
|
||||
{
|
||||
customer: customer_id,
|
||||
return_url: return_url
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,142 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Enterprise::Billing::CreateStripeCustomerService do
|
||||
subject(:create_stripe_customer_service) { described_class }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let!(:admin1) { create(:user, account: account, role: :administrator) }
|
||||
let(:admin2) { create(:user, account: account, role: :administrator) }
|
||||
let(:subscriptions_list) { double }
|
||||
|
||||
describe '#perform' do
|
||||
before do
|
||||
create(
|
||||
:installation_config,
|
||||
{ name: 'CHATWOOT_CLOUD_PLANS', value: [
|
||||
{ 'name' => 'A Plan Name', 'product_id' => ['prod_hacker_random'], 'price_ids' => ['price_hacker_random'] }
|
||||
] }
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not call stripe methods if customer id is present' do
|
||||
account.update!(custom_attributes: { stripe_customer_id: 'cus_random_number' })
|
||||
allow(subscriptions_list).to receive(:data).and_return([])
|
||||
allow(Stripe::Customer).to receive(:create)
|
||||
allow(Stripe::Subscription).to receive(:list).and_return(subscriptions_list)
|
||||
allow(Stripe::Subscription).to receive(:create)
|
||||
.and_return(
|
||||
{
|
||||
plan: { id: 'price_random_number', product: 'prod_random_number' },
|
||||
quantity: 2
|
||||
}.with_indifferent_access
|
||||
)
|
||||
|
||||
create_stripe_customer_service.new(account: account).perform
|
||||
|
||||
expect(Stripe::Customer).not_to have_received(:create)
|
||||
expect(Stripe::Subscription)
|
||||
.to have_received(:create)
|
||||
.with({ customer: 'cus_random_number', items: [{ price: 'price_hacker_random', quantity: 2 }] })
|
||||
|
||||
expect(account.reload.custom_attributes).to eq(
|
||||
{
|
||||
stripe_customer_id: 'cus_random_number',
|
||||
stripe_price_id: 'price_random_number',
|
||||
stripe_product_id: 'prod_random_number',
|
||||
subscribed_quantity: 2,
|
||||
plan_name: 'A Plan Name'
|
||||
}.with_indifferent_access
|
||||
)
|
||||
end
|
||||
|
||||
it 'calls stripe methods to create a customer and updates the account' do
|
||||
customer = double
|
||||
allow(Stripe::Customer).to receive(:create).and_return(customer)
|
||||
allow(customer).to receive(:id).and_return('cus_random_number')
|
||||
allow(Stripe::Subscription)
|
||||
.to receive(:create)
|
||||
.and_return(
|
||||
{
|
||||
plan: { id: 'price_random_number', product: 'prod_random_number' },
|
||||
quantity: 2
|
||||
}.with_indifferent_access
|
||||
)
|
||||
|
||||
create_stripe_customer_service.new(account: account).perform
|
||||
|
||||
expect(Stripe::Customer).to have_received(:create).with({ name: account.name, email: admin1.email })
|
||||
expect(Stripe::Subscription)
|
||||
.to have_received(:create)
|
||||
.with({ customer: customer.id, items: [{ price: 'price_hacker_random', quantity: 2 }] })
|
||||
|
||||
expect(account.reload.custom_attributes).to eq(
|
||||
{
|
||||
stripe_customer_id: customer.id,
|
||||
stripe_price_id: 'price_random_number',
|
||||
stripe_product_id: 'prod_random_number',
|
||||
subscribed_quantity: 2,
|
||||
plan_name: 'A Plan Name'
|
||||
}.with_indifferent_access
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when checking for existing subscriptions' do
|
||||
before do
|
||||
create(
|
||||
:installation_config,
|
||||
{ name: 'CHATWOOT_CLOUD_PLANS', value: [
|
||||
{ 'name' => 'A Plan Name', 'product_id' => ['prod_hacker_random'], 'price_ids' => ['price_hacker_random'] }
|
||||
] }
|
||||
)
|
||||
end
|
||||
|
||||
context 'when account has no stripe_customer_id' do
|
||||
it 'creates a new subscription' do
|
||||
customer = double
|
||||
allow(Stripe::Customer).to receive(:create).and_return(customer)
|
||||
allow(customer).to receive(:id).and_return('cus_random_number')
|
||||
allow(Stripe::Subscription).to receive(:create).and_return(
|
||||
{
|
||||
plan: { id: 'price_random_number', product: 'prod_random_number' },
|
||||
quantity: 2
|
||||
}.with_indifferent_access
|
||||
)
|
||||
|
||||
create_stripe_customer_service.new(account: account).perform
|
||||
|
||||
expect(Stripe::Customer).to have_received(:create)
|
||||
expect(Stripe::Subscription).to have_received(:create)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account has stripe_customer_id' do
|
||||
let(:stripe_customer_id) { 'cus_random_number' }
|
||||
|
||||
before do
|
||||
account.update!(custom_attributes: { stripe_customer_id: stripe_customer_id })
|
||||
end
|
||||
|
||||
context 'when customer has active subscriptions' do
|
||||
before do
|
||||
allow(Stripe::Subscription).to receive(:list).and_return(subscriptions_list)
|
||||
allow(subscriptions_list).to receive(:data).and_return(['subscription'])
|
||||
allow(Stripe::Subscription).to receive(:create)
|
||||
end
|
||||
|
||||
it 'does not create a new subscription' do
|
||||
create_stripe_customer_service.new(account: account).perform
|
||||
|
||||
expect(Stripe::Subscription).not_to have_received(:create)
|
||||
expect(Stripe::Subscription).to have_received(:list).with(
|
||||
{
|
||||
customer: stripe_customer_id,
|
||||
status: 'active',
|
||||
limit: 1
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,335 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Enterprise::Billing::HandleStripeEventService do
|
||||
subject(:stripe_event_service) { described_class }
|
||||
|
||||
let(:event) { double }
|
||||
let(:data) { double }
|
||||
let(:subscription) { double }
|
||||
let!(:account) { create(:account, custom_attributes: { stripe_customer_id: 'cus_123' }) }
|
||||
|
||||
before do
|
||||
# Create cloud plans configuration
|
||||
create(:installation_config, {
|
||||
name: 'CHATWOOT_CLOUD_PLANS',
|
||||
value: [
|
||||
{ 'name' => 'Hacker', 'product_id' => ['plan_id_hacker'], 'price_ids' => ['price_hacker'] },
|
||||
{ 'name' => 'Startups', 'product_id' => ['plan_id_startups'], 'price_ids' => ['price_startups'] },
|
||||
{ 'name' => 'Business', 'product_id' => ['plan_id_business'], 'price_ids' => ['price_business'] },
|
||||
{ 'name' => 'Enterprise', 'product_id' => ['plan_id_enterprise'], 'price_ids' => ['price_enterprise'] }
|
||||
]
|
||||
})
|
||||
|
||||
create(:installation_config, {
|
||||
name: 'CAPTAIN_CLOUD_PLAN_LIMITS',
|
||||
value: {
|
||||
'hacker' => { 'responses' => 0 },
|
||||
'startups' => { 'responses' => 300 },
|
||||
'business' => { 'responses' => 500 },
|
||||
'enterprise' => { 'responses' => 800 }
|
||||
}
|
||||
})
|
||||
# Setup common subscription mocks
|
||||
allow(event).to receive(:data).and_return(data)
|
||||
allow(data).to receive(:object).and_return(subscription)
|
||||
allow(data).to receive(:previous_attributes).and_return({})
|
||||
allow(subscription).to receive(:[]).with('quantity').and_return('10')
|
||||
allow(subscription).to receive(:[]).with('status').and_return('active')
|
||||
allow(subscription).to receive(:[]).with('current_period_end').and_return(1_686_567_520)
|
||||
allow(subscription).to receive(:customer).and_return('cus_123')
|
||||
allow(event).to receive(:type).and_return('customer.subscription.updated')
|
||||
end
|
||||
|
||||
describe 'subscription update handling' do
|
||||
it 'updates account attributes and disables premium features for default plan' do
|
||||
# Setup for default (Hacker) plan
|
||||
allow(subscription).to receive(:[]).with('plan')
|
||||
.and_return({ 'id' => 'test', 'product' => 'plan_id_hacker', 'name' => 'Hacker' })
|
||||
|
||||
stripe_event_service.new.perform(event: event)
|
||||
|
||||
# Verify account attributes were updated
|
||||
expect(account.reload.custom_attributes).to include(
|
||||
'plan_name' => 'Hacker',
|
||||
'stripe_product_id' => 'plan_id_hacker',
|
||||
'subscription_status' => 'active'
|
||||
)
|
||||
|
||||
# Verify premium features are disabled for default plan
|
||||
expect(account).not_to be_feature_enabled('channel_email')
|
||||
expect(account).not_to be_feature_enabled('help_center')
|
||||
expect(account).not_to be_feature_enabled('sla')
|
||||
expect(account).not_to be_feature_enabled('custom_roles')
|
||||
expect(account).not_to be_feature_enabled('audit_logs')
|
||||
end
|
||||
|
||||
it 'resets captain usage on billing period renewal' do
|
||||
# Prime the account with some usage
|
||||
5.times { account.increment_response_usage }
|
||||
expect(account.custom_attributes['captain_responses_usage']).to eq(5)
|
||||
|
||||
# Setup for any plan
|
||||
allow(subscription).to receive(:[]).with('plan')
|
||||
.and_return({ 'id' => 'test', 'product' => 'plan_id_startups', 'name' => 'Startups' })
|
||||
allow(subscription).to receive(:[]).with('current_period_start').and_return(1_686_567_520)
|
||||
|
||||
# Simulate billing period renewal with previous_attributes showing old period
|
||||
allow(data).to receive(:previous_attributes).and_return({ 'current_period_start' => 1_683_975_520 })
|
||||
|
||||
stripe_event_service.new.perform(event: event)
|
||||
|
||||
# Verify usage was reset
|
||||
expect(account.reload.custom_attributes['captain_responses_usage']).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'subscription deletion handling' do
|
||||
it 'calls CreateStripeCustomerService on subscription deletion' do
|
||||
allow(event).to receive(:type).and_return('customer.subscription.deleted')
|
||||
|
||||
# Create a double for the service
|
||||
customer_service = double
|
||||
allow(Enterprise::Billing::CreateStripeCustomerService).to receive(:new)
|
||||
.with(account: account).and_return(customer_service)
|
||||
allow(customer_service).to receive(:perform)
|
||||
|
||||
stripe_event_service.new.perform(event: event)
|
||||
|
||||
# Verify the service was called
|
||||
expect(Enterprise::Billing::CreateStripeCustomerService).to have_received(:new)
|
||||
.with(account: account)
|
||||
expect(customer_service).to have_received(:perform)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'plan-specific feature management' do
|
||||
context 'with default plan (Hacker)' do
|
||||
it 'disables all premium features' do
|
||||
allow(subscription).to receive(:[]).with('plan')
|
||||
.and_return({ 'id' => 'test', 'product' => 'plan_id_hacker', 'name' => 'Hacker' })
|
||||
|
||||
# Enable features first
|
||||
described_class::STARTUP_PLAN_FEATURES.each do |feature|
|
||||
account.enable_features(feature)
|
||||
end
|
||||
account.enable_features(*described_class::BUSINESS_PLAN_FEATURES)
|
||||
account.enable_features(*described_class::ENTERPRISE_PLAN_FEATURES)
|
||||
account.save!
|
||||
|
||||
account.reload
|
||||
expect(account).to be_feature_enabled(described_class::STARTUP_PLAN_FEATURES.first)
|
||||
|
||||
stripe_event_service.new.perform(event: event)
|
||||
|
||||
account.reload
|
||||
|
||||
all_features = described_class::STARTUP_PLAN_FEATURES +
|
||||
described_class::BUSINESS_PLAN_FEATURES +
|
||||
described_class::ENTERPRISE_PLAN_FEATURES
|
||||
|
||||
all_features.each do |feature|
|
||||
expect(account).not_to be_feature_enabled(feature)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with Startups plan' do
|
||||
it 'enables common features but not premium features' do
|
||||
allow(subscription).to receive(:[]).with('plan')
|
||||
.and_return({ 'id' => 'test', 'product' => 'plan_id_startups', 'name' => 'Startups' })
|
||||
|
||||
stripe_event_service.new.perform(event: event)
|
||||
|
||||
# Verify basic (Startups) features are enabled
|
||||
account.reload
|
||||
described_class::STARTUP_PLAN_FEATURES.each do |feature|
|
||||
expect(account).to be_feature_enabled(feature)
|
||||
end
|
||||
|
||||
# But business and enterprise features should be disabled
|
||||
described_class::BUSINESS_PLAN_FEATURES.each do |feature|
|
||||
expect(account).not_to be_feature_enabled(feature)
|
||||
end
|
||||
|
||||
described_class::ENTERPRISE_PLAN_FEATURES.each do |feature|
|
||||
expect(account).not_to be_feature_enabled(feature)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with Business plan' do
|
||||
it 'enables business-specific features' do
|
||||
allow(subscription).to receive(:[]).with('plan')
|
||||
.and_return({ 'id' => 'test', 'product' => 'plan_id_business', 'name' => 'Business' })
|
||||
|
||||
stripe_event_service.new.perform(event: event)
|
||||
|
||||
account.reload
|
||||
described_class::STARTUP_PLAN_FEATURES.each do |feature|
|
||||
expect(account).to be_feature_enabled(feature)
|
||||
end
|
||||
|
||||
described_class::BUSINESS_PLAN_FEATURES.each do |feature|
|
||||
expect(account).to be_feature_enabled(feature)
|
||||
end
|
||||
|
||||
described_class::ENTERPRISE_PLAN_FEATURES.each do |feature|
|
||||
expect(account).not_to be_feature_enabled(feature)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with Enterprise plan' do
|
||||
it 'enables all business and enterprise features' do
|
||||
allow(subscription).to receive(:[]).with('plan')
|
||||
.and_return({ 'id' => 'test', 'product' => 'plan_id_enterprise', 'name' => 'Enterprise' })
|
||||
|
||||
stripe_event_service.new.perform(event: event)
|
||||
|
||||
account.reload
|
||||
described_class::STARTUP_PLAN_FEATURES.each do |feature|
|
||||
expect(account).to be_feature_enabled(feature)
|
||||
end
|
||||
|
||||
described_class::BUSINESS_PLAN_FEATURES.each do |feature|
|
||||
expect(account).to be_feature_enabled(feature)
|
||||
end
|
||||
|
||||
described_class::ENTERPRISE_PLAN_FEATURES.each do |feature|
|
||||
expect(account).to be_feature_enabled(feature)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'manually managed features' do
|
||||
let(:service) { stripe_event_service.new }
|
||||
let(:internal_attrs_service) { instance_double(Internal::Accounts::InternalAttributesService) }
|
||||
|
||||
before do
|
||||
# Mock the internal attributes service
|
||||
allow(Internal::Accounts::InternalAttributesService).to receive(:new).with(account).and_return(internal_attrs_service)
|
||||
end
|
||||
|
||||
context 'when downgrading with manually managed features' do
|
||||
it 'preserves manually managed features even when downgrading plans' do
|
||||
# Setup: account has Enterprise plan with manually managed features
|
||||
allow(subscription).to receive(:[]).with('plan')
|
||||
.and_return({ 'id' => 'test', 'product' => 'plan_id_enterprise', 'name' => 'Enterprise' })
|
||||
|
||||
# Mock manually managed features
|
||||
allow(internal_attrs_service).to receive(:manually_managed_features).and_return(%w[audit_logs custom_roles])
|
||||
|
||||
# First run to apply enterprise plan
|
||||
service.perform(event: event)
|
||||
account.reload
|
||||
|
||||
# Verify features are enabled
|
||||
expect(account).to be_feature_enabled('audit_logs')
|
||||
expect(account).to be_feature_enabled('custom_roles')
|
||||
|
||||
# Now downgrade to Hacker plan (which normally wouldn't have these features)
|
||||
allow(subscription).to receive(:[]).with('plan')
|
||||
.and_return({ 'id' => 'test', 'product' => 'plan_id_hacker', 'name' => 'Hacker' })
|
||||
|
||||
service.perform(event: event)
|
||||
account.reload
|
||||
|
||||
# Manually managed features should still be enabled despite plan downgrade
|
||||
expect(account).to be_feature_enabled('audit_logs')
|
||||
expect(account).to be_feature_enabled('custom_roles')
|
||||
|
||||
# But other premium features should be disabled
|
||||
expect(account).not_to be_feature_enabled('channel_instagram')
|
||||
expect(account).not_to be_feature_enabled('help_center')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'downgrade handling' do
|
||||
let(:service) { stripe_event_service.new }
|
||||
|
||||
before do
|
||||
# Setup internal attributes service mock to return no manually managed features
|
||||
internal_attrs_service = instance_double(Internal::Accounts::InternalAttributesService)
|
||||
allow(Internal::Accounts::InternalAttributesService).to receive(:new).with(account).and_return(internal_attrs_service)
|
||||
allow(internal_attrs_service).to receive(:manually_managed_features).and_return([])
|
||||
end
|
||||
|
||||
context 'when downgrading from Enterprise to Business plan' do
|
||||
before do
|
||||
# Start with Enterprise plan
|
||||
allow(subscription).to receive(:[]).with('plan')
|
||||
.and_return({ 'id' => 'test', 'product' => 'plan_id_enterprise', 'name' => 'Enterprise' })
|
||||
service.perform(event: event)
|
||||
account.reload
|
||||
end
|
||||
|
||||
it 'retains business features but disables enterprise features' do
|
||||
# Verify enterprise features were enabled
|
||||
expect(account).to be_feature_enabled('audit_logs')
|
||||
|
||||
# Downgrade to Business plan
|
||||
allow(subscription).to receive(:[]).with('plan')
|
||||
.and_return({ 'id' => 'test', 'product' => 'plan_id_business', 'name' => 'Business' })
|
||||
service.perform(event: event)
|
||||
|
||||
account.reload
|
||||
expect(account).to be_feature_enabled('sla')
|
||||
expect(account).to be_feature_enabled('custom_roles')
|
||||
expect(account).not_to be_feature_enabled('audit_logs')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when downgrading from Business to Startups plan' do
|
||||
before do
|
||||
# Start with Business plan
|
||||
allow(subscription).to receive(:[]).with('plan')
|
||||
.and_return({ 'id' => 'test', 'product' => 'plan_id_business', 'name' => 'Business' })
|
||||
service.perform(event: event)
|
||||
account.reload
|
||||
end
|
||||
|
||||
it 'retains startup features but disables business features' do
|
||||
# Verify business features were enabled
|
||||
expect(account).to be_feature_enabled('sla')
|
||||
|
||||
# Downgrade to Startups plan
|
||||
allow(subscription).to receive(:[]).with('plan')
|
||||
.and_return({ 'id' => 'test', 'product' => 'plan_id_startups', 'name' => 'Startups' })
|
||||
service.perform(event: event)
|
||||
|
||||
account.reload
|
||||
# Spot check one startup feature
|
||||
expect(account).to be_feature_enabled('channel_instagram')
|
||||
expect(account).not_to be_feature_enabled('sla')
|
||||
expect(account).not_to be_feature_enabled('custom_roles')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when downgrading from Startups to Hacker plan' do
|
||||
before do
|
||||
# Start with Startups plan
|
||||
allow(subscription).to receive(:[]).with('plan')
|
||||
.and_return({ 'id' => 'test', 'product' => 'plan_id_startups', 'name' => 'Startups' })
|
||||
service.perform(event: event)
|
||||
account.reload
|
||||
end
|
||||
|
||||
it 'disables all premium features' do
|
||||
# Verify startup features were enabled
|
||||
expect(account).to be_feature_enabled('channel_instagram')
|
||||
|
||||
# Downgrade to Hacker (default) plan
|
||||
allow(subscription).to receive(:[]).with('plan')
|
||||
.and_return({ 'id' => 'test', 'product' => 'plan_id_hacker', 'name' => 'Hacker' })
|
||||
service.perform(event: event)
|
||||
|
||||
account.reload
|
||||
# Spot check that premium features are disabled
|
||||
expect(account).not_to be_feature_enabled('channel_instagram')
|
||||
expect(account).not_to be_feature_enabled('help_center')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,60 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Enterprise::Billing::TopupCheckoutService do
|
||||
subject(:service) { described_class.new(account: account) }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let(:stripe_customer_id) { 'cus_test123' }
|
||||
let(:invoice_settings) { Struct.new(:default_payment_method).new('pm_test') }
|
||||
let(:stripe_customer) { Struct.new(:invoice_settings, :default_source).new(invoice_settings, nil) }
|
||||
let(:stripe_invoice) { Struct.new(:id).new('inv_test123') }
|
||||
|
||||
before do
|
||||
create(:installation_config, name: 'CHATWOOT_CLOUD_PLANS', value: [
|
||||
{ 'name' => 'Hacker', 'product_id' => ['prod_hacker'], 'price_ids' => ['price_hacker'] },
|
||||
{ 'name' => 'Business', 'product_id' => ['prod_business'], 'price_ids' => ['price_business'] }
|
||||
])
|
||||
|
||||
account.update!(
|
||||
custom_attributes: { plan_name: 'Business', stripe_customer_id: stripe_customer_id },
|
||||
limits: { 'captain_responses' => 500 }
|
||||
)
|
||||
|
||||
allow(Stripe::Customer).to receive(:retrieve).and_return(stripe_customer)
|
||||
allow(Stripe::Invoice).to receive(:create).and_return(stripe_invoice)
|
||||
allow(Stripe::InvoiceItem).to receive(:create)
|
||||
allow(Stripe::Invoice).to receive(:finalize_invoice)
|
||||
allow(Stripe::Invoice).to receive(:pay)
|
||||
allow(Stripe::Billing::CreditGrant).to receive(:create)
|
||||
end
|
||||
|
||||
describe '#create_checkout_session' do
|
||||
it 'successfully processes topup and returns correct response' do
|
||||
result = service.create_checkout_session(credits: 1000)
|
||||
|
||||
expect(result[:credits]).to eq(1000)
|
||||
expect(result[:amount]).to eq(20.0)
|
||||
expect(result[:currency]).to eq('usd')
|
||||
end
|
||||
|
||||
it 'updates account limits after successful topup' do
|
||||
service.create_checkout_session(credits: 1000)
|
||||
|
||||
expect(account.reload.limits['captain_responses']).to eq(1500)
|
||||
end
|
||||
|
||||
it 'raises error for invalid credits' do
|
||||
expect do
|
||||
service.create_checkout_session(credits: 500)
|
||||
end.to raise_error(Enterprise::Billing::TopupCheckoutService::Error)
|
||||
end
|
||||
|
||||
it 'raises error when account is on free plan' do
|
||||
account.update!(custom_attributes: { plan_name: 'Hacker', stripe_customer_id: stripe_customer_id })
|
||||
|
||||
expect do
|
||||
service.create_checkout_session(credits: 1000)
|
||||
end.to raise_error(Enterprise::Billing::TopupCheckoutService::Error)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe Enterprise::Billing::TopupFulfillmentService do
|
||||
subject(:service) { described_class.new(account: account) }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let(:stripe_customer_id) { 'cus_test123' }
|
||||
|
||||
before do
|
||||
account.update!(
|
||||
custom_attributes: { stripe_customer_id: stripe_customer_id },
|
||||
limits: { 'captain_responses' => 1000 }
|
||||
)
|
||||
allow(Stripe::Billing::CreditGrant).to receive(:create)
|
||||
end
|
||||
|
||||
describe '#fulfill' do
|
||||
it 'adds credits to account limits' do
|
||||
service.fulfill(credits: 1000, amount_cents: 2000, currency: 'usd')
|
||||
|
||||
expect(account.reload.limits['captain_responses']).to eq(2000)
|
||||
end
|
||||
|
||||
it 'creates a Stripe credit grant' do
|
||||
service.fulfill(credits: 1000, amount_cents: 2000, currency: 'usd')
|
||||
|
||||
expect(Stripe::Billing::CreditGrant).to have_received(:create).with(
|
||||
hash_including(
|
||||
customer: stripe_customer_id,
|
||||
name: 'Topup: 1000 credits',
|
||||
category: 'paid'
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,64 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Enterprise::ClearbitLookupService do
|
||||
describe '.lookup' do
|
||||
let(:email) { 'test@example.com' }
|
||||
let(:api_key) { 'clearbit_api_key' }
|
||||
let(:clearbit_endpoint) { described_class::CLEARBIT_ENDPOINT }
|
||||
let(:response_body) { build(:clearbit_combined_response) }
|
||||
|
||||
context 'when Clearbit is enabled' do
|
||||
before do
|
||||
stub_request(:get, "#{clearbit_endpoint}?email=#{email}")
|
||||
.with(headers: { 'Authorization' => "Bearer #{api_key}" })
|
||||
.to_return(status: 200, body: response_body, headers: { 'content-type' => ['application/json'] })
|
||||
end
|
||||
|
||||
context 'when the API is working as expected' do
|
||||
it 'returns the person and company information' do
|
||||
with_modified_env CLEARBIT_API_KEY: api_key do
|
||||
result = described_class.lookup(email)
|
||||
|
||||
expect(result).to eq({
|
||||
:avatar => 'https://example.com/avatar.png',
|
||||
:company_name => 'Doe Inc.',
|
||||
:company_size => '1-10',
|
||||
:industry => 'Software',
|
||||
:logo => nil,
|
||||
:name => 'John Doe',
|
||||
:timezone => 'Asia/Kolkata'
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the API returns an error' do
|
||||
before do
|
||||
stub_request(:get, "#{clearbit_endpoint}?email=#{email}")
|
||||
.with(headers: { 'Authorization' => "Bearer #{api_key}" })
|
||||
.to_return(status: 404, body: '', headers: {})
|
||||
end
|
||||
|
||||
it 'logs the error and returns nil' do
|
||||
with_modified_env CLEARBIT_API_KEY: api_key do
|
||||
expect(Rails.logger).to receive(:error)
|
||||
expect(described_class.lookup(email)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when Clearbit is not enabled' do
|
||||
before do
|
||||
GlobalConfig.clear_cache
|
||||
end
|
||||
|
||||
it 'returns nil without making an API call' do
|
||||
with_modified_env CLEARBIT_API_KEY: nil do
|
||||
expect(Net::HTTP).not_to receive(:start)
|
||||
expect(described_class.lookup(email)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,209 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Enterprise::Conversations::PermissionFilterService do
|
||||
let(:account) { create(:account) }
|
||||
# Create conversations with different states
|
||||
let!(:assigned_conversation) { create(:conversation, account: account, inbox: inbox, assignee: agent) }
|
||||
let!(:unassigned_conversation) { create(:conversation, account: account, inbox: inbox, assignee: nil) }
|
||||
let!(:another_assigned_conversation) { create(:conversation, account: account, inbox: inbox, assignee: create(:user, account: account)) }
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let!(:inbox) { create(:inbox, account: account) }
|
||||
let!(:inbox2) { create(:inbox, account: account) }
|
||||
let!(:another_inbox_conversation) { create(:conversation, account: account, inbox: inbox2) }
|
||||
|
||||
# This inbox_member is used to establish the agent's access to the inbox
|
||||
before { create(:inbox_member, user: agent, inbox: inbox) }
|
||||
|
||||
describe '#perform' do
|
||||
context 'when user is an administrator' do
|
||||
it 'returns all conversations' do
|
||||
result = Conversations::PermissionFilterService.new(
|
||||
account.conversations,
|
||||
admin,
|
||||
account
|
||||
).perform
|
||||
|
||||
expect(result).to include(assigned_conversation)
|
||||
expect(result).to include(unassigned_conversation)
|
||||
expect(result).to include(another_assigned_conversation)
|
||||
expect(result.count).to eq(4)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is a regular agent' do
|
||||
it 'returns all conversations in assigned inboxes' do
|
||||
result = Conversations::PermissionFilterService.new(
|
||||
account.conversations,
|
||||
agent,
|
||||
account
|
||||
).perform
|
||||
|
||||
expect(result).to include(assigned_conversation)
|
||||
expect(result).to include(unassigned_conversation)
|
||||
expect(result).to include(another_assigned_conversation)
|
||||
expect(result).not_to include(another_inbox_conversation)
|
||||
expect(result.count).to eq(3)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has conversation_manage permission' do
|
||||
# Test with a new clean state for each test case
|
||||
it 'returns all conversations' do
|
||||
# Create a new isolated test environment
|
||||
test_account = create(:account)
|
||||
test_inbox = create(:inbox, account: test_account)
|
||||
test_inbox2 = create(:inbox, account: test_account)
|
||||
# Create test agent
|
||||
test_agent = create(:user, account: test_account, role: :agent)
|
||||
create(:inbox_member, user: test_agent, inbox: test_inbox)
|
||||
|
||||
# Create custom role with conversation_manage permission
|
||||
test_custom_role = create(:custom_role, account: test_account, permissions: ['conversation_manage'])
|
||||
account_user = AccountUser.find_by(user: test_agent, account: test_account)
|
||||
account_user.update(role: :agent, custom_role: test_custom_role)
|
||||
|
||||
# Create some conversations
|
||||
assigned_conversation = create(:conversation, account: test_account, inbox: test_inbox, assignee: test_agent)
|
||||
unassigned_conversation = create(:conversation, account: test_account, inbox: test_inbox, assignee: nil)
|
||||
other_assigned_conversation = create(:conversation, account: test_account, inbox: test_inbox, assignee: create(:user, account: test_account))
|
||||
other_inbox_conversation = create(:conversation, account: test_account, inbox: test_inbox2, assignee: nil)
|
||||
|
||||
# Run the test
|
||||
result = Conversations::PermissionFilterService.new(
|
||||
test_account.conversations,
|
||||
test_agent,
|
||||
test_account
|
||||
).perform
|
||||
|
||||
# Should have access to all conversations
|
||||
expect(result.count).to eq(3)
|
||||
expect(result).to include(assigned_conversation)
|
||||
expect(result).to include(unassigned_conversation)
|
||||
expect(result).to include(other_assigned_conversation)
|
||||
expect(result).not_to include(other_inbox_conversation)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has conversation_participating_manage permission' do
|
||||
it 'returns only conversations assigned to the agent' do
|
||||
# Create a new isolated test environment
|
||||
test_account = create(:account)
|
||||
test_inbox = create(:inbox, account: test_account)
|
||||
test_inbox2 = create(:inbox, account: test_account)
|
||||
|
||||
# Create test agent
|
||||
test_agent = create(:user, account: test_account, role: :agent)
|
||||
create(:inbox_member, user: test_agent, inbox: test_inbox)
|
||||
|
||||
# Create a custom role with only the conversation_participating_manage permission
|
||||
test_custom_role = create(:custom_role, account: test_account, permissions: %w[conversation_participating_manage])
|
||||
|
||||
account_user = AccountUser.find_by(user: test_agent, account: test_account)
|
||||
account_user.update(role: :agent, custom_role: test_custom_role)
|
||||
|
||||
# Create some conversations
|
||||
other_conversation = create(:conversation, account: test_account, inbox: test_inbox)
|
||||
assigned_conversation = create(:conversation, account: test_account, inbox: test_inbox, assignee: test_agent)
|
||||
other_inbox_conversation = create(:conversation, account: test_account, inbox: test_inbox2, assignee: nil)
|
||||
|
||||
# Run the test
|
||||
result = Conversations::PermissionFilterService.new(
|
||||
test_account.conversations,
|
||||
test_agent,
|
||||
test_account
|
||||
).perform
|
||||
|
||||
# Should only see conversations assigned to this agent
|
||||
expect(result.count).to eq(1)
|
||||
expect(result.first.assignee).to eq(test_agent)
|
||||
expect(result).to include(assigned_conversation)
|
||||
expect(result).not_to include(other_conversation)
|
||||
expect(result).not_to include(other_inbox_conversation)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has conversation_unassigned_manage permission' do
|
||||
it 'returns unassigned conversations AND mine' do
|
||||
# Create a new isolated test environment
|
||||
test_account = create(:account)
|
||||
test_inbox = create(:inbox, account: test_account)
|
||||
test_inbox2 = create(:inbox, account: test_account)
|
||||
|
||||
# Create test agent
|
||||
test_agent = create(:user, account: test_account, role: :agent)
|
||||
create(:inbox_member, user: test_agent, inbox: test_inbox)
|
||||
|
||||
# Create a custom role with only the conversation_unassigned_manage permission
|
||||
test_custom_role = create(:custom_role, account: test_account, permissions: %w[conversation_unassigned_manage])
|
||||
|
||||
account_user = AccountUser.find_by(user: test_agent, account: test_account)
|
||||
account_user.update(role: :agent, custom_role: test_custom_role)
|
||||
|
||||
# Create some conversations
|
||||
assigned_conversation = create(:conversation, account: test_account, inbox: test_inbox, assignee: test_agent)
|
||||
unassigned_conversation = create(:conversation, account: test_account, inbox: test_inbox, assignee: nil)
|
||||
other_assigned_conversation = create(:conversation, account: test_account, inbox: test_inbox, assignee: create(:user, account: test_account))
|
||||
other_inbox_conversation = create(:conversation, account: test_account, inbox: test_inbox2, assignee: nil)
|
||||
|
||||
# Run the test
|
||||
result = Conversations::PermissionFilterService.new(
|
||||
test_account.conversations,
|
||||
test_agent,
|
||||
test_account
|
||||
).perform
|
||||
|
||||
# Should see unassigned conversations AND conversations assigned to this agent
|
||||
expect(result.count).to eq(2)
|
||||
expect(result).to include(unassigned_conversation)
|
||||
expect(result).to include(assigned_conversation)
|
||||
|
||||
# Should NOT include conversations assigned to others
|
||||
expect(result).not_to include(other_assigned_conversation)
|
||||
expect(result).not_to include(other_inbox_conversation)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has both participating and unassigned permissions (hierarchical test)' do
|
||||
it 'gives higher priority to unassigned_manage over participating_manage' do
|
||||
# Create a new isolated test environment
|
||||
test_account = create(:account)
|
||||
test_inbox = create(:inbox, account: test_account)
|
||||
test_inbox2 = create(:inbox, account: test_account)
|
||||
|
||||
# Create test agent
|
||||
test_agent = create(:user, account: test_account, role: :agent)
|
||||
create(:inbox_member, user: test_agent, inbox: test_inbox)
|
||||
|
||||
# Create a custom role with both participating and unassigned permissions
|
||||
permissions = %w[conversation_participating_manage conversation_unassigned_manage]
|
||||
test_custom_role = create(:custom_role, account: test_account, permissions: permissions)
|
||||
|
||||
account_user = AccountUser.find_by(user: test_agent, account: test_account)
|
||||
account_user.update(role: :agent, custom_role: test_custom_role)
|
||||
|
||||
# Create some conversations
|
||||
assigned_to_agent = create(:conversation, account: test_account, inbox: test_inbox, assignee: test_agent)
|
||||
unassigned_conversation = create(:conversation, account: test_account, inbox: test_inbox, assignee: nil)
|
||||
other_assigned_conversation = create(:conversation, account: test_account, inbox: test_inbox, assignee: create(:user, account: test_account))
|
||||
other_inbox_conversation = create(:conversation, account: test_account, inbox: test_inbox2, assignee: nil)
|
||||
|
||||
# Run the test
|
||||
result = Conversations::PermissionFilterService.new(
|
||||
test_account.conversations,
|
||||
test_agent,
|
||||
test_account
|
||||
).perform
|
||||
|
||||
# Should behave the same as conversation_unassigned_manage test
|
||||
# - Show both unassigned and assigned to this agent
|
||||
# - Do not show conversations assigned to others
|
||||
expect(result.count).to eq(2)
|
||||
expect(result).to include(unassigned_conversation)
|
||||
expect(result).to include(assigned_to_agent)
|
||||
expect(result).not_to include(other_assigned_conversation)
|
||||
expect(result).not_to include(other_inbox_conversation)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,366 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe MessageTemplates::HookExecutionService do
|
||||
let(:account) { create(:account, custom_attributes: { plan_name: 'startups' }) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:contact) { create(:contact, account: account) }
|
||||
let(:conversation) { create(:conversation, inbox: inbox, account: account, contact: contact, status: :pending) }
|
||||
let(:assistant) { create(:captain_assistant, account: account) }
|
||||
|
||||
before do
|
||||
create(:captain_inbox, captain_assistant: assistant, inbox: inbox)
|
||||
end
|
||||
|
||||
context 'when captain assistant is configured' do
|
||||
context 'when within business hours' do
|
||||
before do
|
||||
inbox.update!(working_hours_enabled: true)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
open_all_day: true,
|
||||
closed_all_day: false
|
||||
)
|
||||
end
|
||||
|
||||
it 'schedules captain response job for incoming messages on pending conversations' do
|
||||
expect(Captain::Conversation::ResponseBuilderJob).to receive(:perform_later).with(conversation, assistant)
|
||||
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when outside business hours' do
|
||||
before do
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: 'We are currently closed'
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
closed_all_day: true,
|
||||
open_all_day: false
|
||||
)
|
||||
end
|
||||
|
||||
it 'schedules captain response job outside business hours (Captain always responds when configured)' do
|
||||
expect(Captain::Conversation::ResponseBuilderJob).to receive(:perform_later).with(conversation, assistant)
|
||||
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end
|
||||
|
||||
it 'performs captain handoff when quota is exceeded (OOO template will kick in after handoff)' do
|
||||
account.update!(
|
||||
limits: { 'captain_responses' => 100 },
|
||||
custom_attributes: account.custom_attributes.merge('captain_responses_usage' => 100)
|
||||
)
|
||||
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
|
||||
expect(conversation.reload.status).to eq('open')
|
||||
end
|
||||
|
||||
it 'does not send out of office message when Captain is handling' do
|
||||
out_of_office_service = instance_double(MessageTemplates::Template::OutOfOffice)
|
||||
allow(MessageTemplates::Template::OutOfOffice).to receive(:new).and_return(out_of_office_service)
|
||||
allow(out_of_office_service).to receive(:perform).and_return(true)
|
||||
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
|
||||
expect(MessageTemplates::Template::OutOfOffice).not_to have_received(:new)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when business hours are not enabled' do
|
||||
before do
|
||||
inbox.update!(working_hours_enabled: false)
|
||||
end
|
||||
|
||||
it 'schedules captain response job regardless of time' do
|
||||
expect(Captain::Conversation::ResponseBuilderJob).to receive(:perform_later).with(conversation, assistant)
|
||||
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when captain quota is exceeded within business hours' do
|
||||
before do
|
||||
inbox.update!(working_hours_enabled: true)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
open_all_day: true,
|
||||
closed_all_day: false
|
||||
)
|
||||
|
||||
account.update!(
|
||||
limits: { 'captain_responses' => 100 },
|
||||
custom_attributes: account.custom_attributes.merge('captain_responses_usage' => 100)
|
||||
)
|
||||
end
|
||||
|
||||
it 'performs handoff within business hours when quota exceeded' do
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
|
||||
expect(conversation.reload.status).to eq('open')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no captain assistant is configured' do
|
||||
before do
|
||||
CaptainInbox.where(inbox: inbox).destroy_all
|
||||
end
|
||||
|
||||
it 'does not schedule captain response job' do
|
||||
expect(Captain::Conversation::ResponseBuilderJob).not_to receive(:perform_later)
|
||||
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation is not pending' do
|
||||
before do
|
||||
conversation.update!(status: :open)
|
||||
end
|
||||
|
||||
it 'does not schedule captain response job' do
|
||||
expect(Captain::Conversation::ResponseBuilderJob).not_to receive(:perform_later)
|
||||
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when message is outgoing' do
|
||||
it 'does not schedule captain response job' do
|
||||
expect(Captain::Conversation::ResponseBuilderJob).not_to receive(:perform_later)
|
||||
|
||||
create(:message, conversation: conversation, message_type: :outgoing)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when greeting and out of office messages with Captain enabled' do
|
||||
context 'when conversation is pending (Captain is handling)' do
|
||||
before do
|
||||
conversation.update!(status: :pending)
|
||||
end
|
||||
|
||||
it 'does not create greeting message in conversation' do
|
||||
inbox.update!(greeting_enabled: true, greeting_message: 'Hello! How can we help you?', enable_email_collect: false)
|
||||
|
||||
expect do
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end.not_to(change { conversation.reload.messages.template.count })
|
||||
end
|
||||
|
||||
it 'does not create out of office message in conversation' do
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: 'We are currently closed',
|
||||
enable_email_collect: false
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
closed_all_day: true,
|
||||
open_all_day: false
|
||||
)
|
||||
|
||||
expect do
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end.not_to(change { conversation.reload.messages.template.count })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation is open (transferred to agent)' do
|
||||
before do
|
||||
conversation.update!(status: :open)
|
||||
end
|
||||
|
||||
it 'creates greeting message in conversation' do
|
||||
inbox.update!(greeting_enabled: true, greeting_message: 'Hello! How can we help you?', enable_email_collect: false)
|
||||
|
||||
expect do
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end.to change { conversation.reload.messages.template.count }.by(1)
|
||||
|
||||
greeting_message = conversation.reload.messages.template.last
|
||||
expect(greeting_message.content).to eq('Hello! How can we help you?')
|
||||
end
|
||||
|
||||
it 'creates out of office message when outside business hours' do
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: 'We are currently closed',
|
||||
enable_email_collect: false
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
closed_all_day: true,
|
||||
open_all_day: false
|
||||
)
|
||||
|
||||
expect do
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end.to change { conversation.reload.messages.template.count }.by(1)
|
||||
|
||||
out_of_office_message = conversation.reload.messages.template.last
|
||||
expect(out_of_office_message.content).to eq('We are currently closed')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when Captain is not configured' do
|
||||
before do
|
||||
CaptainInbox.where(inbox: inbox).destroy_all
|
||||
end
|
||||
|
||||
it 'creates greeting message in conversation' do
|
||||
inbox.update!(greeting_enabled: true, greeting_message: 'Hello! How can we help you?', enable_email_collect: false)
|
||||
|
||||
expect do
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end.to change { conversation.reload.messages.template.count }.by(1)
|
||||
|
||||
greeting_message = conversation.reload.messages.template.last
|
||||
expect(greeting_message.content).to eq('Hello! How can we help you?')
|
||||
end
|
||||
|
||||
it 'creates out of office message when outside business hours' do
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: 'We are currently closed',
|
||||
enable_email_collect: false
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
closed_all_day: true,
|
||||
open_all_day: false
|
||||
)
|
||||
|
||||
expect do
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end.to change { conversation.reload.messages.template.count }.by(1)
|
||||
|
||||
out_of_office_message = conversation.reload.messages.template.last
|
||||
expect(out_of_office_message.content).to eq('We are currently closed')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation has a campaign' do
|
||||
let(:campaign) { create(:campaign, account: account) }
|
||||
let(:campaign_conversation) { create(:conversation, inbox: inbox, account: account, contact: contact, status: :pending, campaign: campaign) }
|
||||
|
||||
it 'schedules captain response job for incoming messages on pending campaign conversations' do
|
||||
expect(Captain::Conversation::ResponseBuilderJob).to receive(:perform_later).with(campaign_conversation, assistant)
|
||||
|
||||
create(:message, conversation: campaign_conversation, message_type: :incoming)
|
||||
end
|
||||
|
||||
it 'does not send greeting template on campaign conversations' do
|
||||
inbox.update!(greeting_enabled: true, greeting_message: 'Hello! How can we help you?', enable_email_collect: false)
|
||||
|
||||
greeting_service = instance_double(MessageTemplates::Template::Greeting)
|
||||
allow(MessageTemplates::Template::Greeting).to receive(:new).and_return(greeting_service)
|
||||
allow(greeting_service).to receive(:perform).and_return(true)
|
||||
|
||||
create(:message, conversation: campaign_conversation, message_type: :incoming)
|
||||
|
||||
expect(MessageTemplates::Template::Greeting).not_to have_received(:new)
|
||||
end
|
||||
|
||||
it 'does not send out of office template on campaign conversations' do
|
||||
inbox.update!(working_hours_enabled: true, out_of_office_message: 'We are currently closed')
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
closed_all_day: true,
|
||||
open_all_day: false
|
||||
)
|
||||
|
||||
out_of_office_service = instance_double(MessageTemplates::Template::OutOfOffice)
|
||||
allow(MessageTemplates::Template::OutOfOffice).to receive(:new).and_return(out_of_office_service)
|
||||
allow(out_of_office_service).to receive(:perform).and_return(true)
|
||||
|
||||
create(:message, conversation: campaign_conversation, message_type: :incoming)
|
||||
|
||||
expect(MessageTemplates::Template::OutOfOffice).not_to have_received(:new)
|
||||
end
|
||||
|
||||
it 'does not send email collect template on campaign conversations' do
|
||||
contact.update!(email: nil)
|
||||
inbox.update!(enable_email_collect: true)
|
||||
|
||||
email_collect_service = instance_double(MessageTemplates::Template::EmailCollect)
|
||||
allow(MessageTemplates::Template::EmailCollect).to receive(:new).and_return(email_collect_service)
|
||||
allow(email_collect_service).to receive(:perform).and_return(true)
|
||||
|
||||
create(:message, conversation: campaign_conversation, message_type: :incoming)
|
||||
|
||||
expect(MessageTemplates::Template::EmailCollect).not_to have_received(:new)
|
||||
end
|
||||
|
||||
it 'does not send out of office template after handoff on campaign conversations when quota is exceeded' do
|
||||
account.update!(
|
||||
limits: { 'captain_responses' => 100 },
|
||||
custom_attributes: account.custom_attributes.merge('captain_responses_usage' => 100)
|
||||
)
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: 'We are currently closed'
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
closed_all_day: true,
|
||||
open_all_day: false
|
||||
)
|
||||
|
||||
expect do
|
||||
create(:message, conversation: campaign_conversation, message_type: :incoming)
|
||||
end.not_to(change { campaign_conversation.messages.template.count })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when Captain quota is exceeded and handoff happens' do
|
||||
before do
|
||||
account.update!(
|
||||
limits: { 'captain_responses' => 100 },
|
||||
custom_attributes: account.custom_attributes.merge('captain_responses_usage' => 100)
|
||||
)
|
||||
end
|
||||
|
||||
context 'when outside business hours' do
|
||||
before do
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: 'We are currently closed. Please leave your email.',
|
||||
enable_email_collect: false
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
closed_all_day: true,
|
||||
open_all_day: false
|
||||
)
|
||||
end
|
||||
|
||||
it 'sends out of office message after handoff due to quota exceeded' do
|
||||
expect do
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end.to change { conversation.messages.template.count }.by(1)
|
||||
|
||||
expect(conversation.reload.status).to eq('open')
|
||||
ooo_message = conversation.messages.template.last
|
||||
expect(ooo_message.content).to eq('We are currently closed. Please leave your email.')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when within business hours' do
|
||||
before do
|
||||
inbox.update!(
|
||||
working_hours_enabled: true,
|
||||
out_of_office_message: 'We are currently closed.',
|
||||
enable_email_collect: false
|
||||
)
|
||||
inbox.working_hours.find_by(day_of_week: Time.current.in_time_zone(inbox.timezone).wday).update!(
|
||||
open_all_day: true,
|
||||
closed_all_day: false
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not send out of office message after handoff' do
|
||||
expect do
|
||||
create(:message, conversation: conversation, message_type: :incoming)
|
||||
end.not_to(change { conversation.messages.template.count })
|
||||
|
||||
expect(conversation.reload.status).to eq('open')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user