merge dev in ref intrf

This commit is contained in:
Pavel 2024-01-31 23:25:30 +03:00
commit d614710c80
43 changed files with 1635 additions and 251 deletions

@ -1,57 +1,16 @@
include:
- project: "devops/pena-continuous-integration"
file: "/templates/docker/build-template.gitlab-ci.yml"
- project: "devops/pena-continuous-integration"
file: "/templates/docker/deploy-template.gitlab-ci.yml"
stages:
- lint
- test
- clean
- build
- deploy
lint:
image: golangci/golangci-lint:v1.55-alpine
stage: lint
before_script:
- go install github.com/vektra/mockery/v2@v2.26.0
- go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.12.4
script:
- go generate ./internal/...
- golangci-lint version
- golangci-lint run ./internal/...
test:
image: golang:1.21-alpine
stage: test
coverage: /\(statements\)(?:\s+)?(\d+(?:\.\d+)?%)/
script:
- CGO_ENABLED=0 go test ./internal/... -coverprofile=coverage.out
- go tool cover -html=coverage.out -o coverage.html
- go tool cover -func coverage.out
artifacts:
expire_in: "3 days"
paths:
- coverage.html
clean-old:
stage: clean
image:
name: docker/compose:1.28.0
entrypoint: [""]
allow_failure: true
variables:
PRODUCTION_BRANCH: main
STAGING_BRANCH: "staging"
DEPLOY_TO: "staging"
rules:
- if: $CI_COMMIT_BRANCH == $PRODUCTION_BRANCH || $CI_COMMIT_BRANCH == $STAGING_BRANCH
when: on_success
before_script:
- echo DEPLOY_TO = $DEPLOY_TO
script:
- docker-compose -f deployments/$DEPLOY_TO/docker-compose.yaml down --volumes --rmi local
build-app:
stage: build
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
tags:
- gobuild
variables:
DOCKER_BUILD_PATH: "./Dockerfile"
STAGING_BRANCH: "staging"
@ -69,32 +28,22 @@ build-app:
- echo CI_COMMIT_REF_SLUG = $CI_COMMIT_REF_SLUG
- echo DOCKER_BUILD_PATH = $DOCKER_BUILD_PATH
- echo CI_PIPELINE_ID = $CI_PIPELINE_ID
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
- |
/kaniko/executor --context $CI_PROJECT_DIR \
--cache=true --cache-repo=$CI_REGISTRY_IMAGE \
--dockerfile $CI_PROJECT_DIR/$DOCKER_BUILD_PATH --target production \
--destination $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID --build-arg GITLAB_TOKEN=$GITLAB_TOKEN $CI_PROJECT_DIR
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
deploy-to-staging:
stage: deploy
image:
name: docker/compose:1.28.0
entrypoint: [""]
extends: .deploy_template
variables:
DEPLOY_TO: "staging"
BRANCH: "staging"
rules:
- if: $CI_COMMIT_BRANCH == $BRANCH
before_script:
- echo CI_PROJECT_NAME = $CI_PROJECT_NAME
- echo CI_REGISTRY = $CI_REGISTRY
- echo REGISTRY_USER = $REGISTRY_USER
- echo REGISTRY_TOKEN = $REGISTRY_TOKEN
- echo DEPLOY_TO = $DEPLOY_TO
- echo BRANCH = $BRANCH
script:
- docker login -u $REGISTRY_USER -p $REGISTRY_TOKEN $CI_REGISTRY
- docker-compose -f deployments/$DEPLOY_TO/docker-compose.yaml up -d
- if: "$CI_COMMIT_BRANCH == $BRANCH"
after_script:
- ls

@ -44,7 +44,7 @@ linters:
- revive
- rowserrcheck
- staticcheck
- stylecheck
# - stylecheck
- thelper
- typecheck
- unconvert

