discount/1
2024-04-15 03:08:29 +03:00

484 lines
17 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package repository
import (
"context"
"time"
"fmt"
"github.com/sirupsen/logrus"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readconcern"
"go.mongodb.org/mongo-driver/mongo/writeconcern"
"penahub.gitlab.yandexcloud.net/pena-services/accruals-service/internal/core"
"penahub.gitlab.yandexcloud.net/pena-services/accruals-service/internal/fields"
"penahub.gitlab.yandexcloud.net/pena-services/accruals-service/internal/models"
"penahub.gitlab.yandexcloud.net/pena-services/accruals-service/internal/utils/expression"
"penahub.gitlab.yandexcloud.net/pena-services/accruals-service/internal/utils/expression/discount"
mongoWrapper "penahub.gitlab.yandexcloud.net/pena-services/accruals-service/pkg/mongo"
)
type DiscountRepository struct {
mongoDB *mongo.Collection
logger *logrus.Logger
}
func NewDiscountRepository(mongoDB *mongo.Collection, logger *logrus.Logger) *DiscountRepository {
return &DiscountRepository{
mongoDB: mongoDB,
logger: logger,
}
}
func (receiver *DiscountRepository) getLayer(ctx context.Context, keyGroup, keyParam string, layer uint64, groupPrices, privilegeAmounts map[string]uint64, conditions *core.DiscountConditions) ([]models.Discount, error) {
conditionFilter := discount.ConditionFilter(conditions, groupPrices, privilegeAmounts, layer)
if conditionFilter == nil {
return []models.Discount{}, ErrEmptyArgs
}
filter := bson.M{
"$and": []bson.M{
*conditionFilter,
{"$or": []bson.M{{fields.Discount.Deprecated: false}, {fields.Discount.Deprecated: nil}}},
{"$or": []bson.M{{fields.Audit.Deleted: false}, {fields.Audit.Deleted: nil}}},
},
}
receiver.logger.Info("DR cf", conditionFilter)
cursor, err := receiver.mongoDB.Aggregate(ctx, mongo.Pipeline{
bson.D{
{Key: "$match", Value: filter},
},
bson.D{
{Key: "$sort", Value: bson.M{
keyGroup: 1,
keyParam: -1,
}},
},
bson.D{
{Key: "$group", Value: bson.M{
"_id": "$" + keyGroup,
"doc": bson.M{"$first": "$$ROOT"},
}},
},
bson.D{
{Key: "$replaceRoot", Value: bson.M{
"newRoot": "$doc",
}},
},
})
if err != nil {
return []models.Discount{}, err
}
discounts := []models.Discount{}
err = cursor.All(ctx, &discounts)
fmt.Println("DISCOOOO", discounts)
return discounts, err
}
func (receiver *DiscountRepository) Determine(ctx context.Context, conditions *core.DiscountConditions) ([]models.Discount, uint64, error) {
if conditions == nil {
receiver.logger.Errorln("empty condition on <Determine> of <DiscountRepository>")
return []models.Discount{}, uint64(0), ErrEmptyArgs
}
var (
applyed = []models.Discount{}
keyGroup = "condition.product"
keyParam = "condition.term"
//rawPrice = uint64(0)
groupPrices = map[string]uint64{}
privilegeAmounts = map[string]uint64{}
privilegePrices = map[string]uint64{}
privilegeGroups = map[string]string{}
priority = make(map[string]struct{})
noAccept = make(map[string]struct{})
)
for layer := uint64(1); layer < 5; layer++ {
switch layer {
case 1:
for _, condition := range conditions.Optionals {
if condition.Product == nil || condition.Term == nil {
continue
}
// вычисляем количество привилегий во всей корзине
fmt.Println("GRP", *condition.Group)
privilegeAmounts[*condition.Product] += *condition.Term
privilegePrices[*condition.Product] += *condition.PriceFrom
if _, ok := privilegeAmounts[*condition.Product]; !ok {
privilegeGroups[*condition.Product] = *condition.Group
}
}
discounts, err := receiver.getLayer(ctx, keyGroup, keyParam, layer, privilegeAmounts, groupPrices, conditions)
if err != nil {
return []models.Discount{}, uint64(0), err
}
// проверяем юзера, если его тип nko, то нужно просто применить скидку на nko к сумму сумм привилегий
// в getLayer возвращаются все подходящие скидыки для юзера но нужно только nko
if conditions.Common.UserType == "nko" {
for _, discount := range discounts {
if discount.Condition.UserType != nil && *discount.Condition.UserType == "nko" {
var nkoPrice uint64
for _, privilegePrice := range privilegePrices {
nkoPrice += privilegePrice
}
return []models.Discount{discount}, uint64(float64(nkoPrice) * discount.Target.Factor), nil
}
}
}
// если юзер не nko, то выбираем все скидки за первый слой
// сначала делаем все нужные проверки записываем запреты
for privilege, privilegePrice := range privilegePrices {
fmt.Println("PREPPRIV", privilege, privilegePrice)
// проверки существования скидки за 2 слой noAccept не применям скидки к этой группе если есть такие
for _, discount := range discounts {
if discount.Condition.User != nil && *discount.Condition.User == conditions.Common.User && privilegeGroups[privilege] == *discount.Condition.Group && discount.Layer == 2 {
noAccept[privilegeGroups[privilege]] = struct{}{}
}
}
// если к этой привилегии не было запрета в виде слоя 2, и это скидка для этого юзера и слой 1
// то применям к этой привилегии скидку только раз, но никак не ограничиваем скидку на группу,
// только конкретно на привилегию
for _, discount := range discounts {
fmt.Println("PREPPRIV1", groupPrices, *discount.Condition.User, conditions.Common.User, privilege, *discount.Condition.Product, discount.Layer )
if _, ok := noAccept[privilegeGroups[privilege]]; !ok {
fmt.Println("PREPPRIV3", priority, privilegeGroups)
if discount.Condition.User != nil && discount.Condition.Product != nil && privilege == *discount.Condition.Product && discount.Layer == 1 {
fmt.Println("PREPPRIV4", priority, float64(privilegePrice), discount.Target.Products[0].Factor)
groupPrices[privilegeGroups[privilege]] += uint64(float64(privilegePrice) * discount.Target.Products[0].Factor)
applyed = append(applyed, discount)
priority[privilege] = struct{}{}
}
}
}
}
for privilege, privilegePrice := range privilegePrices {
found := false
// приминеям оставшиеся просто где слой 1 и дозаполняем полностью groupPrices
for _, discount := range discounts {
if _, ok := noAccept[privilegeGroups[privilege]]; !ok {
if _, ok := priority[privilege]; !ok {
if discount.Condition.Product != nil && privilege == *discount.Condition.Product && discount.Layer == 1 && discount.Condition.User == nil {
fmt.Println("PREPPRIV5", groupPrices)
groupPrices[privilegeGroups[privilege]] += uint64(float64(privilegePrice) * discount.Target.Factor)
applyed = append(applyed, discount)
found = true
// сли у нас в ревесте есть такая привилегия но ее нет в пришедших скидках, просто добавляем ее в groupPrices с ключем группы
}
}
}
}
fmt.Println("PREPPRIV2", groupPrices)
if !found {
groupPrices[privilegeGroups[privilege]] += privilegePrice
}
}
keyGroup = "condition.group"
keyParam = "condition.priceFrom"
case 2:
discounts, err := receiver.getLayer(ctx, keyGroup, keyParam, layer, privilegeAmounts, groupPrices, conditions)
if err != nil {
return []models.Discount{}, uint64(0), err
}
var price uint64
// проходимся по мапе где ключ группа и значение прайс за группу с учетом скидки за привилегии
// ищем в скидках скидку за эту группу, для этого юзера, за 2 слой, если найдена то применяем
// и добавляем группу в мапу для предотвращения повторного применения скидки за группу
noAccept := make(map[string]struct{})
for group, nowPrice := range groupPrices {
for _, discount := range discounts {
if discount.Condition.Group != nil && *discount.Condition.Group == group && discount.Layer == 2 && discount.Condition.User != nil && *discount.Condition.User == conditions.Common.User {
groupPrices[group] += uint64(float64(nowPrice) * discount.Target.Factor)
noAccept[group] = struct{}{}
}
}
}
// вычисляем оставшиеся скидки за сервис с ограничениями noAccept
for group, nowPrice := range groupPrices {
for _, discount := range discounts {
if discount.Condition.Group != nil && *discount.Condition.Group == group && discount.Layer == 2 {
if _, ok := noAccept[*discount.Condition.Group]; !ok {
groupPrices[group] += uint64(float64(nowPrice) * discount.Target.Factor)
applyed = append(applyed, discount)
}
}
}
}
// вычислям сумму корзины за 1 и 2 слой для 3 слоя
for _, val := range groupPrices {
price += val
}
fmt.Println("GPP", conditions.Common.CartPurchasesAmount)
conditions.Common.CartPurchasesAmount += price
// если сумма корзины на этом этапе равна 0, то лен(groupPrices) =0 следовательно 3,4 слои тоже будут без скидок
// заполним groupPrices и почитаем conditions.Common.CartPurchasesAmount
if conditions.Common.CartPurchasesAmount == 0 {
for pr, p := range privilegePrices {
conditions.Common.CartPurchasesAmount += p
groupPrices[privilegeGroups[pr]] += p
}
}
//restrictions = noAccept
keyGroup = "condition.user"
keyParam = "condition.cartPurchasesAmount"
case 3:
discounts, err := receiver.getLayer(ctx, keyGroup, keyParam, layer, privilegeAmounts, groupPrices, conditions)
if err != nil {
return []models.Discount{}, uint64(0), err
}
for _, discount := range discounts {
if discount.Condition.User != nil && *discount.Condition.User == "" && discount.Layer == 3 {
for gr, pr := range groupPrices {
if _, ok := noAccept[gr]; !ok {
groupPrices[gr] = uint64(float64(pr) * discount.Target.Factor)
}
}
applyed = append(applyed, discount)
}
}
keyGroup = "condition.userType"
keyParam = "condition.purchasesAmount"
case 4:
discounts, err := receiver.getLayer(ctx, keyGroup, keyParam, layer, privilegeAmounts, groupPrices, conditions)
if err != nil {
return []models.Discount{}, uint64(0), err
}
for _, discount := range discounts {
if discount.Condition.User != nil && *discount.Condition.User == "" && discount.Layer == 4 {
for gr, pr := range groupPrices {
if _, ok := noAccept[gr]; !ok {
groupPrices[gr] = uint64(float64(pr) * discount.Target.Factor)
}
}
applyed = append(applyed, discount)
}
}
}
}
var finishPrice uint64
for _, p := range groupPrices {
finishPrice += p
}
return applyed, finishPrice, nil
}
func (receiver *DiscountRepository) FindByID(ctx context.Context, id string) (*models.Discount, error) {
objectID, err := primitive.ObjectIDFromHex(id)
if err != nil {
receiver.logger.Errorf("failed to parse ObjectID <%s> on <FindByID> of <DiscountRepository>: %v", id, err)
return nil, ErrInvalidID
}
filter := bson.M{
fields.Discount.ID: objectID,
fields.Discount.Deprecated: false,
fields.Audit.Deleted: false,
}
discount, err := mongoWrapper.FindOne[models.Discount](ctx, &mongoWrapper.RequestSettings{
Driver: receiver.mongoDB,
Filter: filter,
})
if err != nil {
receiver.logger.Errorf("failed to find discount <%s> on <FindByID> of <DiscountRepository>: %v", id, err)
if err == mongo.ErrNoDocuments {
return nil, ErrNoRecord
}
return nil, ErrFindRecord
}
return discount, nil
}
func (receiver *DiscountRepository) FindByUserID(ctx context.Context, userID string) ([]models.Discount, error) {
filter := bson.M{
"$or": expression.GetValueConditionBSON(fields.DiscountCondition.User, userID),
fields.Discount.Deprecated: false,
fields.Audit.Deleted: false,
}
discounts, err := mongoWrapper.Find[models.Discount](ctx, &mongoWrapper.RequestSettings{
Driver: receiver.mongoDB,
Filter: filter,
})
if err != nil {
receiver.logger.Errorf("failed to find user <%s> discount on <FindByUserID> of <DiscountRepository>: %v", userID, err)
return nil, ErrFindRecord
}
return discounts, nil
}
func (receiver *DiscountRepository) FindAll(ctx context.Context) ([]models.Discount, error) {
filter := bson.M{
fields.Audit.Deleted: false,
fields.Discount.Deprecated: false,
}
discounts, err := mongoWrapper.Find[models.Discount](ctx, &mongoWrapper.RequestSettings{
Driver: receiver.mongoDB,
Filter: filter,
})
if err != nil {
receiver.logger.Errorf("failed to find all discount on <FindAll> of <DiscountRepository>: %v", err)
return nil, ErrFindRecord
}
return discounts, nil
}
func (receiver *DiscountRepository) UpdateOne(ctx context.Context, request *core.UpdateDiscountSettings) (*models.Discount, error) {
if request == nil {
receiver.logger.Errorln("empy update on <UpdateOne> of <DiscountRepository>")
return nil, ErrEmptyArgs
}
update := discount.GetUpdateDiscountExpression(request)
objectID, err := primitive.ObjectIDFromHex(update.ID)
if err != nil {
receiver.logger.Errorf("failed to parse ObjectID <%s> on <UpdateOne> of <DiscountRepository>: %v", update.ID, err)
return nil, ErrInvalidID
}
updatedDiscount := models.Discount{}
options := options.FindOneAndUpdate().SetUpsert(false).SetReturnDocument(options.After)
filter := bson.M{
fields.Discount.ID: objectID,
}
if err := receiver.mongoDB.FindOneAndUpdate(ctx, filter, update.Expression, options).Decode(&updatedDiscount); err != nil {
receiver.logger.Errorf("failed to update discount with id <%s> on <UpdateOne> of <DiscountRepository>: %v", update.ID, err)
if err == mongo.ErrNoDocuments {
return nil, ErrNoRecord
}
return nil, ErrUpdateRecord
}
return &updatedDiscount, nil
}
func (receiver *DiscountRepository) UpdateMany(ctx context.Context, updates []core.UpdateDiscountSettings) error {
if len(updates) < 1 {
receiver.logger.Errorln("empy updates on <UpdateMany>")
return ErrEmptyArgs
}
transactionOptions := options.Transaction().
SetReadConcern(readconcern.Snapshot()).
SetWriteConcern(writeconcern.New(writeconcern.WMajority()))
session, err := receiver.mongoDB.Database().Client().StartSession()
if err != nil {
receiver.logger.Errorln("failed to start session on <UpdateMany> of <DiscountRepository>")
return ErrTransactionSessionStart
}
defer session.EndSession(ctx)
if _, transactionError := session.WithTransaction(ctx, func(sessionContext mongo.SessionContext) (interface{}, error) {
for _, update := range updates {
updateCopy := update
if _, err := receiver.UpdateOne(sessionContext, &updateCopy); err != nil {
receiver.logger.Errorf("failed to update <%s> in transaction on <UpdateMany> of <DiscountRepository>: %v", update.ID, err)
return nil, err
}
}
return nil, nil
}, transactionOptions); transactionError != nil {
receiver.logger.Errorf("failed to transaction on <UpdateMany> of <DiscountRepository>: %v", transactionError)
return ErrTransaction
}
return nil
}
func (receiver *DiscountRepository) Insert(ctx context.Context, discount *models.Discount) (string, error) {
if discount.Condition.User != nil && *discount.Condition.User != "" {
filter := bson.M{"condition.user": *discount.Condition.User, "audit.deleted": false}
update := bson.M{
"$set": bson.M{"audit.deleted": true},
}
_, err := receiver.mongoDB.UpdateMany(ctx, filter, update)
if err != nil && err != mongo.ErrNoDocuments {
return "", ErrAlreadyExistDiscount
}
}
res, err := receiver.mongoDB.InsertOne(ctx, discount.Sanitize())
if err != nil {
receiver.logger.Errorf("failed to insert record on <Insert> of <DiscountRepository>: %v", err)
return "", ErrInsertRecord
}
return res.InsertedID.(primitive.ObjectID).Hex(), nil
}
func (receiver *DiscountRepository) DeleteByID(ctx context.Context, id string) (*models.Discount, error) {
objectID, err := primitive.ObjectIDFromHex(id)
if err != nil {
receiver.logger.Errorf("failed to parse ObjectID <%s> on <DeleteByID> of <DiscountRepository>: %v", id, err)
return nil, ErrInvalidID
}
discount := models.Discount{}
now := time.Now()
options := options.FindOneAndUpdate().SetReturnDocument(options.After)
update := bson.M{"$set": bson.M{
fields.Audit.Deleted: true,
fields.Audit.DeletedAt: now,
fields.Audit.UpdatedAt: now,
}}
filter := bson.M{
fields.Discount.ID: objectID,
}
if err := receiver.mongoDB.FindOneAndUpdate(ctx, filter, update, options).Decode(&discount); err != nil {
receiver.logger.Errorf("failed to set 'deleted=true' with id <%s> on <DeleteByID> of <DiscountRepository>: %v", id, err)
if err == mongo.ErrNoDocuments {
return nil, ErrNoRecord
}
return nil, ErrUpdateRecord
}
return &discount, nil
}