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:
Trey t
2025-12-16 13:52:08 -06:00
parent c51f1ce34a
commit 6dac34e373
98 changed files with 8209 additions and 4425 deletions

View File

@@ -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