Skip to content

Commit

Permalink
feat: Add push/email notification support for SLA (#9140)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
vishnu-narayanan and muhsin-k committed Mar 29, 2024
1 parent 6956436 commit 16282f6
Show file tree
Hide file tree
Showing 13 changed files with 270 additions and 7 deletions.
10 changes: 8 additions & 2 deletions app/javascript/dashboard/i18n/locale/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,54 @@
}}
</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_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
Expand Down Expand Up @@ -352,6 +400,57 @@
}}
</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_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>
Expand All @@ -367,6 +466,7 @@ import {
requestPushPermissions,
verifyServiceWorkerExistence,
} from '../../../../helper/pushHelper';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
export default {
mixins: [alertMixin, configMixin, uiSettingsMixin],
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ def liquid_droppables
user: @agent,
conversation: @conversation,
inbox: @conversation.inbox,
message: @message
message: @message,
sla_policy: @sla_policy
})
end
end

AgentNotifications::ConversationNotificationsMailer.include_mod_with('AgentNotifications::ConversationNotificationsMailer')
8 changes: 6 additions & 2 deletions app/models/application_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -47,3 +49,5 @@ def normalize_empty_string_to_nil(attrs = [])
end
end
end

ApplicationRecord.include_mod_with('Enterprise::ApplicationRecord')
4 changes: 2 additions & 2 deletions app/models/notification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,11 @@ def push_message_title

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
''
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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 changes: 9 additions & 0 deletions enterprise/app/drops/sla_policy_drop.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class SlaPolicyDrop < BaseDrop
def name
@obj.try(:name)
end

def description
@obj.try(:description)
end
end
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions enterprise/app/models/enterprise/application_record.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module Enterprise::ApplicationRecord
def droppables
super + %w[SlaPolicy]
end
end
15 changes: 15 additions & 0 deletions spec/enterprise/drops/sla_policy_drop_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 16282f6

Please sign in to comment.