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

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)
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})

@ -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")
)

@ -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 {

@ -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
}

@ -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
}

@ -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)