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 <muhsinkeramam@gmail.com> * 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 <muhsinkeramam@gmail.com>
This commit is contained in:
@@ -83,7 +83,10 @@
|
|||||||
"CONVERSATION_CREATION": "Send email notifications when a new conversation is created",
|
"CONVERSATION_CREATION": "Send email notifications when a new conversation is created",
|
||||||
"CONVERSATION_MENTION": "Send email notifications when you are mentioned in a conversation",
|
"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",
|
"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": {
|
"API": {
|
||||||
"UPDATE_SUCCESS": "Your notification preferences are updated successfully",
|
"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",
|
"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",
|
"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.",
|
"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": {
|
"PROFILE_IMAGE": {
|
||||||
"LABEL": "Profile Image"
|
"LABEL": "Profile Image"
|
||||||
|
|||||||
@@ -236,6 +236,54 @@
|
|||||||
}}
|
}}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="isSLAEnabled" class="flex items-center gap-2 mb-1">
|
||||||
|
<input
|
||||||
|
v-model="selectedEmailFlags"
|
||||||
|
class="notification--checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
value="email_sla_missed_first_response"
|
||||||
|
@input="handleEmailInput"
|
||||||
|
/>
|
||||||
|
<label for="sla_missed_first_response">
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.SLA_MISSED_FIRST_RESPONSE'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div v-if="isSLAEnabled" class="flex items-center gap-2 mb-1">
|
||||||
|
<input
|
||||||
|
v-model="selectedEmailFlags"
|
||||||
|
class="notification--checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
value="email_sla_missed_next_response"
|
||||||
|
@input="handleEmailInput"
|
||||||
|
/>
|
||||||
|
<label for="sla_missed_next_response">
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.SLA_MISSED_NEXT_RESPONSE'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div v-if="isSLAEnabled" class="flex items-center gap-2 mb-1">
|
||||||
|
<input
|
||||||
|
v-model="selectedEmailFlags"
|
||||||
|
class="notification--checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
value="email_sla_missed_resolution"
|
||||||
|
@input="handleEmailInput"
|
||||||
|
/>
|
||||||
|
<label for="sla_missed_resolution">
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.SLA_MISSED_RESOLUTION'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -352,6 +400,57 @@
|
|||||||
}}
|
}}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isSLAEnabled" class="flex items-center gap-2 mb-1">
|
||||||
|
<input
|
||||||
|
v-model="selectedPushFlags"
|
||||||
|
class="notification--checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
value="push_sla_missed_first_response"
|
||||||
|
@input="handlePushInput"
|
||||||
|
/>
|
||||||
|
<label for="sla_missed_first_response">
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.SLA_MISSED_FIRST_RESPONSE'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isSLAEnabled" class="flex items-center gap-2 mb-1">
|
||||||
|
<input
|
||||||
|
v-model="selectedPushFlags"
|
||||||
|
class="notification--checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
value="push_sla_missed_next_response"
|
||||||
|
@input="handlePushInput"
|
||||||
|
/>
|
||||||
|
<label for="sla_missed_next_response">
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.SLA_MISSED_NEXT_RESPONSE'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isSLAEnabled" class="flex items-center gap-2 mb-1">
|
||||||
|
<input
|
||||||
|
v-model="selectedPushFlags"
|
||||||
|
class="notification--checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
value="push_sla_missed_resolution"
|
||||||
|
@input="handlePushInput"
|
||||||
|
/>
|
||||||
|
<label for="sla_missed_resolution">
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.SLA_MISSED_RESOLUTION'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -367,6 +466,7 @@ import {
|
|||||||
requestPushPermissions,
|
requestPushPermissions,
|
||||||
verifyServiceWorkerExistence,
|
verifyServiceWorkerExistence,
|
||||||
} from '../../../../helper/pushHelper';
|
} from '../../../../helper/pushHelper';
|
||||||
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [alertMixin, configMixin, uiSettingsMixin],
|
mixins: [alertMixin, configMixin, uiSettingsMixin],
|
||||||
@@ -393,13 +493,18 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
|
accountId: 'getCurrentAccountId',
|
||||||
emailFlags: 'userNotificationSettings/getSelectedEmailFlags',
|
emailFlags: 'userNotificationSettings/getSelectedEmailFlags',
|
||||||
pushFlags: 'userNotificationSettings/getSelectedPushFlags',
|
pushFlags: 'userNotificationSettings/getSelectedPushFlags',
|
||||||
uiSettings: 'getUISettings',
|
uiSettings: 'getUISettings',
|
||||||
|
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||||
}),
|
}),
|
||||||
hasPushAPISupport() {
|
hasPushAPISupport() {
|
||||||
return !!('Notification' in window);
|
return !!('Notification' in window);
|
||||||
},
|
},
|
||||||
|
isSLAEnabled() {
|
||||||
|
return this.isFeatureEnabledonAccount(this.accountId, FEATURE_FLAGS.SLA);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
emailFlags(value) {
|
emailFlags(value) {
|
||||||
|
|||||||
@@ -61,7 +61,10 @@ class AgentNotifications::ConversationNotificationsMailer < ApplicationMailer
|
|||||||
user: @agent,
|
user: @agent,
|
||||||
conversation: @conversation,
|
conversation: @conversation,
|
||||||
inbox: @conversation.inbox,
|
inbox: @conversation.inbox,
|
||||||
message: @message
|
message: @message,
|
||||||
|
sla_policy: @sla_policy
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
AgentNotifications::ConversationNotificationsMailer.include_mod_with('AgentNotifications::ConversationNotificationsMailer')
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ class ApplicationRecord < ActiveRecord::Base
|
|||||||
before_validation :validates_column_content_length
|
before_validation :validates_column_content_length
|
||||||
|
|
||||||
# the models that exposed in email templates through liquid
|
# 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
|
# ModelDrop class should exist in app/drops
|
||||||
def to_drop
|
def to_drop
|
||||||
return unless DROPPABLES.include?(self.class.name)
|
return unless droppables.include?(self.class.name)
|
||||||
|
|
||||||
"#{self.class.name}Drop".constantize.new(self)
|
"#{self.class.name}Drop".constantize.new(self)
|
||||||
end
|
end
|
||||||
@@ -47,3 +49,5 @@ class ApplicationRecord < ActiveRecord::Base
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
ApplicationRecord.include_mod_with('Enterprise::ApplicationRecord')
|
||||||
|
|||||||
@@ -118,11 +118,11 @@ class Notification < ApplicationRecord
|
|||||||
|
|
||||||
def push_message_body
|
def push_message_body
|
||||||
case notification_type
|
case notification_type
|
||||||
when 'conversation_creation'
|
when 'conversation_creation', 'sla_missed_first_response'
|
||||||
message_body(conversation.messages.first)
|
message_body(conversation.messages.first)
|
||||||
when 'assigned_conversation_new_message', 'participating_conversation_new_message', 'conversation_mention'
|
when 'assigned_conversation_new_message', 'participating_conversation_new_message', 'conversation_mention'
|
||||||
message_body(secondary_actor)
|
message_body(secondary_actor)
|
||||||
when 'conversation_assignment'
|
when 'conversation_assignment', 'sla_missed_next_response', 'sla_missed_resolution'
|
||||||
message_body(conversation.messages.incoming.last)
|
message_body(conversation.messages.incoming.last)
|
||||||
else
|
else
|
||||||
''
|
''
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<p>Hi {{user.available_name}},</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Conversation #{{conversation.display_id}} in {{ inbox.name }}
|
||||||
|
has missed the SLA for first response under policy {{ sla_policy.name }}.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="{{action_url}}">Please address immediately.</a>
|
||||||
|
</p>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<p>Hi {{user.available_name}},</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Conversation #{{conversation.display_id}} in {{ inbox.name }}
|
||||||
|
has missed the SLA for next response under policy {{ sla_policy.name }}..
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="{{action_url}}">Please address immediately.</a>
|
||||||
|
</p>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<p>Hi {{user.available_name}},</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Conversation #{{conversation.display_id}} in {{ inbox.name }}
|
||||||
|
has missed the SLA for resolution time under policy {{ sla_policy.name }}.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="{{action_url}}">Please address immediately.</a>
|
||||||
|
</p>
|
||||||
9
enterprise/app/drops/sla_policy_drop.rb
Normal file
9
enterprise/app/drops/sla_policy_drop.rb
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
class SlaPolicyDrop < BaseDrop
|
||||||
|
def name
|
||||||
|
@obj.try(:name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def description
|
||||||
|
@obj.try(:description)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -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
|
||||||
5
enterprise/app/models/enterprise/application_record.rb
Normal file
5
enterprise/app/models/enterprise/application_record.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module Enterprise::ApplicationRecord
|
||||||
|
def droppables
|
||||||
|
super + %w[SlaPolicy]
|
||||||
|
end
|
||||||
|
end
|
||||||
15
spec/enterprise/drops/sla_policy_drop_spec.rb
Normal file
15
spec/enterprise/drops/sla_policy_drop_spec.rb
Normal file
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user