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,6 +5,7 @@ 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"
|
||||
@@ -12,10 +13,11 @@ import (
|
||||
)
|
||||
|
||||
// Document-related errors
|
||||
var (
|
||||
ErrDocumentNotFound = errors.New("document not found")
|
||||
ErrDocumentAccessDenied = errors.New("you do not have access to this document")
|
||||
)
|
||||
// DEPRECATED: These constants are deprecated. Use apperrors package instead.
|
||||
// var (
|
||||
// ErrDocumentNotFound = errors.New("document not found")
|
||||
// ErrDocumentAccessDenied = errors.New("you do not have access to this document")
|
||||
// )
|
||||
|
||||
// DocumentService handles document business logic
|
||||
type DocumentService struct {
|
||||
@@ -36,18 +38,18 @@ func (s *DocumentService) GetDocument(documentID, userID uint) (*responses.Docum
|
||||
document, err := s.documentRepo.FindByID(documentID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrDocumentNotFound
|
||||
return nil, apperrors.NotFound("error.document_not_found")
|
||||
}
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Check access via residence
|
||||
hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
if !hasAccess {
|
||||
return nil, ErrDocumentAccessDenied
|
||||
return nil, apperrors.Forbidden("error.document_access_denied")
|
||||
}
|
||||
|
||||
resp := responses.NewDocumentResponse(document)
|
||||
@@ -59,7 +61,7 @@ func (s *DocumentService) ListDocuments(userID uint) ([]responses.DocumentRespon
|
||||
// Get residence IDs (lightweight - no preloads)
|
||||
residenceIDs, err := s.residenceRepo.FindResidenceIDsByUser(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
if len(residenceIDs) == 0 {
|
||||
@@ -68,7 +70,7 @@ func (s *DocumentService) ListDocuments(userID uint) ([]responses.DocumentRespon
|
||||
|
||||
documents, err := s.documentRepo.FindByUser(residenceIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
return responses.NewDocumentListResponse(documents), nil
|
||||
@@ -79,7 +81,7 @@ func (s *DocumentService) ListWarranties(userID uint) ([]responses.DocumentRespo
|
||||
// Get residence IDs (lightweight - no preloads)
|
||||
residenceIDs, err := s.residenceRepo.FindResidenceIDsByUser(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
if len(residenceIDs) == 0 {
|
||||
@@ -88,7 +90,7 @@ func (s *DocumentService) ListWarranties(userID uint) ([]responses.DocumentRespo
|
||||
|
||||
documents, err := s.documentRepo.FindWarranties(residenceIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
return responses.NewDocumentListResponse(documents), nil
|
||||
@@ -99,10 +101,10 @@ func (s *DocumentService) CreateDocument(req *requests.CreateDocumentRequest, us
|
||||
// Check residence access
|
||||
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")
|
||||
}
|
||||
|
||||
documentType := req.DocumentType
|
||||
@@ -131,7 +133,7 @@ func (s *DocumentService) CreateDocument(req *requests.CreateDocumentRequest, us
|
||||
}
|
||||
|
||||
if err := s.documentRepo.Create(document); err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Create images if provided
|
||||
@@ -151,7 +153,7 @@ func (s *DocumentService) CreateDocument(req *requests.CreateDocumentRequest, us
|
||||
// Reload with relations
|
||||
document, err = s.documentRepo.FindByID(document.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
resp := responses.NewDocumentResponse(document)
|
||||
@@ -163,18 +165,18 @@ func (s *DocumentService) UpdateDocument(documentID, userID uint, req *requests.
|
||||
document, err := s.documentRepo.FindByID(documentID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrDocumentNotFound
|
||||
return nil, apperrors.NotFound("error.document_not_found")
|
||||
}
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Check access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
if !hasAccess {
|
||||
return nil, ErrDocumentAccessDenied
|
||||
return nil, apperrors.Forbidden("error.document_access_denied")
|
||||
}
|
||||
|
||||
// Apply updates
|
||||
@@ -222,13 +224,13 @@ func (s *DocumentService) UpdateDocument(documentID, userID uint, req *requests.
|
||||
}
|
||||
|
||||
if err := s.documentRepo.Update(document); err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Reload
|
||||
document, err = s.documentRepo.FindByID(documentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
resp := responses.NewDocumentResponse(document)
|
||||
@@ -240,21 +242,25 @@ func (s *DocumentService) DeleteDocument(documentID, userID uint) error {
|
||||
document, err := s.documentRepo.FindByID(documentID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrDocumentNotFound
|
||||
return apperrors.NotFound("error.document_not_found")
|
||||
}
|
||||
return err
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Check access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
if !hasAccess {
|
||||
return ErrDocumentAccessDenied
|
||||
return apperrors.Forbidden("error.document_access_denied")
|
||||
}
|
||||
|
||||
return s.documentRepo.Delete(documentID)
|
||||
if err := s.documentRepo.Delete(documentID); err != nil {
|
||||
return apperrors.Internal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ActivateDocument activates a document
|
||||
@@ -262,26 +268,26 @@ func (s *DocumentService) ActivateDocument(documentID, userID uint) (*responses.
|
||||
// First check if document exists (even if inactive)
|
||||
var document models.Document
|
||||
if err := s.documentRepo.FindByIDIncludingInactive(documentID, &document); err != nil {
|
||||
return nil, ErrDocumentNotFound
|
||||
return nil, apperrors.NotFound("error.document_not_found")
|
||||
}
|
||||
|
||||
// Check access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
if !hasAccess {
|
||||
return nil, ErrDocumentAccessDenied
|
||||
return nil, apperrors.Forbidden("error.document_access_denied")
|
||||
}
|
||||
|
||||
if err := s.documentRepo.Activate(documentID); err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Reload
|
||||
doc, err := s.documentRepo.FindByID(documentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
resp := responses.NewDocumentResponse(doc)
|
||||
@@ -293,22 +299,22 @@ func (s *DocumentService) DeactivateDocument(documentID, userID uint) (*response
|
||||
document, err := s.documentRepo.FindByID(documentID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrDocumentNotFound
|
||||
return nil, apperrors.NotFound("error.document_not_found")
|
||||
}
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Check access
|
||||
hasAccess, err := s.residenceRepo.HasAccess(document.ResidenceID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
if !hasAccess {
|
||||
return nil, ErrDocumentAccessDenied
|
||||
return nil, apperrors.Forbidden("error.document_access_denied")
|
||||
}
|
||||
|
||||
if err := s.documentRepo.Deactivate(documentID); err != nil {
|
||||
return nil, err
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
document.IsActive = false
|
||||
|
||||
Reference in New Issue
Block a user