package services 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 { CreatePromoCode(ctx context.Context, req *models.PromoCode) (*models.PromoCode, error) EditPromoCode(ctx context.Context, req *models.ReqEditPromoCode) (*models.PromoCode, error) GetPromoCodesList(ctx context.Context, req *models.GetPromoCodesListReq) ([]models.PromoCode, int64, error) ActivatePromo(ctx context.Context, req *models.ActivateReq) (*models.PromoCode, error) 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 { UpdateStatistics(ctx context.Context, req *models.ActivateReq, promoCode *models.PromoCode, userID string) error GetStatistics(ctx context.Context, promoCodeID string) (*models.PromoCodeStats, error) } type PromoDeps struct { Logger *zap.Logger PromoCodeRepo PromoCodeRepository StatsRepo PromoStatsRepository Kafka *tariff.Producer DiscountClient discount.DiscountServiceClient } type PromoCodeService struct { logger *zap.Logger promoCodeRepo PromoCodeRepository statsRepo PromoStatsRepository kafka *tariff.Producer discountClient discount.DiscountServiceClient } func NewPromoCodeService(deps PromoDeps) *PromoCodeService { return &PromoCodeService{ logger: deps.Logger, promoCodeRepo: deps.PromoCodeRepo, statsRepo: deps.StatsRepo, kafka: deps.Kafka, discountClient: deps.DiscountClient, } } func (s *PromoCodeService) CreatePromoCode(ctx context.Context, req *models.PromoCode) (*models.PromoCode, error) { promoCode, err := s.promoCodeRepo.CreatePromoCode(ctx, req) if err != nil { s.logger.Error("Failed to add promocode in database", zap.Error(err)) return nil, err } return promoCode, nil } func (s *PromoCodeService) EditPromoCode(ctx context.Context, req *models.ReqEditPromoCode) (*models.PromoCode, error) { editedPromoCode, err := s.promoCodeRepo.EditPromoCode(ctx, req) if err != nil { s.logger.Error("Failed to edit promocode in database", zap.Error(err)) return nil, err } return editedPromoCode, nil } func (s *PromoCodeService) GetPromoCodesList(ctx context.Context, req *models.GetPromoCodesListReq) ([]models.PromoCode, int64, error) { promoCodes, count, err := s.promoCodeRepo.GetPromoCodesList(ctx, req) if err != nil { s.logger.Error("Failed to get list promocodes from database", zap.Error(err)) return nil, 0, err } return promoCodes, count, nil } // todo одумать еще реализацию этого дела, надо уточнить как разделяется ответственность в бонусе между привилегией и скидкой // разделяется ли она или они всегда вместе, если разделяются то что-то из этого может быть пустым либо все заполеннное, // соответсвенно надо сделать соответствующие проверки до записи в кафку и до отправки в дискаунт сервис func (s *PromoCodeService) ActivatePromo(ctx context.Context, req *models.ActivateReq, userID string) (string, error) { promoCode, err := s.promoCodeRepo.ActivatePromo(ctx, req) if err != nil { 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, req, promoCode, 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 } var postfix string if req.FastLink != "" { postfix = fmt.Sprintf(":(%s)", req.FastLink) } var privileges []models.Privilege privilege := models.Privilege{ PrivilegeID: promoCode.Bonus.Privilege.PrivilegeID, Amount: promoCode.Bonus.Privilege.Amount, } privileges = append(privileges, privilege) fakeTariff := &models.Tariff{ Name: promoCode.Codeword + postfix, Privileges: privileges, Deleted: promoCode.Delete, CreatedAt: promoCode.CreatedAt, } if err := s.kafka.Send(ctx, userID, fakeTariff); err != nil { s.logger.Error("Failed to send fake tariff to Kafka", zap.Error(err)) return "", err } disOverHelm := true discountRequest := &discount.CreateDiscountRequest{ Name: promoCode.Codeword + postfix, Layer: promoCode.Bonus.Discount.Layer, Description: "", Condition: &discount.DiscountCondition{ Coupon: &promoCode.Codeword, User: &userID, }, Target: &discount.DiscountCalculationTarget{ Factor: promoCode.Bonus.Discount.Factor, Overhelm: &disOverHelm, }, } _, err = s.discountClient.CreateDiscount(ctx, discountRequest) if err != nil { s.logger.Error("Failed to create discount", zap.Error(err)) return "", err } return promoCode.Greetings, nil } func (s *PromoCodeService) DeletePromoCode(ctx context.Context, promoCodeID string) error { err := s.promoCodeRepo.DeletePromoCode(ctx, promoCodeID) if err != nil { s.logger.Error("Failed simple delete promocode from database", zap.Error(err)) return err } return nil } func (s *PromoCodeService) CreateFastLink(ctx context.Context, promoCodeID string) (string, error) { xid := genID.GenerateXID() promoID, err := primitive.ObjectIDFromHex(promoCodeID) if err != nil { s.logger.Error("Failed conversion promoCodeID to ObjectID", zap.Error(err)) return "", err } err = s.promoCodeRepo.AddFastLink(ctx, promoID, xid) if err != nil { s.logger.Error("Failed to add fastlink for promocode by promocode id", zap.Error(err)) return "", err } return xid, nil } func (s *PromoCodeService) GetStats(ctx context.Context, promoCodeID string) (*models.PromoCodeStats, error) { promoStats, err := s.statsRepo.GetStatistics(ctx, promoCodeID) if err != nil { s.logger.Error("Failed getting promo stats", zap.Error(err)) return nil, err } return promoStats, nil }