package handlers import ( "errors" "mime/multipart" "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" "github.com/shopspring/decimal" "github.com/treytartt/casera-api/internal/dto/requests" "github.com/treytartt/casera-api/internal/i18n" "github.com/treytartt/casera-api/internal/middleware" "github.com/treytartt/casera-api/internal/models" "github.com/treytartt/casera-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 *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) response, err := h.documentService.ListDocuments(user.ID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, response) } // GetDocument handles GET /api/documents/:id/ func (h *DocumentHandler) GetDocument(c *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) documentID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_document_id")}) return } response, err := h.documentService.GetDocument(uint(documentID), user.ID) if err != nil { switch { case errors.Is(err, services.ErrDocumentNotFound): c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.document_not_found")}) case errors.Is(err, services.ErrDocumentAccessDenied): c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.document_access_denied")}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } return } c.JSON(http.StatusOK, response) } // ListWarranties handles GET /api/documents/warranties/ func (h *DocumentHandler) ListWarranties(c *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) response, err := h.documentService.ListWarranties(user.ID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 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 *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) var req requests.CreateDocumentRequest contentType := c.GetHeader("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 c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_parse_form")}) return } // Parse residence_id (required) residenceIDStr := c.PostForm("residence_id") if residenceIDStr == "" { c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_id_required")}) return } residenceID, err := strconv.ParseUint(residenceIDStr, 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_residence_id")}) return } req.ResidenceID = uint(residenceID) // Parse title (required) req.Title = c.PostForm("title") if req.Title == "" { c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.title_required")}) return } // Parse optional fields req.Description = c.PostForm("description") req.Vendor = c.PostForm("vendor") req.SerialNumber = c.PostForm("serial_number") req.ModelNumber = c.PostForm("model_number") // Parse document_type if docType := c.PostForm("document_type"); docType != "" { dt := models.DocumentType(docType) req.DocumentType = dt } // Parse task_id (optional) if taskIDStr := c.PostForm("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.PostForm("purchase_price"); priceStr != "" { if price, err := decimal.NewFromString(priceStr); err == nil { req.PurchasePrice = &price } } // Parse purchase_date (optional) if dateStr := c.PostForm("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.PostForm("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 && h.storageService != nil { result, err := h.storageService.Upload(uploadedFile, "documents") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.failed_to_upload_file")}) return } 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.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } } response, err := h.documentService.CreateDocument(&req, user.ID) if err != nil { if errors.Is(err, services.ErrResidenceAccessDenied) { c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.residence_access_denied")}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, response) } // UpdateDocument handles PUT/PATCH /api/documents/:id/ func (h *DocumentHandler) UpdateDocument(c *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) documentID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_document_id")}) return } var req requests.UpdateDocumentRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } response, err := h.documentService.UpdateDocument(uint(documentID), user.ID, &req) if err != nil { switch { case errors.Is(err, services.ErrDocumentNotFound): c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.document_not_found")}) case errors.Is(err, services.ErrDocumentAccessDenied): c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.document_access_denied")}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } return } c.JSON(http.StatusOK, response) } // DeleteDocument handles DELETE /api/documents/:id/ func (h *DocumentHandler) DeleteDocument(c *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) documentID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_document_id")}) return } err = h.documentService.DeleteDocument(uint(documentID), user.ID) if err != nil { switch { case errors.Is(err, services.ErrDocumentNotFound): c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.document_not_found")}) case errors.Is(err, services.ErrDocumentAccessDenied): c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.document_access_denied")}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } return } c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.document_deleted")}) } // ActivateDocument handles POST /api/documents/:id/activate/ func (h *DocumentHandler) ActivateDocument(c *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) documentID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_document_id")}) return } response, err := h.documentService.ActivateDocument(uint(documentID), user.ID) if err != nil { switch { case errors.Is(err, services.ErrDocumentNotFound): c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.document_not_found")}) case errors.Is(err, services.ErrDocumentAccessDenied): c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.document_access_denied")}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } return } c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.document_activated"), "document": response}) } // DeactivateDocument handles POST /api/documents/:id/deactivate/ func (h *DocumentHandler) DeactivateDocument(c *gin.Context) { user := c.MustGet(middleware.AuthUserKey).(*models.User) documentID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": i18n.LocalizedMessage(c, "error.invalid_document_id")}) return } response, err := h.documentService.DeactivateDocument(uint(documentID), user.ID) if err != nil { switch { case errors.Is(err, services.ErrDocumentNotFound): c.JSON(http.StatusNotFound, gin.H{"error": i18n.LocalizedMessage(c, "error.document_not_found")}) case errors.Is(err, services.ErrDocumentAccessDenied): c.JSON(http.StatusForbidden, gin.H{"error": i18n.LocalizedMessage(c, "error.document_access_denied")}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) } return } c.JSON(http.StatusOK, gin.H{"message": i18n.LocalizedMessage(c, "message.document_deactivated"), "document": response}) }