From 9895b86666df24a816bb88be33b2a8c8bcffa15d Mon Sep 17 00:00:00 2001 From: nquidox Date: Tue, 28 Oct 2025 20:06:32 +0300 Subject: [PATCH] labels crud added --- internal/api/merch/controller.go | 223 +++++++++++++++++++++++++++++++ internal/api/merch/dto.go | 12 ++ internal/api/merch/model.go | 18 +++ internal/api/merch/repository.go | 48 +++++++ internal/api/merch/service.go | 87 ++++++++++++ 5 files changed, 388 insertions(+) diff --git a/internal/api/merch/controller.go b/internal/api/merch/controller.go index 06fd408..ddf3bd6 100644 --- a/internal/api/merch/controller.go +++ b/internal/api/merch/controller.go @@ -2,6 +2,7 @@ package merch import ( "context" + "errors" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" "merch-parser-api/internal/interfaces" @@ -42,6 +43,14 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup, authMW gin.HandlerFunc, ref imagesGroup.POST("/:uuid", h.controller.uploadMerchImage) imagesGroup.GET("/:uuid", h.controller.getMerchImage) imagesGroup.DELETE("/:uuid", h.controller.deleteMerchImage) + + labelsGroup := merchGroup.Group("/labels") + labelsGroup.POST("/", h.controller.createLabel) + labelsGroup.GET("/", h.controller.getLabels) + labelsGroup.PUT("/:uuid", h.controller.updateLabel) + labelsGroup.DELETE("/:uuid", h.controller.deleteLabel) + labelsGroup.POST("/attach", h.controller.attachLabel) + labelsGroup.POST("/detach", h.controller.detachLabel) } // @Summary Добавить новый мерч @@ -415,3 +424,217 @@ func (co *controller) deleteMerchImage(c *gin.Context) { } c.Status(http.StatusOK) } + +// @Summary Создать новую метку для товара +// @Description Создать новую метку для товара +// @Tags Merch labels +// @Security BearerAuth +// @Param payload body LabelDTO true "payload" +// @Success 200 +// @Failure 400 {object} responses.ErrorResponse400 +// @Failure 500 {object} responses.ErrorResponse500 +// @Router /merch/labels [post] +func (co *controller) createLabel(c *gin.Context) { + const logMsg = "Merch | Create label" + + userUuid, err := co.utils.GetUserUuidFromContext(c) + if err != nil { + c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()}) + log.WithError(err).Error(logMsg) + return + } + + var payload LabelDTO + if err = c.ShouldBindJSON(&payload); err != nil { + c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()}) + log.WithError(err).Error(logMsg) + return + } + + if err = co.service.createLabel(payload, userUuid); err != nil { + c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()}) + log.WithError(err).Error(logMsg) + return + } + + c.Status(http.StatusOK) +} + +// @Summary Получить все метки товаров +// @Description Получить все метки товаров +// @Tags Merch labels +// @Security BearerAuth +// @Success 200 {array} LabelDTO +// @Failure 400 {object} responses.ErrorResponse400 +// @Failure 500 {object} responses.ErrorResponse500 +// @Router /merch/labels [get] +func (co *controller) getLabels(c *gin.Context) { + const logMsg = "Merch | Get labels" + + userUuid, err := co.utils.GetUserUuidFromContext(c) + if err != nil { + c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()}) + log.WithError(err).Error(logMsg) + return + } + + response, err := co.service.getLabels(userUuid) + if err != nil { + c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()}) + log.WithError(err).Error(logMsg) + return + } + + c.JSON(http.StatusOK, response) +} + +// @Summary Изменить метку +// @Description Изменить метку +// @Tags Merch labels +// @Security BearerAuth +// @Param uuid path string true "label uuid" +// @Param payload body LabelDTO true "payload" +// @Success 200 +// @Failure 400 {object} responses.ErrorResponse400 +// @Failure 500 {object} responses.ErrorResponse500 +// @Router /merch/labels/{uuid} [put] +func (co *controller) updateLabel(c *gin.Context) { + const logMsg = "Merch | Update label" + + userUuid, err := co.utils.GetUserUuidFromContext(c) + if err != nil { + c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()}) + log.WithError(err).Error(logMsg) + return + } + + labelUuid := c.Param("uuid") + if labelUuid == "" { + c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "label uuid is empty"}) + log.WithError(err).Error(logMsg) + return + } + + var payload LabelDTO + if err = c.ShouldBindJSON(&payload); err != nil { + c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()}) + log.WithError(err).Error(logMsg) + return + } + + if labelUuid != payload.LabelUuid { + err = errors.New("label uuid is different") + c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()}) + log.WithError(err).Error(logMsg) + return + } + + if err = co.service.updateLabel(userUuid, payload); err != nil { + c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()}) + log.WithError(err).Error(logMsg) + return + } + c.Status(http.StatusOK) +} + +// @Summary Пометить метку как удаленную +// @Description Пометить метку как удаленную +// @Tags Merch labels +// @Security BearerAuth +// @Param uuid path string true "label uuid" +// @Success 200 +// @Failure 400 {object} responses.ErrorResponse400 +// @Failure 500 {object} responses.ErrorResponse500 +// @Router /merch/labels/{uuid} [delete] +func (co *controller) deleteLabel(c *gin.Context) { + const logMsg = "Merch | Delete label" + + userUuid, err := co.utils.GetUserUuidFromContext(c) + if err != nil { + c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()}) + log.WithError(err).Error(logMsg) + return + } + + labelUuid := c.Param("uuid") + if labelUuid == "" { + c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: "label uuid is empty"}) + log.WithError(err).Error(logMsg) + return + } + + if err = co.service.deleteLabel(userUuid, labelUuid); err != nil { + c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()}) + log.WithError(err).Error(logMsg) + return + } + c.Status(http.StatusOK) +} + +// @Summary Прикрепить метку к товару +// @Description Прикрепить метку к товару +// @Tags Merch labels +// @Security BearerAuth +// @Param payload body LabelLink true "payload" +// @Success 200 +// @Failure 400 {object} responses.ErrorResponse400 +// @Failure 500 {object} responses.ErrorResponse500 +// @Router /merch/labels/attach [post] +func (co *controller) attachLabel(c *gin.Context) { + const logMsg = "Merch | Attach label" + + userUuid, err := co.utils.GetUserUuidFromContext(c) + if err != nil { + c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()}) + log.WithError(err).Error(logMsg) + return + } + + var payload LabelLink + if err = c.ShouldBindJSON(&payload); err != nil { + c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()}) + log.WithError(err).Error(logMsg) + return + } + + if err = co.service.attachLabel(userUuid, payload); err != nil { + c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()}) + log.WithError(err).Error(logMsg) + return + } + c.Status(http.StatusOK) +} + +// @Summary Удалить привязку метки к товару +// @Description Удалить привязку метки к товару +// @Tags Merch labels +// @Security BearerAuth +// @Param payload body LabelLink true "payload" +// @Success 200 +// @Failure 400 {object} responses.ErrorResponse400 +// @Failure 500 {object} responses.ErrorResponse500 +// @Router /merch/labels/detach [post] +func (co *controller) detachLabel(c *gin.Context) { + const logMsg = "Merch | Detach label" + + userUuid, err := co.utils.GetUserUuidFromContext(c) + if err != nil { + c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()}) + log.WithError(err).Error(logMsg) + return + } + + var payload LabelLink + if err = c.ShouldBindJSON(&payload); err != nil { + c.JSON(http.StatusBadRequest, responses.ErrorResponse400{Error: err.Error()}) + log.WithError(err).Error(logMsg) + return + } + + if err = co.service.detachLabel(userUuid, payload); err != nil { + c.JSON(http.StatusInternalServerError, responses.ErrorResponse500{Error: err.Error()}) + log.WithError(err).Error(logMsg) + return + } + c.Status(http.StatusOK) +} diff --git a/internal/api/merch/dto.go b/internal/api/merch/dto.go index bc1646e..fc1b077 100644 --- a/internal/api/merch/dto.go +++ b/internal/api/merch/dto.go @@ -58,3 +58,15 @@ type ImageLink struct { Link string `json:"link"` ETag string `json:"etag"` } + +type LabelDTO struct { + LabelUuid string `json:"label_uuid"` + Name string `json:"name"` + Color string `json:"color"` + BgColor string `json:"bg_color"` +} + +type LabelLink struct { + MerchUuid string `json:"merch_uuid"` + LabelUuid string `json:"label_uuid"` +} diff --git a/internal/api/merch/model.go b/internal/api/merch/model.go index 9322547..088cb39 100644 --- a/internal/api/merch/model.go +++ b/internal/api/merch/model.go @@ -50,3 +50,21 @@ type Price struct { Price int `json:"price" gorm:"column:price"` Origin Origin `json:"origin" gorm:"column:origin;type:integer"` } + +type Label struct { + Id uint `json:"-" gorm:"primary_key"` + CreatedAt time.Time `json:"created_at" gorm:"column:created_at"` + UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"` + DeletedAt sql.NullTime `json:"deleted_at" gorm:"column:deleted_at"` + LabelUuid string `json:"label_uuid" gorm:"column:label_uuid"` + UserUuid string `json:"user_uuid" gorm:"column:user_uuid"` + Name string `json:"name" gorm:"column:name"` + Color string `json:"color" gorm:"column:color"` + BgColor string `json:"bg_color" gorm:"column:bg_color"` +} + +type CardLabel struct { + LabelUuid string `json:"label_uuid"` + UserUuid string `json:"user_uuid"` + MerchUuid string `json:"merch_uuid"` +} diff --git a/internal/api/merch/repository.go b/internal/api/merch/repository.go index 98d2695..24ff87b 100644 --- a/internal/api/merch/repository.go +++ b/internal/api/merch/repository.go @@ -32,6 +32,7 @@ type repository interface { getAllUserMerch(userUuid string) ([]Merch, error) prices + labels } type prices interface { @@ -39,6 +40,15 @@ type prices interface { getDistinctPrices(userUuid, merchUuid string, period time.Time) (prices []Price, err error) } +type labels interface { + createLabel(label Label) error + getLabels(userUuid string) ([]Label, error) + updateLabel(userUuid, labelUuid string, label map[string]string) error + deleteLabel(userUuid, labelUuid string) error + attachLabel(label CardLabel) error + detachLabel(label CardLabel) error +} + func (r *Repo) addMerch(bundle merchBundle) error { if err := r.db.Model(&Merch{}).Create(bundle.Merch).Error; err != nil { return err @@ -239,3 +249,41 @@ func (r *Repo) upsertOrigin(model any) error { DoUpdates: clause.AssignmentColumns([]string{"link"}), }).Create(model).Error } + +func (r *Repo) createLabel(label Label) error { + return r.db.Model(&Label{}).Create(label).Error +} +func (r *Repo) getLabels(userUuid string) ([]Label, error) { + var labels []Label + + if err := r.db. + Where("user_uuid = ?", userUuid). + Where("deleted_at IS NULL"). + Find(labels).Error; err != nil { + return nil, err + } + + return labels, nil +} + +func (r *Repo) updateLabel(userUuid, labelUuid string, label map[string]string) error { + return r.db.Model(&Label{}). + Where("user_uuid =? AND label_uuid = ?", userUuid, labelUuid). + Updates(label).Error +} + +func (r *Repo) deleteLabel(userUuid, labelUuid string) error { + return r.db.Model(&Label{}). + Where("user_uuid =? AND label_uuid = ?", userUuid, labelUuid). + Update("deleted_at", time.Now().UTC()).Error +} + +func (r *Repo) attachLabel(label CardLabel) error { + return r.db.Model(&CardLabel{}).Create(&label).Error +} + +func (r *Repo) detachLabel(label CardLabel) error { + return r.db. + Where("userUuid = ? AND label_uuid = ? AND merch_uuid = ?", label.UserUuid, label.LabelUuid, label.MerchUuid). + Delete(&CardLabel{}).Error +} diff --git a/internal/api/merch/service.go b/internal/api/merch/service.go index e749d88..bf49893 100644 --- a/internal/api/merch/service.go +++ b/internal/api/merch/service.go @@ -483,3 +483,90 @@ func (s *service) mtDeleteMerchImage(ctx context.Context, userUuid, merchUuid st }) return nil } + +func (s *service) createLabel(label LabelDTO, userUuid string) error { + now := time.Now().UTC() + + if label.Name == "" { + return fmt.Errorf("label name is required") + } + + newLabel := Label{ + CreatedAt: now, + UpdatedAt: now, + DeletedAt: sql.NullTime{Time: time.Time{}, Valid: false}, + LabelUuid: uuid.NewString(), + UserUuid: userUuid, + Name: label.Name, + Color: label.Color, + BgColor: label.BgColor, + } + + return s.repo.createLabel(newLabel) +} +func (s *service) getLabels(userUuid string) ([]LabelDTO, error) { + stored, err := s.repo.getLabels(userUuid) + if err != nil { + return nil, err + } + + response := make([]LabelDTO, 0, len(stored)) + for _, label := range stored { + response = append(response, LabelDTO{ + LabelUuid: label.LabelUuid, + Name: label.Name, + Color: label.Color, + BgColor: label.BgColor, + }) + } + + return response, nil +} +func (s *service) updateLabel(userUuid string, label LabelDTO) error { + updateMap := make(map[string]string, 3) + + if label.Name != "" { + updateMap["name"] = label.Name + } + + if label.Color != "" { + updateMap["color"] = label.Color + } + + if label.BgColor != "" { + updateMap["bgcolor"] = label.BgColor + } + + return s.repo.updateLabel(userUuid, label.LabelUuid, updateMap) +} + +func (s *service) deleteLabel(userUuid, labelUuid string) error { + return s.repo.deleteLabel(userUuid, labelUuid) +} + +func (s *service) attachLabel(userUuid string, label LabelLink) error { + if label.LabelUuid == "" || label.MerchUuid == "" { + return fmt.Errorf("both label and merch uuid-s are required") + } + + attach := CardLabel{ + LabelUuid: label.LabelUuid, + UserUuid: userUuid, + MerchUuid: label.MerchUuid, + } + + return s.repo.attachLabel(attach) +} + +func (s *service) detachLabel(userUuid string, label LabelLink) error { + if label.LabelUuid == "" || label.MerchUuid == "" { + return fmt.Errorf("both label and merch uuid-s are required") + } + + detach := CardLabel{ + LabelUuid: label.LabelUuid, + UserUuid: userUuid, + MerchUuid: label.MerchUuid, + } + return s.repo.detachLabel(detach) +}