@ -2,7 +2,7 @@
FROM golang:1.20.3-alpine AS build
# Update packages and clear cache
RUN apk update && apk add --no-cache curl && rm -rf /var/cache/apk/*
RUN apk add --no-cache curl
# Set work directory
WORKDIR /app
# Create binary directory
@ -24,9 +24,7 @@ RUN GOOS=linux go build -o bin ./...
FROM alpine:3.18.3 AS test
# Install packages
RUN apk --no-cache add ca-certificates && rm -rf /var/cache/apk/*
# Set GO111MODULE env
ENV GO111MODULE=off
RUN apk --no-cache add ca-certificates
# Create home directory
WORKDIR /app
# Copy build file
@ -44,7 +42,7 @@ CMD [ "./app" ]
FROM alpine:3.18.3 AS production
# Install packages
RUN apk --no-cache add ca-certificates && rm -rf /var/cache/apk/*
RUN apk --no-cache add ca-certificates
# Create home directory
WORKDIR /app
# Copy build file

@ -25,15 +25,16 @@ test.unit: ## run unit tests
go test ./...
test.integration: ## run integration tests
go mod vendor
@make test.integration.up
@make test.integration.start
@make test.integration.down
test.integration.up: ## build integration test environment
docker-compose -f deployments/test/docker-compose.yaml up -d
docker-compose -f deployments/test/docker-compose.yaml up -d --remove-orphans
test.integration.down: ## shutting down integration environment
docker-compose -f deployments/test/docker-compose.yaml down --volumes --rmi local
docker-compose -f deployments/test/docker-compose.yaml down --volumes
test.integration.start: ## run integration test
go test -count=1 ./tests/integration/...

@ -42,3 +42,4 @@ KAFKA_TOPIC_TARIFF - название топика для сообщений т
## Полезные ссылки:
- [**Диаграммы**](./docs/diagram/README.md)
- Для того чтобы создать новые endpoint, нужно прописать их в customer/api/openapi/v1/openapi.yaml, сделать его описание, с помощью инструкций в makefile сгенерировать файлы

@ -18,16 +18,6 @@ tags:
description: история
paths:
/health:
get:
summary: Health Check
description: Check the health status of the API
responses:
200:
description: OK
503:
description: Service Unavailable
/account:
get:
tags:
@ -536,7 +526,7 @@ paths:
summary: Получение лога событий связанных с аккаунтом
operationId: getHistory
security:
- Bearer: []
- Bearer: [ ]
parameters:
- name: page
in: query
@ -561,6 +551,13 @@ paths:
explode: false
schema:
type: string
- name: accountID
in: query
description: Идентификатор аккаунта. Если не указан, будет использоваться идентификатор из токена.
required: false
explode: false
schema:
type: string
responses:
"200":
description: Успешное получение событий
@ -583,6 +580,140 @@ paths:
schema:
$ref: "#/components/schemas/Error"
/recent:
get:
tags:
- history
summary: Получение недавних тарифов
operationId: getRecentTariffs
description: Возвращает список уникальных тарифов из истории. Айди аккаунта получается из заголовка.
security:
- Bearer: []
responses:
'200':
description: Успешный запрос
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/TariffID"
'400':
description: Неверный запрос
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
'404':
description: Тарифы не найдены
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
'500':
description: Внутренняя ошибка сервера
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
requestBody:
content:
application/json:
schema:
type: object
required: [id]
properties:
id:
type: string
example: "807f1f77bcf81cd799439011"
/sendReport:
post:
tags:
- history
summary: отправить акт проделанных работ на почту
operationId: sendReport
security:
- Bearer: []
description: Запрос на отправку акта проделанных работ
requestBody:
content:
application/json:
schema:
type: object
required: [id]
properties:
id:
type: string
example: "807f1f77bcf81cd799439011"
responses:
"200":
description: успешная отправка
"401":
description: Неавторизован
content:
application/json:
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:
@ -748,6 +879,14 @@ components:
"alfabank",
]
TariffID:
type: object
properties:
ID:
type: string
example: "807f1f77bcf81cd799439011"
AccountStatus:
type: string
enum: ["no", "nko", "org"]

@ -0,0 +1,9 @@
DB_HOST=test-pena-auth-db
DB_PORT=27017
ENVIRONMENT=staging
HTTP_HOST=0.0.0.0
HTTP_PORT=8000
DB_USERNAME=test
DB_PASSWORD=test
DB_NAME=admin
DB_AUTH=admin

@ -0,0 +1,191 @@
version: "3"
volumes:
redpanda: null
test-mongodb: null
test-mongoconfdb: null
services:
customer-service:
container_name: customer-service
build:
context: ../../.
dockerfile: Dockerfile
target: test
env_file:
- .env.test
environment:
- HTTP_HOST=0.0.0.0
- HTTP_PORT=8000
- GRPC_HOST=0.0.0.0
- GRPC_PORT=9000
- GRPC_DOMEN=customer-service:9000
- MONGO_HOST=customer-db
- MONGO_PORT=27017
- MONGO_USER=test
- MONGO_PASSWORD=test
- MONGO_DB_NAME=admin
- MONGO_AUTH=admin
- KAFKA_BROKERS=customer-redpanda:9092
- KAFKA_TOPIC_TARIFF=tariffs
- AUTH_MICROSERVICE_USER_URL=http://pena-auth-service:8000/user
- HUBADMIN_MICROSERVICE_TARIFF_URL=http://hub-admin-backend-service:8000/tariff
- CURRENCY_MICROSERVICE_TRANSLATE_URL=http://cbrfworker-service:8000/change
- DISCOUNT_MICROSERVICE_GRPC_HOST=discount-service:9000
- PAYMENT_MICROSERVICE_GRPC_HOST=treasurer-service:9085
ports:
- 8082:8000
- 9092:9000
depends_on:
- customer-db
- customer-migration
- redpanda
networks:
- test
customer-migration:
container_name: customer-migration
build:
context: ../../.
dockerfile: Dockerfile
target: test
command:
[
"sh",
"-c",
'migrate -source file://migrations -database "mongodb://test:test@customer-db:27017/admin?authSource=admin" up',
]
depends_on:
- customer-db
networks:
- test
customer-db:
container_name: customer-db
image: "mongo:6.0.3"
environment:
MONGO_INITDB_ROOT_USERNAME: test
MONGO_INITDB_ROOT_PASSWORD: test
ports:
- "27024:27017"
networks:
- test
redpanda:
container_name: customer-redpanda
tty: true
image: docker.redpanda.com/redpandadata/redpanda:v23.1.13
command:
- redpanda start
- --smp 1
- --overprovisioned
- --kafka-addr internal://0.0.0.0:9092,external://0.0.0.0:19092
# Address the broker advertises to clients that connect to the Kafka API.
# Use the internal addresses to connect to the Redpanda brokers
# from inside the same Docker network.
# Use the external addresses to connect to the Redpanda brokers
# from outside the Docker network.
- --advertise-kafka-addr internal://redpanda:9092,external://localhost:19092
- --pandaproxy-addr internal://0.0.0.0:8082,external://0.0.0.0:18082
# Address the broker advertises to clients that connect to the HTTP Proxy.
- --advertise-pandaproxy-addr internal://redpanda:8082,external://localhost:18082
- --schema-registry-addr internal://0.0.0.0:8081,external://0.0.0.0:18081
# Redpanda brokers use the RPC API to communicate with each other internally.
- --rpc-addr redpanda:33145
- --advertise-rpc-addr redpanda:33145
ports:
- 18081:18081
- 18082:18082
- 19092:19092
- 19644:9644
volumes:
- redpanda:/var/lib/redpanda/data
networks:
- test
healthcheck:
test: ["CMD-SHELL", "rpk cluster health | grep -E 'Healthy:.+true' || exit 1"]
interval: 15s
timeout: 3s
retries: 5
start_period: 5s
console:
tty: true
image: docker.redpanda.com/redpandadata/console:v2.2.4
entrypoint: /bin/sh
command: -c "echo \"$$CONSOLE_CONFIG_FILE\" > /tmp/config.yml; /app/console"
environment:
CONFIG_FILEPATH: /tmp/config.yml
CONSOLE_CONFIG_FILE: |
kafka:
brokers: ["redpanda:9092"]
schemaRegistry:
enabled: true
urls: ["http://redpanda:8081"]
redpanda:
adminApi:
enabled: true
urls: ["http://redpanda:9644"]
connect:
enabled: true
clusters:
- name: local-connect-cluster
url: http://connect:8083
ports:
- 8080:8080
networks:
- test
depends_on:
- redpanda
connect:
tty: true
image: docker.redpanda.com/redpandadata/connectors:latest
hostname: connect
container_name: connect
networks:
- test
# platform: 'linux/amd64'
depends_on:
- redpanda
ports:
- "8083:8083"
environment:
CONNECT_CONFIGURATION: |
key.converter=org.apache.kafka.connect.converters.ByteArrayConverter
value.converter=org.apache.kafka.connect.converters.ByteArrayConverter
group.id=connectors-cluster
offset.storage.topic=_internal_connectors_offsets
config.storage.topic=_internal_connectors_configs
status.storage.topic=_internal_connectors_status
config.storage.replication.factor=-1
offset.storage.replication.factor=-1
status.storage.replication.factor=-1
offset.flush.interval.ms=1000
producer.linger.ms=50
producer.batch.size=131072
CONNECT_BOOTSTRAP_SERVERS: redpanda:9092
CONNECT_GC_LOG_ENABLED: "false"
CONNECT_HEAP_OPTS: -Xms512M -Xmx512M
CONNECT_LOG_LEVEL: info
test-pena-auth-db:
container_name: test-pena-auth-db
init: true
image: "mongo:6.0.3"
command: mongod --quiet --logpath /dev/null
volumes:
- test-mongodb:/data/db
- test-mongoconfdb:/data/configdb
environment:
MONGO_INITDB_ROOT_USERNAME: test
MONGO_INITDB_ROOT_PASSWORD: test
networks:
- test
networks:
test:

@ -24,11 +24,13 @@ services:
- KAFKA_BROKERS=10.6.0.11:9092
- KAFKA_TOPIC_TARIFF=tariffs
- AUTH_MICROSERVICE_USER_URL=https://admin.pena.digital/user
- HUBADMIN_MICROSERVICE_TARIFF_URL=https://admin.pena.digital/strator/tariff
- AUTH_MICROSERVICE_USER_URL=http://10.6.0.11:59300/user
- HUBADMIN_MICROSERVICE_TARIFF_URL=http://10.6.0.11:59303/tariff
- CURRENCY_MICROSERVICE_TRANSLATE_URL=http://10.6.0.11:3131/change
- DISCOUNT_MICROSERVICE_GRPC_HOST=10.6.0.11:9001
- PAYMENT_MICROSERVICE_GRPC_HOST=10.6.0.11:9085
- VERIFICATION_MICROSERVICE_USER_URL=http://10.6.0.17:7035/verification
- TEMPLATEGEN_MICROSERVICE_URL=10.6.0.17
- JWT_PUBLIC_KEY=$JWT_PUBLIC_KEY
- JWT_ISSUER=pena-auth-service
@ -37,9 +39,4 @@ services:
- 8065:8065
- 9065:9065
networks:
- marketplace_penahub_frontend
- default
networks:
marketplace_penahub_frontend:
external: true

@ -0,0 +1,18 @@
version: "3"
services:
integration:
container_name: customer-integration
image: golang:1
volumes:
- ../..:/app:ro,z
working_dir: /app
command: go test ./tests/integration/...
environment:
- CUSTOMER_SERVICE=customer-service:8000
networks:
- test_test
networks:
test_test:
external: true

@ -7,7 +7,7 @@ services:
customer-service:
container_name: customer-service
build:
context: ../../.
context: ../..
dockerfile: Dockerfile
target: test
env_file:
@ -40,17 +40,11 @@ services:
- 9092:9000
depends_on:
customer-db:
condition: service_healthy
condition: service_started
customer-migration:
condition: service_completed_successfully
redpanda:
condition: service_healthy
healthcheck:
test: wget -q --spider http://localhost:8000/health
interval: 2s
timeout: 2s
retries: 5
start_period: 5s
networks:
- test
@ -75,12 +69,6 @@ services:
MONGO_INITDB_ROOT_PASSWORD: test
ports:
- "27024:27017"
healthcheck:
test: mongosh --quiet --eval "db.adminCommand('ping')" >/dev/null
interval: 2s
timeout: 2s
retries: 5
start_period: 5s
networks:
- test
@ -150,38 +138,42 @@ services:
networks:
- test
depends_on:
- redpanda
connect:
condition: service_started
redpanda:
condition: service_healthy
connect:
tty: true
image: docker.redpanda.com/redpandadata/connectors:latest
hostname: connect
container_name: connect
networks:
- test
# hostname: connect
container_name: customer-connect
# platform: 'linux/amd64'
depends_on:
- redpanda
redpanda:
condition: service_healthy
ports:
- "8083:8083"
environment:
CONNECT_CONFIGURATION: |
key.converter=org.apache.kafka.connect.converters.ByteArrayConverter
value.converter=org.apache.kafka.connect.converters.ByteArrayConverter
group.id=connectors-cluster
offset.storage.topic=_internal_connectors_offsets
config.storage.topic=_internal_connectors_configs
status.storage.topic=_internal_connectors_status
config.storage.replication.factor=-1
offset.storage.replication.factor=-1
status.storage.replication.factor=-1
offset.flush.interval.ms=1000
producer.linger.ms=50
producer.batch.size=131072
key.converter=org.apache.kafka.connect.converters.ByteArrayConverter
value.converter=org.apache.kafka.connect.converters.ByteArrayConverter
group.id=connectors-cluster
offset.storage.topic=_internal_connectors_offsets
config.storage.topic=_internal_connectors_configs
status.storage.topic=_internal_connectors_status
config.storage.replication.factor=-1
offset.storage.replication.factor=-1
status.storage.replication.factor=-1
offset.flush.interval.ms=1000
producer.linger.ms=50
producer.batch.size=131072
CONNECT_BOOTSTRAP_SERVERS: redpanda:9092
CONNECT_GC_LOG_ENABLED: "false"
CONNECT_HEAP_OPTS: -Xms512M -Xmx512M
CONNECT_LOG_LEVEL: info
networks:
- test
networks:
test:

6
go.mod

@ -16,7 +16,7 @@ require (
github.com/twmb/franz-go v1.13.6
github.com/twmb/franz-go/pkg/kadm v1.8.1
go.mongodb.org/mongo-driver v1.11.4
go.uber.org/zap v1.24.0
go.uber.org/zap v1.26.0
google.golang.org/genproto v0.0.0-20230223222841-637eb2293923
google.golang.org/grpc v1.53.0
google.golang.org/protobuf v1.31.0
@ -24,7 +24,6 @@ require (
require (
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/benbjohnson/clock v1.3.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
@ -55,11 +54,10 @@ require (
github.com/xdg-go/scram v1.1.1 // indirect
github.com/xdg-go/stringprep v1.0.3 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.12.0 // indirect
golang.org/x/net v0.14.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/text v0.12.0 // indirect
golang.org/x/time v0.3.0 // indirect

16
go.sum

@ -4,8 +4,6 @@ github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMz
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benbjohnson/clock v1.3.3 h1:g+rSsSaAzhHJYcIQE78hJ3AhyjjtQvleKDjlhdBnIhc=
github.com/benbjohnson/clock v1.3.3/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@ -149,6 +147,7 @@ github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
@ -167,16 +166,15 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
go.mongodb.org/mongo-driver v1.11.4 h1:4ayjakA013OdpGyL2K3ZqylTac/rMjrJOMZ1EHizXas=
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@ -209,8 +207,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

@ -13,14 +13,18 @@ type ClientsDeps struct {
CurrencyURL *models.CurrencyMicroserviceURL
DiscountServiceConfiguration *models.DiscountMicroserviceConfiguration
PaymentServiceConfiguration *models.PaymentMicroserviceConfiguration
VerificationURL *models.VerificationMicroserviceURL
TemplategenURL *models.TemplategenMicroserviceURL
}
type Clients struct {
AuthClient *client.AuthClient
HubadminClient *client.HubadminClient
CurrencyClient *client.CurrencyClient
DiscountClient *client.DiscountClient
PaymentClient *client.PaymentClient
AuthClient *client.AuthClient
HubadminClient *client.HubadminClient
CurrencyClient *client.CurrencyClient
DiscountClient *client.DiscountClient
PaymentClient *client.PaymentClient
VerificationClient *client.VerificationClient
TemplateClient *client.TemplateClient
}
func NewClients(deps ClientsDeps) *Clients {
@ -45,5 +49,13 @@ func NewClients(deps ClientsDeps) *Clients {
Logger: deps.Logger,
PaymentServiceHost: deps.PaymentServiceConfiguration.HostGRPC,
}),
VerificationClient: client.NewVerificationClient(client.VerificationClientDeps{
Logger: deps.Logger,
URLs: deps.VerificationURL,
}),
TemplateClient: client.NewTemplateClient(client.TemplateClientDeps{
Logger: deps.Logger,
URLs: deps.TemplategenURL,
}),
}
}

@ -21,6 +21,8 @@ func TestNewClients(t *testing.T) {
CurrencyURL: &models.CurrencyMicroserviceURL{},
DiscountServiceConfiguration: &models.DiscountMicroserviceConfiguration{HostGRPC: "host"},
PaymentServiceConfiguration: &models.PaymentMicroserviceConfiguration{HostGRPC: "host"},
VerificationURL: &models.VerificationMicroserviceURL{Verification: ""},
TemplategenURL: &models.TemplategenMicroserviceURL{Templategen: ""},
})
assert.NotNil(t, clients)

@ -90,6 +90,16 @@ func setDefaultTestingENV(t *testing.T) *models.Config {
PaymentMicroservice: models.PaymentMicroserviceConfiguration{
HostGRPC: "domen",
},
VerificationMicroservice: models.VerificationMicroserviceConfiguration{
URL: models.VerificationMicroserviceURL{
Verification: "domen",
},
},
TemplategenMicroserviceURL: models.TemplategenMicroserviceConfiguration{
URL: models.TemplategenMicroserviceURL{
Templategen: "domen",
},
},
JWT: models.JWTConfiguration{
PrivateKey: "jwt private key",
PublicKey: "jwt public key",
@ -126,6 +136,8 @@ func setDefaultTestingENV(t *testing.T) *models.Config {
t.Setenv("CURRENCY_MICROSERVICE_TRANSLATE_URL", defaultConfiguration.Service.CurrencyMicroservice.URL.Translate)
t.Setenv("DISCOUNT_MICROSERVICE_GRPC_HOST", defaultConfiguration.Service.DiscountMicroservice.HostGRPC)
t.Setenv("PAYMENT_MICROSERVICE_GRPC_HOST", defaultConfiguration.Service.PaymentMicroservice.HostGRPC)
t.Setenv("VERIFICATION_MICROSERVICE_USER_URL", defaultConfiguration.Service.VerificationMicroservice.URL.Verification)
t.Setenv("TEMPLATEGEN_MICROSERVICE_URL", defaultConfiguration.Service.TemplategenMicroserviceURL.URL.Templategen)
t.Setenv("MONGO_HOST", defaultConfiguration.Database.Host)
t.Setenv("MONGO_PORT", defaultConfiguration.Database.Port)

@ -25,6 +25,8 @@ func TestNewControllers(t *testing.T) {
CurrencyURL: &models.CurrencyMicroserviceURL{},
DiscountServiceConfiguration: &models.DiscountMicroserviceConfiguration{HostGRPC: "host"},
PaymentServiceConfiguration: &models.PaymentMicroserviceConfiguration{HostGRPC: "host"},
VerificationURL: &models.VerificationMicroserviceURL{Verification: ""},
TemplategenURL: &models.TemplategenMicroserviceURL{Templategen: ""},
})
repositories := initialize.NewRepositories(initialize.RepositoriesDeps{

@ -36,6 +36,7 @@ func NewServices(deps ServicesDeps) *Services {
historyService := history.New(history.Deps{
Logger: deps.Logger,
Repository: deps.Repositories.HistoryRepository,
AuthClient: deps.Clients.AuthClient,
})
walletService := wallet.New(wallet.Deps{

@ -25,6 +25,8 @@ func TestNewServices(t *testing.T) {
CurrencyURL: &models.CurrencyMicroserviceURL{},
DiscountServiceConfiguration: &models.DiscountMicroserviceConfiguration{HostGRPC: "host"},
PaymentServiceConfiguration: &models.PaymentMicroserviceConfiguration{HostGRPC: "host"},
VerificationURL: &models.VerificationMicroserviceURL{Verification: ""},
TemplategenURL: &models.TemplategenMicroserviceURL{Templategen: ""},
})
brokers := initialize.NewBrokers(initialize.BrokersDeps{

@ -0,0 +1,88 @@
package client
import (
"bytes"
"context"
"encoding/json"
"log"
"mime/multipart"
"net/http"
"go.uber.org/zap"
"penahub.gitlab.yandexcloud.net/pena-services/customer/internal/errors"
"penahub.gitlab.yandexcloud.net/pena-services/customer/internal/models"
)
type TemplateClientDeps struct {
Logger *zap.Logger
URLs *models.TemplategenMicroserviceURL
}
type TemplateClient struct {
logger *zap.Logger
urls *models.TemplategenMicroserviceURL
}
func NewTemplateClient(deps TemplateClientDeps) *TemplateClient {
if deps.Logger == nil {
log.Panicln("logger is nil on <NewTemplateClient>")
}
if deps.URLs == nil {
log.Panicln("urls is nil on <NewTemplateClient>")
}
return &TemplateClient{
logger: deps.Logger,
urls: deps.URLs,
}
}
func (receiver *TemplateClient) SendData(ctx context.Context, data models.RespGeneratorService, fileContents []byte, email string) errors.Error {
tmplURL := receiver.urls.Templategen
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
err := writer.WriteField("email", email)
if err != nil {
return errors.New(err, errors.ErrInternalError)
}
jsonData, err := json.Marshal(data)
if err != nil {
return errors.New(err, errors.ErrInternalError)
}
err = writer.WriteField("data", string(jsonData))
if err != nil {
return errors.New(err, errors.ErrInternalError)
}
fileWriter, err := writer.CreateFormFile("file", "report.docx")
if err != nil {
return errors.New(err, errors.ErrInternalError)
}
_, err = fileWriter.Write(fileContents)
if err != nil {
return errors.New(err, errors.ErrInternalError)
}
err = writer.Close()
if err != nil {
return errors.New(err, errors.ErrInternalError)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tmplURL, body)
if err != nil {
return errors.New(err, errors.ErrInternalError)
}
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil {
return errors.New(err, errors.ErrInternalError)
}
defer resp.Body.Close()
return nil
}

@ -0,0 +1,58 @@
package client
import (
"context"
"fmt"
"log"
"net/url"
"go.uber.org/zap"
"penahub.gitlab.yandexcloud.net/pena-services/customer/internal/errors"
"penahub.gitlab.yandexcloud.net/pena-services/customer/internal/models"
"penahub.gitlab.yandexcloud.net/pena-services/customer/pkg/client"
)
type VerificationClientDeps struct {
Logger *zap.Logger
URLs *models.VerificationMicroserviceURL
}
type VerificationClient struct {
logger *zap.Logger
urls *models.VerificationMicroserviceURL
}
func NewVerificationClient(deps VerificationClientDeps) *VerificationClient {
if deps.Logger == nil {
log.Panicln("logger is nil on <NewVerificationClient>")
}
if deps.URLs == nil {
log.Panicln("urls is nil on <NewVerificationClient>")
}
return &VerificationClient{
logger: deps.Logger,
urls: deps.URLs,
}
}
func (receiver *VerificationClient) GetVerification(ctx context.Context, userID string) (*models.Verification, errors.Error) {
verifURL, err := url.JoinPath(receiver.urls.Verification, userID)
if err != nil {
return nil, errors.New(
fmt.Errorf("failed to join path on <GetVerification> of <VerificationClient>: %w", err),
errors.ErrInternalError,
)
}
response, err := client.Get[models.Verification, models.FastifyError](ctx, &client.RequestSettings{
URL: verifURL,
Headers: map[string]string{"Content-Type": "application/json"},
})
if err != nil {
return nil, errors.New(err, errors.ErrInternalError)
}
return response.Body, nil
}

@ -1,6 +1,7 @@
package history
import (
"fmt"
"log"
"net/http"
@ -38,8 +39,17 @@ func New(deps Deps) *Controller {
}
func (receiver *Controller) GetHistoryList(ctx echo.Context, params swagger.GetHistoryParams) error {
var userID string
if params.AccountID != nil && *params.AccountID != "" {
userID = *params.AccountID
} else {
userID, _ = ctx.Get(models.AuthJWTDecodedUserIDKey).(string)
}
histories, err := receiver.historyService.GetHistoryList(ctx.Request().Context(), &history.GetHistories{
Type: params.Type,
Type: params.Type,
UserID: userID,
Pagination: &models.Pagination{
Page: int64(*params.Page),
Limit: int64(*params.Limit),
@ -52,3 +62,74 @@ func (receiver *Controller) GetHistoryList(ctx echo.Context, params swagger.GetH
return ctx.JSON(http.StatusOK, histories)
}
// TODO:tests.
func (receiver *Controller) GetRecentTariffs(ctx echo.Context) error {
userID, ok := ctx.Get(models.AuthJWTDecodedUserIDKey).(string)
if !ok {
receiver.logger.Error("failed to convert jwt payload to string on <GetRecentTariffs> of <HistoryController>")
return errors.HTTP(ctx, errors.New(
fmt.Errorf("failed to convert jwt payload to string: %s", userID),
errors.ErrInvalidArgs,
))
}
tariffs, err := receiver.historyService.GetRecentTariffs(ctx.Request().Context(), userID)
if err != nil {
receiver.logger.Error("failed to get recent tariffs on <GetRecentTariffs> of <HistoryController>",
zap.String("userId", userID),
zap.Error(err),
)
return errors.HTTP(ctx, err)
}
return ctx.JSON(http.StatusOK, tariffs)
}
// TODO:tests.
func (receiver *Controller) SendReport(ctx echo.Context) error {
historyID := ctx.Param("id")
err := receiver.historyService.GetHistoryByID(ctx.Request().Context(), historyID)
if err != nil {
receiver.logger.Error("failed to send report on <SendReport> of <HistoryController>", zap.Error(err))
return errors.HTTP(ctx, err)
}
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,12 +4,15 @@ 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"
"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"
@ -80,6 +83,9 @@ func (receiver *HistoryRepository) FindMany(ctx context.Context, dto *history.Ge
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,
@ -118,3 +124,218 @@ func (receiver *HistoryRepository) CountAll(ctx context.Context, dto *history.Ge
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 <GetRecentTariffs> of <HistoryRepository>",
zap.String("userId", userID),
zap.Error(err),
)
return nil, errors.New(
fmt.Errorf("failed to get recent tariffs on <GetRecentTariffs> of <HistoryRepository>: %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 <GetRecentTariffs> of <HistoryRepository>",
zap.String("userId", userID),
zap.Error(err),
)
return nil, errors.New(
fmt.Errorf("failed to decode recent tariffs on <GetRecentTariffs> of <HistoryRepository>: %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{}
err := receiver.mongoDB.FindOne(ctx, bson.M{"_id": historyID}).Decode(history)
if err != nil {
receiver.logger.Error(
"failed to find by id in <GetHistoryById> of <HistoryRepository>",
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 <GetDocNumber> of <HistoryRepository>",
zap.String("userId", userID),
zap.Error(err),
)
return nil, errors.New(
fmt.Errorf("failed to get DocNumber list on <GetDocNumber> of <HistoryRepository>: %w", err),
errors.ErrInternalError,
)
}
defer func() {
if err := cursor.Close(ctx); err != nil {
receiver.logger.Error("failed to close cursor on <GetDocNumber> of <HistoryRepository>",
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 <GetDocNumber> of <HistoryRepository>",
zap.String("userId", userID),
zap.Error(err),
)
return nil, errors.New(
fmt.Errorf("failed to decode history on <GetDocNumber> of <HistoryRepository>: %w", err),
errors.ErrInternalError,
)
}
result[history.ID] = count
count++
}
if err := cursor.Err(); err != nil {
receiver.logger.Error("cursor error on <GetDocNumber> of <HistoryRepository>",
zap.String("userId", userID),
zap.Error(err),
)
return nil, errors.New(
fmt.Errorf("cursor error on <GetDocNumber> of <HistoryRepository>: %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 <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
}

@ -207,8 +207,9 @@ func (api *API2) PaginationAccounts(ctx echo.Context, params PaginationAccountsP
return api.error(ctx, http.StatusInternalServerError, "default values missing for PaginationAccounts")
}
page := int64(max(*params.Page, 1))
limit := min(int64(max(*params.Limit, 1)), models.DefaultLimit)
page := int64(math.Max(float64(*params.Page), 1))
limit := int64(math.Max(float64(*params.Limit), 1))
limit = int64(math.Min(float64(limit), float64(models.DefaultLimit)))
count, err := api.account.CountAll(ctx.Request().Context())
if err != nil {
@ -592,3 +593,18 @@ func (api *API2) ChangeCurrency(ctx echo.Context) error {
return ctx.JSON(http.StatusOK, updatedAccount)
}
func (api *API2) CalculateLTV(ctx echo.Context) error {
//TODO implement me
panic("implement me")
}
func (api *API2) GetRecentTariffs(ctx echo.Context) error {
//TODO implement me
panic("implement me")
}
func (api *API2) SendReport(ctx echo.Context) error {
//TODO implement me
panic("implement me")
}

@ -59,12 +59,18 @@ type ServerInterface interface {
// обновляет список одобренных валют
// (PUT /currencies)
UpdateCurrencies(ctx echo.Context) error
// Health Check
// (GET /health)
GetHealth(ctx echo.Context) error
// Получение лога событий связанных с аккаунтом
// (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
// отправить акт проделанных работ на почту
// (POST /sendReport)
SendReport(ctx echo.Context) error
// Изменить валюту кошелька
// (PATCH /wallet)
ChangeCurrency(ctx echo.Context) error
@ -272,15 +278,6 @@ func (w *ServerInterfaceWrapper) UpdateCurrencies(ctx echo.Context) error {
return err
}
// GetHealth converts echo context to params.
func (w *ServerInterfaceWrapper) GetHealth(ctx echo.Context) error {
var err error
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.GetHealth(ctx)
return err
}
// GetHistory converts echo context to params.
func (w *ServerInterfaceWrapper) GetHistory(ctx echo.Context) error {
var err error
@ -310,11 +307,51 @@ func (w *ServerInterfaceWrapper) GetHistory(ctx echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter type: %s", err))
}
// ------------- Optional query parameter "accountID" -------------
err = runtime.BindQueryParameter("form", false, false, "accountID", ctx.QueryParams(), &params.AccountID)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter accountID: %s", err))
}
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.GetHistory(ctx, params)
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
ctx.Set(BearerScopes, []string{})
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.GetRecentTariffs(ctx)
return err
}
// SendReport converts echo context to params.
func (w *ServerInterfaceWrapper) SendReport(ctx echo.Context) error {
var err error
ctx.Set(BearerScopes, []string{})
// Invoke the callback with all the unmarshaled arguments
err = w.Handler.SendReport(ctx)
return err
}
// ChangeCurrency converts echo context to params.
func (w *ServerInterfaceWrapper) ChangeCurrency(ctx echo.Context) error {
var err error
@ -378,8 +415,10 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
router.POST(baseURL+"/cart/pay", wrapper.PayCart)
router.GET(baseURL+"/currencies", wrapper.GetCurrencies)
router.PUT(baseURL+"/currencies", wrapper.UpdateCurrencies)
router.GET(baseURL+"/health", wrapper.GetHealth)
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)
router.POST(baseURL+"/wallet", wrapper.RequestMoney)
@ -388,72 +427,81 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"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",
"WKMkDmpVAAA=",
}
// GetSwagger returns the content of the embedded swagger specification file

@ -0,0 +1,173 @@
package swagger
import (
"log"
"github.com/labstack/echo/v4"
)
type accountController interface {
RemoveAccount(ctx echo.Context) error
GetAccount(ctx echo.Context) error
CreateAccount(ctx echo.Context) error
RemoveDirectAccount(ctx echo.Context, userID string) error
GetDirectAccount(ctx echo.Context, userID string) error
GetAccounts(echo.Context, PaginationAccountsParams) error
SetVerificationStatus(ctx echo.Context, userID string) error
UpdateAccountName(ctx echo.Context) error
}
type currencyController interface {
GetCurrencies(ctx echo.Context) error
PutCurrencies(ctx echo.Context) error
}
type cartController interface {
Remove(echo.Context, RemoveFromCartParams) error
Add(echo.Context, Add2cartParams) error
Pay(echo.Context) error
}
type walletController interface {
ChangeCurrency(ctx echo.Context) error
GetPaymentLink(ctx echo.Context) error
}
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 {
AccountController accountController
CurrencyController currencyController
CartController cartController
WalletController walletController
HistoryController historyController
}
type API struct {
accountController accountController
currencyController currencyController
cartController cartController
walletController walletController
historyController historyController
}
func New(deps Deps) *API {
if deps.AccountController == nil {
log.Panicln("AccountController is nil on <New (API)>")
}
if deps.CurrencyController == nil {
log.Panicln("currencyController is nil on <New (API)>")
}
if deps.CartController == nil {
log.Panicln("cartController is nil on <New (API)>")
}
if deps.WalletController == nil {
log.Panicln("walletController is nil on <New (API)>")
}
if deps.HistoryController == nil {
log.Panicln("historyController is nil on <New (API)>")
}
return &API{
accountController: deps.AccountController,
currencyController: deps.CurrencyController,
cartController: deps.CartController,
walletController: deps.WalletController,
historyController: deps.HistoryController,
}
}
// Account
func (receiver *API) DeleteAccount(ctx echo.Context) error {
return receiver.accountController.RemoveAccount(ctx)
}
func (receiver *API) ChangeAccount(ctx echo.Context) error {
return receiver.accountController.UpdateAccountName(ctx)
}
func (receiver *API) SetAccountVerificationStatus(ctx echo.Context, userID string) error {
return receiver.accountController.SetVerificationStatus(ctx, userID)
}
func (receiver *API) GetAccount(ctx echo.Context) error {
return receiver.accountController.GetAccount(ctx)
}
func (receiver *API) AddAccount(ctx echo.Context) error {
return receiver.accountController.CreateAccount(ctx)
}
func (receiver *API) DeleteDirectAccount(ctx echo.Context, userID string) error {
return receiver.accountController.RemoveDirectAccount(ctx, userID)
}
func (receiver *API) GetDirectAccount(ctx echo.Context, userID string) error {
return receiver.accountController.GetDirectAccount(ctx, userID)
}
func (receiver *API) PaginationAccounts(ctx echo.Context, params PaginationAccountsParams) error {
return receiver.accountController.GetAccounts(ctx, params)
}
// Cart
func (receiver *API) RemoveFromCart(ctx echo.Context, params RemoveFromCartParams) error {
return receiver.cartController.Remove(ctx, params)
}
func (receiver *API) Add2cart(ctx echo.Context, params Add2cartParams) error {
return receiver.cartController.Add(ctx, params)
}
func (receiver *API) PayCart(ctx echo.Context) error {
return receiver.cartController.Pay(ctx)
}
// Currency
func (receiver *API) GetCurrencies(ctx echo.Context) error {
return receiver.currencyController.GetCurrencies(ctx)
}
func (receiver *API) UpdateCurrencies(ctx echo.Context) error {
return receiver.currencyController.PutCurrencies(ctx)
}
// History
func (receiver *API) GetHistory(ctx echo.Context, params GetHistoryParams) error {
return receiver.historyController.GetHistoryList(ctx, params)
}
func (receiver *API) GetRecentTariffs(ctx echo.Context) error {
return receiver.historyController.GetRecentTariffs(ctx)
}
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 {
return receiver.walletController.GetPaymentLink(ctx)
}
func (receiver *API) ChangeCurrency(ctx echo.Context) error {
return receiver.walletController.ChangeCurrency(ctx)
}

@ -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 переведённая на текущий курс
@ -155,6 +160,28 @@ type GetHistoryParams struct {
// Type Тип события
Type *string `form:"type,omitempty" json:"type,omitempty"`
// AccountID Идентификатор аккаунта. Если не указан, будет использоваться идентификатор из токена.
AccountID *string `form:"accountID,omitempty" json:"accountID,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"`
}
// ChangeCurrencyJSONBody defines parameters for ChangeCurrency.
@ -184,6 +211,15 @@ 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
// ChangeCurrencyJSONRequestBody defines body for ChangeCurrency for application/json ContentType.
type ChangeCurrencyJSONRequestBody ChangeCurrencyJSONBody

@ -26,13 +26,15 @@ type ConfigurationGRPC struct {
}
type ServiceConfiguration struct {
AuthMicroservice AuthMicroserviceConfiguration
HubadminMicroservice HubadminMicroserviceConfiguration
CurrencyMicroservice CurrencyMicroserviceConfiguration
DiscountMicroservice DiscountMicroserviceConfiguration
PaymentMicroservice PaymentMicroserviceConfiguration
JWT JWTConfiguration
Kafka KafkaConfiguration
AuthMicroservice AuthMicroserviceConfiguration
HubadminMicroservice HubadminMicroserviceConfiguration
CurrencyMicroservice CurrencyMicroserviceConfiguration
DiscountMicroservice DiscountMicroserviceConfiguration
PaymentMicroservice PaymentMicroserviceConfiguration
VerificationMicroservice VerificationMicroserviceConfiguration
TemplategenMicroserviceURL TemplategenMicroserviceConfiguration
JWT JWTConfiguration
Kafka KafkaConfiguration
}
type KafkaConfiguration struct {
@ -65,6 +67,14 @@ type CurrencyMicroserviceConfiguration struct {
URL CurrencyMicroserviceURL
}
type VerificationMicroserviceConfiguration struct {
URL VerificationMicroserviceURL
}
type TemplategenMicroserviceConfiguration struct {
URL TemplategenMicroserviceURL
}
type PaymentMicroserviceConfiguration struct {
HostGRPC string `env:"PAYMENT_MICROSERVICE_GRPC_HOST,required"`
}
@ -84,3 +94,11 @@ type HubadminMicroserviceURL struct {
type CurrencyMicroserviceURL struct {
Translate string `env:"CURRENCY_MICROSERVICE_TRANSLATE_URL,required"`
}
type VerificationMicroserviceURL struct {
Verification string `env:"VERIFICATION_MICROSERVICE_USER_URL,required"`
}
type TemplategenMicroserviceURL struct {
Templategen string `env:"TEMPLATEGEN_MICROSERVICE_URL,required"`
}

@ -14,6 +14,27 @@ type History struct {
DeletedAt *time.Time `json:"deletedAt,omitempty" bson:"deletedAt,omitempty"`
}
type TariffID struct {
ID string `json:"id" bson:"_id"`
}
type ReportHistory struct {
ID string `json:"id" bson:"_id,omitempty"`
UserID string `json:"userId" bson:"userId"`
Comment string `json:"comment" bson:"comment"`
Key string `json:"key" bson:"key"`
RawDetails RawDetails `json:"rawDetails" bson:"rawDetails"`
Deleted bool `json:"isDeleted" bson:"isDeleted"`
CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
UpdatedAt time.Time `json:"updatedAt" bson:"updatedAt"`
DeletedAt *time.Time `json:"deletedAt,omitempty" bson:"deletedAt,omitempty"`
}
type RawDetails struct {
Tariffs []Tariff `json:"tariffs" bson:"tariffs"`
Price int64 `json:"price" bson:"price"`
}
func (receiver *History) Sanitize() *History {
now := time.Now()

@ -0,0 +1,12 @@
package models
type RespGeneratorService struct {
DocNumber int `json:"docnumber"`
Date string `json:"date"`
OrgTaxNum string `json:"orgtaxnum"`
OrgName Name `json:"orgname"`
Name string `json:"name"`
Amount uint64 `json:"amount"`
Price int64 `json:"price"`
Sum int64 `json:"sum"`
}

@ -0,0 +1,19 @@
package models
import "time"
type Verification struct {
ID string `json:"_id" bson:"_id,omitempty"`
UserID string `json:"userID" bson:"user_id,omitempty"`
Accepted bool `json:"accepted" bson:"accepted"`
Status string `json:"status" bson:"status,omitempty"`
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
Comment string `json:"comment" bson:"comment,omitempty"`
Files []VerificationFile `json:"files" bson:"files,omitempty"`
TaxNumber string `json:"taxnumber" bson:"taxnumber,omitempty"`
}
type VerificationFile struct {
Name string `json:"name" bson:"name"`
URL string `json:"url" bson:"url"`
}

@ -165,7 +165,7 @@ func (receiver *Service) Pay(ctx context.Context, accessToken string, userID str
UserInformation: &discount.UserInformation{
ID: account.UserID,
Type: string(account.Status),
PurchasesAmount: uint64(account.Wallet.PurchasesAmount),
PurchasesAmount: uint64(account.Wallet.Spent),
CartPurchasesAmount: tariffsAmount,
},
Products: transfer.TariffsToProductInformations(tariffs),
@ -180,7 +180,7 @@ func (receiver *Service) Pay(ctx context.Context, accessToken string, userID str
UserInformation: &discount.UserInformation{
ID: account.UserID,
Type: string(account.Status),
PurchasesAmount: uint64(account.Wallet.PurchasesAmount),
PurchasesAmount: uint64(account.Wallet.Spent),
CartPurchasesAmount: tariffsAmount,
},
Products: transfer.TariffsToProductInformations(tariffs),
@ -203,10 +203,13 @@ func (receiver *Service) Pay(ctx context.Context, accessToken string, userID str
go func(tariffs []models.Tariff) {
if _, historyErr := receiver.historyService.CreateHistory(ctx, &models.History{
Key: models.CustomerHistoryKeyPayCart,
UserID: account.UserID,
Comment: "Успешная оплата корзины",
RawDetails: tariffs,
Key: models.CustomerHistoryKeyPayCart,
UserID: account.UserID,
Comment: "Успешная оплата корзины",
RawDetails: map[string]any{
"tariffs": tariffs,
"price": discountResponse.Price,
},
}); historyErr != nil {
receiver.logger.Error("failed to insert history on <Pay> of <CartService>", zap.Error(historyErr))
}
@ -214,6 +217,8 @@ func (receiver *Service) Pay(ctx context.Context, accessToken string, userID str
// TODO: обработать ошибки при отправке сообщений
receiver.logger.Error("send to redpanda", zap.String("userID", account.UserID), zap.Any("bought tariffs", tariffs))
if sendErrors := receiver.tariffBrokerService.SendMany(ctx, account.UserID, tariffs); len(sendErrors) > 0 {
for _, err := range sendErrors {
receiver.logger.Error("failed to send tariffs to broker on <Pay> of <CartService>", zap.Error(err))

@ -5,6 +5,8 @@ import (
"fmt"
"log"
"math"
"os"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.uber.org/zap"
@ -16,12 +18,14 @@ import (
type GetHistories struct {
Pagination *models.Pagination
Type *string
UserID string
}
func (receiver *GetHistories) BSON() bson.M {
query := bson.M{
fields.History.IsDeleted: false,
fields.History.Type: *receiver.Type,
fields.History.UserID: receiver.UserID,
}
return query
@ -31,16 +35,38 @@ type historyRepository interface {
CountAll(context.Context, *GetHistories) (int64, errors.Error)
FindMany(context.Context, *GetHistories) ([]models.History, errors.Error)
Insert(context.Context, *models.History) (*models.History, errors.Error)
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 {
GetUser(ctx context.Context, userID string) (*models.User, errors.Error)
}
type verificationClient interface {
GetUser(ctx context.Context, userID string) (*models.Verification, errors.Error)
}
type temlategenClient interface {
SendData(ctx context.Context, data models.RespGeneratorService, fileContents []byte, email string) errors.Error
}
type Deps struct {
Logger *zap.Logger
Repository historyRepository
Logger *zap.Logger
Repository historyRepository
AuthClient authClient
VerificationClient verificationClient
TemlategenClient temlategenClient
}
type Service struct {
logger *zap.Logger
repository historyRepository
logger *zap.Logger
repository historyRepository
AuthClient authClient
VerificationClient verificationClient
TemlategenClient temlategenClient
}
func New(deps Deps) *Service {
@ -52,9 +78,14 @@ func New(deps Deps) *Service {
log.Panicln("repository is nil on <New (history service)>")
}
if deps.AuthClient == nil {
log.Panicln("auth client is nil on <New (account service)>")
}
return &Service{
logger: deps.Logger,
repository: deps.Repository,
AuthClient: deps.AuthClient,
}
}
@ -105,3 +136,138 @@ func (receiver *Service) CreateHistory(ctx context.Context, history *models.Hist
return createdHistory, nil
}
// TODO:tests.
func (receiver *Service) GetRecentTariffs(ctx context.Context, userID string) ([]models.TariffID, errors.Error) {
if userID == "" {
receiver.logger.Error("user id is missing in <GetRecentTariffs> of <HistoryService>")
return nil, errors.New(
fmt.Errorf("user id is missing: %w", errors.ErrInvalidArgs),
errors.ErrInvalidArgs,
)
}
tariffs, err := receiver.repository.GetRecentTariffs(ctx, userID)
if err != nil {
receiver.logger.Error(
"failed to get recent tariffs in <GetRecentTariffs> of <HistoryService>",
zap.String("userId", userID),
zap.Error(err),
)
return nil, err
}
return tariffs, nil
}
func (receiver *Service) GetHistoryByID(ctx context.Context, historyID string) errors.Error {
if historyID == "" {
receiver.logger.Error("history id is missing in <GetHistoryById> of <HistoryService>")
return errors.New(
fmt.Errorf("history id is missing: %w", errors.ErrInvalidArgs),
errors.ErrInvalidArgs,
)
}
tariffs, err := receiver.repository.GetHistoryByID(ctx, historyID)
if err != nil {
receiver.logger.Error(
"failed to get history by id in <GetHistoryById> of <HistoryService>",
zap.String("historyID", historyID),
zap.Error(err),
)
return err
}
if tariffs.Key != models.CustomerHistoryKeyPayCart {
receiver.logger.Error(
"invalid history record key",
zap.String("historyID", historyID),
zap.Error(err),
)
return err
}
historyMap, err := receiver.repository.GetDocNumber(ctx, tariffs.UserID)
if err != nil {
receiver.logger.Error(
"failed to get history of sorting by date created in <GetDocNumber> of <HistoryService>",
zap.String("historyID", historyID),
zap.Error(err),
)
return err
}
verifuser, err := receiver.VerificationClient.GetUser(ctx, tariffs.UserID)
if err != nil {
receiver.logger.Error("failed to get user verification on <GetHistoryById> of <HistoryService>",
zap.Error(err),
zap.String("userID", tariffs.UserID),
)
return err
}
if !verifuser.Accepted {
receiver.logger.Error(
"verification not accepted",
zap.String("userID", tariffs.UserID),
zap.Error(err),
)
return err
}
authuser, err := receiver.AuthClient.GetUser(ctx, tariffs.UserID)
if err != nil {
receiver.logger.Error("failed to get user on <GetHistoryById> of <HistoryService>",
zap.Error(err),
zap.String("userID", tariffs.UserID),
)
return err
}
fileContents, readerr := os.ReadFile("./report.docx")
if readerr != nil {
return errors.New(
fmt.Errorf("failed to read file: %w", errors.ErrInternalError),
errors.ErrInternalError,
)
}
for _, tariff := range tariffs.RawDetails.Tariffs {
totalAmount := uint64(0)
for _, privilege := range tariff.Privileges {
totalAmount += privilege.Amount
}
data := models.RespGeneratorService{
DocNumber: historyMap[historyID] + 1,
Date: time.Now().Format("2006-01-02"),
OrgTaxNum: verifuser.TaxNumber,
OrgName: models.Name{Orgname: "Orgname"},
Name: tariff.Name,
Amount: totalAmount,
Price: tariffs.RawDetails.Price,
Sum: tariffs.RawDetails.Price,
}
err = receiver.TemlategenClient.SendData(ctx, data, fileContents, authuser.Email)
if err != nil {
receiver.logger.Error("failed to send report to user on <GetHistoryById> of <HistoryService>",
zap.Error(err),
zap.String("userID", tariffs.UserID),
)
return err
}
}
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
}

@ -10,7 +10,7 @@ func CalculateCartPurchasesAmount(tariffs []models.Tariff) uint64 {
privilegesSum := uint64(0)
for _, privilege := range tariff.Privileges {
privilegesSum += privilege.Price
privilegesSum += privilege.Price*privilege.Amount
}
sum += privilegesSum

@ -13,6 +13,12 @@ func TariffsToProductInformations(tarrifs []models.Tariff) []*discount.ProductIn
for _, privilege := range tariff.Privileges {
productInformations = append(productInformations, PrivilegeToProductInformation(privilege))
}
if tariff.Price != 0 {
for i := range productInformations {
productInformations[i].Price = tariff.Price
}
}
}
return productInformations

BIN
main Executable file

Binary file not shown.

@ -45,4 +45,4 @@
}
]
}
]
]

BIN
report.docx Normal file

Binary file not shown.

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

@ -0,0 +1,37 @@
package integration_test
import (
"context"
"fmt"
"testing"
"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"
)
func TestHistoryReport(t *testing.T) {
ctx := context.Background()
jwtUtil := helpers.InitializeJWT()
token, err := jwtUtil.Create("807f1f77bcf81cd799439077")
if ok := assert.NoError(t, err); !ok {
return
}
response, err := client.Post[struct{}, models.ResponseErrorHTTP](ctx, &client.RequestSettings{
URL: "http://" + customerServiceBase + "/sendReport",
Body: swagger.SendReportJSONBody{Id: "10002"},
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)
}

@ -3,6 +3,7 @@ package integration_test
import (
"context"
"fmt"
"os"
"testing"
"github.com/stretchr/testify/assert"
@ -11,6 +12,8 @@ import (
"penahub.gitlab.yandexcloud.net/pena-services/customer/tests/helpers"
)
var customerServiceBase = os.Getenv("CUSTOMER_SERVICE")
func TestSetAccountVerificationStatusNO(t *testing.T) {
jwtUtil := helpers.InitializeJWT()
@ -25,7 +28,7 @@ func TestSetAccountVerificationStatusNO(t *testing.T) {
}
response, getAccountErr := client.Patch[models.Account, models.ResponseErrorHTTP](ctx, &client.RequestSettings{
URL: "http://localhost:8082/account/807f1f77bcf81cd799439077",
URL: "http://" + customerServiceBase + "/account/807f1f77bcf81cd799439077",
Body: models.SetAccountStatus{Status: models.AccountStatusNo},
Headers: map[string]string{"Authorization": fmt.Sprintf("Bearer %s", token)},
})
@ -56,7 +59,7 @@ func TestSetAccountVerificationStatusORG(t *testing.T) {
}
response, getAccountErr := client.Patch[models.Account, models.ResponseErrorHTTP](ctx, &client.RequestSettings{
URL: "http://localhost:8082/account/807f1f77bcf81cd799439077",
URL: "http://" + customerServiceBase + "/account/807f1f77bcf81cd799439077",
Body: models.SetAccountStatus{Status: models.AccountStatusOrg},
Headers: map[string]string{"Authorization": fmt.Sprintf("Bearer %s", token)},
})
@ -87,7 +90,7 @@ func TestSetAccountVerificationStatusNKO(t *testing.T) {
}
response, getAccountErr := client.Patch[models.Account, models.ResponseErrorHTTP](ctx, &client.RequestSettings{
URL: "http://localhost:8082/account/807f1f77bcf81cd799439077",
URL: "http://" + customerServiceBase + "/account/807f1f77bcf81cd799439077",
Body: models.SetAccountStatus{Status: models.AccountStatusNko},
Headers: map[string]string{"Authorization": fmt.Sprintf("Bearer %s", token)},
})
@ -118,7 +121,7 @@ func TestSetAccountVerificationStatusFailure(t *testing.T) {
}
response, getAccountErr := client.Patch[models.Account, models.ResponseErrorHTTP](ctx, &client.RequestSettings{
URL: "http://localhost:8082/account/807f1f77bcf81cd799439077",
URL: "http://" + customerServiceBase + "/account/807f1f77bcf81cd799439077",
Body: models.SetAccountStatus{Status: "radnom-status"},
Headers: map[string]string{"Authorization": fmt.Sprintf("Bearer %s", token)},
})

@ -25,7 +25,7 @@ func TestUpdateAccountName(t *testing.T) {
}
response, getAccountErr := client.Patch[models.Account, models.ResponseErrorHTTP](ctx, &client.RequestSettings{
URL: "http://localhost:8082/account",
URL: "http://" + customerServiceBase + "/account",
Body: models.Name{
FirstName: "Ivan",
Secondname: "Ivanov",