D
Engineering

CS Rotator

CS assignment saat chat masuk — dua strategi: least-loaded (selectBySame) dan weighted (selectByPercentage). Service di internal/service/chat_service.go, dipanggil dari CallbackMessage.

Saat pesan masuk via callback_message, Go API menentukan Customer Service mana yang menangani. Logika ada di internal/service/chat_service.go — satu-satunya “island” clean-architecture di codebase.

Posisi dalam alur

text
CallbackMessage (chat_callback.go:89)
    └─ chat_callback.go:305
       └─ h.svc.GetRotator(ctx, device.StoreId, instance) → returnid_cs
          └─ internal/service/chat_service.go:36

ChatListHandler di-construct dengan service yang di-inject (routes.go:15-20):

go
chatListRepo    := repository.NewChatListRepository(db)
userRepo        := repository.NewUserRepository(db)
chatListService := service.NewChatListService(chatListRepo, userRepo)
chatListHandler := handlers.NewChatListHandler(chatListService)

Service — ChatService

internal/service/chat_service.go:17:

go
type ChatService interface {
    GetRotator(ctx context.Context, storeID string, instanceID string) (string, error)
}

GetRotator orchestration (chat_service.go:36-81)

text
1. fetchRotatorSettings(ctx, storeID)        — chat_service.go:40
   └─ read settings collection, decode SettingsStruct
   └─ error: ErrSettingsNotFound jika tidak ada

2. validateRotatorSettings(settings)         — chat_service.go:46
   └─ error: ErrEmptyTeam jika Team kosong (chat_service.go:160-165)

3. fetchActiveCountPerCS(ctx, storeID, team, instance)  — chat_service.go:51
   └─ aggregate chat_histories:
      $match store_id, team_id $in team, chat_status $ne "close", instance
      $group by team_id, count
   └─ return map[csID]int (init semua team ke 0)

4. switch settings.TypeDistribution:
   ├─ "percentage" → selectByPercentage(settings, activeCounts)
   └─ default      → selectBySame(settings.Team, activeCounts)

Strategi 1 — Least-loaded (selectBySame)

chat_service.go:167-188 — strategi default.

go
func selectBySame(team []string, activeCounts map[string]int) (string, error) {
    if len(team) == 0 {
        return "", ErrEmptyTeam
    }
    selected := team[0]
    min := activeCounts[selected]
    for _, csID := range team[1:] {
        if activeCounts[csID] < min {
            selected = csID
            min = activeCounts[csID]
        }
    }
    return selected, nil
}

Pilih CS dengan jumlah chat aktif paling sedikit. Tie-break: CS pertama ditemukan menang (stabil).

Strategi 2 — Weighted (selectByPercentage)

chat_service.go:190-247 — strategi TypeDistribution == "percentage".

text
1. Sum dist.Total across settings.Distribution → totalBobot
   └─ jika 0: fall back ke selectBySame (TIDAK return ErrInvalidBobot)

2. Build distByID map: Distribution.ID → Distribution

3. For each CS in team:
   ├─ skip jika tidak ada distribution atau dist.Total == 0
   ├─ normalizedWeight = dist.Total / totalBobot
   ├─ score = activeCounts[csID] / normalizedWeight
   │          (rasio beban saat ini vs alokasi)
   └─ pilih score terendah
   └─ tie-break: prefer normalizedWeight lebih tinggi

4. Jika semua skip → fall back ke selectBySame

Model types

models/mongo_settings.go:

go
type GlobalAssignationStruct struct {
    Role             string
    Team             []string       // daftar CS ID
    Distribution     []Distribution // bobot per CS (untuk percentage)
    TypeDistribution string         // "percentage" atau lainnya → selectBySame
}

type SettingsStruct struct {
    // ...
    GlobalAssignation GlobalAssignationStruct `bson:"globalAssignation"`
}

models/mongo_product.go:20:

go
type Distribution struct {
    Name  string
    ID    string
    Total int    // bobot
}

Error cases

ErrorKondisiLokasi
ErrSettingsNotFoundSettings tidak ditemukan untuk storechat_service.go:27, 87-93
ErrEmptyTeamTeam kosong / tidak ada CS terdaftarchat_service.go:28, 160-165
ErrInvalidBobot(dideklarasi, tidak pernah return)chat_service.go:29

Known issue — repos di-inject tapi tidak dipakai

Split-brain DB name

Service memakai config.Env.DbName (chat_service.go:84,112), sementara handler memakai const DBname = "dazo" (handlers/environtment.go:6). Jika .env DB_NAME diubah, service rotator bisa baca DB berbeda dari handler. Lihat Database dan Tech Debt.

Langkah berikutnya