package v1 import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "strconv" "testing" "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/service" ) // ========== Test Setup ========== func setupSlaPolicyHandlerTest(t *testing.T) (*SlaPolicyHandler, *gorm.DB) { t.Helper() db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) require.NoError(t, err) require.NoError(t, db.AutoMigrate( &model.Account{}, &model.SlaPolicy{}, &model.SlaPolicyInbox{}, &model.AppliedSLA{}, &model.SlaEvent{}, &model.Inbox{}, &model.Contact{}, &model.Conversation{}, &model.User{}, &model.Team{}, &model.Audit{}, )) t.Cleanup(func() { sqlDB, _ := db.DB() sqlDB.Close() }) slaPolicyRepo := repository.NewSlaPolicyRepo(db) appliedSlaRepo := repository.NewAppliedSlaRepo(db) slaEventRepo := repository.NewSlaEventRepo(db) slaPolicyInboxRepo := repository.NewSlaPolicyInboxRepo(db) svc := service.NewSlaPolicyService(slaPolicyRepo, appliedSlaRepo, slaEventRepo, slaPolicyInboxRepo) auditSvc := service.NewAuditService(repository.NewAuditRepo(db)) handler := NewSlaPolicyHandler(svc).WithAuditService(auditSvc) // Seed an account for all tests account := &model.Account{Name: "SlaHandlerOrg", Locale: "en", Active: true} require.NoError(t, db.Create(account).Error) return handler, db } func slaHandlerAccountID(db *gorm.DB) string { var account model.Account db.First(&account) return strconv.FormatUint(uint64(account.ID), 10) } func slaHandlerAccountIDUint(db *gorm.DB) uint { var account model.Account db.First(&account) return account.ID } func setupSlaPolicyTestRouter(handler *SlaPolicyHandler) *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() rg := r.Group("/api/v1/accounts/:account_id") rg.GET("/sla_policies", handler.List) rg.POST("/sla_policies", handler.Create) rg.GET("/sla_policies/:id", handler.Get) rg.PUT("/sla_policies/:id", handler.Update) rg.DELETE("/sla_policies/:id", handler.Delete) rg.GET("/sla_policies/:id/inboxes", handler.ListInboxes) rg.POST("/sla_policies/:id/inboxes", handler.AddInbox) rg.DELETE("/sla_policies/:id/inboxes/:inbox_id", handler.RemoveInbox) rg.GET("/applied_slas", handler.ListAppliedSlas) rg.GET("/applied_slas/metrics", handler.GetAppliedSlaMetrics) rg.GET("/applied_slas/download", handler.GetAppliedSlaDownload) return r } func setupSlaPolicyAuditRouter(handler *SlaPolicyHandler, accountID uint, userID uint) *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() r.Use(func(c *gin.Context) { c.Set("account_id", accountID) c.Set("user_id", userID) c.Next() }) rg := r.Group("/api/v1/accounts/:account_id") rg.POST("/sla_policies", handler.Create) rg.PUT("/sla_policies/:id", handler.Update) rg.DELETE("/sla_policies/:id", handler.Delete) return r } // ========== List ========== func TestSlaPolicyHandler_List_Success(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) // Seed two policies slaPolicyRepo := repository.NewSlaPolicyRepo(db) appliedSlaRepo := repository.NewAppliedSlaRepo(db) slaEventRepo := repository.NewSlaEventRepo(db) slaPolicyInboxRepo := repository.NewSlaPolicyInboxRepo(db) svc := service.NewSlaPolicyService(slaPolicyRepo, appliedSlaRepo, slaEventRepo, slaPolicyInboxRepo) _, err := svc.Create(context.Background(), slaHandlerAccountIDUint(db), &service.CreateSlaPolicyRequest{ Name: "Policy-A", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) require.NoError(t, err) _, err = svc.Create(context.Background(), slaHandlerAccountIDUint(db), &service.CreateSlaPolicyRequest{ Name: "Policy-B", FirstResponseTimeThreshold: 20, NextResponseTimeThreshold: 40, ResolutionTimeThreshold: 200, }) require.NoError(t, err) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/"+aid+"/sla_policies", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var got map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &got)) assert.NotContains(t, got, "success") require.IsType(t, []interface{}{}, got["payload"]) payload := got["payload"].([]interface{}) require.Len(t, payload, 2) first := payload[0].(map[string]interface{}) assert.Equal(t, "Policy-A", first["name"]) assert.Equal(t, float64(10), first["first_response_time_threshold"]) assert.NotContains(t, first, "created_at") assert.NotContains(t, first, "account_id") } func TestSlaPolicyHandler_List_NoAccountID(t *testing.T) { gin.SetMode(gin.TestMode) r := gin.New() handler, _ := setupSlaPolicyHandlerTest(t) rg := r.Group("/api/v1/accounts") rg.GET("/sla_policies", handler.List) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/sla_policies", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } // ========== Create ========== func TestSlaPolicyHandler_Create_Success(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) body := map[string]interface{}{ "sla_policy": map[string]interface{}{ "name": "New SLA", "description": "desc", "first_response_time_threshold": 30, "next_response_time_threshold": 60, "resolution_time_threshold": 480, }, } jsonBody, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/"+aid+"/sla_policies", bytes.NewBuffer(jsonBody)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var got map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &got)) assert.NotContains(t, got, "success") payload := got["payload"].(map[string]interface{}) assert.Equal(t, "New SLA", payload["name"]) assert.Equal(t, "desc", payload["description"]) assert.Equal(t, float64(30), payload["first_response_time_threshold"]) assert.NotContains(t, payload, "created_at") assert.NotContains(t, payload, "account_id") } func TestSlaPolicyHandler_Create_ValidationError(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) body := map[string]interface{}{ "sla_policy": map[string]interface{}{ "name": "", }, } jsonBody, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/"+aid+"/sla_policies", bytes.NewBuffer(jsonBody)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestSlaPolicyHandler_MutationsWriteAuditEntries(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) accountID := slaHandlerAccountIDUint(db) router := setupSlaPolicyAuditRouter(handler, accountID, 88) createBody, _ := json.Marshal(map[string]any{"sla_policy": map[string]any{ "name": "Audit SLA", "first_response_time_threshold": 15, "next_response_time_threshold": 30, "resolution_time_threshold": 90, }}) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodPost, "/api/v1/accounts/"+strconv.FormatUint(uint64(accountID), 10)+"/sla_policies", bytes.NewBuffer(createBody)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Request-ID", "sla-audit-create") router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code, w.Body.String()) var created map[string]any require.NoError(t, json.Unmarshal(w.Body.Bytes(), &created)) policyID := uint(created["payload"].(map[string]any)["id"].(float64)) updateBody, _ := json.Marshal(map[string]any{"sla_policy": map[string]any{ "name": "Audit SLA Updated", "first_response_time_threshold": 20, "next_response_time_threshold": 40, "resolution_time_threshold": 100, }}) w = httptest.NewRecorder() req, _ = http.NewRequest(http.MethodPut, "/api/v1/accounts/"+strconv.FormatUint(uint64(accountID), 10)+"/sla_policies/"+strconv.FormatUint(uint64(policyID), 10), bytes.NewBuffer(updateBody)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code, w.Body.String()) w = httptest.NewRecorder() req, _ = http.NewRequest(http.MethodDelete, "/api/v1/accounts/"+strconv.FormatUint(uint64(accountID), 10)+"/sla_policies/"+strconv.FormatUint(uint64(policyID), 10), nil) router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code, w.Body.String()) var audits []model.Audit require.NoError(t, db.Order("id ASC").Find(&audits).Error) require.Len(t, audits, 3) for _, audit := range audits { assert.Equal(t, accountID, *audit.AccountID) assert.Equal(t, "Account", audit.AssociatedType) assert.Equal(t, accountID, *audit.AssociatedID) assert.Equal(t, uint(88), *audit.UserID) assert.Equal(t, "SlaPolicy", audit.AuditableType) assert.Equal(t, policyID, audit.AuditableID) assert.NotEmpty(t, audit.AuditedChanges) } assert.Equal(t, "create", audits[0].Action) assert.Equal(t, "sla-audit-create", audits[0].RequestUUID) assert.Equal(t, "update", audits[1].Action) assert.Equal(t, "destroy", audits[2].Action) } func TestSlaPolicyHandler_Create_NoAccountID(t *testing.T) { gin.SetMode(gin.TestMode) r := gin.New() handler, _ := setupSlaPolicyHandlerTest(t) rg := r.Group("/api/v1/accounts") rg.POST("/sla_policies", handler.Create) body := map[string]interface{}{ "sla_policy": map[string]interface{}{ "name": "New SLA", "first_response_time_threshold": 30, "next_response_time_threshold": 60, "resolution_time_threshold": 480, }, } jsonBody, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/sla_policies", bytes.NewBuffer(jsonBody)) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } // ========== Get ========== func TestSlaPolicyHandler_Get_Success(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) svc := service.NewSlaPolicyService( repository.NewSlaPolicyRepo(db), repository.NewAppliedSlaRepo(db), repository.NewSlaEventRepo(db), repository.NewSlaPolicyInboxRepo(db), ) policy, err := svc.Create(context.Background(), slaHandlerAccountIDUint(db), &service.CreateSlaPolicyRequest{ Name: "GetTest", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) require.NoError(t, err) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/"+aid+"/sla_policies/"+strconv.FormatUint(uint64(policy.ID), 10), nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var got map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &got)) assert.NotContains(t, got, "success") payload := got["payload"].(map[string]interface{}) assert.Equal(t, "GetTest", payload["name"]) assert.Equal(t, float64(10), payload["first_response_time_threshold"]) assert.NotContains(t, payload, "created_at") assert.NotContains(t, payload, "account_id") } func TestSlaPolicyHandler_Get_InvalidID(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/"+aid+"/sla_policies/notanumber", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } // ========== Update ========== func TestSlaPolicyHandler_Update_Success(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) svc := service.NewSlaPolicyService( repository.NewSlaPolicyRepo(db), repository.NewAppliedSlaRepo(db), repository.NewSlaEventRepo(db), repository.NewSlaPolicyInboxRepo(db), ) policy, err := svc.Create(context.Background(), slaHandlerAccountIDUint(db), &service.CreateSlaPolicyRequest{ Name: "ToUpdate", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) require.NoError(t, err) body := map[string]interface{}{ "sla_policy": map[string]interface{}{ "name": "Updated Name", "first_response_time_threshold": 45, }, } jsonBody, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", "/api/v1/accounts/"+aid+"/sla_policies/"+strconv.FormatUint(uint64(policy.ID), 10), bytes.NewBuffer(jsonBody)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var got map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &got)) assert.NotContains(t, got, "success") payload := got["payload"].(map[string]interface{}) assert.Equal(t, "Updated Name", payload["name"]) assert.Equal(t, float64(45), payload["first_response_time_threshold"]) } func TestSlaPolicyHandler_Update_NoAccountID(t *testing.T) { gin.SetMode(gin.TestMode) r := gin.New() handler, _ := setupSlaPolicyHandlerTest(t) rg := r.Group("/api/v1/accounts") rg.PUT("/sla_policies/:id", handler.Update) body := map[string]interface{}{ "sla_policy": map[string]interface{}{ "name": "Nope", }, } jsonBody, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", "/api/v1/accounts/sla_policies/1", bytes.NewBuffer(jsonBody)) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } // ========== Delete ========== func TestSlaPolicyHandler_Delete_Success(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) svc := service.NewSlaPolicyService( repository.NewSlaPolicyRepo(db), repository.NewAppliedSlaRepo(db), repository.NewSlaEventRepo(db), repository.NewSlaPolicyInboxRepo(db), ) policy, err := svc.Create(context.Background(), slaHandlerAccountIDUint(db), &service.CreateSlaPolicyRequest{ Name: "ToDelete", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) require.NoError(t, err) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/api/v1/accounts/"+aid+"/sla_policies/"+strconv.FormatUint(uint64(policy.ID), 10), nil) router.ServeHTTP(w, req) // Chatwoot returns head :ok (200) on destroy, not 204 assert.Equal(t, http.StatusOK, w.Code) assert.Empty(t, w.Body.String()) } func TestSlaPolicyHandler_Delete_NoAccountID(t *testing.T) { gin.SetMode(gin.TestMode) r := gin.New() handler, _ := setupSlaPolicyHandlerTest(t) rg := r.Group("/api/v1/accounts") rg.DELETE("/sla_policies/:id", handler.Delete) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/api/v1/accounts/sla_policies/1", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } // ========== AddInbox ========== func TestSlaPolicyHandler_AddInbox_Success(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) accountUID := slaHandlerAccountIDUint(db) svc := service.NewSlaPolicyService( repository.NewSlaPolicyRepo(db), repository.NewAppliedSlaRepo(db), repository.NewSlaEventRepo(db), repository.NewSlaPolicyInboxRepo(db), ) policy, err := svc.Create(context.Background(), accountUID, &service.CreateSlaPolicyRequest{ Name: "Test SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) require.NoError(t, err) inbox := &model.Inbox{Name: "Inbox1", AccountID: accountUID} require.NoError(t, db.Create(inbox).Error) body := map[string]interface{}{ "inbox_id": inbox.ID, } jsonBody, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/"+aid+"/sla_policies/"+strconv.FormatUint(uint64(policy.ID), 10)+"/inboxes", bytes.NewBuffer(jsonBody)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusCreated, w.Code) } // ========== RemoveInbox ========== func TestSlaPolicyHandler_RemoveInbox_Success(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) accountUID := slaHandlerAccountIDUint(db) svc := service.NewSlaPolicyService( repository.NewSlaPolicyRepo(db), repository.NewAppliedSlaRepo(db), repository.NewSlaEventRepo(db), repository.NewSlaPolicyInboxRepo(db), ) policy, err := svc.Create(context.Background(), accountUID, &service.CreateSlaPolicyRequest{ Name: "Test SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) require.NoError(t, err) inbox := &model.Inbox{Name: "Inbox1", AccountID: accountUID} require.NoError(t, db.Create(inbox).Error) _, err = svc.AddInbox(context.Background(), accountUID, policy.ID, inbox.ID) require.NoError(t, err) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/api/v1/accounts/"+aid+"/sla_policies/"+strconv.FormatUint(uint64(policy.ID), 10)+"/inboxes/"+strconv.FormatUint(uint64(inbox.ID), 10), nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusNoContent, w.Code) } // ========== ListInboxes ========== func TestSlaPolicyHandler_ListInboxes_Success(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) accountUID := slaHandlerAccountIDUint(db) svc := service.NewSlaPolicyService( repository.NewSlaPolicyRepo(db), repository.NewAppliedSlaRepo(db), repository.NewSlaEventRepo(db), repository.NewSlaPolicyInboxRepo(db), ) policy, err := svc.Create(context.Background(), accountUID, &service.CreateSlaPolicyRequest{ Name: "Test SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) require.NoError(t, err) inbox := &model.Inbox{Name: "Inbox1", AccountID: accountUID} require.NoError(t, db.Create(inbox).Error) _, err = svc.AddInbox(context.Background(), accountUID, policy.ID, inbox.ID) require.NoError(t, err) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/"+aid+"/sla_policies/"+strconv.FormatUint(uint64(policy.ID), 10)+"/inboxes", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestSlaPolicyHandler_InboxAssociationChatwootPayloadAndSideEffects(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) accountUID := slaHandlerAccountIDUint(db) svc := service.NewSlaPolicyService( repository.NewSlaPolicyRepo(db), repository.NewAppliedSlaRepo(db), repository.NewSlaEventRepo(db), repository.NewSlaPolicyInboxRepo(db), ) policy, err := svc.Create(context.Background(), accountUID, &service.CreateSlaPolicyRequest{ Name: "Inbox Linked SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) require.NoError(t, err) inbox := &model.Inbox{Name: "Priority Inbox", AccountID: accountUID, ChannelType: "web_widget"} require.NoError(t, db.Create(inbox).Error) addBody, _ := json.Marshal(map[string]any{"inbox_id": inbox.ID}) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodPost, "/api/v1/accounts/"+aid+"/sla_policies/"+strconv.FormatUint(uint64(policy.ID), 10)+"/inboxes", bytes.NewBuffer(addBody)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) require.Equal(t, http.StatusCreated, w.Code, w.Body.String()) var addResp map[string]any require.NoError(t, json.Unmarshal(w.Body.Bytes(), &addResp)) payload := addResp["data"].(map[string]any) assert.Equal(t, float64(policy.ID), payload["sla_policy_id"]) assert.Equal(t, float64(inbox.ID), payload["inbox_id"]) assert.Equal(t, float64(accountUID), payload["account_id"]) assert.Equal(t, true, addResp["success"]) var associationCount int64 require.NoError(t, db.Model(&model.SlaPolicyInbox{}).Where("sla_policy_id = ? AND inbox_id = ? AND account_id = ?", policy.ID, inbox.ID, accountUID).Count(&associationCount).Error) assert.Equal(t, int64(1), associationCount) w = httptest.NewRecorder() req, _ = http.NewRequest(http.MethodGet, "/api/v1/accounts/"+aid+"/sla_policies/"+strconv.FormatUint(uint64(policy.ID), 10)+"/inboxes", nil) router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code, w.Body.String()) var listResp map[string]any require.NoError(t, json.Unmarshal(w.Body.Bytes(), &listResp)) listPayload := listResp["data"].([]any) require.Len(t, listPayload, 1) listed := listPayload[0].(map[string]any) assert.Equal(t, float64(policy.ID), listed["sla_policy_id"]) assert.Equal(t, float64(inbox.ID), listed["inbox_id"]) assert.Equal(t, float64(accountUID), listed["account_id"]) w = httptest.NewRecorder() req, _ = http.NewRequest(http.MethodDelete, "/api/v1/accounts/"+aid+"/sla_policies/"+strconv.FormatUint(uint64(policy.ID), 10)+"/inboxes/"+strconv.FormatUint(uint64(inbox.ID), 10), nil) router.ServeHTTP(w, req) require.Equal(t, http.StatusNoContent, w.Code, w.Body.String()) require.NoError(t, db.Model(&model.SlaPolicyInbox{}).Where("sla_policy_id = ? AND inbox_id = ? AND account_id = ?", policy.ID, inbox.ID, accountUID).Count(&associationCount).Error) assert.Equal(t, int64(0), associationCount) } // ========== Applied SLA reports ========== func TestSlaPolicyHandler_ListAppliedSlas_ChatwootPayloadAndFilters(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) accountUID := slaHandlerAccountIDUint(db) now := time.Now().UTC() policy, conversation, assignee := seedAppliedSlaReportRecord(t, db, accountUID, model.SLAStatusActiveWithMisses, "vip, urgent", now.Add(-time.Hour)) seedAppliedSlaReportRecord(t, db, accountUID, model.SLAStatusHit, "vip", now.Add(-time.Hour)) seedAppliedSlaReportRecord(t, db, accountUID, model.SLAStatusMissed, "other", now.Add(-time.Hour)) w := httptest.NewRecorder() url := "/api/v1/accounts/" + aid + "/applied_slas?" + "since=" + strconv.FormatInt(now.Add(-2*time.Hour).Unix(), 10) + "&until=" + strconv.FormatInt(now.Add(time.Hour).Unix(), 10) + "&inbox_id=" + strconv.FormatUint(uint64(conversation.InboxID), 10) + "&team_id=" + strconv.FormatUint(uint64(*conversation.TeamID), 10) + "&assigned_agent_id=" + strconv.FormatUint(uint64(assignee.ID), 10) + "&sla_policy_id=" + strconv.FormatUint(uint64(policy.ID), 10) + "&label_list=urgent&page=1" req, _ := http.NewRequest("GET", url, nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var got map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &got)) assert.NotContains(t, got, "success") payload := got["payload"].([]interface{}) require.Len(t, payload, 1) item := payload[0].(map[string]interface{}) applied := item["applied_sla"].(map[string]interface{}) assert.Equal(t, "Gold SLA", applied["sla_name"]) assert.Equal(t, "active_with_misses", applied["sla_status"]) assert.Equal(t, float64(10), applied["sla_first_response_time_threshold"]) conversationPayload := item["conversation"].(map[string]interface{}) assert.Equal(t, float64(*conversation.DisplayID), conversationPayload["id"]) assert.Equal(t, "vip, urgent", conversationPayload["labels"]) contact := conversationPayload["contact"].(map[string]interface{}) assert.Equal(t, "SLA Contact", contact["name"]) assigneePayload := conversationPayload["assignee"].(map[string]interface{}) assert.Equal(t, "Agent One", assigneePayload["name"]) events := item["sla_events"].([]interface{}) require.Len(t, events, 1) assert.Equal(t, "nrt", events[0].(map[string]interface{})["event_type"]) meta := got["meta"].(map[string]interface{}) assert.Equal(t, float64(1), meta["count"]) assert.Equal(t, float64(1), meta["current_page"]) } func TestSlaPolicyHandler_GetAppliedSlaMetrics_ChatwootReportShape(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) accountUID := slaHandlerAccountIDUint(db) now := time.Now().UTC() seedAppliedSlaReportRecord(t, db, accountUID, model.SLAStatusHit, "vip", now.Add(-time.Hour)) seedAppliedSlaReportRecord(t, db, accountUID, model.SLAStatusMissed, "vip", now.Add(-time.Hour)) seedAppliedSlaReportRecord(t, db, accountUID, model.SLAStatusActiveWithMisses, "other", now.Add(-time.Hour)) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/"+aid+"/applied_slas/metrics?label_list=vip", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var got map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &got)) assert.Equal(t, float64(2), got["total_applied_slas"]) assert.Equal(t, float64(1), got["number_of_sla_misses"]) assert.Equal(t, "50.0%", got["hit_rate"]) } func TestSlaPolicyHandler_GetAppliedSlaMetrics_NoAccountID(t *testing.T) { gin.SetMode(gin.TestMode) r := gin.New() handler, _ := setupSlaPolicyHandlerTest(t) rg := r.Group("/api/v1/accounts") rg.GET("/applied_slas/metrics", handler.GetAppliedSlaMetrics) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/applied_slas/metrics?conversation_id=1", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } // ========== GetAppliedSlaDownload ========== func TestSlaPolicyHandler_GetAppliedSlaDownload_Success(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) accountUID := slaHandlerAccountIDUint(db) now := time.Now().UTC() seedAppliedSlaReportRecord(t, db, accountUID, model.SLAStatusMissed, "vip", now.Add(-time.Hour)) seedAppliedSlaReportRecord(t, db, accountUID, model.SLAStatusHit, "vip", now.Add(-time.Hour)) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/"+aid+"/applied_slas/download", nil) req.Header.Set("X-Account-ID", aid) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, "attachment; filename=breached_conversation.csv", w.Header().Get("Content-Disposition")) assert.Contains(t, w.Body.String(), "Conversation ID,SLA policy breached,Assignee,Team,Inbox,Labels,Conversation link,Breached events") assert.Contains(t, w.Body.String(), "Gold SLA") assert.Contains(t, w.Body.String(), "frt") assert.NotContains(t, w.Body.String(), "hit") } func seedAppliedSlaReportRecord(t *testing.T, db *gorm.DB, accountID uint, status model.SLAStatus, labels string, createdAt time.Time) (*model.SlaPolicy, *model.Conversation, *model.User) { t.Helper() policy := &model.SlaPolicy{ AccountID: accountID, Name: "Gold SLA", Description: "Gold support", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 30, } require.NoError(t, db.Create(policy).Error) inbox := &model.Inbox{AccountID: accountID, Name: "Priority", ChannelType: "web_widget", ChannelID: 1} require.NoError(t, db.Create(inbox).Error) contact := &model.Contact{AccountID: accountID, Name: "SLA Contact"} require.NoError(t, db.Create(contact).Error) assignee := &model.User{AccountID: accountID, Name: "Agent One", Email: "agent-" + strconv.FormatInt(time.Now().UnixNano(), 10) + "@example.com", Password: "secret", Role: "agent"} require.NoError(t, db.Create(assignee).Error) team := &model.Team{AccountID: accountID, Name: "Escalation"} require.NoError(t, db.Create(team).Error) displayID := uint(100 + time.Now().UnixNano()%100000) conversation := &model.Conversation{ AccountID: accountID, InboxID: inbox.ID, ContactID: contact.ID, AssigneeID: &assignee.ID, TeamID: &team.ID, DisplayID: &displayID, Status: "open", ChannelType: "Channel::WebWidget", Channel: "web_widget", Labels: labels, } require.NoError(t, db.Create(conversation).Error) applied := &model.AppliedSLA{AccountID: accountID, ConversationID: conversation.ID, SlaPolicyID: policy.ID, SLAStatus: status} require.NoError(t, db.Create(applied).Error) require.NoError(t, db.Model(applied).Updates(map[string]any{"created_at": createdAt, "updated_at": createdAt}).Error) applied.CreatedAt = createdAt applied.UpdatedAt = createdAt if status == model.SLAStatusMissed || status == model.SLAStatusActiveWithMisses { eventType := model.SLAEventFRT if status == model.SLAStatusActiveWithMisses { eventType = model.SLAEventNRT } require.NoError(t, db.Create(&model.SlaEvent{ AppliedSlaID: applied.ID, AccountID: accountID, ConversationID: conversation.ID, InboxID: inbox.ID, SlaPolicyID: policy.ID, EventType: eventType, }).Error) } return policy, conversation, assignee }