package service import ( "errors" "fmt" "strconv" "strings" "time" "homework-manager/internal/models" "homework-manager/internal/repository" ) var ( ErrRecurringAssignmentNotFound = errors.New("recurring assignment not found") ErrRecurringUnauthorized = errors.New("unauthorized") ErrInvalidRecurrenceType = errors.New("invalid recurrence type") ErrInvalidEndType = errors.New("invalid end type") ) type RecurringAssignmentService struct { recurringRepo *repository.RecurringAssignmentRepository assignmentRepo *repository.AssignmentRepository } func NewRecurringAssignmentService() *RecurringAssignmentService { return &RecurringAssignmentService{ recurringRepo: repository.NewRecurringAssignmentRepository(), assignmentRepo: repository.NewAssignmentRepository(), } } type CreateRecurringAssignmentInput struct { Title string Description string Subject string Priority string RecurrenceType string RecurrenceInterval int RecurrenceWeekday *int RecurrenceDay *int DueTime string EndType string EndCount *int EndDate *time.Time EditBehavior string ReminderEnabled bool ReminderOffset *int UrgentReminderEnabled bool FirstDueDate time.Time } func (s *RecurringAssignmentService) Create(userID uint, input CreateRecurringAssignmentInput) (*models.RecurringAssignment, error) { if !isValidRecurrenceType(input.RecurrenceType) { return nil, ErrInvalidRecurrenceType } if !isValidEndType(input.EndType) { return nil, ErrInvalidEndType } if input.RecurrenceInterval < 1 { input.RecurrenceInterval = 1 } if input.EditBehavior == "" { input.EditBehavior = models.EditBehaviorThisOnly } recurring := &models.RecurringAssignment{ UserID: userID, Title: input.Title, Description: input.Description, Subject: input.Subject, Priority: input.Priority, RecurrenceType: input.RecurrenceType, RecurrenceInterval: input.RecurrenceInterval, RecurrenceWeekday: input.RecurrenceWeekday, RecurrenceDay: input.RecurrenceDay, DueTime: input.DueTime, EndType: input.EndType, EndCount: input.EndCount, EndDate: input.EndDate, EditBehavior: input.EditBehavior, ReminderEnabled: input.ReminderEnabled, ReminderOffset: input.ReminderOffset, UrgentReminderEnabled: input.UrgentReminderEnabled, IsActive: true, GeneratedCount: 0, } if err := s.recurringRepo.Create(recurring); err != nil { return nil, err } if err := s.generateAssignment(recurring, input.FirstDueDate); err != nil { return nil, err } return recurring, nil } func (s *RecurringAssignmentService) GetByID(userID, recurringID uint) (*models.RecurringAssignment, error) { recurring, err := s.recurringRepo.FindByID(recurringID) if err != nil { return nil, ErrRecurringAssignmentNotFound } if recurring.UserID != userID { return nil, ErrRecurringUnauthorized } return recurring, nil } func (s *RecurringAssignmentService) GetAllByUser(userID uint) ([]models.RecurringAssignment, error) { return s.recurringRepo.FindByUserID(userID) } func (s *RecurringAssignmentService) GetActiveByUser(userID uint) ([]models.RecurringAssignment, error) { return s.recurringRepo.FindActiveByUserID(userID) } type UpdateRecurringInput struct { Title *string Description *string Subject *string Priority *string RecurrenceType *string RecurrenceInterval *int RecurrenceWeekday *int RecurrenceDay *int DueTime *string EndType *string EndCount *int EndDate *time.Time EditBehavior string ReminderEnabled *bool ReminderOffset *int UrgentReminderEnabled *bool } func (s *RecurringAssignmentService) Update(userID, recurringID uint, input UpdateRecurringInput) (*models.RecurringAssignment, error) { recurring, err := s.GetByID(userID, recurringID) if err != nil { return nil, err } if input.Title != nil { recurring.Title = *input.Title } if input.Description != nil { recurring.Description = *input.Description } if input.Subject != nil { recurring.Subject = *input.Subject } if input.Priority != nil { recurring.Priority = *input.Priority } if input.DueTime != nil { recurring.DueTime = *input.DueTime } if input.EditBehavior != "" { recurring.EditBehavior = input.EditBehavior } if input.ReminderEnabled != nil { recurring.ReminderEnabled = *input.ReminderEnabled } if input.ReminderOffset != nil { recurring.ReminderOffset = input.ReminderOffset } if input.UrgentReminderEnabled != nil { recurring.UrgentReminderEnabled = *input.UrgentReminderEnabled } if input.RecurrenceType != nil && *input.RecurrenceType != "" && isValidRecurrenceType(*input.RecurrenceType) { recurring.RecurrenceType = *input.RecurrenceType } if input.RecurrenceInterval != nil && *input.RecurrenceInterval > 0 { recurring.RecurrenceInterval = *input.RecurrenceInterval } if input.RecurrenceWeekday != nil { recurring.RecurrenceWeekday = input.RecurrenceWeekday } if input.RecurrenceDay != nil { recurring.RecurrenceDay = input.RecurrenceDay } if input.EndType != nil && isValidEndType(*input.EndType) { recurring.EndType = *input.EndType } if input.EndCount != nil { recurring.EndCount = input.EndCount } if input.EndDate != nil { recurring.EndDate = input.EndDate } if err := s.recurringRepo.Update(recurring); err != nil { return nil, err } return recurring, nil } func (s *RecurringAssignmentService) SetActive(userID, recurringID uint, isActive bool) error { recurring, err := s.GetByID(userID, recurringID) if err != nil { return err } recurring.IsActive = isActive return s.recurringRepo.Update(recurring) } func (s *RecurringAssignmentService) UpdateAssignmentWithBehavior( userID uint, assignment *models.Assignment, title, description, subject, priority string, dueDate time.Time, reminderEnabled bool, reminderAt *time.Time, urgentReminderEnabled bool, editBehavior string, ) error { if assignment.RecurringAssignmentID == nil { return s.updateSingleAssignment(assignment, title, description, subject, priority, dueDate, reminderEnabled, reminderAt, urgentReminderEnabled) } recurring, err := s.GetByID(userID, *assignment.RecurringAssignmentID) if err != nil { return s.updateSingleAssignment(assignment, title, description, subject, priority, dueDate, reminderEnabled, reminderAt, urgentReminderEnabled) } switch editBehavior { case models.EditBehaviorThisOnly: return s.updateSingleAssignment(assignment, title, description, subject, priority, dueDate, reminderEnabled, reminderAt, urgentReminderEnabled) case models.EditBehaviorThisAndFuture: if err := s.updateSingleAssignment(assignment, title, description, subject, priority, dueDate, reminderEnabled, reminderAt, urgentReminderEnabled); err != nil { return err } recurring.Title = title recurring.Description = description recurring.Subject = subject recurring.Priority = priority recurring.UrgentReminderEnabled = urgentReminderEnabled if err := s.recurringRepo.Update(recurring); err != nil { return err } return s.updateFutureAssignments(recurring.ID, assignment.DueDate, title, description, subject, priority, urgentReminderEnabled) case models.EditBehaviorAll: recurring.Title = title recurring.Description = description recurring.Subject = subject recurring.Priority = priority recurring.UrgentReminderEnabled = urgentReminderEnabled if err := s.recurringRepo.Update(recurring); err != nil { return err } return s.updateAllPendingAssignments(recurring.ID, title, description, subject, priority, urgentReminderEnabled) default: return s.updateSingleAssignment(assignment, title, description, subject, priority, dueDate, reminderEnabled, reminderAt, urgentReminderEnabled) } } func (s *RecurringAssignmentService) updateSingleAssignment( assignment *models.Assignment, title, description, subject, priority string, dueDate time.Time, reminderEnabled bool, reminderAt *time.Time, urgentReminderEnabled bool, ) error { assignment.Title = title assignment.Description = description assignment.Subject = subject assignment.Priority = priority assignment.DueDate = dueDate assignment.ReminderEnabled = reminderEnabled assignment.ReminderAt = reminderAt assignment.UrgentReminderEnabled = urgentReminderEnabled return s.assignmentRepo.Update(assignment) } func (s *RecurringAssignmentService) updateFutureAssignments( recurringID uint, fromDate time.Time, title, description, subject, priority string, urgentReminderEnabled bool, ) error { assignments, err := s.recurringRepo.GetFutureAssignmentsByRecurringID(recurringID, fromDate) if err != nil { return err } for _, a := range assignments { if a.IsCompleted { continue } a.Title = title a.Description = description a.Subject = subject a.Priority = priority a.UrgentReminderEnabled = urgentReminderEnabled if err := s.assignmentRepo.Update(&a); err != nil { return err } } return nil } func (s *RecurringAssignmentService) updateAllPendingAssignments( recurringID uint, title, description, subject, priority string, urgentReminderEnabled bool, ) error { assignments, err := s.recurringRepo.GetAssignmentsByRecurringID(recurringID) if err != nil { return err } for _, a := range assignments { if a.IsCompleted { continue } a.Title = title a.Description = description a.Subject = subject a.Priority = priority a.UrgentReminderEnabled = urgentReminderEnabled if err := s.assignmentRepo.Update(&a); err != nil { return err } } return nil } func (s *RecurringAssignmentService) Delete(userID, recurringID uint, deleteFutureAssignments bool) error { recurring, err := s.GetByID(userID, recurringID) if err != nil { return err } if deleteFutureAssignments { assignments, err := s.recurringRepo.GetFutureAssignmentsByRecurringID(recurringID, time.Now()) if err != nil { return err } for _, a := range assignments { if !a.IsCompleted { s.assignmentRepo.Delete(a.ID) } } } return s.recurringRepo.Delete(recurring.ID) } func (s *RecurringAssignmentService) GenerateNextAssignments() error { recurrings, err := s.recurringRepo.FindDueForGeneration() if err != nil { return err } for _, recurring := range recurrings { pendingCount, err := s.recurringRepo.CountPendingByRecurringID(recurring.ID) if err != nil { continue } if pendingCount == 0 { latest, err := s.recurringRepo.GetLatestAssignmentByRecurringID(recurring.ID) if err != nil { continue } var nextDueDate time.Time if latest != nil { nextDueDate = recurring.CalculateNextDueDate(latest.DueDate) } else { nextDueDate = time.Now() } if nextDueDate.After(time.Now()) { s.generateAssignment(&recurring, nextDueDate) } } } return nil } func (s *RecurringAssignmentService) generateAssignment(recurring *models.RecurringAssignment, dueDate time.Time) error { if recurring.DueTime != "" { parts := strings.Split(recurring.DueTime, ":") if len(parts) == 2 { hour, _ := strconv.Atoi(parts[0]) minute, _ := strconv.Atoi(parts[1]) dueDate = time.Date(dueDate.Year(), dueDate.Month(), dueDate.Day(), hour, minute, 0, 0, dueDate.Location()) } } var reminderAt *time.Time if recurring.ReminderEnabled && recurring.ReminderOffset != nil { t := dueDate.Add(-time.Duration(*recurring.ReminderOffset) * time.Minute) reminderAt = &t } assignment := &models.Assignment{ UserID: userID(recurring.UserID), Title: recurring.Title, Description: recurring.Description, Subject: recurring.Subject, Priority: recurring.Priority, DueDate: dueDate, ReminderEnabled: recurring.ReminderEnabled, ReminderAt: reminderAt, UrgentReminderEnabled: recurring.UrgentReminderEnabled, RecurringAssignmentID: &recurring.ID, } if err := s.assignmentRepo.Create(assignment); err != nil { return err } recurring.GeneratedCount++ return s.recurringRepo.Update(recurring) } func userID(id uint) uint { return id } func isValidRecurrenceType(t string) bool { switch t { case models.RecurrenceNone, models.RecurrenceDaily, models.RecurrenceWeekly, models.RecurrenceMonthly: return true } return false } func isValidEndType(t string) bool { switch t { case models.EndTypeNever, models.EndTypeCount, models.EndTypeDate: return true } return false } func GetRecurrenceTypeLabel(t string) string { switch t { case models.RecurrenceDaily: return "毎日" case models.RecurrenceWeekly: return "毎週" case models.RecurrenceMonthly: return "毎月" default: return "なし" } } func GetEndTypeLabel(t string) string { switch t { case models.EndTypeCount: return "回数指定" case models.EndTypeDate: return "終了日指定" default: return "無期限" } } func FormatRecurringSummary(recurring *models.RecurringAssignment) string { if recurring.RecurrenceType == models.RecurrenceNone { return "" } var parts []string typeLabel := GetRecurrenceTypeLabel(recurring.RecurrenceType) if recurring.RecurrenceInterval > 1 { switch recurring.RecurrenceType { case models.RecurrenceDaily: parts = append(parts, fmt.Sprintf("%d日ごと", recurring.RecurrenceInterval)) case models.RecurrenceWeekly: parts = append(parts, fmt.Sprintf("%d週間ごと", recurring.RecurrenceInterval)) case models.RecurrenceMonthly: parts = append(parts, fmt.Sprintf("%dヶ月ごと", recurring.RecurrenceInterval)) } } else { parts = append(parts, typeLabel) } if recurring.RecurrenceType == models.RecurrenceWeekly && recurring.RecurrenceWeekday != nil { weekdays := []string{"日", "月", "火", "水", "木", "金", "土"} if *recurring.RecurrenceWeekday >= 0 && *recurring.RecurrenceWeekday < 7 { parts = append(parts, fmt.Sprintf("(%s曜日)", weekdays[*recurring.RecurrenceWeekday])) } } if recurring.RecurrenceType == models.RecurrenceMonthly && recurring.RecurrenceDay != nil { parts = append(parts, fmt.Sprintf("(%d日)", *recurring.RecurrenceDay)) } switch recurring.EndType { case models.EndTypeCount: if recurring.EndCount != nil { parts = append(parts, fmt.Sprintf("/ %d回まで", *recurring.EndCount)) } case models.EndTypeDate: if recurring.EndDate != nil { parts = append(parts, fmt.Sprintf("/ %sまで", recurring.EndDate.Format("2006/01/02"))) } } return strings.Join(parts, " ") }