diff --git a/internal/services/agent_team_schedule_service_test.go b/internal/services/agent_team_schedule_service_test.go index 2ef117d..35f7989 100644 --- a/internal/services/agent_team_schedule_service_test.go +++ b/internal/services/agent_team_schedule_service_test.go @@ -8,6 +8,7 @@ import ( "cs-agent/internal/models" "cs-agent/internal/pkg/dto" "cs-agent/internal/pkg/dto/request" + "cs-agent/internal/pkg/dto/response" "cs-agent/internal/pkg/enums" "cs-agent/internal/services" @@ -175,6 +176,274 @@ func TestAgentTeamScheduleServiceUpdateRejectsHistoricalScheduleByDay(t *testing } } +func TestAgentTeamScheduleServiceBatchPreviewExpandsSharedRule(t *testing.T) { + setupAgentTeamScheduleTestDB(t) + createAgentTeamScheduleTestTeams(t, sqls.DB()) + nextMonday := nextTestWeekday(time.Monday) + nextWednesday := nextMonday.AddDate(0, 0, 2) + + preview, err := services.AgentTeamScheduleService.BatchPreview(request.AgentTeamScheduleBatchRequest{ + TeamIDs: []int64{1, 2}, + StartDate: nextMonday.Format(time.DateOnly), + EndDate: nextMonday.AddDate(0, 0, 6).Format(time.DateOnly), + Weekdays: []int{1, 3}, + StartTime: "09:00", + EndTime: "18:00", + Remark: "工作日白班", + }, testOperator()) + if err != nil { + t.Fatalf("BatchPreview() error = %v", err) + } + if preview.Total != 4 || len(preview.Items) != 4 { + t.Fatalf("expected 4 preview items, got total=%d len=%d", preview.Total, len(preview.Items)) + } + if preview.Conflict { + t.Fatalf("expected no conflict, got %+v", preview.Items) + } + teamNames := make(map[int64]string) + for _, item := range preview.Items { + if item.TeamName == "" { + t.Fatalf("expected all preview items to have team names: %+v", preview.Items) + } + teamNames[item.TeamID] = item.TeamName + } + if teamNames[1] == "" || teamNames[2] == "" { + t.Fatalf("expected team ids 1 and 2 with names, got %v", teamNames) + } + type previewKey struct { + teamID int64 + date string + } + itemsByKey := make(map[previewKey]response.AgentTeamScheduleBatchPreviewItem) + for _, item := range preview.Items { + itemsByKey[previewKey{teamID: item.TeamID, date: item.Date}] = item + } + expected := []struct { + teamID int64 + date time.Time + weekday int + }{ + {teamID: 1, date: nextMonday, weekday: 1}, + {teamID: 1, date: nextWednesday, weekday: 3}, + {teamID: 2, date: nextMonday, weekday: 1}, + {teamID: 2, date: nextWednesday, weekday: 3}, + } + for _, want := range expected { + wantDate := want.date.Format(time.DateOnly) + item, ok := itemsByKey[previewKey{teamID: want.teamID, date: wantDate}] + if !ok { + t.Fatalf("expected preview item for teamID=%d date=%s, got %+v", want.teamID, wantDate, preview.Items) + } + if item.Weekday != want.weekday || + item.StartAt != formatTestDateTime(want.date, "09:00:00") || + item.EndAt != formatTestDateTime(want.date, "18:00:00") || + item.Remark != "工作日白班" { + t.Fatalf("unexpected preview item for teamID=%d date=%s: %+v", want.teamID, wantDate, item) + } + } +} + +func TestAgentTeamScheduleServiceBatchPreviewRejectsHistoricalDate(t *testing.T) { + setupAgentTeamScheduleTestDB(t) + createAgentTeamScheduleTestTeams(t, sqls.DB()) + yesterday := time.Now().AddDate(0, 0, -1) + + _, err := services.AgentTeamScheduleService.BatchPreview(request.AgentTeamScheduleBatchRequest{ + TeamIDs: []int64{1}, + StartDate: yesterday.Format(time.DateOnly), + EndDate: yesterday.Format(time.DateOnly), + Weekdays: []int{weekdayForRequest(yesterday)}, + StartTime: "09:00", + EndTime: "18:00", + }, testOperator()) + if err == nil { + t.Fatalf("expected historical batch preview to fail") + } + if !strings.Contains(err.Error(), "历史日期") { + t.Fatalf("expected historical date error, got %v", err) + } +} + +func TestAgentTeamScheduleServiceBatchPreviewRejectsInvalidTimeRange(t *testing.T) { + setupAgentTeamScheduleTestDB(t) + createAgentTeamScheduleTestTeams(t, sqls.DB()) + tomorrow := time.Now().AddDate(0, 0, 1) + + _, err := services.AgentTeamScheduleService.BatchPreview(request.AgentTeamScheduleBatchRequest{ + TeamIDs: []int64{1}, + StartDate: tomorrow.Format(time.DateOnly), + EndDate: tomorrow.Format(time.DateOnly), + Weekdays: []int{weekdayForRequest(tomorrow)}, + StartTime: "18:00", + EndTime: "09:00", + }, testOperator()) + if err == nil { + t.Fatalf("expected invalid time range to fail") + } + if !strings.Contains(err.Error(), "结束时间必须晚于开始时间") { + t.Fatalf("expected invalid time range error, got %v", err) + } +} + +func TestAgentTeamScheduleServiceBatchPreviewRejectsOverLimit(t *testing.T) { + setupAgentTeamScheduleTestDB(t) + createAgentTeamScheduleTestTeams(t, sqls.DB()) + today := time.Now() + + _, err := services.AgentTeamScheduleService.BatchPreview(request.AgentTeamScheduleBatchRequest{ + TeamIDs: []int64{1, 2}, + StartDate: today.Format(time.DateOnly), + EndDate: today.AddDate(0, 0, 260).Format(time.DateOnly), + Weekdays: []int{1, 2, 3, 4, 5, 6, 7}, + StartTime: "09:00", + EndTime: "18:00", + }, testOperator()) + if err == nil { + t.Fatalf("expected over-limit preview to fail") + } + if !strings.Contains(err.Error(), "500") { + t.Fatalf("expected 500 limit error, got %v", err) + } +} + +func TestAgentTeamScheduleServiceBatchPreviewMarksConflicts(t *testing.T) { + db := setupAgentTeamScheduleTestDB(t) + createAgentTeamScheduleTestTeams(t, db) + targetDay := time.Now().AddDate(0, 0, 2) + existing := models.AgentTeamSchedule{ + TeamID: 1, + StartAt: parseTestDateTime(t, formatTestDateTime(targetDay, "10:00:00")), + EndAt: parseTestDateTime(t, formatTestDateTime(targetDay, "12:00:00")), + Status: enums.StatusOk, + } + if err := db.Create(&existing).Error; err != nil { + t.Fatalf("create existing schedule error = %v", err) + } + + preview, err := services.AgentTeamScheduleService.BatchPreview(request.AgentTeamScheduleBatchRequest{ + TeamIDs: []int64{1, 2}, + StartDate: targetDay.Format(time.DateOnly), + EndDate: targetDay.Format(time.DateOnly), + Weekdays: []int{weekdayForRequest(targetDay)}, + StartTime: "09:00", + EndTime: "18:00", + }, testOperator()) + if err != nil { + t.Fatalf("BatchPreview() error = %v", err) + } + if !preview.Conflict { + t.Fatalf("expected preview conflict, got %+v", preview) + } + itemsByTeamID := make(map[int64]response.AgentTeamScheduleBatchPreviewItem) + for _, item := range preview.Items { + itemsByTeamID[item.TeamID] = item + } + team1Item, ok := itemsByTeamID[1] + if !ok { + t.Fatalf("expected team 1 preview item, got %+v", preview.Items) + } + team2Item, ok := itemsByTeamID[2] + if !ok { + t.Fatalf("expected team 2 preview item, got %+v", preview.Items) + } + if !team1Item.Conflict || team1Item.ConflictReason == "" { + t.Fatalf("expected team 1 preview item to be marked as conflict: %+v", team1Item) + } + if team2Item.Conflict { + t.Fatalf("expected team 2 preview item to have no conflict: %+v", team2Item) + } +} + +func TestAgentTeamScheduleServiceBatchGenerateCreatesAllSchedules(t *testing.T) { + db := setupAgentTeamScheduleTestDB(t) + createAgentTeamScheduleTestTeams(t, db) + nextMonday := nextTestWeekday(time.Monday) + + result, err := services.AgentTeamScheduleService.BatchGenerate(request.AgentTeamScheduleBatchRequest{ + TeamIDs: []int64{1, 2}, + StartDate: nextMonday.Format(time.DateOnly), + EndDate: nextMonday.AddDate(0, 0, 2).Format(time.DateOnly), + Weekdays: []int{1, 3}, + StartTime: "09:00", + EndTime: "18:00", + Remark: "批量生成", + }, testOperator()) + if err != nil { + t.Fatalf("BatchGenerate() error = %v", err) + } + if result.Created != 4 { + t.Fatalf("expected 4 created schedules, got %d", result.Created) + } + var schedules []models.AgentTeamSchedule + if err := db.Where("remark = ?", "批量生成"). + Order("team_id ASC, start_at ASC"). + Find(&schedules).Error; err != nil { + t.Fatalf("query generated schedules error = %v", err) + } + if len(schedules) != 4 { + t.Fatalf("expected 4 stored schedules, got %d: %+v", len(schedules), schedules) + } + expected := []struct { + teamID int64 + startAt string + endAt string + }{ + {teamID: 1, startAt: formatTestDateTime(nextMonday, "09:00:00"), endAt: formatTestDateTime(nextMonday, "18:00:00")}, + {teamID: 1, startAt: formatTestDateTime(nextMonday.AddDate(0, 0, 2), "09:00:00"), endAt: formatTestDateTime(nextMonday.AddDate(0, 0, 2), "18:00:00")}, + {teamID: 2, startAt: formatTestDateTime(nextMonday, "09:00:00"), endAt: formatTestDateTime(nextMonday, "18:00:00")}, + {teamID: 2, startAt: formatTestDateTime(nextMonday.AddDate(0, 0, 2), "09:00:00"), endAt: formatTestDateTime(nextMonday.AddDate(0, 0, 2), "18:00:00")}, + } + for i, want := range expected { + got := schedules[i] + if got.TeamID != want.teamID || + got.StartAt.Format(time.DateTime) != want.startAt || + got.EndAt.Format(time.DateTime) != want.endAt { + t.Fatalf("unexpected schedule at index %d: got teamID=%d startAt=%s endAt=%s, want teamID=%d startAt=%s endAt=%s", + i, + got.TeamID, + got.StartAt.Format(time.DateTime), + got.EndAt.Format(time.DateTime), + want.teamID, + want.startAt, + want.endAt, + ) + } + } +} + +func TestAgentTeamScheduleServiceBatchGenerateRejectsConflictsWithoutPartialCreate(t *testing.T) { + db := setupAgentTeamScheduleTestDB(t) + createAgentTeamScheduleTestTeams(t, db) + targetDay := time.Now().AddDate(0, 0, 2) + existing := models.AgentTeamSchedule{ + TeamID: 1, + StartAt: parseTestDateTime(t, formatTestDateTime(targetDay, "10:00:00")), + EndAt: parseTestDateTime(t, formatTestDateTime(targetDay, "12:00:00")), + Status: enums.StatusOk, + } + if err := db.Create(&existing).Error; err != nil { + t.Fatalf("create existing schedule error = %v", err) + } + + _, err := services.AgentTeamScheduleService.BatchGenerate(request.AgentTeamScheduleBatchRequest{ + TeamIDs: []int64{1, 2}, + StartDate: targetDay.Format(time.DateOnly), + EndDate: targetDay.Format(time.DateOnly), + Weekdays: []int{weekdayForRequest(targetDay)}, + StartTime: "09:00", + EndTime: "18:00", + Remark: "不应创建", + }, testOperator()) + if err == nil { + t.Fatalf("expected conflict batch generate to fail") + } + var count int64 + db.Model(&models.AgentTeamSchedule{}).Where("remark = ?", "不应创建").Count(&count) + if count != 0 { + t.Fatalf("expected no partial creates, got %d", count) + } +} + func setupAgentTeamScheduleTestDB(t *testing.T) *gorm.DB { t.Helper() @@ -241,6 +510,26 @@ func formatTestDateTime(date time.Time, clock string) string { return date.Format(time.DateOnly) + " " + clock } +func nextTestWeekday(target time.Weekday) time.Time { + ret := startOfTestDay(time.Now()).AddDate(0, 0, 1) + for ret.Weekday() != target { + ret = ret.AddDate(0, 0, 1) + } + return ret +} + +func startOfTestDay(value time.Time) time.Time { + year, month, day := value.In(time.Local).Date() + return time.Date(year, month, day, 0, 0, 0, 0, time.Local) +} + +func weekdayForRequest(value time.Time) int { + if value.Weekday() == time.Sunday { + return 7 + } + return int(value.Weekday()) +} + func createFutureAgentTeamSchedule(t *testing.T, db *gorm.DB) int64 { t.Helper() tomorrow := time.Now().AddDate(0, 0, 1)