package services import ( "errors" "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" ) // 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 if !s.hasContractorAccess(contractor, userID) { return nil, ErrContractorAccessDenied } resp := responses.NewContractorResponse(contractor) return &resp, nil } // hasContractorAccess checks if user has access to a contractor // Access rules: // - If contractor has no residence: only the creator has access // - If contractor has a residence: all users with access to that residence func (s *ContractorService) hasContractorAccess(contractor *models.Contractor, userID uint) bool { if contractor.ResidenceID == nil { // Personal contractor - only creator has access return contractor.CreatedByID == userID } // Residence contractor - check residence access hasAccess, err := s.residenceRepo.HasAccess(*contractor.ResidenceID, userID) if err != nil { return false } return hasAccess } // ListContractors lists all contractors accessible to a user func (s *ContractorService) ListContractors(userID uint) ([]responses.ContractorResponse, 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 } // FindByUser now handles both personal and residence contractors contractors, err := s.contractorRepo.FindByUser(userID, residenceIDs) if err != nil { return nil, err } return responses.NewContractorListResponse(contractors), nil } // CreateContractor creates a new contractor func (s *ContractorService) CreateContractor(req *requests.CreateContractorRequest, userID uint) (*responses.ContractorResponse, error) { // If residence is provided, check access if req.ResidenceID != nil { 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, reloadErr := s.contractorRepo.FindByID(contractor.ID) if reloadErr != nil { return nil, reloadErr } 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 if !s.hasContractorAccess(contractor, userID) { 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 if !s.hasContractorAccess(contractor, userID) { return ErrContractorAccessDenied } return s.contractorRepo.Delete(contractorID) } // ToggleFavorite toggles the favorite status of a contractor and returns the updated contractor func (s *ContractorService) ToggleFavorite(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 if !s.hasContractorAccess(contractor, userID) { return nil, ErrContractorAccessDenied } _, err = s.contractorRepo.ToggleFavorite(contractorID) if err != nil { return nil, err } // Re-fetch the contractor to get the updated state with all relations contractor, err = s.contractorRepo.FindByID(contractorID) if err != nil { return nil, err } resp := responses.NewContractorResponse(contractor) return &resp, nil } // GetContractorTasks gets all tasks for a contractor func (s *ContractorService) GetContractorTasks(contractorID, userID uint) ([]responses.TaskResponse, error) { contractor, err := s.contractorRepo.FindByID(contractorID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrContractorNotFound } return nil, err } // Check access if !s.hasContractorAccess(contractor, userID) { return nil, ErrContractorAccessDenied } tasks, err := s.contractorRepo.GetTasksForContractor(contractorID) if err != nil { return nil, err } return responses.NewTaskListResponse(tasks), 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 }