249 lines
14 KiB
Go
249 lines
14 KiB
Go
package autoassignment
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"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"
|
|
)
|
|
|
|
func setupCapacityAssignmentDB(t *testing.T) *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.User{},
|
|
&model.AccountUser{},
|
|
&model.Inbox{},
|
|
&model.InboxMember{},
|
|
&model.Contact{},
|
|
&model.Conversation{},
|
|
&model.AssignmentPolicy{},
|
|
&model.InboxAssignmentPolicy{},
|
|
&model.AgentCapacityPolicy{},
|
|
&model.InboxCapacityLimit{},
|
|
&model.Tag{},
|
|
&model.ConversationLabel{},
|
|
))
|
|
return db
|
|
}
|
|
|
|
func TestAssignmentService_SkipsAgentsAtInboxCapacity(t *testing.T) {
|
|
_, rdb := setupTestRedis(t)
|
|
db := setupCapacityAssignmentDB(t)
|
|
svc := NewAssignmentService(db, rdb)
|
|
ctx := context.Background()
|
|
|
|
account := &model.Account{Name: "Capacity Account", FeatureFlags: `{"advanced_assignment":true}`}
|
|
require.NoError(t, db.Create(account).Error)
|
|
inbox := &model.Inbox{AccountID: account.ID, Name: "Capacity Inbox", ChannelType: string(model.InboxChannelTypeWebWidget), EnableAutoAssignment: true}
|
|
require.NoError(t, db.Create(inbox).Error)
|
|
contact := &model.Contact{AccountID: account.ID, Name: "Capacity Contact"}
|
|
require.NoError(t, db.Create(contact).Error)
|
|
|
|
agentAtCapacity := createCapacityUser(t, db, account.ID, "full@test.com")
|
|
agentWithCapacity := createCapacityUser(t, db, account.ID, "available@test.com")
|
|
require.NoError(t, db.Create(&model.InboxMember{InboxID: inbox.ID, UserID: agentAtCapacity.ID}).Error)
|
|
require.NoError(t, db.Create(&model.InboxMember{InboxID: inbox.ID, UserID: agentWithCapacity.ID}).Error)
|
|
|
|
policy := &model.AgentCapacityPolicy{AccountID: account.ID, Name: "Capacity", AssignmentLogic: "round_robin", ExclusionRules: []byte(`{}`)}
|
|
require.NoError(t, db.Create(policy).Error)
|
|
require.NoError(t, db.Model(&model.AccountUser{}).
|
|
Where("account_id = ? AND user_id = ?", account.ID, agentAtCapacity.ID).
|
|
Update("agent_capacity_policy_id", policy.ID).Error)
|
|
require.NoError(t, db.Create(&model.InboxCapacityLimit{AgentCapacityPolicyID: policy.ID, InboxID: inbox.ID, ConversationLimit: 1}).Error)
|
|
|
|
now := time.Now().Unix()
|
|
existing := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, AssigneeID: &agentAtCapacity.ID, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget", LastActivityAt: &now}
|
|
unassigned := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget", LastActivityAt: &now}
|
|
require.NoError(t, db.Create(existing).Error)
|
|
require.NoError(t, db.Create(unassigned).Error)
|
|
|
|
agentID, err := svc.AssignConversation(ctx, unassigned.ID, inbox.ID, account.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, agentWithCapacity.ID, agentID)
|
|
}
|
|
|
|
func TestAssignmentServiceIgnoresCapacityWhenAdvancedAssignmentIsDisabled(t *testing.T) {
|
|
_, rdb := setupTestRedis(t)
|
|
db := setupCapacityAssignmentDB(t)
|
|
account := &model.Account{Name: "Default Assignment"}
|
|
require.NoError(t, db.Create(account).Error)
|
|
inbox := &model.Inbox{AccountID: account.ID, Name: "Inbox", ChannelType: string(model.InboxChannelTypeWebWidget), EnableAutoAssignment: true}
|
|
require.NoError(t, db.Create(inbox).Error)
|
|
agent := createCapacityUser(t, db, account.ID, "default@test.com")
|
|
require.NoError(t, db.Create(&model.InboxMember{InboxID: inbox.ID, UserID: agent.ID}).Error)
|
|
policy := &model.AgentCapacityPolicy{AccountID: account.ID, Name: "Excluded", ExclusionRules: []byte(`{"exclude_older_than_hours":1}`)}
|
|
require.NoError(t, db.Create(policy).Error)
|
|
require.NoError(t, db.Model(&model.AccountUser{}).Where("account_id = ? AND user_id = ?", account.ID, agent.ID).Update("agent_capacity_policy_id", policy.ID).Error)
|
|
require.NoError(t, db.Create(&model.InboxCapacityLimit{AgentCapacityPolicyID: policy.ID, InboxID: inbox.ID, ConversationLimit: 0}).Error)
|
|
contact := &model.Contact{AccountID: account.ID, Name: "Contact"}
|
|
require.NoError(t, db.Create(contact).Error)
|
|
old := time.Now().Add(-2 * time.Hour).Unix()
|
|
conversation := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen), LastActivityAt: &old}
|
|
require.NoError(t, db.Create(conversation).Error)
|
|
|
|
assigned, err := NewAssignmentService(db, rdb).AssignConversation(context.Background(), conversation.ID, inbox.ID, account.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, agent.ID, assigned)
|
|
}
|
|
|
|
func TestAssignmentServiceAppliesCapacityPolicyExclusions(t *testing.T) {
|
|
_, rdb := setupTestRedis(t)
|
|
db := setupCapacityAssignmentDB(t)
|
|
account := &model.Account{Name: "Exclusions", FeatureFlags: `{"advanced_assignment":true}`}
|
|
require.NoError(t, db.Create(account).Error)
|
|
inbox := &model.Inbox{AccountID: account.ID, Name: "Inbox", ChannelType: string(model.InboxChannelTypeWebWidget), EnableAutoAssignment: true}
|
|
require.NoError(t, db.Create(inbox).Error)
|
|
agent := createCapacityUser(t, db, account.ID, "exclusions@test.com")
|
|
require.NoError(t, db.Create(&model.InboxMember{InboxID: inbox.ID, UserID: agent.ID}).Error)
|
|
capacityPolicy := &model.AgentCapacityPolicy{
|
|
AccountID: account.ID,
|
|
Name: "Exclude stale spam",
|
|
ExclusionRules: []byte(`{"excluded_labels":["spam"],"exclude_older_than_hours":24}`),
|
|
}
|
|
require.NoError(t, db.Create(capacityPolicy).Error)
|
|
require.NoError(t, db.Model(&model.AccountUser{}).Where("account_id = ? AND user_id = ?", account.ID, agent.ID).Update("agent_capacity_policy_id", capacityPolicy.ID).Error)
|
|
require.NoError(t, db.Create(&model.InboxCapacityLimit{AgentCapacityPolicyID: capacityPolicy.ID, InboxID: inbox.ID, ConversationLimit: 10}).Error)
|
|
contact := &model.Contact{AccountID: account.ID, Name: "Contact"}
|
|
require.NoError(t, db.Create(contact).Error)
|
|
|
|
now := time.Now().Unix()
|
|
old := time.Now().Add(-25 * time.Hour).Unix()
|
|
recent := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen), LastActivityAt: &now}
|
|
labeled := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen), LastActivityAt: &now}
|
|
stale := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen), LastActivityAt: &old}
|
|
unknownAge := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen)}
|
|
require.NoError(t, db.Create([]*model.Conversation{recent, labeled, stale, unknownAge}).Error)
|
|
tag := &model.Tag{AccountID: account.ID, Name: "spam"}
|
|
require.NoError(t, db.Create(tag).Error)
|
|
require.NoError(t, db.Create(&model.ConversationLabel{AccountID: account.ID, ConversationID: labeled.ID, TagID: tag.ID}).Error)
|
|
|
|
assigned, err := NewAssignmentService(db, rdb).AssignUnassignedConversations(context.Background(), inbox.ID, account.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []uint{recent.ID}, assigned)
|
|
for _, excluded := range []*model.Conversation{labeled, stale, unknownAge} {
|
|
require.NoError(t, db.First(excluded, excluded.ID).Error)
|
|
require.Nil(t, excluded.AssigneeID)
|
|
}
|
|
}
|
|
|
|
func TestAssignmentServiceAppliesEachAgentsCapacityPolicyExclusions(t *testing.T) {
|
|
_, rdb := setupTestRedis(t)
|
|
db := setupCapacityAssignmentDB(t)
|
|
account := &model.Account{Name: "Per-agent exclusions", FeatureFlags: `{"advanced_assignment":true}`}
|
|
require.NoError(t, db.Create(account).Error)
|
|
inbox := &model.Inbox{AccountID: account.ID, Name: "Inbox", ChannelType: string(model.InboxChannelTypeWebWidget), EnableAutoAssignment: true}
|
|
require.NoError(t, db.Create(inbox).Error)
|
|
spamAgent := createCapacityUser(t, db, account.ID, "spam-policy@test.com")
|
|
vipAgent := createCapacityUser(t, db, account.ID, "vip-policy@test.com")
|
|
require.NoError(t, db.Create([]*model.InboxMember{{InboxID: inbox.ID, UserID: spamAgent.ID}, {InboxID: inbox.ID, UserID: vipAgent.ID}}).Error)
|
|
|
|
spamPolicy := &model.AgentCapacityPolicy{AccountID: account.ID, Name: "Exclude spam", ExclusionRules: []byte(`{"excluded_labels":["spam"]}`)}
|
|
vipPolicy := &model.AgentCapacityPolicy{AccountID: account.ID, Name: "Exclude vip", ExclusionRules: []byte(`{"excluded_labels":["vip"]}`)}
|
|
require.NoError(t, db.Create(spamPolicy).Error)
|
|
require.NoError(t, db.Create(vipPolicy).Error)
|
|
require.NoError(t, db.Model(&model.AccountUser{}).Where("account_id = ? AND user_id = ?", account.ID, spamAgent.ID).Update("agent_capacity_policy_id", spamPolicy.ID).Error)
|
|
require.NoError(t, db.Model(&model.AccountUser{}).Where("account_id = ? AND user_id = ?", account.ID, vipAgent.ID).Update("agent_capacity_policy_id", vipPolicy.ID).Error)
|
|
require.NoError(t, db.Create([]*model.InboxCapacityLimit{
|
|
{AgentCapacityPolicyID: spamPolicy.ID, InboxID: inbox.ID, ConversationLimit: 10},
|
|
{AgentCapacityPolicyID: vipPolicy.ID, InboxID: inbox.ID, ConversationLimit: 10},
|
|
}).Error)
|
|
|
|
contact := &model.Contact{AccountID: account.ID, Name: "Contact"}
|
|
require.NoError(t, db.Create(contact).Error)
|
|
now := time.Now().Unix()
|
|
spam := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen), LastActivityAt: &now}
|
|
vip := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen), LastActivityAt: &now}
|
|
require.NoError(t, db.Create([]*model.Conversation{spam, vip}).Error)
|
|
spamTag := &model.Tag{AccountID: account.ID, Name: "spam"}
|
|
vipTag := &model.Tag{AccountID: account.ID, Name: "vip"}
|
|
require.NoError(t, db.Create([]*model.Tag{spamTag, vipTag}).Error)
|
|
require.NoError(t, db.Create([]*model.ConversationLabel{
|
|
{AccountID: account.ID, ConversationID: spam.ID, TagID: spamTag.ID},
|
|
{AccountID: account.ID, ConversationID: vip.ID, TagID: vipTag.ID},
|
|
}).Error)
|
|
|
|
assigned, err := NewAssignmentService(db, rdb).AssignUnassignedConversations(context.Background(), inbox.ID, account.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []uint{spam.ID, vip.ID}, assigned)
|
|
require.NoError(t, db.First(spam, spam.ID).Error)
|
|
require.NoError(t, db.First(vip, vip.ID).Error)
|
|
require.Equal(t, vipAgent.ID, *spam.AssigneeID)
|
|
require.Equal(t, spamAgent.ID, *vip.AssigneeID)
|
|
}
|
|
|
|
func TestAssignmentEligibilityRejectsSoftDeletedUserAndAccountUser(t *testing.T) {
|
|
for _, deleted := range []string{"user", "account user"} {
|
|
t.Run(deleted, func(t *testing.T) {
|
|
db := setupCapacityAssignmentDB(t)
|
|
account := &model.Account{Name: deleted}
|
|
require.NoError(t, db.Create(account).Error)
|
|
inbox := &model.Inbox{AccountID: account.ID, Name: "Inbox", ChannelType: string(model.InboxChannelTypeWebWidget), EnableAutoAssignment: true}
|
|
require.NoError(t, db.Create(inbox).Error)
|
|
agent := createCapacityUser(t, db, account.ID, deleted+"@test.com")
|
|
require.NoError(t, db.Create(&model.InboxMember{InboxID: inbox.ID, UserID: agent.ID}).Error)
|
|
contact := &model.Contact{AccountID: account.ID, Name: "Contact"}
|
|
require.NoError(t, db.Create(contact).Error)
|
|
conversation := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen)}
|
|
require.NoError(t, db.Create(conversation).Error)
|
|
|
|
if deleted == "user" {
|
|
require.NoError(t, db.Delete(agent).Error)
|
|
} else {
|
|
require.NoError(t, db.Where("account_id = ? AND user_id = ?", account.ID, agent.ID).Delete(&model.AccountUser{}).Error)
|
|
}
|
|
|
|
agents, err := NewAssignmentService(db, nil).getEligibleAgents(context.Background(), inbox.ID, account.ID, nil, false)
|
|
require.NoError(t, err)
|
|
require.Empty(t, agents)
|
|
assigned, err := repository.AutoAssignConversation(context.Background(), db, account.ID, inbox.ID, conversation.ID, agent.ID)
|
|
require.NoError(t, err)
|
|
require.False(t, assigned)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLongestWaitingSortsUnknownActivityLast(t *testing.T) {
|
|
testLongestWaitingSortsUnknownActivityLast(t, setupCapacityAssignmentDB(t))
|
|
}
|
|
|
|
func testLongestWaitingSortsUnknownActivityLast(t *testing.T, db *gorm.DB) {
|
|
t.Helper()
|
|
account := &model.Account{Name: "Longest waiting"}
|
|
require.NoError(t, db.Create(account).Error)
|
|
inbox := &model.Inbox{AccountID: account.ID, Name: "Inbox", ChannelType: string(model.InboxChannelTypeWebWidget)}
|
|
require.NoError(t, db.Create(inbox).Error)
|
|
contact := &model.Contact{AccountID: account.ID, Name: "Contact"}
|
|
require.NoError(t, db.Create(contact).Error)
|
|
olderActivity := time.Now().Add(-2 * time.Hour).Unix()
|
|
newerActivity := time.Now().Add(-time.Hour).Unix()
|
|
unknown := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen)}
|
|
newer := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen), LastActivityAt: &newerActivity}
|
|
older := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen), LastActivityAt: &olderActivity}
|
|
require.NoError(t, db.Create([]*model.Conversation{unknown, newer, older}).Error)
|
|
|
|
conversations, err := NewAssignmentService(db, nil).findUnassignedConversations(
|
|
context.Background(), inbox.ID, account.ID, &model.AssignmentPolicy{ConversationPriority: 1},
|
|
)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []uint{older.ID, newer.ID, unknown.ID}, []uint{conversations[0].ID, conversations[1].ID, conversations[2].ID})
|
|
}
|
|
|
|
func createCapacityUser(t *testing.T, db *gorm.DB, accountID uint, email string) *model.User {
|
|
t.Helper()
|
|
user := &model.User{AccountID: accountID, Name: email, Email: email, Password: "hashed", Active: true, Available: true}
|
|
require.NoError(t, db.Create(user).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{AccountID: accountID, UserID: user.ID, Role: "agent", Availability: "online"}).Error)
|
|
return user
|
|
}
|