diff --git a/internal/merch/controller.go b/internal/merch/controller.go index 008979f..46fa177 100644 --- a/internal/merch/controller.go +++ b/internal/merch/controller.go @@ -38,6 +38,10 @@ func (h *Handler) RegisterRoutes(r *gin.RouterGroup) { originsGroup.GET("", h.controller.getOrigins) originsGroup.DELETE("", h.controller.deleteOrigin) + chartsGroup := r.Group("/prices") + chartsGroup.GET("", h.controller.getChartsPrices) + chartsGroup.GET("/:uuid", h.controller.getDistinctPrices) + } // create godoc @@ -105,13 +109,13 @@ func (co *controller) getMany(c *gin.Context) { // @Description Update merch general info (except extra data) // @Tags Merch // @Accept json -// @Param uuid path string true "merch uuid" -// @Param payload body updateMerchDTO true "payload" +// @Param uuid path string true "merch uuid" +// @Param payload body updateMerchDTO true "payload" // @Produce json -// @Success 200 {object} merchDTO -// @Failure 400 {object} responses.BadRequest -// @Failure 401 {object} responses.Unauthorized -// @Failure 500 {object} responses.InternalServerError +// @Success 200 {object} merchDTO +// @Failure 400 {object} responses.BadRequest +// @Failure 401 {object} responses.Unauthorized +// @Failure 500 {object} responses.InternalServerError // @Router /merch/{uuid} [PUT] func (co *controller) updateMerch(c *gin.Context) { merchUuid := c.Param("id") @@ -150,13 +154,13 @@ func (co *controller) updateMerch(c *gin.Context) { // @Description Update ONLY merch extra data // @Tags Merch // @Accept json -// @Param uuid path string true "merch uuid" -// @Param payload body extraDataDTO true "payload" +// @Param uuid path string true "merch uuid" +// @Param payload body extraDataDTO true "payload" // @Produce json -// @Success 200 {object} extraDataDTO -// @Failure 400 {object} responses.BadRequest -// @Failure 401 {object} responses.Unauthorized -// @Failure 500 {object} responses.InternalServerError +// @Success 200 {object} extraDataDTO +// @Failure 400 {object} responses.BadRequest +// @Failure 401 {object} responses.Unauthorized +// @Failure 500 {object} responses.InternalServerError // @Router /merch/extra/{uuid} [PUT] func (co *controller) updateExtraData(c *gin.Context) { merchUuid := c.Param("id") @@ -303,3 +307,66 @@ func (co *controller) deleteOrigin(c *gin.Context) { logDebug(controllerLogHeader, "delete origin success") c.Status(http.StatusNoContent) } + +// getChartsPrices godoc +// +// @Summary Получить цены мерча за период +// @Description Получить цены мерча за период +// @Tags Merch +// @Security BearerAuth +// @Produce json +// @Param days query string false "period in days" +// @Success 200 {array} PricesResponse +// @Failure 400 {object} responses.BadRequest +// @Failure 401 {object} responses.Unauthorized +// @Failure 500 {object} responses.InternalServerError +// @Router /prices [get] +func (co *controller) getChartsPrices(c *gin.Context) { + response, err := co.service.getPrices(c, getUserId(c), getDays(c)) + if err != nil { + c.JSON(http.StatusBadRequest, responses.InternalServerError{Error: err.Error()}) + logErr(controllerLogHeader, err) + return + } + + c.JSON(http.StatusOK, response) +} + +// getDistinctPrices godoc +// +// @Summary Получить перепады цен мерча за период по его merch_uuid +// @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 +// @Success 204 +// @Failure 400 {object} responses.BadRequest +// @Failure 401 {object} responses.Unauthorized +// @Failure 500 {object} responses.InternalServerError +// @Router /prices/{uuid} [get] +func (co *controller) getDistinctPrices(c *gin.Context) { + merchUuid := c.Param("uuid") + if merchUuid == "" { + err := errors.New("MerchUuid is empty") + c.JSON(http.StatusBadRequest, responses.BadRequest{Error: err.Error()}) + logErr(controllerLogHeader, err) + return + } + + response, err := co.service.getDistinctPrices(c, getUserId(c), merchUuid, getDays(c)) + if err != nil { + c.JSON(http.StatusBadRequest, responses.InternalServerError{Error: err.Error()}) + logErr(controllerLogHeader, err) + return + } + + if response == nil { + c.Status(http.StatusNoContent) + return + } + + c.JSON(http.StatusOK, response) +} diff --git a/internal/merch/dto.go b/internal/merch/dto.go index dbc747f..89e0218 100644 --- a/internal/merch/dto.go +++ b/internal/merch/dto.go @@ -45,3 +45,20 @@ type extraDataDTO struct { MerchUuid string `json:"merch_uuid"` Links []originLink `json:"links"` } + +// prices dtos +type PriceEntry struct { + CreatedAt int64 `json:"created_at"` + Value int `json:"value"` +} + +type OriginWithPrices struct { + Origin string `json:"origin"` + Prices []PriceEntry +} + +type PricesResponse struct { + Name string `json:"name"` + MerchUuid string `json:"merch_uuid"` + Origins []OriginWithPrices `json:"origins"` +} diff --git a/internal/merch/repository.go b/internal/merch/repository.go index 644d22d..5e93631 100644 --- a/internal/merch/repository.go +++ b/internal/merch/repository.go @@ -19,6 +19,7 @@ type Repository interface { getMerchIdByUuid(ctx context.Context, userId int64, uuid string) (int64, error) getMerchUuidMap(ctx context.Context, merchUuids []string) (map[string]int64, error) + getAllUserMerch(ctx context.Context, userId int64) ([]Merch, error) updateMerch(ctx context.Context, userId int64, merch *updateMerchDTO) (*merchDTO, error) updateExtraData(ctx context.Context, merchId int64, insertData []ExtraData) ([]ExtraData, error) @@ -39,6 +40,8 @@ type Origins interface { type Prices interface { insertPrices(ctx context.Context, prices []Price) error + getPricesWithDays(ctx context.Context, userId int64, days time.Time) ([]Price, error) + getDistinctPrices(ctx context.Context, userId int64, merchUuid string, days time.Time) ([]Price, error) } type Tasks interface { @@ -219,6 +222,32 @@ func (r *repo) getMerchUuidMap(ctx context.Context, merchUuids []string) (map[st return merchUuidMap, nil } +func (r *repo) getAllUserMerch(ctx context.Context, userId int64) ([]Merch, error) { + var userMerch []Merch + + q := `SELECT id, merch_uuid, name FROM merch WHERE user_id = $1 AND deleted_at IS NULL` + + rows, err := r.db.Query(ctx, q, userId) + if err != nil { + return nil, err + } + + for rows.Next() { + var m Merch + if err = rows.Scan(&m.Id, &m.MerchUuid, &m.Name); err != nil { + rows.Close() + return nil, err + } + userMerch = append(userMerch, m) + } + + rows.Close() + if err = rows.Err(); err != nil { + return nil, err + } + return userMerch, nil +} + func (r *repo) deleteOneMerchRecord(ctx context.Context, userId int64, merchUuid string, delTime time.Time) error { tx, err := r.db.Begin(ctx) if err != nil { @@ -365,3 +394,78 @@ func (r *repo) getTaskData(ctx context.Context) ([]taskData, error) { return result, nil } + +func (r *repo) getPricesWithDays(ctx context.Context, userId int64, days time.Time) ([]Price, error) { + q := ` + SELECT mp.created_at, mp.merch_id, mp.price, mp.origin_id + FROM merch_prices AS mp + JOIN merch AS m ON m.id = mp.merch_id + WHERE m.user_id = $1 + AND mp.created_at > $2 + AND mp.deleted_at IS NULL + AND m.deleted_at IS NULL + ` + + rows, err := r.db.Query(ctx, q, userId, days) + if err != nil { + return nil, err + } + + var result []Price + for rows.Next() { + var p Price + if err = rows.Scan(&p.CreatedAt, &p.MerchId, &p.Price, &p.OriginId); err != nil { + rows.Close() + return nil, err + } + result = append(result, p) + } + rows.Close() + + if err = rows.Err(); err != nil { + return nil, err + } + + return result, nil +} + +func (r *repo) getDistinctPrices(ctx context.Context, userId int64, merchUuid string, days time.Time) ([]Price, error) { + q := ` + SELECT price, created_at, origin_id + FROM ( + SELECT DISTINCT ON (price) price, created_at, origin_id + FROM merch_prices + WHERE merch_id = ( + SELECT id + FROM merch + WHERE merch_uuid = $1 + AND user_id = $2 + AND deleted_at IS NULL + ) + AND deleted_at IS NULL + AND created_at > $3 + ) + ORDER BY created_at; + ` + fmt.Println(merchUuid, userId, days) + rows, err := r.db.Query(ctx, q, merchUuid, userId, days) + if err != nil { + return nil, err + } + + var result []Price + for rows.Next() { + var p Price + if err = rows.Scan(&p.Price, &p.CreatedAt, &p.OriginId); err != nil { + rows.Close() + return nil, err + } + result = append(result, p) + } + + rows.Close() + if err = rows.Err(); err != nil { + return nil, err + } + return result, nil +} diff --git a/internal/merch/service.go b/internal/merch/service.go index a7e78e3..979f39e 100644 --- a/internal/merch/service.go +++ b/internal/merch/service.go @@ -2,10 +2,11 @@ package merch import ( "context" + "errors" + "fmt" + "github.com/google/uuid" "merch-api/internal/user" "merch-api/pkg/utils" - - "github.com/google/uuid" ) const serviceLogHeader string = "[Service]" @@ -111,6 +112,9 @@ func (s *service) createMerch(ctx context.Context, userId int64, payload *newMer return s.repo.createMerch(ctx, newMerch, merchExtra) } +// getOriginsMaps +// first return name:id +// second id:name func (s *service) getOriginsMaps(ctx context.Context) (map[string]int64, map[int64]string, error) { origins, err := s.repo.getOrigins(ctx) if err != nil { @@ -197,3 +201,121 @@ func (s *service) updateExtraData(ctx context.Context, userId int64, payload *ex func (s *service) deleteOneMerchRecord(ctx context.Context, userId int64, merchUuid string) error { return s.repo.deleteOneMerchRecord(ctx, userId, merchUuid, s.utils.TimeNowUTC()) } + +func (s *service) getPrices(ctx context.Context, userId int64, days int) ([]PricesResponse, error) { + fmt.Println("Enter service") + merchList, err := s.repo.getAllUserMerch(ctx, userId) + if err != nil { + logErr(serviceLogHeader, err) + return nil, err + } + + if len(merchList) == 0 { + errMsg := errors.New("no merch found") + logErr(serviceLogHeader, errMsg) + return nil, errMsg + } + + merchMap := make(map[int64]Merch, len(merchList)) + for _, merch := range merchList { + merchMap[merch.Id] = merch + } + + var response []PricesResponse + for _, item := range merchList { + response = append(response, PricesResponse{ + MerchUuid: item.MerchUuid, + Name: item.Name, + Origins: []OriginWithPrices{}, + }) + } + + pricesList, err := s.repo.getPricesWithDays(ctx, userId, getPeriod(days)) + if err != nil { + logErr(serviceLogHeader, err) + return nil, err + } + + _, originNamesMap, err := s.getOriginsMaps(ctx) + if err != nil { + logErr(serviceLogHeader, err) + return nil, err + } + + pricesMap := make(map[string]map[string][]PriceEntry) + for _, item := range pricesList { + merchUuid := merchMap[item.MerchId].MerchUuid + + if _, ok := pricesMap[merchUuid]; !ok { + pricesMap[merchUuid] = make(map[string][]PriceEntry) + } + + originName := originNamesMap[item.OriginId] + pricesMap[merchUuid][originName] = append(pricesMap[merchUuid][originName], PriceEntry{ + CreatedAt: item.CreatedAt.Unix(), + Value: item.Price, + }) + } + + for i := range response { + for _, name := range originNamesMap { + prices := pricesMap[response[i].MerchUuid][name] + if prices == nil { + continue + } + + response[i].Origins = append(response[i].Origins, OriginWithPrices{ + Origin: name, + Prices: prices, + }) + } + } + + return response, nil +} + +func (s *service) getDistinctPrices(ctx context.Context, userId int64, merchUuid string, days int) (*PricesResponse, error) { + result, err := s.repo.getDistinctPrices(ctx, userId, merchUuid, getPeriod(days)) + if err != nil { + logErr(serviceLogHeader, err) + return nil, err + } + + if result == nil { + + return nil, nil + } + + _, originNamesMap, err := s.getOriginsMaps(ctx) + if err != nil { + logErr(serviceLogHeader, err) + return nil, err + } + + response := PricesResponse{ + MerchUuid: merchUuid, + Origins: []OriginWithPrices{}, + } + + pricesMap := make(map[string][]PriceEntry) + for _, item := range result { + originName := originNamesMap[item.OriginId] + if _, ok := pricesMap[originName]; !ok { + pricesMap[originName] = make([]PriceEntry, 0) + } + + pricesMap[originName] = append(pricesMap[originName], PriceEntry{ + CreatedAt: item.CreatedAt.Unix(), + Value: item.Price, + }) + } + + for key, item := range pricesMap { + response.Origins = append(response.Origins, OriginWithPrices{ + Origin: key, + Prices: item, + }) + } + + return &response, nil +}