From c87b2109a9d4bd982135f441f0b933975e2e05f2 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 8 May 2025 12:52:17 +0530 Subject: [PATCH] feat: allow auto resolve waiting option (#11436) --- app/controllers/api/v1/accounts_controller.rb | 2 +- .../i18n/locale/en/generalSettings.json | 10 +++-- .../account/components/AutoResolve.vue | 30 ++++++++++++-- .../account/components/SectionLayout.vue | 4 +- .../v3/components/Form/WithLabel.vue | 4 +- app/jobs/conversations/resolution_job.rb | 12 +++++- app/models/account.rb | 7 ++-- app/models/conversation.rb | 7 +++- .../api/v1/accounts_controller_spec.rb | 3 +- .../jobs/conversations/resolution_job_spec.rb | 40 ++++++++++++++----- 10 files changed, 92 insertions(+), 27 deletions(-) diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index b247b9715..c610dc846 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -92,7 +92,7 @@ class Api::V1::AccountsController < Api::BaseController end def settings_params - params.permit(:auto_resolve_after, :auto_resolve_message) + params.permit(:auto_resolve_after, :auto_resolve_message, :auto_resolve_ignore_waiting) end def check_signup_enabled diff --git a/app/javascript/dashboard/i18n/locale/en/generalSettings.json b/app/javascript/dashboard/i18n/locale/en/generalSettings.json index e586fe761..e6800820b 100644 --- a/app/javascript/dashboard/i18n/locale/en/generalSettings.json +++ b/app/javascript/dashboard/i18n/locale/en/generalSettings.json @@ -46,7 +46,7 @@ }, "AUTO_RESOLVE": { "TITLE": "Auto-resolve conversations", - "NOTE": "This configuration would allow you to automatically end the conversation after a certain period. Set the duration and customize the message to the user below." + "NOTE": "This configuration would allow you to automatically resolve the conversation after a certain period. Set the duration and customize the message to the user below." }, "NAME": { "LABEL": "Account name", @@ -68,16 +68,20 @@ "PLACEHOLDER": "Your company's support email", "ERROR": "" }, + "AUTO_RESOLVE_IGNORE_WAITING": { + "LABEL": "Exclude unattended conversations", + "HELP": "If toggled, the system will not resolve conversations that have been waiting for an agent reply." + }, "AUTO_RESOLVE_DURATION": { "LABEL": "Inactivity duration for resolution", "HELP": "Duration after a conversation should auto resolve if there is no activity", "PLACEHOLDER": "30", - "ERROR": "Please enter a valid auto resolve duration (minimum 1 day and maximum 999 days)", + "ERROR": "Auto resolve duration should be between 10 minutes and 999 days", "API": { "SUCCESS": "Auto resolve settings updated successfully", "ERROR": "Failed to update auto resolve settings" }, - "UPDATE_BUTTON": "Update Auto-resolve", + "UPDATE_BUTTON": "Update", "MESSAGE_LABEL": "Custom resolution message", "MESSAGE_PLACEHOLDER": "Conversation was marked resolved by system due to 15 days of inactivity", "MESSAGE_HELP": "This message is sent to the customer when a conversation is automatically resolved by the system due to inactivity." diff --git a/app/javascript/dashboard/routes/dashboard/settings/account/components/AutoResolve.vue b/app/javascript/dashboard/routes/dashboard/settings/account/components/AutoResolve.vue index e3fb424ca..9816fc7e0 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/account/components/AutoResolve.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/account/components/AutoResolve.vue @@ -13,6 +13,7 @@ import NextButton from 'dashboard/components-next/button/Button.vue'; const { t } = useI18n(); const duration = ref(0); const message = ref(''); +const ignoreWaiting = ref(false); const isEnabled = ref(false); const { currentAccount, updateAccount } = useAccount(); @@ -20,11 +21,15 @@ const { currentAccount, updateAccount } = useAccount(); watch( currentAccount, () => { - const { auto_resolve_after, auto_resolve_message } = - currentAccount.value?.settings || {}; + const { + auto_resolve_after, + auto_resolve_message, + auto_resolve_ignore_waiting, + } = currentAccount.value?.settings || {}; duration.value = auto_resolve_after; message.value = auto_resolve_message; + ignoreWaiting.value = auto_resolve_ignore_waiting; if (duration.value) { isEnabled.value = true; @@ -43,9 +48,15 @@ const updateAccountSettings = async settings => { }; const handleSubmit = async () => { + if (duration.value < 10) { + useAlert(t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_DURATION.ERROR')); + return Promise.resolve(); + } + return updateAccountSettings({ auto_resolve_after: duration.value, auto_resolve_message: message.value, + auto_resolve_ignore_waiting: ignoreWaiting.value, }); }; @@ -56,6 +67,7 @@ const handleDisable = async () => { return updateAccountSettings({ auto_resolve_after: null, auto_resolve_message: '', + auto_resolve_ignore_waiting: false, }); }; @@ -76,7 +88,7 @@ const toggleAutoResolve = async () => { -
+ { @@ -105,6 +117,16 @@ const toggleAutoResolve = async () => { " /> + + +

+ {{ t('GENERAL_SETTINGS.FORM.AUTO_RESOLVE_IGNORE_WAITING.HELP') }} +

+
@@ -25,7 +25,7 @@ defineProps({

{{ title }}

-

+

{{ description }}

diff --git a/app/javascript/v3/components/Form/WithLabel.vue b/app/javascript/v3/components/Form/WithLabel.vue index 846071e6d..08e0b026b 100644 --- a/app/javascript/v3/components/Form/WithLabel.vue +++ b/app/javascript/v3/components/Form/WithLabel.vue @@ -34,13 +34,13 @@ defineProps({
{{ errorMessage }}
{{ helpMessage }} diff --git a/app/jobs/conversations/resolution_job.rb b/app/jobs/conversations/resolution_job.rb index 1bfc1c2e0..37a9d834c 100644 --- a/app/jobs/conversations/resolution_job.rb +++ b/app/jobs/conversations/resolution_job.rb @@ -3,7 +3,7 @@ class Conversations::ResolutionJob < ApplicationJob def perform(account:) # limiting the number of conversations to be resolved to avoid any performance issues - resolvable_conversations = account.conversations.resolvable(account.auto_resolve_after).limit(Limits::BULK_ACTIONS_LIMIT) + resolvable_conversations = conversation_scope(account).limit(Limits::BULK_ACTIONS_LIMIT) resolvable_conversations.each do |conversation| # send message from bot that conversation has been resolved # do this is account.auto_resolve_message is set @@ -11,4 +11,14 @@ class Conversations::ResolutionJob < ApplicationJob conversation.toggle_status end end + + private + + def conversation_scope(account) + if account.auto_resolve_ignore_waiting + account.conversations.resolvable_not_waiting(account.auto_resolve_after) + else + account.conversations.resolvable_all(account.auto_resolve_after) + end + end end diff --git a/app/models/account.rb b/app/models/account.rb index 762cd505f..1664ce29b 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -34,10 +34,11 @@ class Account < ApplicationRecord 'properties': { 'auto_resolve_after': { 'type': %w[integer null], 'minimum': 10, 'maximum': 1_439_856 }, - 'auto_resolve_message': { 'type': %w[string null] } + 'auto_resolve_message': { 'type': %w[string null] }, + 'auto_resolve_ignore_waiting': { 'type': %w[boolean null] } }, 'required': [], - 'additionalProperties': false + 'additionalProperties': true }.to_json.freeze DEFAULT_QUERY_SETTING = { @@ -50,7 +51,7 @@ class Account < ApplicationRecord schema: SETTINGS_PARAMS_SCHEMA, attribute_resolver: ->(record) { record.settings } - store_accessor :settings, :auto_resolve_after, :auto_resolve_message + store_accessor :settings, :auto_resolve_after, :auto_resolve_message, :auto_resolve_ignore_waiting has_many :account_users, dependent: :destroy_async has_many :agent_bot_inboxes, dependent: :destroy_async diff --git a/app/models/conversation.rb b/app/models/conversation.rb index 492c0cfb6..d7efff139 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -76,11 +76,16 @@ class Conversation < ApplicationRecord scope :assigned, -> { where.not(assignee_id: nil) } scope :assigned_to, ->(agent) { where(assignee_id: agent.id) } scope :unattended, -> { where(first_reply_created_at: nil).or(where.not(waiting_since: nil)) } - scope :resolvable, lambda { |auto_resolve_after| + scope :resolvable_not_waiting, lambda { |auto_resolve_after| return none if auto_resolve_after.to_i.zero? open.where('last_activity_at < ? AND waiting_since IS NULL', Time.now.utc - auto_resolve_after.minutes) } + scope :resolvable_all, lambda { |auto_resolve_after| + return none if auto_resolve_after.to_i.zero? + + open.where('last_activity_at < ?', Time.now.utc - auto_resolve_after.minutes) + } scope :last_user_message_at, lambda { joins( diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb index 618d6fa5f..ec49ecd39 100644 --- a/spec/controllers/api/v1/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts_controller_spec.rb @@ -190,6 +190,7 @@ RSpec.describe 'Accounts API', type: :request do support_email: 'care@example.com', auto_resolve_after: 40, auto_resolve_message: 'Auto resolved', + auto_resolve_ignore_waiting: false, timezone: 'Asia/Kolkata', industry: 'Technology', company_size: '1-10' @@ -207,7 +208,7 @@ RSpec.describe 'Accounts API', type: :request do expect(account.reload.domain).to eq(params[:domain]) expect(account.reload.support_email).to eq(params[:support_email]) - %w[auto_resolve_after auto_resolve_message].each do |attribute| + %w[auto_resolve_after auto_resolve_message auto_resolve_ignore_waiting].each do |attribute| expect(account.reload.settings[attribute]).to eq(params[attribute.to_sym]) end diff --git a/spec/jobs/conversations/resolution_job_spec.rb b/spec/jobs/conversations/resolution_job_spec.rb index 490845eec..0103bd72b 100644 --- a/spec/jobs/conversations/resolution_job_spec.rb +++ b/spec/jobs/conversations/resolution_job_spec.rb @@ -17,18 +17,40 @@ RSpec.describe Conversations::ResolutionJob do expect(conversation.reload.status).to eq('open') end - it 'resolves the issue if time of inactivity is more than the auto resolve duration' do - account.update(auto_resolve_after: 14_400) # 10 days in minutes - conversation.update(last_activity_at: 13.days.ago, waiting_since: nil) - described_class.perform_now(account: account) - expect(conversation.reload.status).to eq('resolved') + context 'when auto_resolve_ignore_waiting is true' do + it 'resolves non-waiting conversations if time of inactivity is more than auto resolve duration' do + account.update(auto_resolve_after: 14_400, auto_resolve_ignore_waiting: true) # 10 days in minutes + conversation.update(last_activity_at: 13.days.ago, waiting_since: nil) + described_class.perform_now(account: account) + expect(conversation.reload.status).to eq('resolved') + end + + it 'does not resolve waiting conversations even if time of inactivity is more than auto resolve duration' do + account.update(auto_resolve_after: 14_400, auto_resolve_ignore_waiting: true) # 10 days in minutes + conversation.update(last_activity_at: 13.days.ago, waiting_since: 13.days.ago) + described_class.perform_now(account: account) + expect(conversation.reload.status).to eq('open') + end end - it 'resolved only a limited number of conversations in a single execution' do + context 'when auto_resolve_ignore_waiting is false' do + it 'resolves all conversations if time of inactivity is more than auto resolve duration' do + account.update(auto_resolve_after: 14_400, auto_resolve_ignore_waiting: false) # 10 days in minutes + # Create one waiting conversation and one non-waiting conversation + waiting_conversation = create(:conversation, account: account, last_activity_at: 13.days.ago, waiting_since: 13.days.ago) + non_waiting_conversation = create(:conversation, account: account, last_activity_at: 13.days.ago, waiting_since: nil) + + described_class.perform_now(account: account) + + expect(waiting_conversation.reload.status).to eq('resolved') + expect(non_waiting_conversation.reload.status).to eq('resolved') + end + end + + it 'resolves only a limited number of conversations in a single execution' do stub_const('Limits::BULK_ACTIONS_LIMIT', 2) - account.update(auto_resolve_after: 14_400) # 10 days in minutes - conversations = create_list(:conversation, 3, account: account, last_activity_at: 13.days.ago) - conversations.each { |conversation| conversation.update(waiting_since: nil) } + account.update(auto_resolve_after: 14_400, auto_resolve_ignore_waiting: false) # 10 days in minutes + create_list(:conversation, 3, account: account, last_activity_at: 13.days.ago) described_class.perform_now(account: account) expect(account.conversations.resolved.count).to eq(Limits::BULK_ACTIONS_LIMIT) end