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:
312
internal/services/contractor_service.go
Normal file
312
internal/services/contractor_service.go
Normal file
@@ -0,0 +1,312 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user