mirror of
https://github.com/abhinavxd/libredesk.git
synced 2025-10-23 05:11:57 +00:00
fix: various bugs in SLA calculation
prevents multiple update queries unnecessarily on applied sla table. clear next sla deadline in conversations properly when there's no deadline to be met. uses the new status column in the applied sla table to determine if the sla is still active and has to be calculated again.
This commit is contained in:
@@ -57,15 +57,15 @@
|
||||
|
||||
<div class="flex items-center mt-2 space-x-2">
|
||||
<SlaBadge
|
||||
v-if="conversation.first_response_due_at"
|
||||
:dueAt="conversation.first_response_due_at"
|
||||
v-if="conversation.first_response_deadline_at"
|
||||
:dueAt="conversation.first_response_deadline_at"
|
||||
:actualAt="conversation.first_reply_at"
|
||||
:label="'FRD'"
|
||||
:showExtra="false"
|
||||
/>
|
||||
<SlaBadge
|
||||
v-if="conversation.resolution_due_at"
|
||||
:dueAt="conversation.resolution_due_at"
|
||||
v-if="conversation.resolution_deadline_at"
|
||||
:dueAt="conversation.resolution_deadline_at"
|
||||
:actualAt="conversation.resolved_at"
|
||||
:label="'RD'"
|
||||
:showExtra="false"
|
||||
|
@@ -27,8 +27,8 @@
|
||||
<div class="flex justify-start items-center space-x-2">
|
||||
<p class="font-medium">First reply at</p>
|
||||
<SlaBadge
|
||||
v-if="conversation.first_response_due_at"
|
||||
:dueAt="conversation.first_response_due_at"
|
||||
v-if="conversation.first_response_deadline_at"
|
||||
:dueAt="conversation.first_response_deadline_at"
|
||||
:actualAt="conversation.first_reply_at"
|
||||
:key="conversation.uuid"
|
||||
/>
|
||||
@@ -46,8 +46,8 @@
|
||||
<div class="flex justify-start items-center space-x-2">
|
||||
<p class="font-medium">Resolved at</p>
|
||||
<SlaBadge
|
||||
v-if="conversation.resolution_due_at"
|
||||
:dueAt="conversation.resolution_due_at"
|
||||
v-if="conversation.resolution_deadline_at"
|
||||
:dueAt="conversation.resolution_deadline_at"
|
||||
:actualAt="conversation.resolved_at"
|
||||
:key="conversation.uuid"
|
||||
/>
|
||||
|
@@ -61,10 +61,11 @@ type Conversation struct {
|
||||
SLAPolicyID null.Int `db:"sla_policy_id" json:"sla_policy_id"`
|
||||
SlaPolicyName null.String `db:"sla_policy_name" json:"sla_policy_name"`
|
||||
NextSLADeadlineAt null.Time `db:"next_sla_deadline_at" json:"next_sla_deadline_at"`
|
||||
FirstResponseDueAt null.Time `db:"first_response_deadline_at" json:"first_response_deadline_at"`
|
||||
ResolutionDueAt null.Time `db:"resolution_deadline_at" json:"resolution_deadline_at"`
|
||||
SLAStatus null.String `db:"sla_status" json:"sla_status"`
|
||||
BCC json.RawMessage `db:"bcc" json:"bcc"`
|
||||
CC json.RawMessage `db:"cc" json:"cc"`
|
||||
FirstResponseDueAt null.Time `db:"-" json:"first_response_due_at"`
|
||||
ResolutionDueAt null.Time `db:"-" json:"resolution_due_at"`
|
||||
PreviousConversations []Conversation `db:"-" json:"previous_conversations"`
|
||||
Total int `db:"total" json:"-"`
|
||||
}
|
||||
|
@@ -28,6 +28,9 @@ type AppliedSLA struct {
|
||||
ResolutionDeadlineAt time.Time `db:"resolution_deadline_at"`
|
||||
FirstResponseBreachedAt null.Time `db:"first_response_breached_at"`
|
||||
ResolutionBreachedAt null.Time `db:"resolution_breached_at"`
|
||||
FirstResponseAt null.Time `db:"first_response_at"`
|
||||
ResolvedAt null.Time `db:"resolved_at"`
|
||||
FirstResponseMetAt null.Time `db:"first_response_met_at"`
|
||||
ResolutionMetAt null.Time `db:"resolution_met_at"`
|
||||
|
||||
ConversationFirstResponseAt null.Time `db:"conversation_first_response_at"`
|
||||
ConversationResolvedAt null.Time `db:"conversation_resolved_at"`
|
||||
}
|
||||
|
@@ -43,14 +43,12 @@ next_sla_deadline_at = LEAST(
|
||||
WHERE id IN (SELECT conversation_id FROM new_sla);
|
||||
|
||||
-- name: get-pending-slas
|
||||
-- Get all the applied SLAs that are not yet breached or met and is also set on the conversation.
|
||||
-- This make sure when SLA is changed, we don't update the breached or met status of the previous SLA.
|
||||
SELECT a.id, a.first_response_deadline_at, c.first_reply_at as first_response_at,
|
||||
a.resolution_deadline_at, c.resolved_at as resolved_at
|
||||
-- Get all the applied SLAs (applied to a conversation) that are pending
|
||||
SELECT a.id, a.first_response_deadline_at, c.first_reply_at as conversation_first_response_at,
|
||||
a.resolution_deadline_at, c.resolved_at as conversation_resolved_at, c.id as conversation_id, a.first_response_met_at, a.resolution_met_at, a.first_response_breached_at, a.resolution_breached_at
|
||||
FROM applied_slas a
|
||||
JOIN conversations c ON a.conversation_id = c.id and c.sla_policy_id = a.sla_policy_id
|
||||
WHERE (first_response_breached_at IS NULL AND first_response_met_at IS NULL)
|
||||
OR (resolution_breached_at IS NULL AND resolution_met_at IS NULL);
|
||||
WHERE a.status = 'pending'::applied_sla_status;
|
||||
|
||||
-- name: update-breach
|
||||
UPDATE applied_slas SET
|
||||
@@ -66,8 +64,29 @@ UPDATE applied_slas SET
|
||||
updated_at = NOW()
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: get-latest-sla-deadlines
|
||||
SELECT first_response_deadline_at, resolution_deadline_at
|
||||
FROM applied_slas
|
||||
WHERE conversation_id = $1
|
||||
ORDER BY created_at DESC LIMIT 1;
|
||||
-- name: set-next-sla-deadline
|
||||
UPDATE conversations c
|
||||
SET next_sla_deadline_at = CASE
|
||||
WHEN c.status_id IN (SELECT id from conversation_statuses where name in ('Resolved', 'Closed')) THEN NULL
|
||||
WHEN c.first_reply_at IS NOT NULL AND c.resolved_at IS NULL AND a.resolution_deadline_at IS NOT NULL THEN a.resolution_deadline_at
|
||||
WHEN c.first_reply_at IS NULL AND c.resolved_at IS NULL AND a.first_response_deadline_at IS NOT NULL THEN a.first_response_deadline_at
|
||||
WHEN a.first_response_deadline_at IS NOT NULL AND a.resolution_deadline_at IS NOT NULL THEN LEAST(a.first_response_deadline_at, a.resolution_deadline_at)
|
||||
ELSE NULL
|
||||
END
|
||||
FROM applied_slas a
|
||||
WHERE a.conversation_id = c.id
|
||||
AND c.id = $1;
|
||||
|
||||
-- name: update-sla-status
|
||||
UPDATE applied_slas
|
||||
SET
|
||||
status = CASE
|
||||
WHEN first_response_met_at IS NOT NULL AND resolution_met_at IS NOT NULL THEN 'met'::applied_sla_status
|
||||
WHEN first_response_breached_at IS NOT NULL AND resolution_breached_at IS NOT NULL THEN 'breached'::applied_sla_status
|
||||
WHEN (first_response_met_at IS NOT NULL OR first_response_breached_at IS NOT NULL)
|
||||
AND (resolution_met_at IS NOT NULL OR resolution_breached_at IS NOT NULL) THEN 'partially_met'::applied_sla_status
|
||||
WHEN first_response_met_at IS NULL AND first_response_breached_at IS NULL THEN 'pending'::applied_sla_status
|
||||
ELSE 'pending'::applied_sla_status
|
||||
END,
|
||||
updated_at = NOW()
|
||||
WHERE applied_slas.id = $1;
|
||||
|
@@ -2,7 +2,6 @@ package sla
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -78,7 +77,8 @@ type queries struct {
|
||||
GetPendingSLAs *sqlx.Stmt `query:"get-pending-slas"`
|
||||
UpdateBreach *sqlx.Stmt `query:"update-breach"`
|
||||
UpdateMet *sqlx.Stmt `query:"update-met"`
|
||||
GetLatestDeadlines *sqlx.Stmt `query:"get-latest-sla-deadlines"`
|
||||
SetNextSLADeadline *sqlx.Stmt `query:"set-next-sla-deadline"`
|
||||
UpdateSLAStatus *sqlx.Stmt `query:"update-sla-status"`
|
||||
}
|
||||
|
||||
// New creates a new SLA manager.
|
||||
@@ -254,23 +254,14 @@ func (m *Manager) ApplySLA(startTime time.Time, conversationID, assignedTeamID,
|
||||
return sla, nil
|
||||
}
|
||||
|
||||
// GetLatestDeadlines returns the latest deadlines for a conversation.
|
||||
func (m *Manager) GetLatestDeadlines(conversationID int) (time.Time, time.Time, error) {
|
||||
var first, resolution time.Time
|
||||
err := m.q.GetLatestDeadlines.QueryRow(conversationID).Scan(&first, &resolution)
|
||||
if err == sql.ErrNoRows {
|
||||
return first, resolution, nil
|
||||
}
|
||||
return first, resolution, err
|
||||
}
|
||||
|
||||
// Run starts the SLA evaluation loop and evaluates pending SLAs.
|
||||
func (m *Manager) Run(ctx context.Context, evalInterval time.Duration) {
|
||||
m.wg.Add(1)
|
||||
defer m.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(evalInterval)
|
||||
defer ticker.Stop()
|
||||
m.wg.Add(1)
|
||||
defer func() {
|
||||
m.wg.Done()
|
||||
ticker.Stop()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
@@ -315,23 +306,30 @@ func (m *Manager) evaluatePendingSLAs(ctx context.Context) error {
|
||||
|
||||
// evaluateSLA evaluates an SLA policy on an applied SLA.
|
||||
func (m *Manager) evaluateSLA(sla models.AppliedSLA) error {
|
||||
now := time.Now()
|
||||
m.lo.Debug("evaluating SLA", "conversation_id", sla.ConversationID, "applied_sla_id", sla.ID)
|
||||
checkDeadline := func(deadline time.Time, metAt null.Time, slaType string) error {
|
||||
if deadline.IsZero() {
|
||||
m.lo.Debug("deadline zero, skipping checking the deadline")
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if !metAt.Valid && now.After(deadline) {
|
||||
m.lo.Debug("SLA breached as current time is after deadline", "deadline", deadline, "now", now, "sla_type", slaType)
|
||||
if _, err := m.q.UpdateBreach.Exec(sla.ID, slaType); err != nil {
|
||||
return fmt.Errorf("updating SLA breach: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if metAt.Valid {
|
||||
if metAt.Time.After(deadline) {
|
||||
m.lo.Debug("SLA breached as met_at is after deadline", "deadline", deadline, "met_at", metAt.Time, "sla_type", slaType)
|
||||
if _, err := m.q.UpdateBreach.Exec(sla.ID, slaType); err != nil {
|
||||
return fmt.Errorf("updating SLA breach: %w", err)
|
||||
}
|
||||
} else {
|
||||
m.lo.Debug("SLA type met", "deadline", deadline, "met_at", metAt.Time, "sla_type", slaType)
|
||||
if _, err := m.q.UpdateMet.Exec(sla.ID, slaType); err != nil {
|
||||
return fmt.Errorf("updating SLA met: %w", err)
|
||||
}
|
||||
@@ -340,11 +338,31 @@ func (m *Manager) evaluateSLA(sla models.AppliedSLA) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := checkDeadline(sla.FirstResponseDeadlineAt, sla.FirstResponseAt, SLATypeFirstResponse); err != nil {
|
||||
return err
|
||||
// If first response is not breached and not met, check the deadline and set them.
|
||||
if !sla.FirstResponseBreachedAt.Valid && !sla.FirstResponseMetAt.Valid {
|
||||
m.lo.Debug("checking deadline", "deadline", sla.FirstResponseDeadlineAt, "met_at", sla.ConversationFirstResponseAt.Time, "sla_type", SLATypeFirstResponse)
|
||||
if err := checkDeadline(sla.FirstResponseDeadlineAt, sla.ConversationFirstResponseAt, SLATypeFirstResponse); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := checkDeadline(sla.ResolutionDeadlineAt, sla.ResolvedAt, SLATypeResolution); err != nil {
|
||||
return err
|
||||
|
||||
// If resolution is not breached and not met, check the deadine and set them.
|
||||
if !sla.ResolutionBreachedAt.Valid && !sla.ResolutionMetAt.Valid {
|
||||
m.lo.Debug("checking deadline", "deadline", sla.ResolutionDeadlineAt, "met_at", sla.ConversationResolvedAt.Time, "sla_type", SLATypeResolution)
|
||||
if err := checkDeadline(sla.ResolutionDeadlineAt, sla.ConversationResolvedAt, SLATypeResolution); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Update the conversation next SLA deadline.
|
||||
if _, err := m.q.SetNextSLADeadline.Exec(sla.ConversationID); err != nil {
|
||||
return fmt.Errorf("setting conversation next SLA deadline: %w", err)
|
||||
}
|
||||
|
||||
// Update status of applied SLA.
|
||||
if _, err := m.q.UpdateSLAStatus.Exec(sla.ID); err != nil {
|
||||
return fmt.Errorf("updating applied SLA status: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user