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

@@ -4,7 +4,7 @@ import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
"github.com/treytartt/casera-api/internal/admin/dto"
@@ -47,15 +47,14 @@ type UpdateCompletionImageRequest struct {
}
// List handles GET /api/admin/completion-images
func (h *AdminCompletionImageHandler) List(c *gin.Context) {
func (h *AdminCompletionImageHandler) List(c echo.Context) error {
var filters dto.PaginationParams
if err := c.ShouldBindQuery(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&filters); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Optional completion_id filter
completionIDStr := c.Query("completion_id")
completionIDStr := c.QueryParam("completion_id")
var images []models.TaskCompletionImage
var total int64
@@ -90,8 +89,7 @@ func (h *AdminCompletionImageHandler) List(c *gin.Context) {
query = query.Offset(filters.GetOffset()).Limit(filters.GetPerPage())
if err := query.Find(&images).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch completion images"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch completion images"})
}
// Build response with task info
@@ -100,47 +98,41 @@ func (h *AdminCompletionImageHandler) List(c *gin.Context) {
responses[i] = h.toResponse(&image)
}
c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
return c.JSON(http.StatusOK, dto.NewPaginatedResponse(responses, total, filters.GetPage(), filters.GetPerPage()))
}
// Get handles GET /api/admin/completion-images/:id
func (h *AdminCompletionImageHandler) Get(c *gin.Context) {
func (h *AdminCompletionImageHandler) Get(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid image ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid image ID"})
}
var image models.TaskCompletionImage
if err := h.db.First(&image, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Completion image not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Completion image not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch completion image"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch completion image"})
}
c.JSON(http.StatusOK, h.toResponse(&image))
return c.JSON(http.StatusOK, h.toResponse(&image))
}
// Create handles POST /api/admin/completion-images
func (h *AdminCompletionImageHandler) Create(c *gin.Context) {
func (h *AdminCompletionImageHandler) Create(c echo.Context) error {
var req CreateCompletionImageRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
// Verify completion exists
var completion models.TaskCompletion
if err := h.db.First(&completion, req.CompletionID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusBadRequest, gin.H{"error": "Task completion not found"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Task completion not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to verify completion"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to verify completion"})
}
image := models.TaskCompletionImage{
@@ -150,35 +142,30 @@ func (h *AdminCompletionImageHandler) Create(c *gin.Context) {
}
if err := h.db.Create(&image).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create completion image"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to create completion image"})
}
c.JSON(http.StatusCreated, h.toResponse(&image))
return c.JSON(http.StatusCreated, h.toResponse(&image))
}
// Update handles PUT /api/admin/completion-images/:id
func (h *AdminCompletionImageHandler) Update(c *gin.Context) {
func (h *AdminCompletionImageHandler) Update(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid image ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid image ID"})
}
var image models.TaskCompletionImage
if err := h.db.First(&image, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Completion image not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Completion image not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch completion image"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch completion image"})
}
var req UpdateCompletionImageRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
if req.ImageURL != nil {
@@ -189,53 +176,46 @@ func (h *AdminCompletionImageHandler) Update(c *gin.Context) {
}
if err := h.db.Save(&image).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update completion image"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to update completion image"})
}
c.JSON(http.StatusOK, h.toResponse(&image))
return c.JSON(http.StatusOK, h.toResponse(&image))
}
// Delete handles DELETE /api/admin/completion-images/:id
func (h *AdminCompletionImageHandler) Delete(c *gin.Context) {
func (h *AdminCompletionImageHandler) Delete(c echo.Context) error {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid image ID"})
return
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": "Invalid image ID"})
}
var image models.TaskCompletionImage
if err := h.db.First(&image, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Completion image not found"})
return
return c.JSON(http.StatusNotFound, map[string]interface{}{"error": "Completion image not found"})
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch completion image"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to fetch completion image"})
}
if err := h.db.Delete(&image).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete completion image"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete completion image"})
}
c.JSON(http.StatusOK, gin.H{"message": "Completion image deleted successfully"})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Completion image deleted successfully"})
}
// BulkDelete handles DELETE /api/admin/completion-images/bulk
func (h *AdminCompletionImageHandler) BulkDelete(c *gin.Context) {
func (h *AdminCompletionImageHandler) BulkDelete(c echo.Context) error {
var req dto.BulkDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]interface{}{"error": err.Error()})
}
if err := h.db.Where("id IN ?", req.IDs).Delete(&models.TaskCompletionImage{}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete completion images"})
return
return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "Failed to delete completion images"})
}
c.JSON(http.StatusOK, gin.H{"message": "Completion images deleted successfully", "count": len(req.IDs)})
return c.JSON(http.StatusOK, map[string]interface{}{"message": "Completion images deleted successfully", "count": len(req.IDs)})
}
// toResponse converts a TaskCompletionImage model to AdminCompletionImageResponse