Resolved at
diff --git a/internal/conversation/models/models.go b/internal/conversation/models/models.go
index c6b1b57..ab47664 100644
--- a/internal/conversation/models/models.go
+++ b/internal/conversation/models/models.go
@@ -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:"-"`
}
diff --git a/internal/sla/models/models.go b/internal/sla/models/models.go
index 9f08916..70763ea 100644
--- a/internal/sla/models/models.go
+++ b/internal/sla/models/models.go
@@ -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"`
}
diff --git a/internal/sla/queries.sql b/internal/sla/queries.sql
index 55ad45f..b02a00a 100644
--- a/internal/sla/queries.sql
+++ b/internal/sla/queries.sql
@@ -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;
\ No newline at end of file
+-- 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;
diff --git a/internal/sla/sla.go b/internal/sla/sla.go
index c042c12..437913e 100644
--- a/internal/sla/sla.go
+++ b/internal/sla/sla.go
@@ -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
}