From 962aa779a00c9eb334e40d3a7e53cf0761da4492 Mon Sep 17 00:00:00 2001 From: Pavel Date: Fri, 22 Dec 2023 18:12:43 +0300 Subject: [PATCH 1/4] add calculateltv route --- api/openapi/v1/openapi.yaml | 60 +++++++ .../controller/rest/history/history.go | 35 ++++ internal/interface/repository/history.go | 68 +++++++- internal/interface/swagger/api.2.go | 4 + internal/interface/swagger/api.gen.go | 157 ++++++++++-------- internal/interface/swagger/api.go | 5 + internal/interface/swagger/models.gen.go | 25 +++ internal/service/history/history.go | 11 ++ 8 files changed, 295 insertions(+), 70 deletions(-) diff --git a/api/openapi/v1/openapi.yaml b/api/openapi/v1/openapi.yaml index 20a0caa..17bec8c 100644 --- a/api/openapi/v1/openapi.yaml +++ b/api/openapi/v1/openapi.yaml @@ -578,6 +578,7 @@ paths: tags: - history summary: Получение недавних тарифов + operationId: getRecentTariffs description: Возвращает список уникальных тарифов из истории. Айди аккаунта получается из заголовка. security: - Bearer: [] @@ -647,6 +648,65 @@ paths: schema: $ref: "#/components/schemas/Error" + /history/ltv: + post: + tags: + - history + summary: Расчет среднего времени жизни платящего клиента (LTV) + operationId: calculateLTV + security: + - Bearer: [ ] + requestBody: + description: Период для расчета LTV + required: true + content: + application/json: + schema: + type: object + properties: + from: + type: integer + format: int64 + description: Начальная дата в формате Unix timestamp. Если 0, устанавливает начало истории. + to: + type: integer + format: int64 + description: Конечная дата в формате Unix timestamp. Если 0, устанавливает текущее время. + required: + - from + - to + responses: + '200': + description: Успешный расчет LTV + content: + application/json: + schema: + type: object + properties: + ltv: + type: integer + format: int64 + description: Среднее количество дней между первым и последним платежом + '400': + description: Неверный запрос, если from больше, чем to + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Неавторизован + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + '500': + description: Внутренняя ошибка сервера + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + + components: schemas: Account: diff --git a/internal/interface/controller/rest/history/history.go b/internal/interface/controller/rest/history/history.go index 305d5e5..218f1e7 100644 --- a/internal/interface/controller/rest/history/history.go +++ b/internal/interface/controller/rest/history/history.go @@ -100,3 +100,38 @@ func (receiver *Controller) SendReport(ctx echo.Context) error { return ctx.NoContent(http.StatusOK) } + +// TODO:tests. +func (receiver *Controller) CalculateLTV(ctx echo.Context) error { + var req swagger.CalculateLTVJSONBody + + if err := ctx.Bind(&req); err != nil { + receiver.logger.Error("failed to bind request", zap.Error(err)) + return errors.HTTP(ctx, errors.New( + fmt.Errorf("failed to bind request: %s", err), + errors.ErrInvalidArgs, + )) + } + + if req.From > req.To && req.To != 0 { + receiver.logger.Error("From timestamp must be less than To timestamp unless To is 0") + return errors.HTTP(ctx, errors.New( + fmt.Errorf("From timestamp must be less than To timestamp unless To is 0"), + errors.ErrInvalidArgs, + )) + } + + ltv, err := receiver.historyService.CalculateCustomerLTV(ctx.Request().Context(), req.From, req.To) + if err != nil { + receiver.logger.Error("failed to calculate LTV", zap.Error(err)) + return errors.HTTP(ctx, err) + } + + response := struct { + LTV int64 `json:"LTV"` + }{ + LTV: ltv, + } + + return ctx.JSON(http.StatusOK, response) +} diff --git a/internal/interface/repository/history.go b/internal/interface/repository/history.go index 0bf70ff..ef483da 100644 --- a/internal/interface/repository/history.go +++ b/internal/interface/repository/history.go @@ -3,18 +3,18 @@ package repository import ( "context" "fmt" - "log" - "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" + "log" "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/errors" "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/fields" "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/models" "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/service/history" mongoWrapper "penahub.gitlab.yandexcloud.net/pena-services/customer/pkg/mongo" + "time" ) type HistoryRepositoryDeps struct { @@ -273,3 +273,67 @@ func (receiver *HistoryRepository) GetDocNumber(ctx context.Context, userID stri 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) + } + if to != 0 { + timeRange["$lte"] = time.Unix(to, 0) + } + timeFilter["createdAt"] = timeRange + } + + pipeline := mongo.Pipeline{ + {{"$match", bson.M{"key": models.CustomerHistoryKeyPayCart, "isDeleted": false}}}, + {{"$match", timeFilter}}, + {{"$group", bson.M{ + "_id": "$userId", + "firstPayment": bson.M{"$first": "$createdAt"}, + "lastPayment": bson.M{"$last": "$createdAt"}, + }}}, + {{"$project", bson.M{ + "lifeTimeInDays": bson.M{"$divide": []interface{}{ + bson.M{"$subtract": []interface{}{"$lastPayment", "$firstPayment"}}, + 86400000, + }}, + }}}, + {{"$group", 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 cursor.Close(ctx) + + 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 := results[0].AverageLTV + return int64(averageLTV), nil +} diff --git a/internal/interface/swagger/api.2.go b/internal/interface/swagger/api.2.go index 7652328..d8728b8 100644 --- a/internal/interface/swagger/api.2.go +++ b/internal/interface/swagger/api.2.go @@ -92,3 +92,7 @@ func (api *API2) ChangeCurrency(_ echo.Context) error { func (api *API2) RequestMoney(_ echo.Context) error { panic("TODO") } + +func (api *API2) CalculateLTV(_ echo.Context) error { + panic("TODO") +} diff --git a/internal/interface/swagger/api.gen.go b/internal/interface/swagger/api.gen.go index 0d8f0d1..dd867ee 100644 --- a/internal/interface/swagger/api.gen.go +++ b/internal/interface/swagger/api.gen.go @@ -62,6 +62,9 @@ type ServerInterface interface { // Получение лога событий связанных с аккаунтом // (GET /history) GetHistory(ctx echo.Context, params GetHistoryParams) error + // Расчет среднего времени жизни платящего клиента (LTV) + // (POST /history/ltv) + CalculateLTV(ctx echo.Context) error // Получение недавних тарифов // (GET /recent) GetRecentTariffs(ctx echo.Context) error @@ -309,6 +312,17 @@ func (w *ServerInterfaceWrapper) GetHistory(ctx echo.Context) error { return err } +// CalculateLTV converts echo context to params. +func (w *ServerInterfaceWrapper) CalculateLTV(ctx echo.Context) error { + var err error + + ctx.Set(BearerScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.CalculateLTV(ctx) + return err +} + // GetRecentTariffs converts echo context to params. func (w *ServerInterfaceWrapper) GetRecentTariffs(ctx echo.Context) error { var err error @@ -395,6 +409,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/currencies", wrapper.GetCurrencies) router.PUT(baseURL+"/currencies", wrapper.UpdateCurrencies) router.GET(baseURL+"/history", wrapper.GetHistory) + router.POST(baseURL+"/history/ltv", wrapper.CalculateLTV) router.GET(baseURL+"/recent", wrapper.GetRecentTariffs) router.POST(baseURL+"/sendReport", wrapper.SendReport) router.PATCH(baseURL+"/wallet", wrapper.ChangeCurrency) @@ -405,74 +420,80 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "aC42/y8AAP//Dabnb25UAAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/interface/swagger/api.go b/internal/interface/swagger/api.go index 6f8d338..7257ceb 100644 --- a/internal/interface/swagger/api.go +++ b/internal/interface/swagger/api.go @@ -37,6 +37,7 @@ type historyController interface { GetHistoryList(ctx echo.Context, params GetHistoryParams) error GetRecentTariffs(ctx echo.Context) error SendReport(ctx echo.Context) error + CalculateLTV(ctx echo.Context) error } type Deps struct { @@ -157,6 +158,10 @@ func (receiver *API) SendReport(ctx echo.Context) error { return receiver.historyController.SendReport(ctx) } +func (receiver *API) CalculateLTV(ctx echo.Context) error { + return receiver.historyController.CalculateLTV(ctx) +} + // Wallet func (receiver *API) RequestMoney(ctx echo.Context) error { diff --git a/internal/interface/swagger/models.gen.go b/internal/interface/swagger/models.gen.go index 71d0622..7e15e8e 100644 --- a/internal/interface/swagger/models.gen.go +++ b/internal/interface/swagger/models.gen.go @@ -100,6 +100,11 @@ type Name struct { // PaymentType defines model for PaymentType. type PaymentType string +// TariffID defines model for TariffID. +type TariffID struct { + ID *string `json:"ID,omitempty"` +} + // Wallet defines model for Wallet. type Wallet struct { // Cash Сумма money переведённая на текущий курс @@ -157,6 +162,20 @@ type GetHistoryParams struct { Type *string `form:"type,omitempty" json:"type,omitempty"` } +// CalculateLTVJSONBody defines parameters for CalculateLTV. +type CalculateLTVJSONBody struct { + // From Начальная дата в формате Unix timestamp. Если 0, устанавливает начало истории. + From int64 `json:"from"` + + // To Конечная дата в формате Unix timestamp. Если 0, устанавливает текущее время. + To int64 `json:"to"` +} + +// GetRecentTariffsJSONBody defines parameters for GetRecentTariffs. +type GetRecentTariffsJSONBody struct { + Id string `json:"id"` +} + // SendReportJSONBody defines parameters for SendReport. type SendReportJSONBody struct { Id string `json:"id"` @@ -189,6 +208,12 @@ type SetAccountVerificationStatusJSONRequestBody SetAccountVerificationStatusJSO // UpdateCurrenciesJSONRequestBody defines body for UpdateCurrencies for application/json ContentType. type UpdateCurrenciesJSONRequestBody = UpdateCurrenciesJSONBody +// CalculateLTVJSONRequestBody defines body for CalculateLTV for application/json ContentType. +type CalculateLTVJSONRequestBody CalculateLTVJSONBody + +// GetRecentTariffsJSONRequestBody defines body for GetRecentTariffs for application/json ContentType. +type GetRecentTariffsJSONRequestBody GetRecentTariffsJSONBody + // SendReportJSONRequestBody defines body for SendReport for application/json ContentType. type SendReportJSONRequestBody SendReportJSONBody diff --git a/internal/service/history/history.go b/internal/service/history/history.go index 2acaed1..f2e957f 100644 --- a/internal/service/history/history.go +++ b/internal/service/history/history.go @@ -37,6 +37,7 @@ type historyRepository interface { GetRecentTariffs(context.Context, string) ([]models.TariffID, errors.Error) // new GetHistoryByID(context.Context, string) (*models.ReportHistory, errors.Error) GetDocNumber(context.Context, string) (map[string]int, errors.Error) + CalculateCustomerLTV(ctx context.Context, from, to int64) (int64, errors.Error) } type authClient interface { @@ -259,3 +260,13 @@ func (receiver *Service) GetHistoryByID(ctx context.Context, historyID string) e } return nil } + +func (receiver *Service) CalculateCustomerLTV(ctx context.Context, from, to int64) (int64, errors.Error) { + ltv, err := receiver.repository.CalculateCustomerLTV(ctx, from, to) + if err != nil { + receiver.logger.Error("failed to calculate LTV", zap.Error(err)) + return 0, err + } + + return ltv, nil +} From 0a8f18420729427ba63cfe0c6e5896bf0720aa22 Mon Sep 17 00:00:00 2001 From: Pavel Date: Sat, 23 Dec 2023 01:27:45 +0300 Subject: [PATCH 2/4] add test ltv --- internal/interface/repository/history.go | 17 +++----- tests/integration/calculate_LTV_test.go | 54 ++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 tests/integration/calculate_LTV_test.go diff --git a/internal/interface/repository/history.go b/internal/interface/repository/history.go index ef483da..f93bf7e 100644 --- a/internal/interface/repository/history.go +++ b/internal/interface/repository/history.go @@ -279,25 +279,24 @@ func (receiver *HistoryRepository) CalculateCustomerLTV(ctx context.Context, fro if from != 0 || to != 0 { timeRange := bson.M{} if from != 0 { - timeRange["$gte"] = time.Unix(from, 0) + timeRange["$gte"] = time.Unix(from, 0).UTC().Format(time.RFC3339Nano) } if to != 0 { - timeRange["$lte"] = time.Unix(to, 0) + timeRange["$lte"] = time.Unix(to, 0).UTC().Format(time.RFC3339Nano) } timeFilter["createdAt"] = timeRange } - pipeline := mongo.Pipeline{ {{"$match", bson.M{"key": models.CustomerHistoryKeyPayCart, "isDeleted": false}}}, {{"$match", timeFilter}}, {{"$group", bson.M{ "_id": "$userId", - "firstPayment": bson.M{"$first": "$createdAt"}, - "lastPayment": bson.M{"$last": "$createdAt"}, + "firstPayment": bson.M{"$min": "$createdAt"}, + "lastPayment": bson.M{"$max": "$createdAt"}, }}}, {{"$project", bson.M{ "lifeTimeInDays": bson.M{"$divide": []interface{}{ - bson.M{"$subtract": []interface{}{"$lastPayment", "$firstPayment"}}, + bson.M{"$subtract": []interface{}{bson.M{"$toDate": "$lastPayment"}, bson.M{"$toDate": "$firstPayment"}}}, 86400000, }}, }}}, @@ -318,7 +317,6 @@ func (receiver *HistoryRepository) CalculateCustomerLTV(ctx context.Context, fro ) } defer cursor.Close(ctx) - var results []struct{ AverageLTV float64 } if err := cursor.All(ctx, &results); err != nil { receiver.logger.Error("failed to getting result LTV of ", @@ -329,11 +327,10 @@ func (receiver *HistoryRepository) CalculateCustomerLTV(ctx context.Context, fro errors.ErrInternalError, ) } - if len(results) == 0 { return 0, nil } - averageLTV := results[0].AverageLTV - return int64(averageLTV), nil + averageLTV := int64(results[0].AverageLTV) + return averageLTV, nil } diff --git a/tests/integration/calculate_LTV_test.go b/tests/integration/calculate_LTV_test.go new file mode 100644 index 0000000..25268db --- /dev/null +++ b/tests/integration/calculate_LTV_test.go @@ -0,0 +1,54 @@ +package integration + +import ( + "context" + "fmt" + "github.com/stretchr/testify/assert" + "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/interface/swagger" + "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/models" + "penahub.gitlab.yandexcloud.net/pena-services/customer/pkg/client" + "penahub.gitlab.yandexcloud.net/pena-services/customer/tests/helpers" + "testing" + "time" +) + +func TestCalculateLTV(t *testing.T) { + ctx := context.Background() + jwtUtil := helpers.InitializeJWT() + token, err := jwtUtil.Create("807f1f77bcf81cd799439077") + + if ok := assert.NoError(t, err); !ok { + return + } + + layout := "2006-01-02T15:04:05.000Z" + fromString := "2023-11-08T22:29:48.719Z" + toString := "2023-12-27T15:00:00.000Z" + fromTime, err := time.Parse(layout, fromString) + if err != nil { + fmt.Println("error:", err) + } + toTime, err := time.Parse(layout, toString) + if err != nil { + fmt.Println("error:", err) + } + from := fromTime.Unix() + to := toTime.Unix() + fmt.Println(from, to) + + response, err := client.Post[struct{}, models.ResponseErrorHTTP](ctx, &client.RequestSettings{ + URL: "http://" + "localhost:8000" + "/history/ltv", + Body: swagger.CalculateLTVJSONBody{From: from, To: to}, + Headers: map[string]string{"Authorization": fmt.Sprintf("Bearer %s", token)}, + }) + if ok := assert.NoError(t, err); !ok { + return + } + if ok := assert.Nil(t, response.Error); !ok { + return + } + + assert.Equal(t, 200, response.StatusCode) + + fmt.Println(response.Body) +} From 61fb3f4e3ad2a1a165182f294475e3c867ca74d9 Mon Sep 17 00:00:00 2001 From: Pavel Date: Sat, 23 Dec 2023 11:14:01 +0300 Subject: [PATCH 3/4] fix ltv linter --- internal/interface/repository/history.go | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/internal/interface/repository/history.go b/internal/interface/repository/history.go index f93bf7e..6ded62e 100644 --- a/internal/interface/repository/history.go +++ b/internal/interface/repository/history.go @@ -3,18 +3,20 @@ package repository import ( "context" "fmt" + "log" + "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" - "log" + "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/errors" "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/fields" "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/models" "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/service/history" mongoWrapper "penahub.gitlab.yandexcloud.net/pena-services/customer/pkg/mongo" - "time" ) type HistoryRepositoryDeps struct { @@ -287,20 +289,20 @@ func (receiver *HistoryRepository) CalculateCustomerLTV(ctx context.Context, fro timeFilter["createdAt"] = timeRange } pipeline := mongo.Pipeline{ - {{"$match", bson.M{"key": models.CustomerHistoryKeyPayCart, "isDeleted": false}}}, - {{"$match", timeFilter}}, - {{"$group", bson.M{ + {{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"}, }}}, - {{"$project", bson.M{ + {{Key: "$project", Value: bson.M{ "lifeTimeInDays": bson.M{"$divide": []interface{}{ bson.M{"$subtract": []interface{}{bson.M{"$toDate": "$lastPayment"}, bson.M{"$toDate": "$firstPayment"}}}, 86400000, }}, }}}, - {{"$group", bson.M{ + {{Key: "$group", Value: bson.M{ "_id": nil, "averageLTV": bson.M{"$avg": "$lifeTimeInDays"}, }}}, @@ -316,7 +318,11 @@ func (receiver *HistoryRepository) CalculateCustomerLTV(ctx context.Context, fro errors.ErrInternalError, ) } - defer cursor.Close(ctx) + 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 ", From e45f4d9aa54a4009945c09cb2cb31eeec7937854 Mon Sep 17 00:00:00 2001 From: Pavel Date: Sat, 23 Dec 2023 11:19:14 +0300 Subject: [PATCH 4/4] fix ltv linter#2 --- internal/interface/repository/history.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/interface/repository/history.go b/internal/interface/repository/history.go index 6ded62e..dbf0435 100644 --- a/internal/interface/repository/history.go +++ b/internal/interface/repository/history.go @@ -11,7 +11,6 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.uber.org/zap" - "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/errors" "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/fields" "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/models"