Initial commit: MyCrib API in Go
Complete rewrite of Django REST API to Go with: - Gin web framework for HTTP routing - GORM for database operations - GoAdmin for admin panel - Gorush integration for push notifications - Redis for caching and job queues Features implemented: - User authentication (login, register, logout, password reset) - Residence management (CRUD, sharing, share codes) - Task management (CRUD, kanban board, completions) - Contractor management (CRUD, specialties) - Document management (CRUD, warranties) - Notifications (preferences, push notifications) - Subscription management (tiers, limits) Infrastructure: - Docker Compose for local development - Database migrations and seed data - Admin panel for data management 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
601
internal/services/task_service.go
Normal file
601
internal/services/task_service.go
Normal file
@@ -0,0 +1,601 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/mycrib-api/internal/dto/requests"
|
||||
"github.com/treytartt/mycrib-api/internal/dto/responses"
|
||||
"github.com/treytartt/mycrib-api/internal/models"
|
||||
"github.com/treytartt/mycrib-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
|
||||
}
|
||||
|
||||
// NewTaskService creates a new task service
|
||||
func NewTaskService(taskRepo *repositories.TaskRepository, residenceRepo *repositories.ResidenceRepository) *TaskService {
|
||||
return &TaskService{
|
||||
taskRepo: taskRepo,
|
||||
residenceRepo: residenceRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// === 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
|
||||
func (s *TaskService) ListTasks(userID uint) (*responses.TaskListResponse, error) {
|
||||
// Get all residence IDs accessible to user
|
||||
residences, err := s.residenceRepo.FindByUser(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
residenceIDs := make([]uint, len(residences))
|
||||
for i, r := range residences {
|
||||
residenceIDs[i] = r.ID
|
||||
}
|
||||
|
||||
if len(residenceIDs) == 0 {
|
||||
return &responses.TaskListResponse{Count: 0, Results: []responses.TaskResponse{}}, nil
|
||||
}
|
||||
|
||||
tasks, err := s.taskRepo.FindByUser(userID, residenceIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := responses.NewTaskListResponse(tasks)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// GetTasksByResidence gets tasks for a specific residence (kanban board)
|
||||
func (s *TaskService) GetTasksByResidence(residenceID, userID uint, daysThreshold int) (*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
|
||||
}
|
||||
|
||||
board, err := s.taskRepo.GetKanbanData(residenceID, daysThreshold)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := responses.NewKanbanBoardResponse(board, residenceID)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// CreateTask creates a new task
|
||||
func (s *TaskService) CreateTask(req *requests.CreateTaskRequest, userID uint) (*responses.TaskResponse, error) {
|
||||
// Check residence access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(req.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasAccess {
|
||||
return nil, ErrResidenceAccessDenied
|
||||
}
|
||||
|
||||
task := &models.Task{
|
||||
ResidenceID: req.ResidenceID,
|
||||
CreatedByID: userID,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
CategoryID: req.CategoryID,
|
||||
PriorityID: req.PriorityID,
|
||||
StatusID: req.StatusID,
|
||||
FrequencyID: req.FrequencyID,
|
||||
AssignedToID: req.AssignedToID,
|
||||
DueDate: req.DueDate,
|
||||
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
|
||||
}
|
||||
|
||||
resp := responses.NewTaskResponse(task)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// UpdateTask updates a task
|
||||
func (s *TaskService) UpdateTask(taskID, userID uint, req *requests.UpdateTaskRequest) (*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
|
||||
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.StatusID != nil {
|
||||
task.StatusID = req.StatusID
|
||||
}
|
||||
if req.FrequencyID != nil {
|
||||
task.FrequencyID = req.FrequencyID
|
||||
}
|
||||
if req.AssignedToID != nil {
|
||||
task.AssignedToID = req.AssignedToID
|
||||
}
|
||||
if req.DueDate != nil {
|
||||
task.DueDate = req.DueDate
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
resp := responses.NewTaskResponse(task)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// DeleteTask deletes a task
|
||||
func (s *TaskService) DeleteTask(taskID, userID uint) error {
|
||||
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
|
||||
}
|
||||
|
||||
return s.taskRepo.Delete(taskID)
|
||||
}
|
||||
|
||||
// === Task Actions ===
|
||||
|
||||
// MarkInProgress marks a task as in progress
|
||||
func (s *TaskService) MarkInProgress(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
|
||||
hasAccess, err := s.residenceRepo.HasAccess(task.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasAccess {
|
||||
return nil, ErrTaskAccessDenied
|
||||
}
|
||||
|
||||
// Find "In Progress" status
|
||||
status, err := s.taskRepo.FindStatusByName("In Progress")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.taskRepo.MarkInProgress(taskID, status.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reload
|
||||
task, err = s.taskRepo.FindByID(taskID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := responses.NewTaskResponse(task)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// CancelTask cancels a task
|
||||
func (s *TaskService) CancelTask(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
|
||||
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
|
||||
}
|
||||
|
||||
resp := responses.NewTaskResponse(task)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// UncancelTask uncancels a task
|
||||
func (s *TaskService) UncancelTask(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
|
||||
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
|
||||
}
|
||||
|
||||
resp := responses.NewTaskResponse(task)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// ArchiveTask archives a task
|
||||
func (s *TaskService) ArchiveTask(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
|
||||
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
|
||||
}
|
||||
|
||||
resp := responses.NewTaskResponse(task)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// UnarchiveTask unarchives a task
|
||||
func (s *TaskService) UnarchiveTask(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
|
||||
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
|
||||
}
|
||||
|
||||
resp := responses.NewTaskResponse(task)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// === Task Completions ===
|
||||
|
||||
// CreateCompletion creates a task completion
|
||||
func (s *TaskService) CreateCompletion(req *requests.CreateTaskCompletionRequest, userID uint) (*responses.TaskCompletionResponse, 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,
|
||||
PhotoURL: req.PhotoURL,
|
||||
}
|
||||
|
||||
if err := s.taskRepo.CreateCompletion(completion); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reload
|
||||
completion, err = s.taskRepo.FindCompletionByID(completion.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := responses.NewTaskCompletionResponse(completion)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// 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.TaskCompletionListResponse, error) {
|
||||
// Get all residence IDs
|
||||
residences, err := s.residenceRepo.FindByUser(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
residenceIDs := make([]uint, len(residences))
|
||||
for i, r := range residences {
|
||||
residenceIDs[i] = r.ID
|
||||
}
|
||||
|
||||
if len(residenceIDs) == 0 {
|
||||
return &responses.TaskCompletionListResponse{Count: 0, Results: []responses.TaskCompletionResponse{}}, nil
|
||||
}
|
||||
|
||||
completions, err := s.taskRepo.FindCompletionsByUser(userID, residenceIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp := responses.NewTaskCompletionListResponse(completions)
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// DeleteCompletion deletes a task completion
|
||||
func (s *TaskService) DeleteCompletion(completionID, userID uint) error {
|
||||
completion, err := s.taskRepo.FindCompletionByID(completionID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrCompletionNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Check access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(completion.Task.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasAccess {
|
||||
return ErrTaskAccessDenied
|
||||
}
|
||||
|
||||
return s.taskRepo.DeleteCompletion(completionID)
|
||||
}
|
||||
|
||||
// === 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
|
||||
}
|
||||
|
||||
// GetStatuses returns all task statuses
|
||||
func (s *TaskService) GetStatuses() ([]responses.TaskStatusResponse, error) {
|
||||
statuses, err := s.taskRepo.GetAllStatuses()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]responses.TaskStatusResponse, len(statuses))
|
||||
for i, st := range statuses {
|
||||
result[i] = *responses.NewTaskStatusResponse(&st)
|
||||
}
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user