From ca0e4f77081dc2d01e8df4d842d7f7d089089d1d Mon Sep 17 00:00:00 2001 From: Pavel Date: Sun, 3 Mar 2024 17:18:18 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=80=D0=B0=D0=B7=D0=B3=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B2=20=D0=BE=D1=88=D0=B8?= =?UTF-8?q?=D0=B1=D0=BA=D0=B0=D1=85,=20=D1=83=D0=B1=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B0=20=D1=82=D1=80=D0=B0=D0=BD=D0=B7=D0=B0=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../promocode/promocode_controller.go | 14 ++- internal/repository/errors.go | 11 ++- internal/repository/promocode_repository.go | 85 ++++++++----------- internal/repository/promocode_stats.go | 15 +++- internal/services/promocode_service.go | 27 ++++++ tests/e2e/promo_test.go | 1 - 6 files changed, 94 insertions(+), 59 deletions(-) diff --git a/internal/controller/promocode/promocode_controller.go b/internal/controller/promocode/promocode_controller.go index a83c468..b8192ee 100644 --- a/internal/controller/promocode/promocode_controller.go +++ b/internal/controller/promocode/promocode_controller.go @@ -103,7 +103,6 @@ func (p *PromoCodeController) Activate(c *fiber.Ctx) error { } userID := c.Locals(models.AuthJWTDecodedUserIDKey).(string) - if userID == "" { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "failed to get jwt payload"}) } @@ -121,11 +120,18 @@ func (p *PromoCodeController) Activate(c *fiber.Ctx) error { if err != nil { p.logger.Error("Failed to activate promocode", zap.Error(err)) - if errors.Is(err, repository.ErrPromoCodeNotFound) { + switch { + case errors.Is(err, repository.ErrPromoCodeNotFound): return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "PromoCode not found"}) + case errors.Is(err, repository.ErrPromoCodeAlreadyActivated): + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "PromoCode already activated"}) + case errors.Is(err, repository.ErrPromoCodeExpired): + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()}) + case errors.Is(err, repository.ErrPromoCodeExhausted): + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "PromoCode exhausted"}) + default: + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"}) } - - return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"}) } return c.Status(fiber.StatusOK).JSON(models.ActivateResp{Greetings: greetings}) diff --git a/internal/repository/errors.go b/internal/repository/errors.go index d61a2da..c941611 100644 --- a/internal/repository/errors.go +++ b/internal/repository/errors.go @@ -3,8 +3,11 @@ package repository import "errors" var ( - ErrPromoUserNotFound = errors.New("user not found") - ErrAlreadyReported = errors.New("already reported") - ErrDuplicateCodeword = errors.New("duplicate codeword") - ErrPromoCodeNotFound = errors.New("promo code not found") + ErrPromoUserNotFound = errors.New("user not found") + ErrAlreadyReported = errors.New("already reported") + ErrDuplicateCodeword = errors.New("duplicate codeword") + ErrPromoCodeNotFound = errors.New("promo code not found") + ErrPromoCodeExpired = errors.New("promo code is expired") + ErrPromoCodeExhausted = errors.New("promo code is exhausted") + ErrPromoCodeAlreadyActivated = errors.New("promo code is already activated") ) diff --git a/internal/repository/promocode_repository.go b/internal/repository/promocode_repository.go index 354006c..47826f5 100644 --- a/internal/repository/promocode_repository.go +++ b/internal/repository/promocode_repository.go @@ -187,63 +187,52 @@ func (r *PromoCodeRepository) GetPromoCodesList(ctx context.Context, req *models } func (r *PromoCodeRepository) ActivatePromo(ctx context.Context, req *models.ActivateReq) (*models.PromoCode, error) { - session, err := r.mdb.Database().Client().StartSession() - if err != nil { - return nil, err - } - defer session.EndSession(ctx) - var promoCode models.PromoCode - transactionErr := mongo.WithSession(ctx, session, func(sc mongo.SessionContext) error { - var filter bson.M - - if req.Codeword != "" { - filter = bson.M{ - "codeword": req.Codeword, - "delete": false, - "outdated": false, - "offLimit": false, - "activationCount": bson.M{"$gt": 0}, - "dueTo": bson.M{"$gt": time.Now().Unix()}, - } - } else if req.FastLink != "" { - filter = bson.M{ - "fastLinks": req.FastLink, - "delete": false, - "outdated": false, - "offLimit": false, - "activationCount": bson.M{"$gt": 0}, - "dueTo": bson.M{"$gt": time.Now().Unix()}, - } + var filter bson.M + if req.Codeword != "" { + filter = bson.M{ + "codeword": req.Codeword, } - - update := bson.M{ - "$inc": bson.M{"activationCount": -1}, + } else if req.FastLink != "" { + filter = bson.M{ + "fastLinks": req.FastLink, } - opts := options.FindOneAndUpdate().SetReturnDocument(options.After) - - var updatedPromoCode models.PromoCode - err := r.mdb.FindOneAndUpdate(sc, filter, update, opts).Decode(&updatedPromoCode) - if err != nil { - if err == mongo.ErrNoDocuments { - return ErrPromoCodeNotFound - } - return err - } - - promoCode = updatedPromoCode - - return nil - }) - - if transactionErr != nil { - return nil, transactionErr } + opts := options.FindOneAndUpdate().SetReturnDocument(options.After) + + err := r.mdb.FindOneAndUpdate(ctx, filter, bson.M{"$inc": bson.M{"activationCount": -1}}, opts).Decode(&promoCode) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, ErrPromoCodeNotFound + } + return nil, err + } + + if promoCode.ActivationCount <= 0 && promoCode.DueTo > time.Now().Unix() { + if !promoCode.OffLimit { + update := bson.M{"$set": bson.M{"offLimit": true}} + _, err := r.mdb.UpdateOne(ctx, filter, update) + if err != nil { + return nil, err + } + } + } return &promoCode, nil } +func (r *PromoCodeRepository) IncreaseActivationCount(ctx context.Context, promoCodeID primitive.ObjectID) error { + filter := bson.M{"_id": promoCodeID} + update := bson.M{"$inc": bson.M{"activationCount": 1}} + _, err := r.mdb.UpdateOne(ctx, filter, update) + if err != nil { + return err + } + + return nil +} + func (r *PromoCodeRepository) DeletePromoCode(ctx context.Context, promoCodeID string) error { id, err := primitive.ObjectIDFromHex(promoCodeID) if err != nil { diff --git a/internal/repository/promocode_stats.go b/internal/repository/promocode_stats.go index 1f0a215..fe61c2b 100644 --- a/internal/repository/promocode_stats.go +++ b/internal/repository/promocode_stats.go @@ -19,15 +19,26 @@ func NewStatsRepository(deps Deps) *StatsRepository { } func (r *StatsRepository) UpdateStatistics(ctx context.Context, key, userID string) error { + filter := bson.M{"_id": key, "usageCount." + userID: bson.M{"$exists": true}} + + count, err := r.mdb.CountDocuments(ctx, filter) + if err != nil { + return err + } + + if count > 0 { + return ErrPromoCodeAlreadyActivated + } + update := bson.M{ "$inc": bson.M{"usageCount." + userID: 1}, "$push": bson.M{"usageHistory." + userID: time.Now()}, } opts := options.Update().SetUpsert(true) - filter := bson.M{"_id": key} + filter = bson.M{"_id": key} - _, err := r.mdb.UpdateOne(ctx, filter, update, opts) + _, err = r.mdb.UpdateOne(ctx, filter, update, opts) return err } diff --git a/internal/services/promocode_service.go b/internal/services/promocode_service.go index ddcdd58..a5670e7 100644 --- a/internal/services/promocode_service.go +++ b/internal/services/promocode_service.go @@ -4,11 +4,14 @@ import ( "codeword/internal/kafka/tariff" "codeword/internal/models" "codeword/internal/proto/discount" + "codeword/internal/repository" "codeword/internal/utils/genID" "context" + "errors" "fmt" "go.mongodb.org/mongo-driver/bson/primitive" "go.uber.org/zap" + "time" ) type PromoCodeRepository interface { @@ -19,6 +22,7 @@ type PromoCodeRepository interface { DeletePromoCode(ctx context.Context, promoCodeID string) error GetPromoCodeByID(ctx context.Context, promoCodeID primitive.ObjectID) (*models.PromoCode, error) AddFastLink(ctx context.Context, promoCodeID primitive.ObjectID, xid string) error + IncreaseActivationCount(ctx context.Context, promoCodeID primitive.ObjectID) error } type PromoStatsRepository interface { @@ -92,9 +96,32 @@ func (s *PromoCodeService) ActivatePromo(ctx context.Context, req *models.Activa s.logger.Error("Failed to activate promocode", zap.Error(err)) return "", err } + //todo такая реализация проверок кажется довольно массивной, думаю как то это стоит сделать параллельно обхаживая все условия + if promoCode.DueTo < time.Now().Unix() && promoCode.OffLimit { + err := s.promoCodeRepo.IncreaseActivationCount(ctx, promoCode.ID) + if err != nil { + return "", err + } + return "", fmt.Errorf("%w: expired on %s", repository.ErrPromoCodeExpired, time.Unix(promoCode.DueTo, 0).Format(time.RFC3339)) + } + + if promoCode.DueTo == 0 && promoCode.ActivationCount < 0 { + err := s.promoCodeRepo.IncreaseActivationCount(ctx, promoCode.ID) + if err != nil { + return "", err + } + return "", repository.ErrPromoCodeExhausted + } err = s.statsRepo.UpdateStatistics(ctx, promoCode.ID.Hex(), userID) if err != nil { + if errors.Is(err, repository.ErrPromoCodeAlreadyActivated) { + err := s.promoCodeRepo.IncreaseActivationCount(ctx, promoCode.ID) + if err != nil { + return "", err + } + return "", repository.ErrPromoCodeAlreadyActivated + } s.logger.Error("Failed add in stats", zap.Error(err)) return "", err } diff --git a/tests/e2e/promo_test.go b/tests/e2e/promo_test.go index 8de7845..b42461a 100644 --- a/tests/e2e/promo_test.go +++ b/tests/e2e/promo_test.go @@ -466,7 +466,6 @@ func TestActivatePromoCode(t *testing.T) { } assert.Equal(t, fiber.StatusOK, statusCode) - var response models.ActivateResp err := json.Unmarshal(resBody, &response) assert.NoError(t, err)