добавлены разграничения в ошибках, убрана транзакция

This commit is contained in:
Pavel 2024-03-03 17:18:18 +03:00
parent cbd82e8779
commit ca0e4f7708
6 changed files with 94 additions and 59 deletions

@ -103,7 +103,6 @@ func (p *PromoCodeController) Activate(c *fiber.Ctx) error {
} }
userID := c.Locals(models.AuthJWTDecodedUserIDKey).(string) userID := c.Locals(models.AuthJWTDecodedUserIDKey).(string)
if userID == "" { if userID == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "failed to get jwt payload"}) 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 { if err != nil {
p.logger.Error("Failed to activate promocode", zap.Error(err)) 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"}) 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}) return c.Status(fiber.StatusOK).JSON(models.ActivateResp{Greetings: greetings})

@ -3,8 +3,11 @@ package repository
import "errors" import "errors"
var ( var (
ErrPromoUserNotFound = errors.New("user not found") ErrPromoUserNotFound = errors.New("user not found")
ErrAlreadyReported = errors.New("already reported") ErrAlreadyReported = errors.New("already reported")
ErrDuplicateCodeword = errors.New("duplicate codeword") ErrDuplicateCodeword = errors.New("duplicate codeword")
ErrPromoCodeNotFound = errors.New("promo code not found") 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")
) )

@ -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) { 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 var promoCode models.PromoCode
transactionErr := mongo.WithSession(ctx, session, func(sc mongo.SessionContext) error { var filter bson.M
var filter bson.M if req.Codeword != "" {
filter = bson.M{
if req.Codeword != "" { "codeword": 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()},
}
} }
} else if req.FastLink != "" {
update := bson.M{ filter = bson.M{
"$inc": bson.M{"activationCount": -1}, "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 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 { func (r *PromoCodeRepository) DeletePromoCode(ctx context.Context, promoCodeID string) error {
id, err := primitive.ObjectIDFromHex(promoCodeID) id, err := primitive.ObjectIDFromHex(promoCodeID)
if err != nil { if err != nil {

@ -19,15 +19,26 @@ func NewStatsRepository(deps Deps) *StatsRepository {
} }
func (r *StatsRepository) UpdateStatistics(ctx context.Context, key, userID string) error { 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{ update := bson.M{
"$inc": bson.M{"usageCount." + userID: 1}, "$inc": bson.M{"usageCount." + userID: 1},
"$push": bson.M{"usageHistory." + userID: time.Now()}, "$push": bson.M{"usageHistory." + userID: time.Now()},
} }
opts := options.Update().SetUpsert(true) 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 return err
} }

@ -4,11 +4,14 @@ import (
"codeword/internal/kafka/tariff" "codeword/internal/kafka/tariff"
"codeword/internal/models" "codeword/internal/models"
"codeword/internal/proto/discount" "codeword/internal/proto/discount"
"codeword/internal/repository"
"codeword/internal/utils/genID" "codeword/internal/utils/genID"
"context" "context"
"errors"
"fmt" "fmt"
"go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap" "go.uber.org/zap"
"time"
) )
type PromoCodeRepository interface { type PromoCodeRepository interface {
@ -19,6 +22,7 @@ type PromoCodeRepository interface {
DeletePromoCode(ctx context.Context, promoCodeID string) error DeletePromoCode(ctx context.Context, promoCodeID string) error
GetPromoCodeByID(ctx context.Context, promoCodeID primitive.ObjectID) (*models.PromoCode, error) GetPromoCodeByID(ctx context.Context, promoCodeID primitive.ObjectID) (*models.PromoCode, error)
AddFastLink(ctx context.Context, promoCodeID primitive.ObjectID, xid string) error AddFastLink(ctx context.Context, promoCodeID primitive.ObjectID, xid string) error
IncreaseActivationCount(ctx context.Context, promoCodeID primitive.ObjectID) error
} }
type PromoStatsRepository interface { 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)) s.logger.Error("Failed to activate promocode", zap.Error(err))
return "", 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) err = s.statsRepo.UpdateStatistics(ctx, promoCode.ID.Hex(), userID)
if err != nil { 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)) s.logger.Error("Failed add in stats", zap.Error(err))
return "", err return "", err
} }

@ -466,7 +466,6 @@ func TestActivatePromoCode(t *testing.T) {
} }
assert.Equal(t, fiber.StatusOK, statusCode) assert.Equal(t, fiber.StatusOK, statusCode)
var response models.ActivateResp var response models.ActivateResp
err := json.Unmarshal(resBody, &response) err := json.Unmarshal(resBody, &response)
assert.NoError(t, err) assert.NoError(t, err)