package repository import ( "context" "fmt" "log" codeword_rpc "gitea.pena/PenaSide/customer/internal/proto/codeword" "gitea.pena/PenaSide/customer/internal/utils" "time" "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.uber.org/zap" mongoWrapper "penahub.gitlab.yandexcloud.net/backend/penahub_common/mongo" "gitea.pena/PenaSide/customer/internal/errors" "gitea.pena/PenaSide/customer/internal/fields" "gitea.pena/PenaSide/customer/internal/models" "gitea.pena/PenaSide/customer/internal/service/history" ) type HistoryRepositoryDeps struct { Logger *zap.Logger MongoDB *mongo.Collection } type HistoryRepository struct { logger *zap.Logger mongoDB *mongo.Collection } func NewHistoryRepository(deps HistoryRepositoryDeps) *HistoryRepository { if deps.Logger == nil { log.Panicln("logger is nil on ") } if deps.MongoDB == nil { log.Panicln("mongodb is nil on ") } return &HistoryRepository{ logger: deps.Logger, mongoDB: deps.MongoDB, } } func (receiver *HistoryRepository) Insert(ctx context.Context, history *models.History) (*models.History, errors.Error) { result, err := receiver.mongoDB.InsertOne(ctx, history.Sanitize()) if err != nil { receiver.logger.Error("failed to insert history on of ", zap.Any("history", history), zap.Error(err), ) return nil, errors.New( fmt.Errorf("failed to insert history on of : %w", err), errors.ErrInternalError, ) } insertedID := result.InsertedID.(primitive.ObjectID).Hex() history.ID = insertedID return history, nil } func (receiver *HistoryRepository) FindMany(ctx context.Context, dto *history.GetHistories) ([]models.History, errors.Error) { findOptions := options.Find() findOptions.SetSkip((dto.Pagination.Page - 1) * dto.Pagination.Limit) findOptions.SetLimit(dto.Pagination.Limit) findOptions.SetSort(bson.D{{ Key: "createdAt", Value: -1, }}) histories, err := mongoWrapper.Find[models.History](ctx, &mongoWrapper.RequestSettings{ Driver: receiver.mongoDB, Options: findOptions, Filter: dto.BSON(), }) if err != nil { receiver.logger.Error("failed to find many histories on of ", zap.Int64("page", dto.Pagination.Page), zap.Int64("limit", dto.Pagination.Limit), zap.Int64("skip", (dto.Pagination.Page-1)*dto.Pagination.Limit), zap.Error(err), ) return nil, errors.New( fmt.Errorf("failed to find many histories on of : %w", err), errors.ErrInternalError, ) } return histories, nil } func (receiver *HistoryRepository) CountAll(ctx context.Context, dto *history.GetHistories) (int64, errors.Error) { count, err := receiver.mongoDB.CountDocuments(ctx, dto.BSON()) if err != nil { receiver.logger.Error("failed to count all documents on of ", zap.Error(err), ) return 0, errors.New( fmt.Errorf("failed to count all documents on of : %w", err), errors.ErrInternalError, ) } return count, nil } // TODO:tests // GetRecentTariffs method for processing a user request with data aggregation with a limit of 100 sorted in descending order. func (receiver *HistoryRepository) GetRecentTariffs(ctx context.Context, userID string) ([]models.TariffID, errors.Error) { matchStage := bson.D{ {Key: "$match", Value: bson.D{ {Key: fields.History.UserID, Value: userID}, {Key: fields.History.IsDeleted, Value: false}, {Key: fields.History.Type, Value: models.CustomerHistoryKeyPayCart}, }}, } unwindStage := bson.D{ {Key: "$unwind", Value: bson.D{ {Key: "path", Value: "$rawDetails.tariffs"}, }}, } groupStage := bson.D{ {Key: "$group", Value: bson.D{ {Key: "_id", Value: "$rawDetails.tariffs.id"}, }}, } sortStage := bson.D{ {Key: "$sort", Value: bson.D{ {Key: "createdAt", Value: -1}, }}, } limitStage := bson.D{ {Key: "$limit", Value: 100}, } cursor, err := receiver.mongoDB.Aggregate(ctx, mongo.Pipeline{matchStage, unwindStage, sortStage, groupStage, limitStage}) if err != nil { receiver.logger.Error("failed to get recent tariffs on of ", zap.String("userId", userID), zap.Error(err), ) return nil, errors.New( fmt.Errorf("failed to get recent tariffs on of : %w", err), errors.ErrInternalError, ) } var result []models.TariffID if err := cursor.All(ctx, &result); err != nil { receiver.logger.Error("failed to decode recent tariffs on of ", zap.String("userId", userID), zap.Error(err), ) return nil, errors.New( fmt.Errorf("failed to decode recent tariffs on of : %w", err), errors.ErrInternalError, ) } return result, nil } // TODO:tests. func (receiver *HistoryRepository) GetHistoryByID(ctx context.Context, historyID string) (*models.ReportHistory, errors.Error) { history := &models.ReportHistory{} objID, err := primitive.ObjectIDFromHex(historyID) if err != nil { return nil, errors.New(fmt.Errorf("failed to convert history ID: %w", err), errors.ErrInternalError) } err = receiver.mongoDB.FindOne(ctx, bson.M{"_id": objID}).Decode(history) if err != nil { receiver.logger.Error( "failed to find by id in of ", zap.String("historyID", historyID), zap.Error(err), ) if err == mongo.ErrNoDocuments { return nil, errors.New( fmt.Errorf("history not found with ID: %s", historyID), errors.ErrNotFound, ) } return nil, errors.New( fmt.Errorf("failed to find by id: %w", err), errors.ErrInternalError, ) } return history, nil } // TODO:tests. func (receiver *HistoryRepository) GetDocNumber(ctx context.Context, userID string) (map[string]int, errors.Error) { findOptions := options.Find() findOptions.SetSort(bson.D{{Key: "createdAt", Value: 1}}) filter := bson.M{ fields.History.UserID: userID, } cursor, err := receiver.mongoDB.Find(ctx, filter, findOptions) if err != nil { receiver.logger.Error("failed to get DocNumber list on of ", zap.String("userId", userID), zap.Error(err), ) return nil, errors.New( fmt.Errorf("failed to get DocNumber list on of : %w", err), errors.ErrInternalError, ) } defer func() { if err := cursor.Close(ctx); err != nil { receiver.logger.Error("failed to close cursor on of ", zap.String("userId", userID), zap.Error(err), ) } }() result := make(map[string]int) var count int for cursor.Next(ctx) { var history models.History if err := cursor.Decode(&history); err != nil { receiver.logger.Error("failed to decode history on of ", zap.String("userId", userID), zap.Error(err), ) return nil, errors.New( fmt.Errorf("failed to decode history on of : %w", err), errors.ErrInternalError, ) } result[history.ID] = count count++ } if err := cursor.Err(); err != nil { receiver.logger.Error("cursor error on of ", zap.String("userId", userID), zap.Error(err), ) return nil, errors.New( fmt.Errorf("cursor error on of : %w", err), errors.ErrInternalError, ) } return result, nil } func (receiver *HistoryRepository) CalculateCustomerLTV(ctx context.Context, from, to int64) (int64, errors.Error) { timeFilter := bson.M{} if from != 0 || to != 0 { timeRange := bson.M{} if from != 0 { timeRange["$gte"] = time.Unix(from, 0).UTC().Format(time.RFC3339Nano) } if to != 0 { timeRange["$lte"] = time.Unix(to, 0).UTC().Format(time.RFC3339Nano) } timeFilter["createdAt"] = timeRange } pipeline := mongo.Pipeline{ {{Key: "$match", Value: bson.M{"key": models.CustomerHistoryKeyPayCart, "isDeleted": false}}}, {{Key: "$match", Value: timeFilter}}, {{Key: "$group", Value: bson.M{ "_id": "$userId", "firstPayment": bson.M{"$min": "$createdAt"}, "lastPayment": bson.M{"$max": "$createdAt"}, }}}, {{Key: "$project", Value: bson.M{ "lifeTimeInDays": bson.M{"$divide": []interface{}{ bson.M{"$subtract": []interface{}{bson.M{"$toDate": "$lastPayment"}, bson.M{"$toDate": "$firstPayment"}}}, 86400000, }}, }}}, {{Key: "$group", Value: bson.M{ "_id": nil, "averageLTV": bson.M{"$avg": "$lifeTimeInDays"}, }}}, } cursor, err := receiver.mongoDB.Aggregate(ctx, pipeline) if err != nil { receiver.logger.Error("failed to calculate customer LTV of ", zap.Error(err), ) return 0, errors.New( fmt.Errorf("failed to calculate customer LTV of : %w", err), errors.ErrInternalError, ) } defer func() { if err := cursor.Close(ctx); err != nil { receiver.logger.Error("failed to close cursor", zap.Error(err)) } }() var results []struct{ AverageLTV float64 } if err := cursor.All(ctx, &results); err != nil { receiver.logger.Error("failed to getting result LTV of ", zap.Error(err), ) return 0, errors.New( fmt.Errorf("failed to getting result LTV of : %w", err), errors.ErrInternalError, ) } if len(results) == 0 { return 0, nil } averageLTV := int64(results[0].AverageLTV) return averageLTV, nil } func (receiver *HistoryRepository) GetHistoryByListUsers(ctx context.Context, accounts []models.Account) (map[string][]models.History, error) { historyMap := make(map[string][]models.History) var IDs []string for _, account := range accounts { IDs = append(IDs, account.UserID) } filter := bson.M{"userId": bson.M{"$in": IDs}} cursor, err := receiver.mongoDB.Find(ctx, filter) if err != nil { return nil, err } defer cursor.Close(ctx) for cursor.Next(ctx) { var history models.History if err := cursor.Decode(&history); err != nil { return nil, err } historyMap[history.UserID] = append(historyMap[history.UserID], history) } if err := cursor.Err(); err != nil { return nil, err } return historyMap, nil } func (receiver *HistoryRepository) GetPayUsersPromoHistory(ctx context.Context, codewordData map[string][]*codeword_rpc.PromoActivationResp_UserTime, from, to int64) (map[string]int64, error) { match := utils.MatchGen(codewordData, from, to) pipeline := []bson.M{ match, { "$group": bson.M{ "_id": "$userId", "priceSum": bson.M{"$sum": "$rawDetails.price"}, }, }, } cursor, err := receiver.mongoDB.Aggregate(ctx, pipeline) if err != nil { return nil, err } defer cursor.Close(ctx) userSum := make(map[string]int64) for cursor.Next(ctx) { var result struct { UserID string `bson:"_id"` PriceSum int64 `bson:"priceSum"` } if err := cursor.Decode(&result); err != nil { return nil, err } userSum[result.UserID] = result.PriceSum } if err := cursor.Err(); err != nil { return nil, err } return userSum, nil }