Merge branch 'LTV' into 'dev'

Ltv

See merge request pena-services/customer!31
This commit is contained in:
Mikhail 2024-01-13 23:39:51 +00:00
commit 75dc9314f5
9 changed files with 340 additions and 70 deletions

@ -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:

@ -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)
}

@ -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 <CalculateCustomerLTV> of <HistoryRepository>",
zap.Error(err),
)
return 0, errors.New(
fmt.Errorf("failed to calculate customer LTV <CalculateCustomerLTV> of <HistoryRepository>: %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 <CalculateCustomerLTV> of <HistoryRepository>",
zap.Error(err),
)
return 0, errors.New(
fmt.Errorf("failed to getting result LTV <CalculateCustomerLTV> of <HistoryRepository>: %w", err),
errors.ErrInternalError,
)
}
if len(results) == 0 {
return 0, nil
}
averageLTV := int64(results[0].AverageLTV)
return averageLTV, nil
}

@ -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")
}

@ -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{
"H4sIAAAAAAAC/+xce28bV3b/KhfT/rGLjilKUeqN/nPkbOsCyQZ27MCwhN0ReSnNmpxhZoabqAEBkdy1",
"Y0ix63SLBmnsNNkF+l9L0WJISST1Fc79RsU59877UqIkW7Y2D0DmY2buuef5O+eew8+Nkluruw53At9Y",
"+tzwSxu8ZtHLa6WS23ACfFn33Dr3ApvTF7+1y/gP/8yq1avcWDJ+Vbxama9cvbpWqvxqvlS++s47i2+9",
"U5yfN0wj2KzjFX7g2c660TSNkuUFqbvvHXf7lK8WjFXTsANeI3pya6gPLM+zNmlNj1sBL1+jhSuuV7MC",
"Y8koWwG/Etg1riOzzKv8lLfY/nV5U2p7Favq8+jqNdetcsvByx2rxvHKv/d4xVgy/m4uFsScksLcB3hN",
"0zT8wAoa/klXK4Hdkhc3TaNRL5923w2fezfOId5PrWqVBydR+rG8qtk0DY9/0rA9ZNo9UqyIBKUq0SMV",
"xyJmRDIykgJObno1os9d+z0vBUhfmke4TadRw7UdF1e4j39dbz1xb7y3dy3n/rLllfMWUbK88oZbLXMP",
"35W5X/LsemC7jrFkwNcwEk8Y9OAQurAHfTgUO+IBdBkcQFdsibbYNswEu2/cufYBwz+/uaM1IL+kWeQb",
"mMAeW76zvMBgCIcwZMt37iyY7K3w7SITLRjCCHowQUpMBkfQFw+hK9rQhb5oixaSOUbCJrArtuibMUxg",
"n4mWaMNEbMEExtCfRnjxbR29/LO67W2+7zrBhobub6GP64oHDIa0CpLUhzEMxRNcFpc8SPGK/eL99385",
"87p3uaWTyb8Tu2Zf8u7du3fTiy4UF7QG4DRqa1o1eAYTGEFfbE1j383b7+YfmLEQ9fTU7tIs1in9e57n",
"enmtrXHft9Z52tjR+pjjBqziNpyybofS/pbdcvrOxeKiGTsZ2wn+cTG+23YCvs693H5K+BQzokRH/D/b",
"fuB6mxqjc2s17qSDiYEi/BKVFdWdjA26cIi6PsFPYSQ6TDykCxYXtOZ1MbHiHC72lHHGsz69zgPLrvoa",
"pfw/4oxUd7LsEUzghegwOBJb0Ic9+voQJvAjDEVb7JikvXAAQ7x6V3RgT3REm4k/op8RO6IttsQ2PTU0",
"L/QbQxiaUghfRmKIlkARvYCueMygF0qPqYX78rKJfBIufghdfCN2yCtJa0WhttjvfdcpMAZfwy5eugeH",
"6NCQ3B9hD/eFWtGGIRzhdgfQhSMkEYaM/FoXVaYHE/GkkLLLz1eMwPLsSsVfMZburUyV1YphHvflalMn",
"TPlBUhVKDT9wa9wryEXfc8pca4YXHtwzppuO1XTtySHZjKzWNPyGtHGd0X+gwFHa4iu25wchboq3AF9D",
"D7ow1m25ZpfLVT79HphAD4bioe5e11vX3Pgc/2crKysYvCaoYc9R31Rw7OIXWq/JS65TPp4QLc9zvPnQ",
"2kQOfhRqjgIwayE6MY3Adu67lQriFcM0PrE/tZHda9xbk59sum7Ndfgmel53za6i5GzHD6xqtUaJAGIv",
"f4Nuqhumsbawdiu+26pWLHqpw0gfkdLeuJ4XnvzsjIqXY8LHEc7MIjFfBzO+Fx0YwQi6jDYusQ96nx56",
"IPEUxii7EP8gJkCnIh7BEPYZvdwSraRXmC9eLc7PEO9Mo9TwPO6UNjVU/aBdhpFCHIrHsyEElCHKMv/4",
"PyOqETvwAj2c9G+IdEQbnW8PPeME+QD7BEj+VGDwP+Scd8mBo9eO+CRaeK94ilhR/EnFCkSSA8RN5GMP",
"pc9FINWHFwg75XcYdw8wTkC3IMPEAfpcsSU60IeRRJ0H6IL3FNqM9w99M/TNMtYg9fj9GIMOCXBM6+2b",
"TBGiYktfPFVUqvgGP+JqZxNhveGVNiyf+9dqYUacYfVz2BWPSIFEK1K1mBsUNScyFoptybuxeIqypjAn",
"dmBAnqhLqncodoyZCPPr/BTkEAYiXC/x7lhsoywTMhvgZT3RQub1iL0j9QzxiJSgTUKXDgvhMkEB1J4O",
"jFE3jDNAwNA+IqcT+qYs28P95gOG9K8Nzw42b2GKKX3Bu9zyJBRfo1e/Din7l48/oliV5Nl7TsA9Fmxw",
"Frj3ucM+tYMNevs7+Zgl9jtW93jF/sxkvLBeYCvq+cxaK5X5/MJbi2+vGIgcKMklECbXj6jdCIK60URi",
"bafi6sWWgDekMD2VmdErUnYJhYYIVtA0ewRhuuxKjKfkFV3KOCmvCc0gr2ePmfgjyg4OxAO0VTQ1JEHq",
"6xYMYIhqwtAV4CdfSO1EkRco0ATkmJYVZmFXUmSFGEx0iLYERRKYHSJpUnFgBEPDNP7APV8yY75QLBQp",
"Cte5Y9VtY8l4q1AsYHioW8EGCXjOiktUEnlomHqk8q6HMsvNqCyTngkOZQwQ2+QjMKJY+ABES4aE22E5",
"DLXXr7uOL5VsoViUyYgTKFu06vWqXaLb5xCSxmW1GQs4UkfS2xAd0SJn/AU5yX5EdyzhrCk2TWOxOP/S",
"iJNJpIY0eAZ9lG0E9Qehi0gZprF0LzbJe6vNVUSAtZqFqZ0R7WaokD2FoYTai20MDKktov5Z6z76kFAP",
"Vpumsc51HvFrYp8yAYw8Yb1DxbkBxjWKQX0mHsOAlLhLCUkrLBNgZCkw+DfYhz0YkiSGcJC6XC7RiZVt",
"CANGQfWA9tHNRIL9GInsQRfjq6SqJ/OTFyr56uHtzGoEG65n/ytJL6el/8SDN0xFE9xQSvpG6CSSsHgB",
"JPxA6irTzinul/BmX7RnNxT4LsvTJFrty8UyniChoVNspm4FpQ1t6XKAvhPG4kmozlTLlHgSBiESQGEj",
"n1+ot2hNGE+GUzeeU9/lDctZTznZTxrcD951y5svTVaynK4R1bfS8pFQ2qPkLVVP3ghf3yKkvRcz+zL7",
"eniuoDvu5wCxDOXOpBzI/1mcfN31dV7++5BP6EmZTKo1kSPCJZFdmJgxtKhQTmgkstu+eBRlErtiG71x",
"TnWvlctvGjj421KYWKykINPEqtWVphnBxLnPZbmqeSxe/BY9nTwewKwIcYgEBuj7aNEeOruWqh3h35EM",
"8RIYT5jyH6jgP0r1gpGZwMIJ3JbDwVPA53Xb46VEfK9bnlXjAfd84lx6Czeu6/IyG79C6ByepC3F1bs4",
"Hwu8BjcTQs+WYlZfj4bDXy4l/L0oqPGMonPac00LvLOb3V9SmDzrQo8k0iAsfCoo/ooNLIs68SPpCFW9",
"Y5I2ub4OSf9sbydi+Z8t7uVbXAzup9scatu0bCKVq24zspBhWFVNc0wB9NMmBFPS6FQxCu2VCldbMJQF",
"3zgdUDbaE9vZIpUsNLYpyHeopKV/hsnEE7xLpiQyXSbcpsAcg2fwDTw3GfwvvvkvGIoHkiTZCAF/paQ8",
"/iJn/7eiTPoO9+yKUp1bYdvJ6/UFZ8uK0ockZ2onyp/CNJvNN8U7afSJYHBSny67c/hqhj1OsZmTkDEJ",
"Tx+tsxUt4ry08oOsqk8wctNBU4eOXAYy7EYtRBSpx2R3ffYPTPYZPYK+bCmaENR4GJ8zaB6fs9UPrXXb",
"obfXwq2cYKGJRpw0TWKbDpG66H3VaaBosXnZX1OldhfVZEHm/EmDe5uxPdetdW4krbfMK1ajGhhL87oj",
"kCxVRMZgCl0zklC1a3YwhYZiUUPFecFF2qskdSnqzZzJwvMdm4EbWNUPrXX55PjITsvLvF86Q2kyx3Sd",
"9pGxxkXr0BQOtLrK5PFeF14ofXpAmHN/qjGGrbFxaprW9Zu85v6B/9pza8uyMzKj5zqdsI8PMWc6jn9t",
"mDSR8k1kaT19TPVTQ6CyCrsvT3DjeLNPL2UUOH26h7hKoqUse5OPjXWYtPY4zPgsm+RROFQ1PxmsqLFM",
"teVpt5E5g5bIU1eIWyj9JE3jz8jhBDKYhA0ekfQ6PxvHeYwjweDQQLIMPtY8Qv8+V7dkK6u+gP2f0A37",
"HVUrdmQoqk9DnrXAETUKtDFMpV1gHiBtqmjx2ivT1F8VkZ5rMrhcNel4HypVP6AitTQ1vfhlo4tCSwpt",
"5ypPy/FV5xRZYsJGNo7dvnXdMI3la9dPMz1zVjAVQaNuop8rA6ASN0bn/onkYkJdu7thm5es3sXPSjA5",
"bCGiMNTQMPY2daBmeHu2VPplsbV5acSLnk/WYg9PFvAF2fBty1HdELx8KquNtqKOtM+vcmjbG/GQwjTD",
"DucYLmV6Cv/9GtJTU9MwO4SjsDtom3zvkxlXVw3q06trpqb7YU+dEcTlFAoZuRJfgcF/qONjOipO1j/M",
"VOvqMFlAjU+9ZRlzOH3BbB9RYcZtqwzzxnXjVZ4ypAsBHi+5Xnn2OkBoGa++DjDLsUZSu2D/coGSfHMQ",
"tY+9kLMtiW3RqIt4ElboyM8h5MwWMUYJlxf6OOnxPF5SzNDXDb8iONSjduNHutohtZ2q1nGxE9IQQWhZ",
"TKQkNDVGFDfg5fucssXK0KoG+W46MqCcj75Jm5IzDOfBCGl7sF/iyM3qK6jCz2Sk0WDHDBAiaWbyRHcQ",
"5zXSoooXZFGywjCNigtqA5QajbnaOJOXyqzn7Qthx1fpqQnxhNIx8QUMYZdgXHSIJ1sFz+V2xqq/q0cg",
"IWvYU72Kz53yTV53ZSF05iR5ItpRh/mB6DDVWBalz+EgaOTq8NJdvE1z+BdRcMnt/8RMPMm0y9YSliQ+",
"PiunM/ITJB7WVWAiHop2KllP62L8UwbTKpvfRb3joT6Gqp+pdy6tOFcYPKdjLeqooQbyRNdjtvrZlb8Y",
"EI5BdbOZSZcemGsXiPpckA6CjOkhkdKaV2G/oOY5vHQkad/DpAKtZqzGgOWk7EPyFjsyqg5oOqsP/V+q",
"laPxOTWOkhha6+RmRZg8GJxAT3RozDe+tsDgm+jip5iDjclTTMRDqmOKByTJCW2ypTJQ8uchz+S8WrJD",
"U43C0BnrmIoy+wwRRzt8ajRCw2QVh8TwmI4i98PLqFiYHVqWKZgcGgin+fcIVhxKqlVjYjgtJ0fuhumf",
"WOjJTY3lMN2UTujleCzq5Xih5Bxi7Itk0eB4txPnum9oC4DMhVVXWDc1P5lTxYuDHz9MseSxrKmkzPkn",
"VZz/Tj/ymEdHp4gH0aRC6AqPc0cJl6+c/PSe8isM/kqsGsUzqymXT54l4WZxvUR93mQSpeQbHUdqPIKc",
"0a74klKhEQwzrr0rV4yGAell1rGnTgREi9G8LvpCeeQR/qpLeE0nNYZELnlMgWCSnkeK3L9mcjDnt25K",
"N/W+mt58OV7LiiZu4+T/7eJCUTcNu5b4fZ7jNDP6HZ9jp7Nv3PrNlcWF+asMISvJv0tw8cRx7Kq7bjtp",
"L0sf/TbgfqC7ob7hOvyD6Hdj4tuuvlMM/9Pd5/Gg4Tm3vWr6ro0gqPtLc3O+HfCC15hTJ6JTf3riOFYl",
"f2sgGxZURS0xv6uE9SrCRForqrZzX7/pddddr+K2Z/kZgVNmrolR6YvL2r5PjtOmzv3C1LYDh4k8TnRO",
"4TU1x426vpjQlQzzjkTnSonT8sMcZM4ObKTLlIamCqvmmUUHjkJkm/59BPWISA81z0icM5L3D29By5hy",
"eQRI48vVBjU3JCpUVJBWN4T5RHO1+f8BAAD//3AfamloTwAA",
"H4sIAAAAAAAC/+xcbXMbR3L+K1ObfLArKxCk6ejMbzLlS5Q6+1x6c6lE1t0SGJB7Anbh3YVtxoUqEvCJ",
"VpEWI+dScV0s+ey7qnxLQIgQQRIA/0LPP0p1z+z7gAQpihJj31XJILC709PT/fTTPT37pVFya3XX4U7g",
"G3NfGn5phdcs+nitVHIbToAf655b515gc/rhd3YZ/8O/sGr1KjfmjF8Vr1amK1evLpUqv5oula++997s",
"O+8Vp6cN0whW63iFH3i2s2w0TaNkeUHq7vvH3T7mpxlj0TTsgNdIntwY6gvL86xVGtPjVsDL12jgiuvV",
"rMCYM8pWwK8Edo3rxCzzKj/lLbZ/Xd6Uml7Fqvo8unrJdavccvByx6pxvPLvPV4x5oy/m4oXYkqtwtRH",
"eE3TNPzAChr+SVerBbslL26aRqNePu28Gz73brzE8n5uVas8OEnST+RVzaZpePzThu2h0u6TYUUiKFOJ",
"Hqk0FikjWiMjucDJSS9G8rlLf+ClAOVL6win6TRqOLbj4ggP8F/XW07cG8/tfct5MG955bxHlCyvvOJW",
"y9zDv8rcL3l2PbBdx5gz4DsYiG0GXTiEDuxCDw7FlngIHQYH0BFroiU2DTOh7ht3r33E8J/f3tU6kF/S",
"DPJnGMEum787P8OgD4fQZ/N3786Y7J3wz1km1qEPA+jCCCUxGRxBT2xAR7SgAz3REuso5hAFG8GOWKNf",
"hjCCfSbWRQtGYg1GMITeOMGL7+rk5V/UbW/1Q9cJVjRyfw89HFc8ZNCnUVCkHgyhL7ZxWBzyIKUr9taH",
"H7498bj3uKVbk38ndU0+5L179+6lB50pzmgdwGnUlrRm8BRGMICeWBunvpt33s8/MOMh6ump2aVVrDP6",
"DzzP9fJWW+O+by3ztLOj9zHHDVjFbThl3Qyl/8275fSds8VZMwYZ2wn+cTa+23YCvsy93HxK+BQzkkQn",
"/D/bfuB6qxqnc2s17qSDiYFL+A0aK5o7ORt04BBtfYTfwkC0mdigC2ZntO51MbHiJSD2lHHGsz6/zgPL",
"rvoao/xf0ow0d/LsAYzguWgzOBJr0INd+vkQRvAC+qIltkyyXjiAPl69I9qwK9qixcRXiDNiS7TEmtik",
"p4buhbjRh74pF+GbaBmiIXCJnkNHPGbQDVePqYF78rKRfBIOfggd/ENsESpJb8VFXWd/8F2nwBh8Bzt4",
"6S4cIqChuC9gF+eFVtGCPhzhdPegA0coIvQZ4VoHTaYLI7FdSPnllwtGYHl2peIvGHP3F8au1YJhHvfj",
"YlO3mPKLpCmUGn7g1rhXkIN+4JS51g0vPLhnXDcdq+nak0OyGXmtafgN6eM6p/9IkaO0x1dszw9C3hRP",
"Ab6DLnRgqJtyzS6Xq3z8PTCCLvTFhu5e11vW3PgM/88WFhYweI3Qwp6hvang2MEftKjJS65TPl4Qrc5z",
"uvnYWkUN3g4tRxGYpZCdmEZgOw/cSgX5imEan9qf26juJe4tyW9WXbfmOnwVkdddsqu4crbjB1a1WqNE",
"ALmXv0I31Q3TWJpZuhXfbVUrFn3UcaTbZLQ3rucXT353RsPLKeGTiGdmmZivoxk/ijYMYAAdRhOX3AfR",
"p4sIJJ7AENcu5D/ICRBUxCPowz6jj2tiPYkK08WrxekJ4p1plBqex53Sqkaqn7TDMDKIQ/F4MoaAa4hr",
"mX/8n5DViC14jggn8Q2Zjmgh+HYRGUeoB9gnQvLHAoP/JnDeIQBH1I70JNbxXvEEuaL4o4oVyCT3kDcR",
"xh5KzEUi1YPnSDvlbxh3DzBOQKcgw8QBYq5YE23owUCyzgOE4F3FNuP5Q88MsVnGGpQefx9i0KEFHNJ4",
"+yZTgqjY0hNPlJQqvsELHO1sS1hveKUVy+f+tVqYEWdU/Qx2xCMyILEemVqsDYqaIxkLxabU3VA8wbWm",
"MCe2YI+QqEOmdyi2jIkE8+v8FOIQByJeL/nuUGziWibWbA8v64p1VF6X1DtQzxCPyAhatOgSsJAuExVA",
"62nDEG3DOAMFDP0jAp0Qm7JqD+ebDxgSXxueHazewhRTYsH73PIkFV+iT78OJfuXT25TrErq7AMn4B4L",
"VjgL3AfcYZ/bwQr9+Xv5mDn2e1b3eMX+wmS8sFxgC+r5zFoqlfn0zDuz7y4YyBwoySUSJsePpF0JgrrR",
"RGFtp+Lqly1Bb8hguiozo09k7JIK9ZGsoGt2icJ02JWYT8krOpRxUl4TukHezh4z8RWuHRyIh+ir6Goo",
"grTXNdiDPpoJQyjAb76W1olLXqBAExAwzSvOwq6kxAo5mGiTbAmJJDE7RNGk4cAA+oZpfMY9XypjulAs",
"FCkK17lj1W1jzninUCxgeKhbwQot8JQVl6gk89Ao9UjlXRsyy82YLJPIBIcyBohNwgiMKBY+ANmSIel2",
"WA5D6/XrruNLI5spFmUy4gTKF616vWqX6PYppKRxWW3CAo60kfQ0RFusExh/TSDZi+SOVzjrik3TmC1O",
"n5twMonUiAZPoYdrG1H9vRAiUo5pzN2PXfL+YnMRGWCtZmFqZ0Sz6StmT2EoYfZiEwNDaopof9ayjxgS",
"2sFi0zSWuQ4RvyP1KRfAyBPWO1Sc28O4RjGox8Rj2CMj7lBCsh6WCTCyFBj8G+zDLvRpJfpwkLpcDtGO",
"ja0Pe4yC6gHNo5OJBPsxE9mFDsZXKVVX5ifPVfLVxduZ1QhWXM/+V1q9nJX+Ew/eMBNNaEMZ6RthkyjC",
"7AWI8BOZq0w7x8Av8c2eaE3uKPBDVqdJttqTg2WQIGGhY3ymbgWlFW3pcg+xE4ZiOzRnqmVKPgl7IRPA",
"xUY9P1d/ojdhPOmPnXjOfOdXLGc5BbKfNrgfvO+WV89trWQ5XbNU30vPR0FpjlK3VD15I7B+nZj2bqzs",
"y4z18ExRd5zPAXIZyp3JOFD/k4B83fV1KP9jqCdEUiaTak3kiHhJ5BcmZgzrVCgnNhL5bU88ijKJHbGJ",
"aJwz3Wvl8ptGDv5/GUy8rGQg45ZVaytNM6KJU1/KclXzWL74PSKd3B7ArAh5iCQGiH00aBfBbl3VjvDf",
"gQzxkhiPmMIPNPAX0rxgYCa4cIK35XjwGPJ53fZ4KRHf65Zn1XjAPZ80l57Cjeu6vMzGn5A6hztpc3H1",
"Ls7HAq/BzcSiZ0sxi6/HwuGvl5L+XhTVeErROY1c4wLv5G731xQnz0LokWQaxIVPRcVfsYNlWSd+JYFQ",
"1TtGaZfr6Zj0L/52Ipf/xePO3+Nicj/e59DaxmUTqVx1k5GH9MOqalpjiqCfNiEYk0anilHor1S4WoO+",
"LPjG6YDy0a7YzBapZKGxRUG+TSUt/TNMJrbxLpmSyHSZeJsicwyewp/hmcngf/CP/4K+eChFko0Q8DdK",
"yuMfcv5/K8qk73LPrijTuRW2nbxeLDhbVpTeJDlTO1F+F6bZbL4p6KSxJ6LBSXu67ODw7QRzHOMzJzFj",
"Wjx9tM5WtEjz0ssPsqY+wshNG01t2nLZk2E3aiGiSD0kv+uxf2Cyz+gR9GRL0Yioxka8z6B5fM5XP7aW",
"bYf+vBZO5QQPTTTipGUSm7SJ1EH0VbuBYp1Ny/6aKrW7qCYLcudPG9xbjf25bi1zI+m9ZV6xGtXAmJvW",
"bYFkpSIx9sbINaEIVbtmB2NkKBY1UrwsuUijStKWot7MiTw837EZuIFV/dhalk+Ot+y0uszj0hlKkzml",
"66yPnDUuWoeucKC1VSa39zrwXNnTQ+Kc+2OdMWyNjVPTtK3f5DX3M/5rz63Ny87IjJ3rbMI+PsScaTv+",
"tXHSRMo3kqX19DbVz42ByirsvtzBjePNPn2UUeD06R7yKsmWsupNPja2YbLa4zjj02ySR+FQ1fxksKLG",
"MtWWp51GZg9aMk9dIW6m9LN0jT+hhhPMYBQ2eESr1/7FOV7GORIKDh0kq+Bj3SPE96m6JVtZ9QXs/4RO",
"2O+oWrEjR1F9GnKvBY6oUaCFYSoNgXmCtKqixWuvTFN/VSR6rsngctWk43moVP2AitTS1fTLLxtdFFtS",
"bDtXeZqPr3rJJUucsJGNY3duXTdMY/7a9dOcnjkrmYqoUSfRz5UhUIkbo33/RHIxoq7dnbDNS1bv4mcl",
"lBy2EFEYamgUe4c6UDO6PVsqfV5qbV6a5UXkk7XYw5MX+IJ8+I7lqG4IXj6V10ZTUVvaL29y6Nsr8SGF",
"cY4dnmO4lOkp/OU1pKempmG2D0dhd9AmYe/2hKOrBvVXV2lPJ8MeL7leefJcOLSOV58LT1LaT2oY9i9X",
"YM43yFAL1XN5viMxLTruIbbDKhX5OtKubCI/SLh96Ocpr5+qBp8lOV2mscWqlhpVK+C/uX3XOK8KbsVz",
"a9pUq0OFukOxFfKt3ZBrdZn4itQ6kNVHdsexv2CBXeN+YNXqBQb/oTogiibDIECUeaiSij6tQ092VahB",
"ckd8CpN1LQfumAOMQ2rTfFWCp/qkeokG58IZupZpAWgqi5P43A8y0ZW7nqoRYI06fakhHzosto44HW2e",
"KyQpK82djJCtOMNjyrDy13257/oCdunwltzwEZtUK5DN5etEEHbJ7QYsIsh4j3SjSbR8KgCj/eKkJkmP",
"hFfFC8IrWcNQDap7cf6WaClCY2GwI3cAxNfQMxnB04AF7huSlr97Ifr6Nn1wQ2xTRii+hj7sEJOM9hFl",
"t+LkqP+XhAnQGd7QpmnzRbm6jAgMXoSHUyIbFdtx92SqH4e99Zvbd98eGwE8XlLq0u+efEtJYZcs9JFu",
"B4Wa79UBGgRtGYWiQoLcUqFSXBppozbkfLdndstGbpHSM7I9xdTEn2OqN2lS8iSXf24hyz7Hg4eLr2Av",
"ciKaFh1vmyCRyuFUEh3eDIy60GZoadFiU3VTJqpzsvZz2SFIQzyHqsu1S6lS1rHHoorPnfJNXnfldtDE",
"pcKRaEXnbA4wRsv22qiIGB6Hj8guXrqDt2laICIJLrn/n1iPTCrtsjXGJoWPO4aoU+iEFQ+ryzASG6KV",
"KlmmbTF+ocu4/Z0fohM0oT2Gpp/Z9ZlbcK4weEaskvoK6RhNovc7uwfUke9NCQ+DdrL1mQ49MNc0FXX7",
"oRx0ACd9VK605FXYW8Rd8dKBlB3zjQ30mqF6GYJ8X8AGocWWjKp7IVF+W40cHSJWh/ISR3fbuRNzTLZH",
"jKAr2vSyg/jaAsMESF38BA5kloXK3KDdHPGQVnJEk1xXdTjC81Bn8tRusk9dHQgkAj6k0vQ+Q8bRCp8a",
"HSRUNIiW4TERof3wMtoyyb66QRai5NGp8J0mu0QrDqXUqj07PDMsDx730y+a6cpJDeWR4jHnQebjw6Hn",
"g0LJ09gxFsnS6fGwE1f83tBGKFkRVCy3kzpFnjPFi6MfP43x5KGsLKfc+We1RfmD/uB3nh2dIh5E57VC",
"KDwOjhKQr0B+/MmaKwz+pmow0cn9FOQTsiRgFsdL7FKaTLKUfLv3ICoeiBbsiG8oFRpAPwPtHTlidCSa",
"PmaBPbUvKtYZvbUAsVBu/IbvtgqvaacOYxIkDykQjNKnMiP415yfzuHWTQlTH6oz7OeDWlb03oG4/Ptu",
"caaoq64tJd5SdpxlRm8zO/YdFTdu/fbK7Mz01VQNbpKXUlTdZdtJoyx99buA+4HuhvqK6/CPordnxbdd",
"fa8Y/k93n8eDhufc8arpu1aCoO7PTU35dsALXmNK9YWMfQHPcapKvnElGxbUvkLiLQZqsV5FmMgU9Gzn",
"gX7Sy667XMVpT/IylVNmrokXRlxc1vZj8qUCqe6HMLVtw2EijxPtU6CmpulC1x0YQkk/DyQ6KCVNyy9z",
"lDl7bE3tUVlRQ2TuDvVWB9GGo5DZpt8Sox4R2aHmGYluC0L/8Bb0jDGXR4Q0vlxNUHNDokJF23LqhjCf",
"aC42/y8AAP//Dabnb25UAAA=",
}
// GetSwagger returns the content of the embedded swagger specification file

@ -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 {

@ -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

@ -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
}

@ -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)
}