package handlers import ( "mime/multipart" "net/http" "strconv" "strings" "time" "github.com/labstack/echo/v4" "github.com/shopspring/decimal" "github.com/treytartt/honeydue-api/internal/apperrors" "github.com/treytartt/honeydue-api/internal/dto/requests" "github.com/treytartt/honeydue-api/internal/dto/responses" "github.com/treytartt/honeydue-api/internal/i18n" "github.com/treytartt/honeydue-api/internal/middleware" "github.com/treytartt/honeydue-api/internal/models" "github.com/treytartt/honeydue-api/internal/repositories" "github.com/treytartt/honeydue-api/internal/services" ) // DocumentHandler handles document-related HTTP requests type DocumentHandler struct { documentService *services.DocumentService storageService *services.StorageService } // NewDocumentHandler creates a new document handler func NewDocumentHandler(documentService *services.DocumentService, storageService *services.StorageService) *DocumentHandler { return &DocumentHandler{ documentService: documentService, storageService: storageService, } } // ListDocuments handles GET /api/documents/ func (h *DocumentHandler) ListDocuments(c echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } // Build filter from supported query params. var filter *repositories.DocumentFilter if c.QueryParam("residence") != "" || c.QueryParam("document_type") != "" || c.QueryParam("is_active") != "" || c.QueryParam("expiring_soon") != "" || c.QueryParam("search") != "" { filter = &repositories.DocumentFilter{ DocumentType: c.QueryParam("document_type"), Search: c.QueryParam("search"), } if rid := c.QueryParam("residence"); rid != "" { if parsed, err := strconv.ParseUint(rid, 10, 32); err == nil { residenceID := uint(parsed) filter.ResidenceID = &residenceID } } if ia := c.QueryParam("is_active"); ia != "" { isActive := ia == "true" || ia == "1" filter.IsActive = &isActive } if es := c.QueryParam("expiring_soon"); es != "" { if parsed, err := strconv.Atoi(es); err == nil { if parsed < 1 || parsed > 3650 { return apperrors.BadRequest("error.days_out_of_range") } filter.ExpiringSoon = &parsed } } } response, err := h.documentService.ListDocuments(user.ID, filter) if err != nil { return err } return c.JSON(http.StatusOK, response) } // GetDocument handles GET /api/documents/:id/ func (h *DocumentHandler) GetDocument(c echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } documentID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_document_id") } response, err := h.documentService.GetDocument(uint(documentID), user.ID) if err != nil { return err } return c.JSON(http.StatusOK, response) } // ListWarranties handles GET /api/documents/warranties/ func (h *DocumentHandler) ListWarranties(c echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } response, err := h.documentService.ListWarranties(user.ID) if err != nil { return apperrors.Internal(err) } return c.JSON(http.StatusOK, response) } // CreateDocument handles POST /api/documents/ // Supports both JSON and multipart form data (for file uploads) func (h *DocumentHandler) CreateDocument(c echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } var req requests.CreateDocumentRequest contentType := c.Request().Header.Get("Content-Type") // Check if this is a multipart form request (file upload) if strings.HasPrefix(contentType, "multipart/form-data") { // Parse multipart form if err := c.Request().ParseMultipartForm(32 << 20); err != nil { // 32MB max return apperrors.BadRequest("error.failed_to_parse_form") } // Parse residence_id (required) residenceIDStr := c.FormValue("residence_id") if residenceIDStr == "" { return apperrors.BadRequest("error.residence_id_required") } residenceID, err := strconv.ParseUint(residenceIDStr, 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_residence_id") } req.ResidenceID = uint(residenceID) // Parse title (required) req.Title = c.FormValue("title") if req.Title == "" { return apperrors.BadRequest("error.title_required") } // Parse optional fields req.Description = c.FormValue("description") req.Vendor = c.FormValue("vendor") req.SerialNumber = c.FormValue("serial_number") req.ModelNumber = c.FormValue("model_number") // Parse document_type if docType := c.FormValue("document_type"); docType != "" { dt := models.DocumentType(docType) req.DocumentType = dt } // Parse task_id (optional) if taskIDStr := c.FormValue("task_id"); taskIDStr != "" { if taskID, err := strconv.ParseUint(taskIDStr, 10, 32); err == nil { tid := uint(taskID) req.TaskID = &tid } } // Parse purchase_price (optional) if priceStr := c.FormValue("purchase_price"); priceStr != "" { if price, err := decimal.NewFromString(priceStr); err == nil { req.PurchasePrice = &price } } // Parse purchase_date (optional) if dateStr := c.FormValue("purchase_date"); dateStr != "" { if t, err := time.Parse(time.RFC3339, dateStr); err == nil { req.PurchaseDate = &t } else if t, err := time.Parse("2006-01-02", dateStr); err == nil { req.PurchaseDate = &t } } // Parse expiry_date (optional) if dateStr := c.FormValue("expiry_date"); dateStr != "" { if t, err := time.Parse(time.RFC3339, dateStr); err == nil { req.ExpiryDate = &t } else if t, err := time.Parse("2006-01-02", dateStr); err == nil { req.ExpiryDate = &t } } // Handle file upload (look for "file", "document", or "image" field) var uploadedFile *multipart.FileHeader for _, fieldName := range []string{"file", "document", "image", "images"} { if file, err := c.FormFile(fieldName); err == nil { uploadedFile = file break } } if uploadedFile != nil { if h.storageService == nil { return apperrors.Internal(nil) } result, err := h.storageService.Upload(uploadedFile, "documents") if err != nil { return apperrors.BadRequest("error.failed_to_upload_file") } req.FileURL = result.URL req.FileName = result.FileName req.MimeType = result.MimeType fileSize := result.FileSize req.FileSize = &fileSize } } else { // Standard JSON request if err := c.Bind(&req); err != nil { return apperrors.BadRequest("error.invalid_request") } } if err := c.Validate(&req); err != nil { return err } response, err := h.documentService.CreateDocument(&req, user.ID) if err != nil { return err } return c.JSON(http.StatusCreated, response) } // UpdateDocument handles PUT/PATCH /api/documents/:id/ func (h *DocumentHandler) UpdateDocument(c echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } documentID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_document_id") } var req requests.UpdateDocumentRequest if err := c.Bind(&req); err != nil { return apperrors.BadRequest("error.invalid_request") } if err := c.Validate(&req); err != nil { return err } response, err := h.documentService.UpdateDocument(uint(documentID), user.ID, &req) if err != nil { return err } return c.JSON(http.StatusOK, response) } // DeleteDocument handles DELETE /api/documents/:id/ func (h *DocumentHandler) DeleteDocument(c echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } documentID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_document_id") } err = h.documentService.DeleteDocument(uint(documentID), user.ID) if err != nil { return err } return c.JSON(http.StatusOK, responses.MessageResponse{Message: i18n.LocalizedMessage(c, "message.document_deleted")}) } // ActivateDocument handles POST /api/documents/:id/activate/ func (h *DocumentHandler) ActivateDocument(c echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } documentID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_document_id") } response, err := h.documentService.ActivateDocument(uint(documentID), user.ID) if err != nil { return err } return c.JSON(http.StatusOK, response) } // DeactivateDocument handles POST /api/documents/:id/deactivate/ func (h *DocumentHandler) DeactivateDocument(c echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } documentID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_document_id") } response, err := h.documentService.DeactivateDocument(uint(documentID), user.ID) if err != nil { return err } return c.JSON(http.StatusOK, response) } // UploadDocumentImage handles POST /api/documents/:id/images/ func (h *DocumentHandler) UploadDocumentImage(c echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } documentID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_document_id") } // Parse multipart form if err := c.Request().ParseMultipartForm(32 << 20); err != nil { return apperrors.BadRequest("error.failed_to_parse_form") } // Look for file in common field names var uploadedFile *multipart.FileHeader for _, fieldName := range []string{"image", "file"} { if file, err := c.FormFile(fieldName); err == nil { uploadedFile = file break } } if uploadedFile == nil { return apperrors.BadRequest("error.no_file_provided") } if h.storageService == nil { return apperrors.Internal(nil) } result, err := h.storageService.Upload(uploadedFile, "images") if err != nil { return apperrors.BadRequest("error.failed_to_upload_file") } caption := c.FormValue("caption") response, err := h.documentService.UploadDocumentImage(uint(documentID), user.ID, result.URL, caption) if err != nil { return err } return c.JSON(http.StatusCreated, response) } // DeleteDocumentImage handles DELETE /api/documents/:id/images/:imageId/ func (h *DocumentHandler) DeleteDocumentImage(c echo.Context) error { user, err := middleware.MustGetAuthUser(c) if err != nil { return err } documentID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_document_id") } imageID, err := strconv.ParseUint(c.Param("imageId"), 10, 32) if err != nil { return apperrors.BadRequest("error.invalid_image_id") } response, err := h.documentService.DeleteDocumentImage(uint(documentID), uint(imageID), user.ID) if err != nil { return err } return c.JSON(http.StatusOK, response) }