From 16282f6a66741dd62dfa56fa4e50173272032c36 Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Fri, 29 Mar 2024 20:27:21 +1100 Subject: [PATCH] feat: Add push/email notification support for SLA (#9140) * feat: update SLA evaluation logic * Update enterprise/app/services/sla/evaluate_applied_sla_service.rb Co-authored-by: Muhsin Keloth * chore: refactor spec to bring down expecations in a single block * chore: fix process_account_applied_sla spec * chore: add spec to test multiple nrt misses * feat: persist sla notifications * feat: revert persist sla notifications * feat: add SLA push/email notification support * chore: refactor sla_status to include active_with_misses * chore: add support for sla push/email notifications * chore: refactor * chore: add liquid templates * chore: add spec for liquid templates * chore: add spec for sla email notifications * chore: add spec for SlaPolicyDrop * chore: refactor to ee namespace * chore: set enterprise test type to mailer * feat: enable sla notification settings only if SLA enabled * chore: refactor * chore: fix spec --------- Co-authored-by: Muhsin Keloth --- .../dashboard/i18n/locale/en/settings.json | 10 +- .../settings/profile/NotificationSettings.vue | 105 ++++++++++++++++++ .../conversation_notifications_mailer.rb | 5 +- app/models/application_record.rb | 8 +- app/models/notification.rb | 4 +- .../sla_missed_first_response.liquid | 10 ++ .../sla_missed_next_response.liquid | 10 ++ .../sla_missed_resolution.liquid | 10 ++ enterprise/app/drops/sla_policy_drop.rb | 9 ++ .../conversation_notifications_mailer.rb | 32 ++++++ .../models/enterprise/application_record.rb | 5 + spec/enterprise/drops/sla_policy_drop_spec.rb | 15 +++ .../conversation_notifications_mailer_spec.rb | 54 +++++++++ 13 files changed, 270 insertions(+), 7 deletions(-) create mode 100644 app/views/mailers/agent_notifications/conversation_notifications_mailer/sla_missed_first_response.liquid create mode 100644 app/views/mailers/agent_notifications/conversation_notifications_mailer/sla_missed_next_response.liquid create mode 100644 app/views/mailers/agent_notifications/conversation_notifications_mailer/sla_missed_resolution.liquid create mode 100644 enterprise/app/drops/sla_policy_drop.rb create mode 100644 enterprise/app/mailers/enterprise/agent_notifications/conversation_notifications_mailer.rb create mode 100644 enterprise/app/models/enterprise/application_record.rb create mode 100644 spec/enterprise/drops/sla_policy_drop_spec.rb create mode 100644 spec/enterprise/mailers/enterprise/agent_notifications/conversation_notifications_mailer_spec.rb diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index 9a4bde2c8..d9834c545 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -83,7 +83,10 @@ "CONVERSATION_CREATION": "Send email notifications when a new conversation is created", "CONVERSATION_MENTION": "Send email notifications when you are mentioned in a conversation", "ASSIGNED_CONVERSATION_NEW_MESSAGE": "Send email notifications when a new message is created in an assigned conversation", - "PARTICIPATING_CONVERSATION_NEW_MESSAGE": "Send email notifications when a new message is created in a participating conversation" + "PARTICIPATING_CONVERSATION_NEW_MESSAGE": "Send email notifications when a new message is created in a participating conversation", + "SLA_MISSED_FIRST_RESPONSE": "Send email notifications when a conversation misses first response SLA", + "SLA_MISSED_NEXT_RESPONSE": "Send email notifications when a conversation misses next response SLA", + "SLA_MISSED_RESOLUTION": "Send email notifications when a conversation misses resolution SLA" }, "API": { "UPDATE_SUCCESS": "Your notification preferences are updated successfully", @@ -98,7 +101,10 @@ "ASSIGNED_CONVERSATION_NEW_MESSAGE": "Send push notifications when a new message is created in an assigned conversation", "PARTICIPATING_CONVERSATION_NEW_MESSAGE": "Send push notifications when a new message is created in a participating conversation", "HAS_ENABLED_PUSH": "You have enabled push for this browser.", - "REQUEST_PUSH": "Enable push notifications" + "REQUEST_PUSH": "Enable push notifications", + "SLA_MISSED_FIRST_RESPONSE": "Send push notifications when a conversation misses first response SLA", + "SLA_MISSED_NEXT_RESPONSE": "Send push notifications when a conversation misses next response SLA", + "SLA_MISSED_RESOLUTION": "Send push notifications when a conversation misses resolution SLA" }, "PROFILE_IMAGE": { "LABEL": "Profile Image" diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue index 7214d0deb..cdbc40ec4 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue @@ -236,6 +236,54 @@ }} +
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
@@ -367,6 +466,7 @@ import { requestPushPermissions, verifyServiceWorkerExistence, } from '../../../../helper/pushHelper'; +import { FEATURE_FLAGS } from 'dashboard/featureFlags'; export default { mixins: [alertMixin, configMixin, uiSettingsMixin], @@ -393,13 +493,18 @@ export default { }, computed: { ...mapGetters({ + accountId: 'getCurrentAccountId', emailFlags: 'userNotificationSettings/getSelectedEmailFlags', pushFlags: 'userNotificationSettings/getSelectedPushFlags', uiSettings: 'getUISettings', + isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount', }), hasPushAPISupport() { return !!('Notification' in window); }, + isSLAEnabled() { + return this.isFeatureEnabledonAccount(this.accountId, FEATURE_FLAGS.SLA); + }, }, watch: { emailFlags(value) { diff --git a/app/mailers/agent_notifications/conversation_notifications_mailer.rb b/app/mailers/agent_notifications/conversation_notifications_mailer.rb index 874449adf..bc498d43b 100644 --- a/app/mailers/agent_notifications/conversation_notifications_mailer.rb +++ b/app/mailers/agent_notifications/conversation_notifications_mailer.rb @@ -61,7 +61,10 @@ class AgentNotifications::ConversationNotificationsMailer < ApplicationMailer user: @agent, conversation: @conversation, inbox: @conversation.inbox, - message: @message + message: @message, + sla_policy: @sla_policy }) end end + +AgentNotifications::ConversationNotificationsMailer.include_mod_with('AgentNotifications::ConversationNotificationsMailer') diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 117950e35..64fc8cebf 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -5,11 +5,13 @@ class ApplicationRecord < ActiveRecord::Base before_validation :validates_column_content_length # the models that exposed in email templates through liquid - DROPPABLES = %w[Account Channel Conversation Inbox User Message].freeze + def droppables + %w[Account Channel Conversation Inbox User Message] + end # ModelDrop class should exist in app/drops def to_drop - return unless DROPPABLES.include?(self.class.name) + return unless droppables.include?(self.class.name) "#{self.class.name}Drop".constantize.new(self) end @@ -47,3 +49,5 @@ class ApplicationRecord < ActiveRecord::Base end end end + +ApplicationRecord.include_mod_with('Enterprise::ApplicationRecord') diff --git a/app/models/notification.rb b/app/models/notification.rb index c71bdbb62..b5834225a 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -118,11 +118,11 @@ class Notification < ApplicationRecord def push_message_body case notification_type - when 'conversation_creation' + when 'conversation_creation', 'sla_missed_first_response' message_body(conversation.messages.first) when 'assigned_conversation_new_message', 'participating_conversation_new_message', 'conversation_mention' message_body(secondary_actor) - when 'conversation_assignment' + when 'conversation_assignment', 'sla_missed_next_response', 'sla_missed_resolution' message_body(conversation.messages.incoming.last) else '' diff --git a/app/views/mailers/agent_notifications/conversation_notifications_mailer/sla_missed_first_response.liquid b/app/views/mailers/agent_notifications/conversation_notifications_mailer/sla_missed_first_response.liquid new file mode 100644 index 000000000..d7988ad5f --- /dev/null +++ b/app/views/mailers/agent_notifications/conversation_notifications_mailer/sla_missed_first_response.liquid @@ -0,0 +1,10 @@ +

Hi {{user.available_name}},

+ +

+ Conversation #{{conversation.display_id}} in {{ inbox.name }} + has missed the SLA for first response under policy {{ sla_policy.name }}. +

+ +

+Please address immediately. +

diff --git a/app/views/mailers/agent_notifications/conversation_notifications_mailer/sla_missed_next_response.liquid b/app/views/mailers/agent_notifications/conversation_notifications_mailer/sla_missed_next_response.liquid new file mode 100644 index 000000000..d7bf8d445 --- /dev/null +++ b/app/views/mailers/agent_notifications/conversation_notifications_mailer/sla_missed_next_response.liquid @@ -0,0 +1,10 @@ +

Hi {{user.available_name}},

+ +

+ Conversation #{{conversation.display_id}} in {{ inbox.name }} + has missed the SLA for next response under policy {{ sla_policy.name }}.. +

+ +

+Please address immediately. +

diff --git a/app/views/mailers/agent_notifications/conversation_notifications_mailer/sla_missed_resolution.liquid b/app/views/mailers/agent_notifications/conversation_notifications_mailer/sla_missed_resolution.liquid new file mode 100644 index 000000000..efd24913e --- /dev/null +++ b/app/views/mailers/agent_notifications/conversation_notifications_mailer/sla_missed_resolution.liquid @@ -0,0 +1,10 @@ +

Hi {{user.available_name}},

+ +

+ Conversation #{{conversation.display_id}} in {{ inbox.name }} + has missed the SLA for resolution time under policy {{ sla_policy.name }}. +

+ +

+Please address immediately. +

diff --git a/enterprise/app/drops/sla_policy_drop.rb b/enterprise/app/drops/sla_policy_drop.rb new file mode 100644 index 000000000..ea9fbe34d --- /dev/null +++ b/enterprise/app/drops/sla_policy_drop.rb @@ -0,0 +1,9 @@ +class SlaPolicyDrop < BaseDrop + def name + @obj.try(:name) + end + + def description + @obj.try(:description) + end +end diff --git a/enterprise/app/mailers/enterprise/agent_notifications/conversation_notifications_mailer.rb b/enterprise/app/mailers/enterprise/agent_notifications/conversation_notifications_mailer.rb new file mode 100644 index 000000000..df71beb10 --- /dev/null +++ b/enterprise/app/mailers/enterprise/agent_notifications/conversation_notifications_mailer.rb @@ -0,0 +1,32 @@ +module Enterprise::AgentNotifications::ConversationNotificationsMailer + def sla_missed_first_response(conversation, agent, sla_policy) + return unless smtp_config_set_or_development? + + @agent = agent + @conversation = conversation + @sla_policy = sla_policy + subject = "Conversation [ID - #{@conversation.display_id}] missed SLA for first response" + @action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id) + send_mail_with_liquid(to: @agent.email, subject: subject) and return + end + + def sla_missed_next_response(conversation, agent, sla_policy) + return unless smtp_config_set_or_development? + + @agent = agent + @conversation = conversation + @sla_policy = sla_policy + @action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id) + send_mail_with_liquid(to: @agent.email, subject: "Conversation [ID - #{@conversation.display_id}] missed SLA for next response") and return + end + + def sla_missed_resolution(conversation, agent, sla_policy) + return unless smtp_config_set_or_development? + + @agent = agent + @conversation = conversation + @sla_policy = sla_policy + @action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id) + send_mail_with_liquid(to: @agent.email, subject: "Conversation [ID - #{@conversation.display_id}] missed SLA for resolution time") and return + end +end diff --git a/enterprise/app/models/enterprise/application_record.rb b/enterprise/app/models/enterprise/application_record.rb new file mode 100644 index 000000000..a05f60767 --- /dev/null +++ b/enterprise/app/models/enterprise/application_record.rb @@ -0,0 +1,5 @@ +module Enterprise::ApplicationRecord + def droppables + super + %w[SlaPolicy] + end +end diff --git a/spec/enterprise/drops/sla_policy_drop_spec.rb b/spec/enterprise/drops/sla_policy_drop_spec.rb new file mode 100644 index 000000000..c1be13c70 --- /dev/null +++ b/spec/enterprise/drops/sla_policy_drop_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +describe SlaPolicyDrop do + subject(:sla_policy_drop) { described_class.new(sla_policy) } + + let!(:sla_policy) { create(:sla_policy) } + + it 'returns name' do + expect(sla_policy_drop.name).to eq sla_policy.name + end + + it 'returns description' do + expect(sla_policy_drop.description).to eq sla_policy.description + end +end diff --git a/spec/enterprise/mailers/enterprise/agent_notifications/conversation_notifications_mailer_spec.rb b/spec/enterprise/mailers/enterprise/agent_notifications/conversation_notifications_mailer_spec.rb new file mode 100644 index 000000000..e5e2b14da --- /dev/null +++ b/spec/enterprise/mailers/enterprise/agent_notifications/conversation_notifications_mailer_spec.rb @@ -0,0 +1,54 @@ +require 'rails_helper' + +# rails helper is using infer filetype to detect rspec type +# so we need to include type: :mailer to make this test work in enterprise namespace +RSpec.describe AgentNotifications::ConversationNotificationsMailer, type: :mailer do + let(:class_instance) { described_class.new } + let!(:account) { create(:account) } + let(:agent) { create(:user, email: 'agent1@example.com', account: account) } + let(:conversation) { create(:conversation, assignee: agent, account: account) } + + before do + allow(described_class).to receive(:new).and_return(class_instance) + allow(class_instance).to receive(:smtp_config_set_or_development?).and_return(true) + end + + describe 'sla_missed_first_response' do + let(:sla_policy) { create(:sla_policy, account: account) } + let(:mail) { described_class.with(account: account).sla_missed_first_response(conversation, agent, sla_policy).deliver_now } + + it 'renders the subject' do + expect(mail.subject).to eq("Conversation [ID - #{conversation.display_id}] missed SLA for first response") + end + + it 'renders the receiver email' do + expect(mail.to).to eq([agent.email]) + end + end + + describe 'sla_missed_next_response' do + let(:sla_policy) { create(:sla_policy, account: account) } + let(:mail) { described_class.with(account: account).sla_missed_next_response(conversation, agent, sla_policy).deliver_now } + + it 'renders the subject' do + expect(mail.subject).to eq("Conversation [ID - #{conversation.display_id}] missed SLA for next response") + end + + it 'renders the receiver email' do + expect(mail.to).to eq([agent.email]) + end + end + + describe 'sla_missed_resolution' do + let(:sla_policy) { create(:sla_policy, account: account) } + let(:mail) { described_class.with(account: account).sla_missed_resolution(conversation, agent, sla_policy).deliver_now } + + it 'renders the subject' do + expect(mail.subject).to eq("Conversation [ID - #{conversation.display_id}] missed SLA for resolution time") + end + + it 'renders the receiver email' do + expect(mail.to).to eq([agent.email]) + end + end +end