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
CallbackMessage (chat_callback.go:89)
└─ chat_callback.go:305
└─ h.svc.GetRotator(ctx, device.StoreId, instance) → returnid_cs
└─ internal/service/chat_service.go:36ChatListHandler di-construct dengan service yang di-inject (routes.go:15-20):
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:
type ChatService interface {
GetRotator(ctx context.Context, storeID string, instanceID string) (string, error)
}GetRotator orchestration (chat_service.go:36-81)
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.
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".
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 selectBySameModel types
models/mongo_settings.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:
type Distribution struct {
Name string
ID string
Total int // bobot
}Error cases
| Error | Kondisi | Lokasi |
|---|---|---|
ErrSettingsNotFound | Settings tidak ditemukan untuk store | chat_service.go:27, 87-93 |
ErrEmptyTeam | Team kosong / tidak ada CS terdaftar | chat_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
- Cara broadcast mengirim? Baca Broadcast Scheduler.
- Alur lengkap pesan? Baca Message Flow.