diff --git a/api/openapi/v1/openapi.yaml b/api/openapi/v1/openapi.yaml index c5810a6..fdf1340 100644 --- a/api/openapi/v1/openapi.yaml +++ b/api/openapi/v1/openapi.yaml @@ -655,6 +655,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 ea0d84a..58a1030 100644 --- a/internal/interface/controller/rest/history/history.go +++ b/internal/interface/controller/rest/history/history.go @@ -98,3 +98,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..dbf0435 100644 --- a/internal/interface/repository/history.go +++ b/internal/interface/repository/history.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "time" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" @@ -273,3 +274,68 @@ 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).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 +} 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 e073c8b..fa4a7af 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 @@ -316,6 +319,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 @@ -402,6 +416,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) @@ -412,76 +427,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 916f605..1e7155a 100644 --- a/internal/interface/swagger/models.gen.go +++ b/internal/interface/swagger/models.gen.go @@ -170,6 +170,20 @@ type GetRecentTariffsJSONBody struct { Id string `json:"id"` } +// 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"` @@ -202,6 +216,9 @@ 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 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 +} 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) +}