Migrate from Gin to Echo framework and add comprehensive integration tests
Major changes: - Migrate all handlers from Gin to Echo framework - Add new apperrors, echohelpers, and validator packages - Update middleware for Echo compatibility - Add ArchivedHandler to task categorization chain (archived tasks go to cancelled_tasks column) - Add 6 new integration tests: - RecurringTaskLifecycle: NextDueDate advancement for weekly/monthly tasks - MultiUserSharing: Complex sharing with user removal - TaskStateTransitions: All state transitions and kanban column changes - DateBoundaryEdgeCases: Threshold boundary testing - CascadeOperations: Residence deletion cascade effects - MultiUserOperations: Shared residence collaboration - Add single-purpose repository functions for kanban columns (GetOverdueTasks, GetDueSoonTasks, etc.) - Fix RemoveUser route param mismatch (userId -> user_id) - Fix determineExpectedColumn helper to correctly prioritize in_progress over overdue 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,17 +5,18 @@ import (
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/treytartt/casera-api/internal/apperrors"
|
||||
"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")
|
||||
)
|
||||
// Deprecated: Use apperrors.NotFound("error.contractor_not_found") instead
|
||||
// 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 {
|
||||
@@ -36,14 +37,14 @@ func (s *ContractorService) GetContractor(contractorID, userID uint) (*responses
|
||||
contractor, err := s.contractorRepo.FindByID(contractorID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrContractorNotFound
|
||||
return nil, apperrors.NotFound("error.contractor_not_found")
|
||||
}
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Check access
|
||||
if !s.hasContractorAccess(contractor, userID) {
|
||||
return nil, ErrContractorAccessDenied
|
||||
return nil, apperrors.Forbidden("error.contractor_access_denied")
|
||||
}
|
||||
|
||||
resp := responses.NewContractorResponse(contractor)
|
||||
@@ -73,13 +74,13 @@ func (s *ContractorService) ListContractors(userID uint) ([]responses.Contractor
|
||||
// Get residence IDs (lightweight - no preloads)
|
||||
residenceIDs, err := s.residenceRepo.FindResidenceIDsByUser(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// FindByUser now handles both personal and residence contractors
|
||||
contractors, err := s.contractorRepo.FindByUser(userID, residenceIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
return responses.NewContractorListResponse(contractors), nil
|
||||
@@ -91,10 +92,10 @@ func (s *ContractorService) CreateContractor(req *requests.CreateContractorReque
|
||||
if req.ResidenceID != nil {
|
||||
hasAccess, err := s.residenceRepo.HasAccess(*req.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
if !hasAccess {
|
||||
return nil, ErrResidenceAccessDenied
|
||||
return nil, apperrors.Forbidden("error.residence_access_denied")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,20 +123,20 @@ func (s *ContractorService) CreateContractor(req *requests.CreateContractorReque
|
||||
}
|
||||
|
||||
if err := s.contractorRepo.Create(contractor); err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Set specialties if provided
|
||||
if len(req.SpecialtyIDs) > 0 {
|
||||
if err := s.contractorRepo.SetSpecialties(contractor.ID, req.SpecialtyIDs); err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload with relations
|
||||
contractor, reloadErr := s.contractorRepo.FindByID(contractor.ID)
|
||||
if reloadErr != nil {
|
||||
return nil, reloadErr
|
||||
return nil, apperrors.Internal(reloadErr)
|
||||
}
|
||||
|
||||
resp := responses.NewContractorResponse(contractor)
|
||||
@@ -147,14 +148,14 @@ func (s *ContractorService) UpdateContractor(contractorID, userID uint, req *req
|
||||
contractor, err := s.contractorRepo.FindByID(contractorID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrContractorNotFound
|
||||
return nil, apperrors.NotFound("error.contractor_not_found")
|
||||
}
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Check access
|
||||
if !s.hasContractorAccess(contractor, userID) {
|
||||
return nil, ErrContractorAccessDenied
|
||||
return nil, apperrors.Forbidden("error.contractor_access_denied")
|
||||
}
|
||||
|
||||
// Apply updates
|
||||
@@ -199,20 +200,20 @@ func (s *ContractorService) UpdateContractor(contractorID, userID uint, req *req
|
||||
contractor.ResidenceID = req.ResidenceID
|
||||
|
||||
if err := s.contractorRepo.Update(contractor); err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Update specialties if provided
|
||||
if req.SpecialtyIDs != nil {
|
||||
if err := s.contractorRepo.SetSpecialties(contractorID, req.SpecialtyIDs); err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload
|
||||
contractor, err = s.contractorRepo.FindByID(contractorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
resp := responses.NewContractorResponse(contractor)
|
||||
@@ -224,17 +225,21 @@ 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 apperrors.NotFound("error.contractor_not_found")
|
||||
}
|
||||
return err
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Check access
|
||||
if !s.hasContractorAccess(contractor, userID) {
|
||||
return ErrContractorAccessDenied
|
||||
return apperrors.Forbidden("error.contractor_access_denied")
|
||||
}
|
||||
|
||||
return s.contractorRepo.Delete(contractorID)
|
||||
if err := s.contractorRepo.Delete(contractorID); err != nil {
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToggleFavorite toggles the favorite status of a contractor and returns the updated contractor
|
||||
@@ -242,25 +247,25 @@ func (s *ContractorService) ToggleFavorite(contractorID, userID uint) (*response
|
||||
contractor, err := s.contractorRepo.FindByID(contractorID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrContractorNotFound
|
||||
return nil, apperrors.NotFound("error.contractor_not_found")
|
||||
}
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Check access
|
||||
if !s.hasContractorAccess(contractor, userID) {
|
||||
return nil, ErrContractorAccessDenied
|
||||
return nil, apperrors.Forbidden("error.contractor_access_denied")
|
||||
}
|
||||
|
||||
_, err = s.contractorRepo.ToggleFavorite(contractorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(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
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
resp := responses.NewContractorResponse(contractor)
|
||||
@@ -272,19 +277,19 @@ func (s *ContractorService) GetContractorTasks(contractorID, userID uint) ([]res
|
||||
contractor, err := s.contractorRepo.FindByID(contractorID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrContractorNotFound
|
||||
return nil, apperrors.NotFound("error.contractor_not_found")
|
||||
}
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Check access
|
||||
if !s.hasContractorAccess(contractor, userID) {
|
||||
return nil, ErrContractorAccessDenied
|
||||
return nil, apperrors.Forbidden("error.contractor_access_denied")
|
||||
}
|
||||
|
||||
tasks, err := s.contractorRepo.GetTasksForContractor(contractorID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
return responses.NewTaskListResponse(tasks), nil
|
||||
@@ -295,15 +300,15 @@ func (s *ContractorService) ListContractorsByResidence(residenceID, userID uint)
|
||||
// Check user has access to the residence
|
||||
hasAccess, err := s.residenceRepo.HasAccess(residenceID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
if !hasAccess {
|
||||
return nil, ErrResidenceAccessDenied
|
||||
return nil, apperrors.Forbidden("error.residence_access_denied")
|
||||
}
|
||||
|
||||
contractors, err := s.contractorRepo.FindByResidence(residenceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
return responses.NewContractorListResponse(contractors), nil
|
||||
@@ -313,7 +318,7 @@ func (s *ContractorService) ListContractorsByResidence(residenceID, userID uint)
|
||||
func (s *ContractorService) GetSpecialties() ([]responses.ContractorSpecialtyResponse, error) {
|
||||
specialties, err := s.contractorRepo.GetAllSpecialties()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
result := make([]responses.ContractorSpecialtyResponse, len(specialties))
|
||||
|
||||
Reference in New Issue
Block a user