200 lines
5.0 KiB
Go
200 lines
5.0 KiB
Go
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())
|
|
}
|
|
}
|