package handler import ( "errors" "strconv" "accounting-app/pkg/api" "accounting-app/internal/service" "github.com/gin-gonic/gin" ) // ImageHandler handles HTTP requests for transaction image operations // Feature: accounting-feature-upgrade // Validates: Requirements 4.1-4.13 type ImageHandler struct { imageService *service.ImageService } // NewImageHandler creates a new ImageHandler instance func NewImageHandler(imageService *service.ImageService) *ImageHandler { return &ImageHandler{ imageService: imageService, } } // UploadImage handles POST /api/v1/transactions/:id/images // Uploads an image attachment for a transaction // Validates: Requirements 4.3, 4.4, 4.9-4.13 func (h *ImageHandler) UploadImage(c *gin.Context) { userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } // Parse transaction ID transactionID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { api.BadRequest(c, "Invalid transaction ID") return } // Get compression level from query parameter (default: medium) compressionStr := c.DefaultQuery("compression", "medium") compression := service.CompressionLevel(compressionStr) // Validate compression level if compression != service.CompressionLow && compression != service.CompressionMedium && compression != service.CompressionHigh { api.BadRequest(c, "Invalid compression level. Must be 'low', 'medium', or 'high'") return } // Get uploaded file file, err := c.FormFile("image") if err != nil { api.BadRequest(c, "No image file provided") return } // Upload image input := service.UploadImageInput{ UserID: userId.(uint), TransactionID: uint(transactionID), File: file, Compression: compression, } image, err := h.imageService.UploadImage(input) if err != nil { handleImageError(c, err) return } api.Created(c, image) } // GetImage handles GET /api/v1/images/:id // Retrieves an image file by ID // Validates: Requirements 4.8 func (h *ImageHandler) GetImage(c *gin.Context) { userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } // Parse image ID imageID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { api.BadRequest(c, "Invalid image ID") return } // Get image record image, err := h.imageService.GetImage(userId.(uint), uint(imageID)) if err != nil { handleImageError(c, err) return } // Serve the file c.File(image.FilePath) } // GetTransactionImages handles GET /api/v1/transactions/:id/images // Retrieves all images for a transaction // Validates: Requirements 4.8 func (h *ImageHandler) GetTransactionImages(c *gin.Context) { userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } // Parse transaction ID transactionID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { api.BadRequest(c, "Invalid transaction ID") return } // Get images images, err := h.imageService.GetImagesByTransaction(userId.(uint), uint(transactionID)) if err != nil { handleImageError(c, err) return } api.Success(c, images) } // DeleteImage handles DELETE /api/v1/transactions/:id/images/:imageId // Deletes an image attachment // Validates: Requirements 4.7 func (h *ImageHandler) DeleteImage(c *gin.Context) { userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } // Parse transaction ID transactionID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { api.BadRequest(c, "Invalid transaction ID") return } // Parse image ID imageID, err := strconv.ParseUint(c.Param("imageId"), 10, 32) if err != nil { api.BadRequest(c, "Invalid image ID") return } // Delete image err = h.imageService.DeleteImage(userId.(uint), uint(imageID), uint(transactionID)) if err != nil { handleImageError(c, err) return } api.NoContent(c) } // RegisterRoutes registers all image routes to the given router group func (h *ImageHandler) RegisterRoutes(rg *gin.RouterGroup) { // Transaction image routes transactions := rg.Group("/transactions") { transactions.POST("/:id/images", h.UploadImage) transactions.GET("/:id/images", h.GetTransactionImages) transactions.DELETE("/:id/images/:imageId", h.DeleteImage) } // Direct image access route rg.GET("/images/:id", h.GetImage) } // handleImageError handles common image service errors func handleImageError(c *gin.Context, err error) { switch { case errors.Is(err, service.ErrInvalidImageFormat): api.BadRequest(c, "Invalid image format. Only JPEG, PNG, and HEIC are supported") case errors.Is(err, service.ErrImageTooLarge): api.RequestEntityTooLarge(c, "Image size exceeds 10MB limit") case errors.Is(err, service.ErrMaxImagesExceeded): api.BadRequest(c, "Maximum 9 images per transaction") case errors.Is(err, service.ErrImageTransactionNotFound): api.NotFound(c, "Transaction not found") case errors.Is(err, service.ErrImageNotFound): api.NotFound(c, "Image not found") default: api.InternalError(c, "Failed to process image: "+err.Error()) } }