package services import ( "errors" "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" ) // Contractor-related errors var ( ErrContractorNotFound = errors.New("contractor not found") ErrContractorAccessDenied = errors.New("you do not have access to this contractor") ) // ContractorService handles contractor business logic type ContractorService struct { contractorRepo *repositories.ContractorRepository residenceRepo *repositories.ResidenceRepository } // NewContractorService creates a new contractor service func NewContractorService(contractorRepo *repositories.ContractorRepository, residenceRepo *repositories.ResidenceRepository) *ContractorService { return &ContractorService{ contractorRepo: contractorRepo, residenceRepo: residenceRepo, } } // GetContractor gets a contractor by ID with access check func (s *ContractorService) GetContractor(contractorID, userID uint) (*responses.ContractorResponse, error) { contractor, err := s.contractorRepo.FindByID(contractorID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrContractorNotFound } return nil, err } // Check access via residence hasAccess, err := s.residenceRepo.HasAccess(contractor.ResidenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrContractorAccessDenied } resp := responses.NewContractorResponse(contractor) return &resp, nil } // ListContractors lists all contractors accessible to a user func (s *ContractorService) ListContractors(userID uint) (*responses.ContractorListResponse, error) { 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.ContractorListResponse{Count: 0, Results: []responses.ContractorResponse{}}, nil } contractors, err := s.contractorRepo.FindByUser(residenceIDs) if err != nil { return nil, err } resp := responses.NewContractorListResponse(contractors) return &resp, nil } // CreateContractor creates a new contractor func (s *ContractorService) CreateContractor(req *requests.CreateContractorRequest, userID uint) (*responses.ContractorResponse, error) { // Check residence access hasAccess, err := s.residenceRepo.HasAccess(req.ResidenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrResidenceAccessDenied } isFavorite := false if req.IsFavorite != nil { isFavorite = *req.IsFavorite } contractor := &models.Contractor{ ResidenceID: req.ResidenceID, CreatedByID: userID, Name: req.Name, Company: req.Company, Phone: req.Phone, Email: req.Email, Website: req.Website, Notes: req.Notes, StreetAddress: req.StreetAddress, City: req.City, StateProvince: req.StateProvince, PostalCode: req.PostalCode, Rating: req.Rating, IsFavorite: isFavorite, IsActive: true, } if err := s.contractorRepo.Create(contractor); err != nil { return nil, err } // Set specialties if provided if len(req.SpecialtyIDs) > 0 { if err := s.contractorRepo.SetSpecialties(contractor.ID, req.SpecialtyIDs); err != nil { return nil, err } } // Reload with relations contractor, err = s.contractorRepo.FindByID(contractor.ID) if err != nil { return nil, err } resp := responses.NewContractorResponse(contractor) return &resp, nil } // UpdateContractor updates a contractor func (s *ContractorService) UpdateContractor(contractorID, userID uint, req *requests.UpdateContractorRequest) (*responses.ContractorResponse, error) { contractor, err := s.contractorRepo.FindByID(contractorID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrContractorNotFound } return nil, err } // Check access hasAccess, err := s.residenceRepo.HasAccess(contractor.ResidenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrContractorAccessDenied } // Apply updates if req.Name != nil { contractor.Name = *req.Name } if req.Company != nil { contractor.Company = *req.Company } if req.Phone != nil { contractor.Phone = *req.Phone } if req.Email != nil { contractor.Email = *req.Email } if req.Website != nil { contractor.Website = *req.Website } if req.Notes != nil { contractor.Notes = *req.Notes } if req.StreetAddress != nil { contractor.StreetAddress = *req.StreetAddress } if req.City != nil { contractor.City = *req.City } if req.StateProvince != nil { contractor.StateProvince = *req.StateProvince } if req.PostalCode != nil { contractor.PostalCode = *req.PostalCode } if req.Rating != nil { contractor.Rating = req.Rating } if req.IsFavorite != nil { contractor.IsFavorite = *req.IsFavorite } if err := s.contractorRepo.Update(contractor); err != nil { return nil, err } // Update specialties if provided if req.SpecialtyIDs != nil { if err := s.contractorRepo.SetSpecialties(contractorID, req.SpecialtyIDs); err != nil { return nil, err } } // Reload contractor, err = s.contractorRepo.FindByID(contractorID) if err != nil { return nil, err } resp := responses.NewContractorResponse(contractor) return &resp, nil } // DeleteContractor soft-deletes a contractor func (s *ContractorService) DeleteContractor(contractorID, userID uint) error { contractor, err := s.contractorRepo.FindByID(contractorID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrContractorNotFound } return err } // Check access hasAccess, err := s.residenceRepo.HasAccess(contractor.ResidenceID, userID) if err != nil { return err } if !hasAccess { return ErrContractorAccessDenied } return s.contractorRepo.Delete(contractorID) } // ToggleFavorite toggles the favorite status of a contractor func (s *ContractorService) ToggleFavorite(contractorID, userID uint) (*responses.ToggleFavoriteResponse, error) { contractor, err := s.contractorRepo.FindByID(contractorID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrContractorNotFound } return nil, err } // Check access hasAccess, err := s.residenceRepo.HasAccess(contractor.ResidenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrContractorAccessDenied } newStatus, err := s.contractorRepo.ToggleFavorite(contractorID) if err != nil { return nil, err } message := "Contractor removed from favorites" if newStatus { message = "Contractor added to favorites" } return &responses.ToggleFavoriteResponse{ Message: message, IsFavorite: newStatus, }, nil } // GetContractorTasks gets all tasks for a contractor func (s *ContractorService) GetContractorTasks(contractorID, userID uint) (*responses.TaskListResponse, error) { contractor, err := s.contractorRepo.FindByID(contractorID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrContractorNotFound } return nil, err } // Check access hasAccess, err := s.residenceRepo.HasAccess(contractor.ResidenceID, userID) if err != nil { return nil, err } if !hasAccess { return nil, ErrContractorAccessDenied } tasks, err := s.contractorRepo.GetTasksForContractor(contractorID) if err != nil { return nil, err } resp := responses.NewTaskListResponse(tasks) return &resp, nil } // GetSpecialties returns all contractor specialties func (s *ContractorService) GetSpecialties() ([]responses.ContractorSpecialtyResponse, error) { specialties, err := s.contractorRepo.GetAllSpecialties() if err != nil { return nil, err } result := make([]responses.ContractorSpecialtyResponse, len(specialties)) for i, sp := range specialties { result[i] = responses.NewContractorSpecialtyResponse(&sp) } return result, nil }