package services import ( "context" "errors" "time" "github.com/rs/zerolog/log" "gorm.io/gorm" "github.com/treytartt/casera-api/internal/dto/requests" "github.com/treytartt/casera-api/internal/dto/responses" "github.com/treytartt/casera-api/internal/models" "github.com/treytartt/casera-api/internal/repositories" ) // Task-related errors var ( ErrTaskNotFound = errors.New("task not found") ErrTaskAccessDenied = errors.New("you do not have access to this task") ErrTaskAlreadyCancelled = errors.New("task is already cancelled") ErrTaskAlreadyArchived = errors.New("task is already archived") ErrCompletionNotFound = errors.New("task completion not found") ) // TaskService handles task business logic type TaskService struct { taskRepo *repositories.TaskRepository residenceRepo *repositories.ResidenceRepository residenceService *ResidenceService notificationService *NotificationService emailService *EmailService } // NewTaskService creates a new task service func NewTaskService(taskRepo *repositories.TaskRepository, residenceRepo *repositories.ResidenceRepository) *TaskService { return &TaskService{ taskRepo: taskRepo, residenceRepo: residenceRepo, } } // SetNotificationService sets the notification service (for breaking circular dependency) func (s *TaskService) SetNotificationService(ns *NotificationService) { s.notificationService = ns } // SetEmailService sets the email service func (s *TaskService) SetEmailService(es *EmailService) { s.emailService = es } // SetResidenceService sets the residence service (for getting summary in CRUD responses) func (s *TaskService) SetResidenceService(rs *ResidenceService) { s.residenceService = rs } // getSummaryForUser returns an empty summary placeholder. // DEPRECATED: Summary calculation has been removed from CRUD responses for performance. // Clients should calculate summary from kanban data instead (which already includes all tasks). // The summary field is kept in responses for backward compatibility but will always be empty. func (s *TaskService) getSummaryForUser(_ uint) responses.TotalSummary { // Return empty summary - clients should calculate from kanban data return responses.TotalSummary{} } // === Task CRUD === // GetTask gets a task by ID with access check func (s *TaskService) GetTask(taskID, userID uint) (*responses.TaskResponse, error) { task, err := s.taskRepo.FindByID(taskID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrTaskNotFound } return nil, err } // Check access via residence hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrTaskAccessDenied } resp := responses.NewTaskResponse(task) return &resp, nil } // ListTasks lists all tasks accessible to a user as a kanban board. // The `now` parameter should be the start of day in the user's timezone for accurate overdue detection. func (s *TaskService) ListTasks(userID uint, now time.Time) (*responses.KanbanBoardResponse, error) { // Get all residence IDs accessible to user (lightweight - no preloads) residenceIDs, err := s.residenceRepo.FindResidenceIDsByUser(userID) if err != nil { return nil, err } if len(residenceIDs) == 0 { // Return empty kanban board return &responses.KanbanBoardResponse{ Columns: []responses.KanbanColumnResponse{}, DaysThreshold: 30, ResidenceID: "all", }, nil } // Get kanban data aggregated across all residences using user's timezone-aware time board, err := s.taskRepo.GetKanbanDataForMultipleResidences(residenceIDs, 30, now) if err != nil { return nil, err } resp := responses.NewKanbanBoardResponseForAll(board) // Include summary for dashboard stats summary := s.getSummaryForUser(userID) resp.Summary = &summary return &resp, nil } // GetTasksByResidence gets tasks for a specific residence (kanban board). // The `now` parameter should be the start of day in the user's timezone for accurate overdue detection. func (s *TaskService) GetTasksByResidence(residenceID, userID uint, daysThreshold int, now time.Time) (*responses.KanbanBoardResponse, error) { // Check access hasAccess, err := s.residenceRepo.HasAccess(residenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrResidenceAccessDenied } if daysThreshold <= 0 { daysThreshold = 30 // Default } // Get kanban data using user's timezone-aware time board, err := s.taskRepo.GetKanbanData(residenceID, daysThreshold, now) if err != nil { return nil, err } resp := responses.NewKanbanBoardResponse(board, residenceID) // Include summary for dashboard stats summary := s.getSummaryForUser(userID) resp.Summary = &summary return &resp, nil } // CreateTask creates a new task. // The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) { // Check residence access hasAccess, err := s.residenceRepo.HasAccess(req.ResidenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrResidenceAccessDenied } dueDate := req.DueDate.ToTimePtr() task := &models.Task{ ResidenceID: req.ResidenceID, CreatedByID: userID, Title: req.Title, Description: req.Description, CategoryID: req.CategoryID, PriorityID: req.PriorityID, FrequencyID: req.FrequencyID, CustomIntervalDays: req.CustomIntervalDays, InProgress: req.InProgress, AssignedToID: req.AssignedToID, DueDate: dueDate, NextDueDate: dueDate, // Initialize next_due_date to due_date EstimatedCost: req.EstimatedCost, ContractorID: req.ContractorID, } if err := s.taskRepo.Create(task); err != nil { return nil, err } // Reload with relations task, err = s.taskRepo.FindByID(task.ID) if err != nil { return nil, err } return &responses.TaskWithSummaryResponse{ Data: responses.NewTaskResponseWithTime(task, 30, now), Summary: s.getSummaryForUser(userID), }, nil } // UpdateTask updates a task. // The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRequest, now time.Time) (*responses.TaskWithSummaryResponse, error) { task, err := s.taskRepo.FindByID(taskID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrTaskNotFound } return nil, err } // Check access hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrTaskAccessDenied } // Apply updates if req.Title != nil { task.Title = *req.Title } if req.Description != nil { task.Description = *req.Description } if req.CategoryID != nil { task.CategoryID = req.CategoryID } if req.PriorityID != nil { task.PriorityID = req.PriorityID } if req.FrequencyID != nil { task.FrequencyID = req.FrequencyID } if req.CustomIntervalDays != nil { task.CustomIntervalDays = req.CustomIntervalDays } if req.InProgress != nil { task.InProgress = *req.InProgress } if req.AssignedToID != nil { task.AssignedToID = req.AssignedToID } if req.DueDate != nil { newDueDate := req.DueDate.ToTimePtr() task.DueDate = newDueDate // Also update NextDueDate if the task doesn't have completions yet // (if it has completions, NextDueDate should be managed by completion logic) if len(task.Completions) == 0 { task.NextDueDate = newDueDate } } if req.EstimatedCost != nil { task.EstimatedCost = req.EstimatedCost } if req.ActualCost != nil { task.ActualCost = req.ActualCost } if req.ContractorID != nil { task.ContractorID = req.ContractorID } if err := s.taskRepo.Update(task); err != nil { return nil, err } // Reload task, err = s.taskRepo.FindByID(task.ID) if err != nil { return nil, err } return &responses.TaskWithSummaryResponse{ Data: responses.NewTaskResponseWithTime(task, 30, now), Summary: s.getSummaryForUser(userID), }, nil } // DeleteTask deletes a task func (s *TaskService) DeleteTask(taskID, userID uint) (*responses.DeleteWithSummaryResponse, error) { task, err := s.taskRepo.FindByID(taskID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrTaskNotFound } return nil, err } // Check access hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrTaskAccessDenied } if err := s.taskRepo.Delete(taskID); err != nil { return nil, err } return &responses.DeleteWithSummaryResponse{ Data: "task deleted", Summary: s.getSummaryForUser(userID), }, nil } // === Task Actions === // MarkInProgress marks a task as in progress. // The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. func (s *TaskService) MarkInProgress(taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) { task, err := s.taskRepo.FindByID(taskID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrTaskNotFound } return nil, err } // Check access hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrTaskAccessDenied } if err := s.taskRepo.MarkInProgress(taskID); err != nil { return nil, err } // Reload task, err = s.taskRepo.FindByID(taskID) if err != nil { return nil, err } return &responses.TaskWithSummaryResponse{ Data: responses.NewTaskResponseWithTime(task, 30, now), Summary: s.getSummaryForUser(userID), }, nil } // CancelTask cancels a task. // The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. func (s *TaskService) CancelTask(taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) { task, err := s.taskRepo.FindByID(taskID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrTaskNotFound } return nil, err } // Check access hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrTaskAccessDenied } if task.IsCancelled { return nil, ErrTaskAlreadyCancelled } if err := s.taskRepo.Cancel(taskID); err != nil { return nil, err } // Reload task, err = s.taskRepo.FindByID(taskID) if err != nil { return nil, err } return &responses.TaskWithSummaryResponse{ Data: responses.NewTaskResponseWithTime(task, 30, now), Summary: s.getSummaryForUser(userID), }, nil } // UncancelTask uncancels a task. // The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. func (s *TaskService) UncancelTask(taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) { task, err := s.taskRepo.FindByID(taskID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrTaskNotFound } return nil, err } // Check access hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrTaskAccessDenied } if err := s.taskRepo.Uncancel(taskID); err != nil { return nil, err } // Reload task, err = s.taskRepo.FindByID(taskID) if err != nil { return nil, err } return &responses.TaskWithSummaryResponse{ Data: responses.NewTaskResponseWithTime(task, 30, now), Summary: s.getSummaryForUser(userID), }, nil } // ArchiveTask archives a task. // The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. func (s *TaskService) ArchiveTask(taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) { task, err := s.taskRepo.FindByID(taskID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrTaskNotFound } return nil, err } // Check access hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrTaskAccessDenied } if task.IsArchived { return nil, ErrTaskAlreadyArchived } if err := s.taskRepo.Archive(taskID); err != nil { return nil, err } // Reload task, err = s.taskRepo.FindByID(taskID) if err != nil { return nil, err } return &responses.TaskWithSummaryResponse{ Data: responses.NewTaskResponseWithTime(task, 30, now), Summary: s.getSummaryForUser(userID), }, nil } // UnarchiveTask unarchives a task. // The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. func (s *TaskService) UnarchiveTask(taskID, userID uint, now time.Time) (*responses.TaskWithSummaryResponse, error) { task, err := s.taskRepo.FindByID(taskID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrTaskNotFound } return nil, err } // Check access hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrTaskAccessDenied } if err := s.taskRepo.Unarchive(taskID); err != nil { return nil, err } // Reload task, err = s.taskRepo.FindByID(taskID) if err != nil { return nil, err } return &responses.TaskWithSummaryResponse{ Data: responses.NewTaskResponseWithTime(task, 30, now), Summary: s.getSummaryForUser(userID), }, nil } // === Task Completions === // CreateCompletion creates a task completion. // The `now` parameter should be the start of day in the user's timezone for accurate kanban categorization. func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest, userID uint, now time.Time) (*responses.TaskCompletionWithSummaryResponse, error) { // Get the task task, err := s.taskRepo.FindByID(req.TaskID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrTaskNotFound } return nil, err } // Check access hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrTaskAccessDenied } completedAt := time.Now().UTC() if req.CompletedAt != nil { completedAt = *req.CompletedAt } completion := &models.TaskCompletion{ TaskID: req.TaskID, CompletedByID: userID, CompletedAt: completedAt, Notes: req.Notes, ActualCost: req.ActualCost, Rating: req.Rating, } if err := s.taskRepo.CreateCompletion(completion); err != nil { return nil, err } // Update next_due_date and in_progress based on frequency // - If frequency is "Once" (days = nil or 0), set next_due_date to nil (marks as completed) // - If frequency is "Custom", use task.CustomIntervalDays for recurrence // - If frequency is recurring, calculate next_due_date = completion_date + frequency_days // and reset in_progress to false so task shows in correct kanban column // // Note: Frequency is no longer preloaded for performance, so we load it separately if needed var intervalDays *int if task.FrequencyID != nil { frequency, err := s.taskRepo.GetFrequencyByID(*task.FrequencyID) if err == nil && frequency != nil { if frequency.Name == "Custom" { // Custom frequency - use task's custom_interval_days intervalDays = task.CustomIntervalDays } else { // Standard frequency - use frequency's days intervalDays = frequency.Days } } } if intervalDays == nil || *intervalDays == 0 { // One-time task - clear next_due_date (completion is determined by NextDueDate == nil + has completions) task.NextDueDate = nil task.InProgress = false } else { // Recurring task - calculate next due date from completion date + interval nextDue := completedAt.AddDate(0, 0, *intervalDays) task.NextDueDate = &nextDue // Reset in_progress to false so task appears in upcoming/due_soon // instead of staying in "In Progress" column task.InProgress = false } if err := s.taskRepo.Update(task); err != nil { log.Error().Err(err).Uint("task_id", task.ID).Msg("Failed to update task after completion") } // Create images if provided for _, imageURL := range req.ImageURLs { if imageURL != "" { img := &models.TaskCompletionImage{ CompletionID: completion.ID, ImageURL: imageURL, } if err := s.taskRepo.CreateCompletionImage(img); err != nil { log.Error().Err(err).Uint("completion_id", completion.ID).Msg("Failed to create completion image") } } } // Reload completion with user info and images completion, err = s.taskRepo.FindCompletionByID(completion.ID) if err != nil { return nil, err } // Reload task with updated completions (so client can update kanban column) task, err = s.taskRepo.FindByID(req.TaskID) if err != nil { // Non-fatal - still return the completion, just without the task log.Warn().Err(err).Uint("task_id", req.TaskID).Msg("Failed to reload task after completion") resp := responses.NewTaskCompletionResponse(completion) return &responses.TaskCompletionWithSummaryResponse{ Data: resp, Summary: s.getSummaryForUser(userID), }, nil } // Send notification to residence owner and other users s.sendTaskCompletedNotification(task, completion) // Return completion with updated task (includes kanban_column for UI update) resp := responses.NewTaskCompletionWithTaskResponseWithTime(completion, task, 30, now) return &responses.TaskCompletionWithSummaryResponse{ Data: resp, Summary: s.getSummaryForUser(userID), }, nil } // QuickComplete creates a minimal task completion (for widget use) // Returns only success/error, no response body func (s *TaskService) QuickComplete(taskID uint, userID uint) error { // Get the task task, err := s.taskRepo.FindByID(taskID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrTaskNotFound } return err } // Check access hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) if err != nil { return err } if !hasAccess { return ErrTaskAccessDenied } completedAt := time.Now().UTC() completion := &models.TaskCompletion{ TaskID: taskID, CompletedByID: userID, CompletedAt: completedAt, Notes: "Completed from widget", } if err := s.taskRepo.CreateCompletion(completion); err != nil { return err } // Update next_due_date and in_progress based on frequency // Determine interval days: Custom frequency uses task.CustomIntervalDays, otherwise use frequency.Days // Note: Frequency is no longer preloaded for performance, so we load it separately if needed var quickIntervalDays *int var frequencyName = "unknown" if task.FrequencyID != nil { frequency, err := s.taskRepo.GetFrequencyByID(*task.FrequencyID) if err == nil && frequency != nil { frequencyName = frequency.Name if frequency.Name == "Custom" { quickIntervalDays = task.CustomIntervalDays } else { quickIntervalDays = frequency.Days } } } if quickIntervalDays == nil || *quickIntervalDays == 0 { // One-time task - clear next_due_date (completion is determined by NextDueDate == nil + has completions) log.Info(). Uint("task_id", task.ID). Bool("has_frequency", task.FrequencyID != nil). Msg("QuickComplete: One-time task, clearing next_due_date") task.NextDueDate = nil task.InProgress = false } else { // Recurring task - calculate next due date from completion date + interval nextDue := completedAt.AddDate(0, 0, *quickIntervalDays) // frequencyName was already set when loading frequency above log.Info(). Uint("task_id", task.ID). Str("frequency_name", frequencyName). Int("interval_days", *quickIntervalDays). Time("completed_at", completedAt). Time("next_due_date", nextDue). Msg("QuickComplete: Recurring task, setting next_due_date") task.NextDueDate = &nextDue // Reset in_progress to false task.InProgress = false } if err := s.taskRepo.Update(task); err != nil { log.Error().Err(err).Uint("task_id", task.ID).Msg("Failed to update task after quick completion") return err // Return error so caller knows the update failed } log.Info().Uint("task_id", task.ID).Msg("QuickComplete: Task updated successfully") // Send notification (fire and forget) go s.sendTaskCompletedNotification(task, completion) return nil } // sendTaskCompletedNotification sends notifications when a task is completed func (s *TaskService) sendTaskCompletedNotification(task *models.Task, completion *models.TaskCompletion) { // Get all users with access to this residence users, err := s.residenceRepo.GetResidenceUsers(task.ResidenceID) if err != nil { log.Error().Err(err).Uint("task_id", task.ID).Msg("Failed to get residence users for notification") return } // Get residence name residence, err := s.residenceRepo.FindByIDSimple(task.ResidenceID) residenceName := "your property" if err == nil && residence != nil { residenceName = residence.Name } completedByName := "Someone" if completion.CompletedBy.ID > 0 { completedByName = completion.CompletedBy.GetFullName() } // Notify all users for _, user := range users { isCompleter := user.ID == completion.CompletedByID // Send push notification (to everyone EXCEPT the person who completed it) if !isCompleter && s.notificationService != nil { go func(userID uint) { ctx := context.Background() if err := s.notificationService.CreateAndSendTaskNotification( ctx, userID, models.NotificationTaskCompleted, task, ); err != nil { log.Error().Err(err).Uint("user_id", userID).Uint("task_id", task.ID).Msg("Failed to send task completion push notification") } }(user.ID) } // Send email notification (to everyone INCLUDING the person who completed it) // Check user's email notification preferences first if s.emailService != nil && user.Email != "" && s.notificationService != nil { prefs, err := s.notificationService.GetPreferences(user.ID) if err != nil || (prefs != nil && prefs.EmailTaskCompleted) { // Send email if we couldn't get prefs (fail-open) or if email notifications are enabled go func(u models.User) { if err := s.emailService.SendTaskCompletedEmail( u.Email, u.GetFullName(), task.Title, completedByName, residenceName, ); err != nil { log.Error().Err(err).Str("email", u.Email).Uint("task_id", task.ID).Msg("Failed to send task completion email") } else { log.Info().Str("email", u.Email).Uint("task_id", task.ID).Msg("Task completion email sent") } }(user) } } } } // GetCompletion gets a task completion by ID func (s *TaskService) GetCompletion(completionID, userID uint) (*responses.TaskCompletionResponse, error) { completion, err := s.taskRepo.FindCompletionByID(completionID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrCompletionNotFound } return nil, err } // Check access via task's residence hasAccess, err := s.residenceRepo.HasAccess(completion.Task.ResidenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrTaskAccessDenied } resp := responses.NewTaskCompletionResponse(completion) return &resp, nil } // ListCompletions lists all task completions for a user func (s *TaskService) ListCompletions(userID uint) ([]responses.TaskCompletionResponse, error) { // Get all residence IDs (lightweight - no preloads) residenceIDs, err := s.residenceRepo.FindResidenceIDsByUser(userID) if err != nil { return nil, err } if len(residenceIDs) == 0 { return []responses.TaskCompletionResponse{}, nil } completions, err := s.taskRepo.FindCompletionsByUser(userID, residenceIDs) if err != nil { return nil, err } return responses.NewTaskCompletionListResponse(completions), nil } // DeleteCompletion deletes a task completion func (s *TaskService) DeleteCompletion(completionID, userID uint) (*responses.DeleteWithSummaryResponse, error) { completion, err := s.taskRepo.FindCompletionByID(completionID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrCompletionNotFound } return nil, err } // Check access hasAccess, err := s.residenceRepo.HasAccess(completion.Task.ResidenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrTaskAccessDenied } if err := s.taskRepo.DeleteCompletion(completionID); err != nil { return nil, err } return &responses.DeleteWithSummaryResponse{ Data: "completion deleted", Summary: s.getSummaryForUser(userID), }, nil } // GetCompletionsByTask gets all completions for a specific task func (s *TaskService) GetCompletionsByTask(taskID, userID uint) ([]responses.TaskCompletionResponse, error) { // Get the task to check access task, err := s.taskRepo.FindByID(taskID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrTaskNotFound } return nil, err } // Check access via residence hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrTaskAccessDenied } // Get completions for the task completions, err := s.taskRepo.FindCompletionsByTask(taskID) if err != nil { return nil, err } return responses.NewTaskCompletionListResponse(completions), nil } // === Lookups === // GetCategories returns all task categories func (s *TaskService) GetCategories() ([]responses.TaskCategoryResponse, error) { categories, err := s.taskRepo.GetAllCategories() if err != nil { return nil, err } result := make([]responses.TaskCategoryResponse, len(categories)) for i, c := range categories { result[i] = *responses.NewTaskCategoryResponse(&c) } return result, nil } // GetPriorities returns all task priorities func (s *TaskService) GetPriorities() ([]responses.TaskPriorityResponse, error) { priorities, err := s.taskRepo.GetAllPriorities() if err != nil { return nil, err } result := make([]responses.TaskPriorityResponse, len(priorities)) for i, p := range priorities { result[i] = *responses.NewTaskPriorityResponse(&p) } return result, nil } // GetFrequencies returns all task frequencies func (s *TaskService) GetFrequencies() ([]responses.TaskFrequencyResponse, error) { frequencies, err := s.taskRepo.GetAllFrequencies() if err != nil { return nil, err } result := make([]responses.TaskFrequencyResponse, len(frequencies)) for i, f := range frequencies { result[i] = *responses.NewTaskFrequencyResponse(&f) } return result, nil }