package controllers import ( "errors" "net/http" "strconv" "strings" "github.com/gin-gonic/gin" "gorm.io/gorm" shopModels "goaresv3/app/shop/models" "goaresv3/config" ) type UpsertProductCategoryRequest struct { Title string `json:"title" binding:"required,max=254"` Slug string `json:"slug" binding:"required,max=254"` Description string `json:"description"` Keywords string `json:"keywords"` ParentID *uint `json:"parent_id"` } type UpsertProductTagRequest struct { Name string `json:"name" binding:"required,max=254"` } type UpsertProductRequest struct { Title string `json:"title" binding:"required,max=254"` Images string `json:"images" binding:"required"` Price float64 `json:"price"` Width int `json:"width"` Height int `json:"height"` Quality int `json:"quality"` Format string `json:"format" binding:"omitempty,max=10"` Content string `json:"content"` Slug string `json:"slug" binding:"required,max=254"` CategoryIDs []uint `json:"category_ids"` TagIDs []uint `json:"tag_ids"` } type UpsertCartItemRequest struct { ProductID uint `json:"product_id" binding:"required"` Quantity int `json:"quantity" binding:"required,min=1"` } func parseShopID(c *gin.Context, key string) (uint, bool) { id, err := strconv.ParseUint(strings.TrimSpace(c.Param(key)), 10, 64) if err != nil || id == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) return 0, false } return uint(id), true } // ListProductCategories godoc // @Summary List product categories // @Description Returns all categories with children. // @Tags Shop // @Produce json // @Success 200 {array} map[string]interface{} // @Failure 500 {object} map[string]string // @Router /api/v1/shop/categories [get] func ListProductCategories(c *gin.Context) { var items []shopModels.ProductCategory if err := config.DB.Preload("Children").Order("id DESC").Find(&items).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch categories"}) return } c.JSON(http.StatusOK, items) } // CreateProductCategory godoc // @Summary Create product category // @Description Creates a new shop category. // @Tags Shop // @Security BearerAuth // @Accept json // @Produce json // @Param request body UpsertProductCategoryRequest true "category payload" // @Success 201 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 409 {object} map[string]string // @Router /api/v1/shop/categories [post] func CreateProductCategory(c *gin.Context) { var req UpsertProductCategoryRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } item := shopModels.ProductCategory{ Title: req.Title, Slug: req.Slug, Description: req.Description, Keywords: req.Keywords, ParentID: req.ParentID, } if err := config.DB.Create(&item).Error; err != nil { c.JSON(http.StatusConflict, gin.H{"error": "failed to create category"}) return } c.JSON(http.StatusCreated, item) } // UpdateProductCategory godoc // @Summary Update product category // @Description Updates a category by id. // @Tags Shop // @Security BearerAuth // @Accept json // @Produce json // @Param id path int true "category id" // @Param request body UpsertProductCategoryRequest true "category payload" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Failure 409 {object} map[string]string // @Router /api/v1/shop/categories/{id} [put] func UpdateProductCategory(c *gin.Context) { id, ok := parseShopID(c, "id") if !ok { return } var req UpsertProductCategoryRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var item shopModels.ProductCategory if err := config.DB.First(&item, id).Error; errors.Is(err, gorm.ErrRecordNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "category not found"}) return } else if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch category"}) return } if err := config.DB.Model(&item).Updates(map[string]any{ "title": req.Title, "slug": req.Slug, "description": req.Description, "keywords": req.Keywords, "parent_id": req.ParentID, }).Error; err != nil { c.JSON(http.StatusConflict, gin.H{"error": "failed to update category"}) return } _ = config.DB.First(&item, id).Error c.JSON(http.StatusOK, item) } // DeleteProductCategory godoc // @Summary Delete product category // @Description Deletes a category by id. // @Tags Shop // @Security BearerAuth // @Produce json // @Param id path int true "category id" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/shop/categories/{id} [delete] func DeleteProductCategory(c *gin.Context) { id, ok := parseShopID(c, "id") if !ok { return } res := config.DB.Delete(&shopModels.ProductCategory{}, id) if res.Error != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete category"}) return } if res.RowsAffected == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "category not found"}) return } c.JSON(http.StatusOK, gin.H{"message": "category deleted"}) } // ListProductTags godoc // @Summary List product tags // @Description Returns all product tags. // @Tags Shop // @Produce json // @Success 200 {array} map[string]interface{} // @Failure 500 {object} map[string]string // @Router /api/v1/shop/tags [get] func ListProductTags(c *gin.Context) { var items []shopModels.ProductTag if err := config.DB.Order("id DESC").Find(&items).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch tags"}) return } c.JSON(http.StatusOK, items) } // CreateProductTag godoc // @Summary Create product tag // @Description Creates a new product tag. // @Tags Shop // @Security BearerAuth // @Accept json // @Produce json // @Param request body UpsertProductTagRequest true "tag payload" // @Success 201 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 409 {object} map[string]string // @Router /api/v1/shop/tags [post] func CreateProductTag(c *gin.Context) { var req UpsertProductTagRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } item := shopModels.ProductTag{Name: req.Name} if err := config.DB.Create(&item).Error; err != nil { c.JSON(http.StatusConflict, gin.H{"error": "failed to create tag"}) return } c.JSON(http.StatusCreated, item) } // UpdateProductTag godoc // @Summary Update product tag // @Description Updates a tag by id. // @Tags Shop // @Security BearerAuth // @Accept json // @Produce json // @Param id path int true "tag id" // @Param request body UpsertProductTagRequest true "tag payload" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Failure 409 {object} map[string]string // @Router /api/v1/shop/tags/{id} [put] func UpdateProductTag(c *gin.Context) { id, ok := parseShopID(c, "id") if !ok { return } var req UpsertProductTagRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var item shopModels.ProductTag if err := config.DB.First(&item, id).Error; errors.Is(err, gorm.ErrRecordNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "tag not found"}) return } else if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch tag"}) return } if err := config.DB.Model(&item).Update("name", req.Name).Error; err != nil { c.JSON(http.StatusConflict, gin.H{"error": "failed to update tag"}) return } _ = config.DB.First(&item, id).Error c.JSON(http.StatusOK, item) } // DeleteProductTag godoc // @Summary Delete product tag // @Description Deletes a tag by id. // @Tags Shop // @Security BearerAuth // @Produce json // @Param id path int true "tag id" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/shop/tags/{id} [delete] func DeleteProductTag(c *gin.Context) { id, ok := parseShopID(c, "id") if !ok { return } res := config.DB.Delete(&shopModels.ProductTag{}, id) if res.Error != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete tag"}) return } if res.RowsAffected == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "tag not found"}) return } c.JSON(http.StatusOK, gin.H{"message": "tag deleted"}) } // ListProducts godoc // @Summary List products // @Description Returns all products with categories and tags. // @Tags Shop // @Produce json // @Success 200 {array} map[string]interface{} // @Failure 500 {object} map[string]string // @Router /api/v1/shop/products [get] func ListProducts(c *gin.Context) { var items []shopModels.Product if err := config.DB.Preload("Categories").Preload("Tags").Order("id DESC").Find(&items).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch products"}) return } c.JSON(http.StatusOK, items) } // GetProduct godoc // @Summary Get product // @Description Returns product details by id. // @Tags Shop // @Produce json // @Param id path int true "product id" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/shop/products/{id} [get] func GetProduct(c *gin.Context) { id, ok := parseShopID(c, "id") if !ok { return } var item shopModels.Product if err := config.DB.Preload("Categories").Preload("Tags").First(&item, id).Error; errors.Is(err, gorm.ErrRecordNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "product not found"}) return } else if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch product"}) return } c.JSON(http.StatusOK, item) } // CreateProduct godoc // @Summary Create product // @Description Creates a new product and assigns category/tag relations. // @Tags Shop // @Security BearerAuth // @Accept json // @Produce json // @Param request body UpsertProductRequest true "product payload" // @Success 201 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 409 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/shop/products [post] func CreateProduct(c *gin.Context) { var req UpsertProductRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } item := shopModels.Product{ Title: req.Title, Images: req.Images, Price: req.Price, Width: req.Width, Height: req.Height, Quality: req.Quality, Format: req.Format, Content: req.Content, Slug: req.Slug, } if err := config.DB.Create(&item).Error; err != nil { c.JSON(http.StatusConflict, gin.H{"error": "failed to create product"}) return } if err := assignProductRelations(item.ID, req.CategoryIDs, req.TagIDs); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to assign product relations"}) return } _ = config.DB.Preload("Categories").Preload("Tags").First(&item, item.ID).Error c.JSON(http.StatusCreated, item) } // UpdateProduct godoc // @Summary Update product // @Description Updates a product and reassigns category/tag relations. // @Tags Shop // @Security BearerAuth // @Accept json // @Produce json // @Param id path int true "product id" // @Param request body UpsertProductRequest true "product payload" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Failure 409 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/shop/products/{id} [put] func UpdateProduct(c *gin.Context) { id, ok := parseShopID(c, "id") if !ok { return } var req UpsertProductRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var item shopModels.Product if err := config.DB.First(&item, id).Error; errors.Is(err, gorm.ErrRecordNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "product not found"}) return } else if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch product"}) return } if err := config.DB.Model(&item).Updates(map[string]any{ "title": req.Title, "images": req.Images, "price": req.Price, "width": req.Width, "height": req.Height, "quality": req.Quality, "format": req.Format, "content": req.Content, "slug": req.Slug, }).Error; err != nil { c.JSON(http.StatusConflict, gin.H{"error": "failed to update product"}) return } if err := assignProductRelations(item.ID, req.CategoryIDs, req.TagIDs); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to assign product relations"}) return } _ = config.DB.Preload("Categories").Preload("Tags").First(&item, id).Error c.JSON(http.StatusOK, item) } // DeleteProduct godoc // @Summary Delete product // @Description Deletes a product by id. // @Tags Shop // @Security BearerAuth // @Produce json // @Param id path int true "product id" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/shop/products/{id} [delete] func DeleteProduct(c *gin.Context) { id, ok := parseShopID(c, "id") if !ok { return } res := config.DB.Delete(&shopModels.Product{}, id) if res.Error != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete product"}) return } if res.RowsAffected == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "product not found"}) return } c.JSON(http.StatusOK, gin.H{"message": "product deleted"}) } // GetMyCart godoc // @Summary Get my cart // @Description Returns the authenticated user's cart with items. // @Tags Shop // @Security BearerAuth // @Produce json // @Success 200 {object} map[string]interface{} // @Failure 500 {object} map[string]string // @Router /api/v1/shop/cart [get] func GetMyCart(c *gin.Context) { userID := c.GetUint("user_id") var cart shopModels.Cart err := config.DB.Preload("Items.Product").Where("user_id = ?", userID).First(&cart).Error if errors.Is(err, gorm.ErrRecordNotFound) { c.JSON(http.StatusOK, gin.H{"items": []any{}}) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch cart"}) return } c.JSON(http.StatusOK, cart) } // AddCartItem godoc // @Summary Add item to my cart // @Description Creates or increments a cart item for authenticated user. // @Tags Shop // @Security BearerAuth // @Accept json // @Produce json // @Param request body UpsertCartItemRequest true "cart item payload" // @Success 200 {object} map[string]interface{} // @Success 201 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/shop/cart/items [post] func AddCartItem(c *gin.Context) { userID := c.GetUint("user_id") var req UpsertCartItemRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } cart, err := ensureCart(userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get cart"}) return } var item shopModels.CartItem err = config.DB.Where("cart_id = ? AND product_id = ?", cart.ID, req.ProductID).First(&item).Error if errors.Is(err, gorm.ErrRecordNotFound) { item = shopModels.CartItem{CartID: cart.ID, ProductID: req.ProductID, Quantity: req.Quantity} if err := config.DB.Create(&item).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to add cart item"}) return } c.JSON(http.StatusCreated, item) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch cart item"}) return } if err := config.DB.Model(&item).Update("quantity", item.Quantity+req.Quantity).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update cart item"}) return } _ = config.DB.First(&item, item.ID).Error c.JSON(http.StatusOK, item) } // UpdateCartItem godoc // @Summary Update my cart item // @Description Updates a cart item owned by authenticated user. // @Tags Shop // @Security BearerAuth // @Accept json // @Produce json // @Param itemId path int true "cart item id" // @Param request body UpsertCartItemRequest true "cart item payload" // @Success 200 {object} map[string]interface{} // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/shop/cart/items/{itemId} [put] func UpdateCartItem(c *gin.Context) { itemID, ok := parseShopID(c, "itemId") if !ok { return } userID := c.GetUint("user_id") var req UpsertCartItemRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var item shopModels.CartItem err := config.DB.Joins("JOIN carts ON carts.id = cart_items.cart_id"). Where("cart_items.id = ? AND carts.user_id = ?", itemID, userID). First(&item).Error if errors.Is(err, gorm.ErrRecordNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "cart item not found"}) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch cart item"}) return } if err := config.DB.Model(&item).Updates(map[string]any{ "product_id": req.ProductID, "quantity": req.Quantity, }).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update cart item"}) return } _ = config.DB.First(&item, itemID).Error c.JSON(http.StatusOK, item) } // DeleteCartItem godoc // @Summary Delete item from my cart // @Description Deletes a cart item owned by authenticated user. // @Tags Shop // @Security BearerAuth // @Produce json // @Param itemId path int true "cart item id" // @Success 200 {object} map[string]string // @Failure 400 {object} map[string]string // @Failure 404 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /api/v1/shop/cart/items/{itemId} [delete] func DeleteCartItem(c *gin.Context) { itemID, ok := parseShopID(c, "itemId") if !ok { return } userID := c.GetUint("user_id") var item shopModels.CartItem err := config.DB.Joins("JOIN carts ON carts.id = cart_items.cart_id"). Where("cart_items.id = ? AND carts.user_id = ?", itemID, userID). First(&item).Error if errors.Is(err, gorm.ErrRecordNotFound) { c.JSON(http.StatusNotFound, gin.H{"error": "cart item not found"}) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch cart item"}) return } if err := config.DB.Delete(&item).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete cart item"}) return } c.JSON(http.StatusOK, gin.H{"message": "cart item deleted"}) } func ensureCart(userID uint) (*shopModels.Cart, error) { var cart shopModels.Cart err := config.DB.Where("user_id = ?", userID).First(&cart).Error if errors.Is(err, gorm.ErrRecordNotFound) { cart = shopModels.Cart{UserID: userID} if createErr := config.DB.Create(&cart).Error; createErr != nil { return nil, createErr } return &cart, nil } if err != nil { return nil, err } return &cart, nil } func assignProductRelations(productID uint, categoryIDs, tagIDs []uint) error { var p shopModels.Product if err := config.DB.First(&p, productID).Error; err != nil { return err } if categoryIDs != nil { var categories []shopModels.ProductCategory if len(categoryIDs) > 0 { if err := config.DB.Where("id IN ?", categoryIDs).Find(&categories).Error; err != nil { return err } } if err := config.DB.Model(&p).Association("Categories").Replace(categories); err != nil { return err } } if tagIDs != nil { var tags []shopModels.ProductTag if len(tagIDs) > 0 { if err := config.DB.Where("id IN ?", tagIDs).Find(&tags).Error; err != nil { return err } } if err := config.DB.Model(&p).Association("Tags").Replace(tags); err != nil { return err } } return nil }