# Pull Request Template ## Description Blocked contacts are now excluded from SLA assignment, processing, reports, and conversation SLA UI while they remain blocked. Existing SLA records are preserved, and SLA behavior resumes if the contact is unblocked. Fixes https://linear.app/chatwoot/issue/CW-7435/sla-should-not-trigger-for-blocked-contacts ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? - `bundle exec rspec spec/enterprise/models/conversation_spec.rb spec/enterprise/models/applied_sla_spec.rb spec/enterprise/services/enterprise/action_service_spec.rb spec/enterprise/services/sla/evaluate_applied_sla_service_spec.rb spec/enterprise/jobs/sla/process_account_applied_slas_job_spec.rb spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb spec/enterprise/controllers/api/v1/accounts/conversations_controller_spec.rb spec/enterprise/controllers/enterprise/api/v1/accounts/conversations_controller_spec.rb spec/enterprise/presenters/conversations/event_data_presenter_spec.rb` — 78 examples, 0 failures - `bundle exec rubocop enterprise/app/controllers/api/v1/accounts/applied_slas_controller.rb enterprise/app/jobs/sla/process_account_applied_slas_job.rb enterprise/app/models/applied_sla.rb enterprise/app/models/enterprise/concerns/conversation.rb enterprise/app/presenters/enterprise/conversations/event_data_presenter.rb enterprise/app/services/enterprise/action_service.rb enterprise/app/services/sla/evaluate_applied_sla_service.rb lib/tasks/apply_sla.rake spec/enterprise/controllers/api/v1/accounts/applied_slas_controller_spec.rb spec/enterprise/controllers/api/v1/accounts/conversations_controller_spec.rb spec/enterprise/controllers/enterprise/api/v1/accounts/conversations_controller_spec.rb spec/enterprise/jobs/sla/process_account_applied_slas_job_spec.rb spec/enterprise/models/applied_sla_spec.rb spec/enterprise/models/conversation_spec.rb spec/enterprise/presenters/conversations/event_data_presenter_spec.rb spec/enterprise/services/enterprise/action_service_spec.rb spec/enterprise/services/sla/evaluate_applied_sla_service_spec.rb` — no offenses - `pnpm exec vitest --no-watch --no-cache --no-coverage app/javascript/dashboard/components/widgets/conversation/specs/ConversationCard.spec.js` — 2 tests passed - `pnpm exec eslint app/javascript/dashboard/components-next/Conversation/ConversationCard/CardMessagePreviewWithMeta.vue app/javascript/dashboard/components-next/Conversation/ConversationCard/ConversationCardExpanded.vue app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue app/javascript/dashboard/components/widgets/conversation/specs/ConversationCard.spec.js` — passed with existing raw-text warnings in `ConversationHeader.vue` - `git diff --cached --check` — clean ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
136 lines
5.1 KiB
Ruby
136 lines
5.1 KiB
Ruby
# == Schema Information
|
|
#
|
|
# Table name: applied_slas
|
|
#
|
|
# id :bigint not null, primary key
|
|
# sla_status :integer default("active")
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# account_id :bigint not null
|
|
# conversation_id :bigint not null
|
|
# sla_policy_id :bigint not null
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_applied_slas_on_account_id (account_id)
|
|
# index_applied_slas_on_account_sla_policy_conversation (account_id,sla_policy_id,conversation_id) UNIQUE
|
|
# index_applied_slas_on_conversation_id (conversation_id)
|
|
# index_applied_slas_on_sla_policy_id (sla_policy_id)
|
|
#
|
|
class AppliedSla < ApplicationRecord
|
|
belongs_to :account
|
|
belongs_to :sla_policy
|
|
belongs_to :conversation
|
|
|
|
has_many :sla_events, dependent: :destroy_async
|
|
|
|
validates :account_id, uniqueness: { scope: %i[sla_policy_id conversation_id] }
|
|
before_validation :ensure_account_id
|
|
|
|
enum sla_status: { active: 0, hit: 1, missed: 2, active_with_misses: 3 }
|
|
|
|
scope :filter_by_date_range, ->(range) { where(created_at: range) if range.present? }
|
|
scope :filter_by_inbox_id, ->(inbox_id) { joins(:conversation).where(conversations: { inbox_id: inbox_id }) if inbox_id.present? }
|
|
scope :filter_by_team_id, ->(team_id) { joins(:conversation).where(conversations: { team_id: team_id }) if team_id.present? }
|
|
scope :filter_by_sla_policy_id, ->(sla_policy_id) { where(sla_policy_id: sla_policy_id) if sla_policy_id.present? }
|
|
scope :filter_by_label_list, lambda { |label_list|
|
|
joins(:conversation).where('conversations.cached_label_list LIKE ?', "%#{label_list}%") if label_list.present?
|
|
}
|
|
scope :filter_by_assigned_agent_id, lambda { |assigned_agent_id|
|
|
joins(:conversation).where(conversations: { assignee_id: assigned_agent_id }) if assigned_agent_id.present?
|
|
}
|
|
scope :missed, -> { where(sla_status: %i[missed active_with_misses]) }
|
|
scope :with_sla_applicable_conversation, -> { where(conversation_id: Conversation.with_sla_applicable_contact.select(:id)) }
|
|
|
|
after_update_commit :push_conversation_event
|
|
|
|
def push_event_data
|
|
sla_due_at_values = due_at_values
|
|
|
|
{
|
|
id: id,
|
|
sla_id: sla_policy_id,
|
|
sla_status: sla_status,
|
|
created_at: created_at.to_i,
|
|
updated_at: updated_at.to_i,
|
|
sla_description: sla_policy.description,
|
|
sla_name: sla_policy.name,
|
|
sla_first_response_time_threshold: sla_policy.first_response_time_threshold,
|
|
sla_next_response_time_threshold: sla_policy.next_response_time_threshold,
|
|
sla_only_during_business_hours: sla_policy.only_during_business_hours,
|
|
sla_resolution_time_threshold: sla_policy.resolution_time_threshold,
|
|
sla_frt_due_at: sla_due_at_values[:frt],
|
|
sla_nrt_due_at: sla_due_at_values[:nrt],
|
|
sla_rt_due_at: sla_due_at_values[:rt]
|
|
}
|
|
end
|
|
|
|
def due_at_values
|
|
working_hours_by_day_cache = conversation.inbox.working_hours.index_by(&:day_of_week) if sla_policy.only_during_business_hours?
|
|
|
|
{
|
|
frt: frt_due_at(working_hours_by_day_cache: working_hours_by_day_cache),
|
|
nrt: nrt_due_at(working_hours_by_day_cache: working_hours_by_day_cache),
|
|
rt: rt_due_at(working_hours_by_day_cache: working_hours_by_day_cache)
|
|
}
|
|
end
|
|
|
|
def frt_due_at(working_hours_by_day_cache: nil)
|
|
return nil if sla_policy.first_response_time_threshold.blank?
|
|
|
|
calculate_due_at(
|
|
conversation.created_at,
|
|
sla_policy.first_response_time_threshold,
|
|
working_hours_by_day_cache: working_hours_by_day_cache
|
|
)
|
|
end
|
|
|
|
def nrt_due_at(working_hours_by_day_cache: nil)
|
|
return nil if sla_policy.next_response_time_threshold.blank?
|
|
return nil if conversation.waiting_since.blank?
|
|
|
|
calculate_due_at(
|
|
conversation.waiting_since,
|
|
sla_policy.next_response_time_threshold,
|
|
working_hours_by_day_cache: working_hours_by_day_cache
|
|
)
|
|
end
|
|
|
|
def rt_due_at(working_hours_by_day_cache: nil)
|
|
return nil if sla_policy.resolution_time_threshold.blank?
|
|
|
|
calculate_due_at(
|
|
conversation.created_at,
|
|
sla_policy.resolution_time_threshold,
|
|
working_hours_by_day_cache: working_hours_by_day_cache
|
|
)
|
|
end
|
|
|
|
def calculate_due_at(start_time, threshold_seconds, working_hours_by_day_cache: nil)
|
|
return (start_time + threshold_seconds.to_i.seconds).to_i unless sla_policy.only_during_business_hours?
|
|
|
|
Sla::BusinessHoursService.new(
|
|
inbox: conversation.inbox,
|
|
start_time: start_time,
|
|
threshold_seconds: threshold_seconds,
|
|
working_hours_by_day_cache: working_hours_by_day_cache
|
|
).deadline.to_i
|
|
end
|
|
|
|
private
|
|
|
|
def push_conversation_event
|
|
# right now we simply use `CONVERSATION_UPDATED` event to notify the frontend
|
|
# we can eventually start using `CONVERSATION_SLA_UPDATED` event as required later
|
|
# for now the updated event should suffice
|
|
|
|
return unless saved_change_to_sla_status?
|
|
|
|
conversation.dispatch_conversation_updated_event
|
|
end
|
|
|
|
def ensure_account_id
|
|
self.account_id ||= sla_policy&.account_id
|
|
end
|
|
end
|