diff --git a/internal/api/merch/controller.go b/internal/api/merch/controller.go index 41a6983..63b28f2 100644 --- a/internal/api/merch/controller.go +++ b/internal/api/merch/controller.go @@ -27,7 +27,6 @@ func newController(service *service, utils interfaces.Utils, expires time.Durati func (h *Handler) RegisterRoutes(r *gin.RouterGroup, authMW gin.HandlerFunc, refreshMW gin.HandlerFunc) { merchGroup := r.Group("/merch", authMW) - merchGroup.POST("/", h.controller.addMerch) merchGroup.GET("/:uuid", h.controller.getSingleMerch) merchGroup.GET("/", h.controller.getAllMerch) @@ -51,6 +50,10 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup, authMW gin.HandlerFunc, ref labelsGroup.POST("/attach", h.controller.attachLabel) labelsGroup.POST("/detach", h.controller.detachLabel) labelsGroup.GET("/:uuid", h.controller.getMerchLabels) + + zeroPricesGroup := merchGroup.Group("/zeroprices", authMW) + zeroPricesGroup.GET("", h.controller.getZeroPrices) + zeroPricesGroup.DELETE("", h.controller.deleteZeroPrices) } // @Summary Добавить новый мерч @@ -92,6 +95,7 @@ func (co *controller) addMerch(c *gin.Context) { // @Description Получить всю информацию про мерч по его uuid // @Tags Merch // @Security BearerAuth +// @Produce json // @Param uuid path string true "merch_uuid" // @Success 200 {object} MerchDTO // @Failure 400 {object} responses.ErrorResponse400 @@ -125,6 +129,7 @@ func (co *controller) getSingleMerch(c *gin.Context) { // @Description Получить все записи мерча // @Tags Merch // @Security BearerAuth +// @Produce json // @Success 200 {array} ListResponse // @Failure 400 {object} responses.ErrorResponse400 // @Failure 500 {object} responses.ErrorResponse500 @@ -151,6 +156,7 @@ func (co *controller) getAllMerch(c *gin.Context) { // @Description Обновить информацию про мерч по его uuid в json-е // @Tags Merch // @Security BearerAuth +// Accept json // @Param body body UpdateMerchDTO true "merch_uuid" // @Success 200 // @Failure 400 {object} responses.ErrorResponse400 @@ -214,6 +220,7 @@ func (co *controller) deleteMerch(c *gin.Context) { // @Description Получить цены мерча за период // @Tags Merch // @Security BearerAuth +// @Produce json // @Param days query string false "period in days" // @Success 200 {array} PricesResponse // @Failure 400 {object} responses.ErrorResponse400 @@ -243,6 +250,7 @@ func (co *controller) getChartsPrices(c *gin.Context) { // @Description Получить перепады цен мерча за период по его merch_uuid // @Tags Merch // @Security BearerAuth +// @Produce json // @Param uuid path string true "merch_uuid" // @Param days query string false "period in days" // @Success 200 {object} PricesResponse @@ -338,6 +346,7 @@ func (co *controller) uploadMerchImage(c *gin.Context) { // @Description Получить картинки по merch_uuid и query параметрам // @Tags Merch images // @Security BearerAuth +// @Produce json // @Param uuid path string true "merch_uuid" // @Param type query string true "image type" // @Success 200 {object} ImageLink @@ -429,6 +438,7 @@ func (co *controller) deleteMerchImage(c *gin.Context) { // @Description Создать новую метку для товара // @Tags Merch labels // @Security BearerAuth +// Accept json // @Param payload body LabelDTO true "payload" // @Success 200 // @Failure 400 {object} responses.ErrorResponse400 @@ -464,6 +474,7 @@ func (co *controller) createLabel(c *gin.Context) { // @Description Получить все метки товаров // @Tags Merch labels // @Security BearerAuth +// @Produce json // @Success 200 {array} LabelsList // @Failure 400 {object} responses.ErrorResponse400 // @Failure 500 {object} responses.ErrorResponse500 @@ -492,6 +503,7 @@ func (co *controller) getLabels(c *gin.Context) { // @Description Изменить метку // @Tags Merch labels // @Security BearerAuth +// Accept json // @Param uuid path string true "label uuid" // @Param payload body LabelDTO true "payload" // @Success 200 @@ -568,6 +580,7 @@ func (co *controller) deleteLabel(c *gin.Context) { // @Description Прикрепить метку к товару // @Tags Merch labels // @Security BearerAuth +// Accept json // @Param payload body LabelLink true "payload" // @Success 200 // @Failure 400 {object} responses.ErrorResponse400 @@ -602,6 +615,7 @@ func (co *controller) attachLabel(c *gin.Context) { // @Description Удалить привязку метки к товару // @Tags Merch labels // @Security BearerAuth +// Accept json // @Param payload body LabelLink true "payload" // @Success 200 // @Failure 400 {object} responses.ErrorResponse400 @@ -636,6 +650,7 @@ func (co *controller) detachLabel(c *gin.Context) { // @Description Получить метки товара по его uuid // @Tags Merch labels // @Security BearerAuth +// @Produce json // @Param uuid path string true "label uuid" // @Success 200 // @Failure 400 {object} responses.ErrorResponse400 @@ -666,3 +681,67 @@ func (co *controller) getMerchLabels(c *gin.Context) { } c.JSON(http.StatusOK, response) } + +// @Summary Получить нулевые цены +// @Description Получить нулевые цены +// @Tags Merch zero prices +// @Security BearerAuth +// @Produce json +// @Success 200 {array} ZeroPrice +// @Failure 400 {object} responses.ErrorResponse400 +// @Failure 500 {object} responses.ErrorResponse500 +// @Router /merch/zeroprices [get] +func (co *controller) getZeroPrices(c *gin.Context) { + const logMsg = "Merch | Get zero prices" + + 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.getZeroPrices(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 zero prices +// @Security BearerAuth +// Accept json +// @Param payload body DeleteZeroPrices true "payload" +// @Success 200 +// @Failure 400 {object} responses.ErrorResponse400 +// @Failure 500 {object} responses.ErrorResponse500 +// @Router /merch/zeroprices [delete] +func (co *controller) deleteZeroPrices(c *gin.Context) { + const logMsg = "Merch | Delete zero prices" + + 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 DeleteZeroPrices + 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.deleteZeroPrices(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 2f267b1..b10aa6b 100644 --- a/internal/api/merch/dto.go +++ b/internal/api/merch/dto.go @@ -1,5 +1,7 @@ package merch +import "time" + type merchBundle struct { Merch *Merch Surugaya *Surugaya @@ -78,3 +80,14 @@ type LabelLink struct { MerchUuid string `json:"merch_uuid"` LabelUuid string `json:"label_uuid"` } + +type ZeroPrice struct { + CreatedAt time.Time `json:"created_at"` + MerchUuid string `json:"merch_uuid"` + Name string `json:"name"` + Origin string `json:"origin"` +} + +type DeleteZeroPrices struct { + MerchUuids []string `json:"merch_uuids"` +} diff --git a/internal/api/merch/model.go b/internal/api/merch/model.go index 935a194..2dd6e0e 100644 --- a/internal/api/merch/model.go +++ b/internal/api/merch/model.go @@ -44,8 +44,8 @@ func (Mandarake) TableName() string { type Price struct { Id uint `json:"id" gorm:"primary_key"` CreatedAt time.Time `json:"created_at" gorm:"column:created_at"` - UpdatedAt sql.NullTime `json:"updated_at" gorm:"column:updated_at"` - DeletedAt sql.NullTime `json:"deleted_at" gorm:"column:deleted_at"` + UpdatedAt sql.NullTime `json:"updated_at,omitempty" gorm:"column:updated_at"` + DeletedAt sql.NullTime `json:"deleted_at,omitempty" gorm:"column:deleted_at"` MerchUuid string `json:"merch_uuid" gorm:"column:merch_uuid"` Price int `json:"price" gorm:"column:price"` Origin Origin `json:"origin" gorm:"column:origin;type:integer"` diff --git a/internal/api/merch/repository.go b/internal/api/merch/repository.go index b5ecf39..1af8df0 100644 --- a/internal/api/merch/repository.go +++ b/internal/api/merch/repository.go @@ -38,6 +38,9 @@ type repository interface { type prices interface { getPricesWithDays(userUuid string, period time.Time) ([]Price, error) getDistinctPrices(userUuid, merchUuid string, period time.Time) (prices []Price, err error) + + getZeroPrices(userUuid string) ([]ZeroPrice, error) + deleteZeroPrices(userUuid string, list []string) error } type labels interface { @@ -311,3 +314,43 @@ func (r *Repo) getAttachedLabelsByUuid(userUuid, merchUuid string) ([]CardLabel, return labelsList, nil } + +func (r *Repo) getZeroPrices(userUuid string) ([]ZeroPrice, error) { + var priceList []ZeroPrice + if err := r.db.Raw(` + WITH price_with_neighbors AS ( + SELECT + p.created_at, p.merch_uuid, p.price, p.origin, m.name, + LAG(price) OVER (PARTITION BY p.merch_uuid ORDER BY p.created_at, p.id) AS prev_price, + LEAD(price) OVER (PARTITION BY p.merch_uuid ORDER BY p.created_at, p.id) AS next_price + FROM prices AS p + JOIN merch as m ON m.merch_uuid = p.merch_uuid + WHERE p.deleted_at IS NULL + AND m.user_uuid = ?) + + SELECT + created_at, merch_uuid, origin, name + FROM price_with_neighbors + WHERE + price = 0 + AND prev_price IS NOT NULL + AND prev_price > 0 + AND next_price IS NOT NULL + AND next_price > 0; + `, userUuid).Scan(&priceList).Error; err != nil { + return nil, err + } + return priceList, nil +} + +func (r *Repo) deleteZeroPrices(userUuid string, list []string) error { + subQuery := r.db.Table("merch"). + Select("merch_uuid"). + Where("user_uuid = ?", userUuid) + + return r.db.Model(&Price{}). + Where("merch_uuid IN ?", list). + Where("merch_uuid IN (?)", subQuery). + Update("deleted_at", time.Now().UTC()). + Error +} diff --git a/internal/api/merch/service.go b/internal/api/merch/service.go index 4689453..259563d 100644 --- a/internal/api/merch/service.go +++ b/internal/api/merch/service.go @@ -612,3 +612,11 @@ func (s *service) getMerchLabels(userUuid, merchUuid string) ([]string, error) { return response, nil } + +func (s *service) getZeroPrices(userUuid string) ([]ZeroPrice, error) { + return s.repo.getZeroPrices(userUuid) +} + +func (s *service) deleteZeroPrices(userUuid string, list DeleteZeroPrices) error { + return s.repo.deleteZeroPrices(userUuid, list.MerchUuids) +}