feat(sla): persist applied sla on conversations

This commit is contained in:
2026-06-05 07:16:05 +08:00
parent 4cf68d1639
commit a11bb965fa
6 changed files with 270 additions and 30 deletions
@@ -82,6 +82,9 @@ func (s *ConversationCrudTestSuite) SetupSuite() {
&model.AccountUser{},
&model.Team{},
&model.TeamMember{},
&model.SlaPolicy{},
&model.AppliedSLA{},
&model.SlaEvent{},
)
s.Require().NoError(err)
@@ -95,6 +98,8 @@ func (s *ConversationCrudTestSuite) SetupSuite() {
teamRepo := repository.NewTeamRepo(db)
teamMemberRepo := repository.NewTeamMemberRepo(db)
conversationSvc := service.NewConversationService(convRepo, msgRepo, dispatcher, inboxMemberSvc, accountUserRepo, teamRepo, teamMemberRepo)
appliedSlaSvc := service.NewAppliedSlaService(repository.NewAppliedSlaRepo(db), repository.NewSlaEventRepo(db), repository.NewSlaPolicyRepo(db), convRepo)
conversationSvc.SetAppliedSlaService(appliedSlaSvc)
mockLLM := &mockConvCrudLLMProvider{}
messageSvc := service.NewMessageService(msgRepo, dispatcher, mockLLM)
handler := NewConversationHandler(conversationSvc, messageSvc)
@@ -163,6 +168,8 @@ func (s *ConversationCrudTestSuite) SetupTest() {
}
func (s *ConversationCrudTestSuite) TearDownTest() {
s.db.Exec("DELETE FROM sla_events")
s.db.Exec("DELETE FROM applied_slas")
s.db.Exec("DELETE FROM conversation_labels")
s.db.Exec("DELETE FROM conversations")
s.db.Exec("DELETE FROM contact_inboxes")
@@ -174,6 +181,7 @@ func (s *ConversationCrudTestSuite) TearDownTest() {
s.db.Exec("DELETE FROM accounts")
s.db.Exec("DELETE FROM teams")
s.db.Exec("DELETE FROM team_members")
s.db.Exec("DELETE FROM sla_policies")
}
func (s *ConversationCrudTestSuite) accountURL() string {
@@ -352,6 +360,44 @@ func (s *ConversationCrudTestSuite) TestUpdate_Success() {
assert.Equal(s.T(), "resolved", resp["status"])
}
func (s *ConversationCrudTestSuite) TestUpdate_WithSlaPolicyCreatesAppliedSlaPayload() {
policy := &model.SlaPolicy{
AccountID: s.testAccount.ID,
Name: "Gold SLA",
Description: "Priority customers",
FirstResponseTimeThreshold: 15,
NextResponseTimeThreshold: 30,
ResolutionTimeThreshold: 120,
}
s.Require().NoError(s.db.Create(policy).Error)
body, _ := json.Marshal(map[string]interface{}{
"sla_policy_id": policy.ID,
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", s.convURL(s.testConv.ID), bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
var resp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &resp)
assert.NoError(s.T(), err)
assert.Equal(s.T(), float64(policy.ID), resp["sla_policy_id"])
applied, ok := resp["applied_sla"].(map[string]interface{})
s.Require().True(ok)
assert.Equal(s.T(), float64(policy.ID), applied["sla_id"])
assert.Equal(s.T(), "active", applied["sla_status"])
assert.Equal(s.T(), "Gold SLA", applied["sla_name"])
assert.Equal(s.T(), float64(15), applied["sla_first_response_time_threshold"])
var count int64
s.Require().NoError(s.db.Model(&model.AppliedSLA{}).Where("conversation_id = ?", s.testConv.ID).Count(&count).Error)
assert.Equal(s.T(), int64(1), count)
}
func (s *ConversationCrudTestSuite) TestUpdate_InvalidAccountID() {
body, _ := json.Marshal(map[string]interface{}{
"status": "resolved",
@@ -55,6 +55,7 @@ type chatwootConversationPayload struct {
Priority string `json:"priority"`
WaitingSince int64 `json:"waiting_since"`
SlaPolicyID *uint `json:"sla_policy_id"`
AppliedSLA map[string]any `json:"applied_sla,omitempty"`
}
type chatwootConversationMeta struct {
@@ -135,7 +136,7 @@ func serializeConversation(ctx context.Context, db *gorm.DB, conversation *model
}
}
return chatwootConversationPayload{
payload := chatwootConversationPayload{
Meta: serializeConversationMeta(ctx, db, conversation),
ID: conversationDisplayID(conversation),
Messages: messages,
@@ -163,6 +164,40 @@ func serializeConversation(ctx context.Context, db *gorm.DB, conversation *model
WaitingSince: int64Value(conversation.WaitingSince),
SlaPolicyID: conversation.SlaPolicyID,
}
if appliedSLA := serializeAppliedSlaForConversation(ctx, db, conversation.ID); appliedSLA != nil {
payload.AppliedSLA = appliedSLA
}
return payload
}
func serializeAppliedSlaForConversation(ctx context.Context, db *gorm.DB, conversationID uint) map[string]any {
if db == nil || conversationID == 0 {
return nil
}
var applied model.AppliedSLA
if err := db.WithContext(ctx).
Preload("SlaPolicy").
Where("conversation_id = ?", conversationID).
First(&applied).Error; err != nil {
return nil
}
return map[string]any{
"id": applied.ID,
"sla_id": applied.SlaPolicyID,
"sla_status": applied.SLAStatus,
"created_at": applied.CreatedAt.Unix(),
"updated_at": applied.UpdatedAt.Unix(),
"sla_description": applied.SlaPolicy.Description,
"sla_name": applied.SlaPolicy.Name,
"sla_first_response_time_threshold": applied.SlaPolicy.FirstResponseTimeThreshold,
"sla_next_response_time_threshold": applied.SlaPolicy.NextResponseTimeThreshold,
"sla_only_during_business_hours": applied.SlaPolicy.OnlyDuringBusinessHours,
"sla_resolution_time_threshold": applied.SlaPolicy.ResolutionTimeThreshold,
}
}
func serializeConversationMeta(ctx context.Context, db *gorm.DB, conversation *model.Conversation) chatwootConversationMeta {