Merge branch 'fastlinks' into 'dev'

Fastlinks

See merge request pena-services/codeword!13
This commit is contained in:
Mikhail 2024-03-09 15:57:55 +00:00
commit d85c096bd4
49 changed files with 5291 additions and 573 deletions

28
.env

@ -21,6 +21,26 @@ PUBLIC_CURVE_KEY="-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAEbnIvjIMle4rqVol6K
PRIVATE_CURVE_KEY="-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIKn0BKwF3vZvODgWAnUIwQhd8de5oZhY48gc23EWfrfs\n-----END PRIVATE KEY-----"
JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgHgnvr7O2tiApjJfid1orFnIGm6980fZp+Lpbjo+NC/0whMFga2B
iw5b1G2Q/B2u0tpO1Fs/E8z7Lv1nYfr5jx2S8x6BdA4TS2kB9Kf0wn0+7wSlyikH
oKhbtzwXHZl17GsyEi6wHnsqNBSauyIWhpha8i+Y+3GyaOY536H47qyXAgMBAAEC
gYAOphnVPXbk6lpYzdkLC1Xn5EOEuNfOLLURLxBnPWozZo26r/Mtahu/9mYhrYlv
PP8r6mxta3VIil8iOdZyOLa/4d1LPd+UehgEXIJEiYXLtn7RS5eUnoPuQxssfs1k
OWjdN8p6SzppleegFTvGRX4KM3cDLfSphOk8JuBCrpSSYQJBAOdqizTSrdKMTuVe
c7Jk1JOJkyFuFs+N5zeryyeFGH7IpRdWy0rkWMxIUAi8Ap1vYVBPHv4tDOo3sy5X
VLc/knkCQQCE62pg+0TmsrhO/2Pgog6MLBkzlzXYMRp/01HbmznwYF+ejfPnzLkz
hnUlxRUNK3lhXM/7H6oAjvqF2R72u/OPAkEAterkmdbQfEZ+MwNoEiH/lie9OLdx
SSI1VGdBYcTYN7qFRW6eizYstBJYkDU0HQ0Uw+we4hMKJwk4W0KdvxxDiQJAeqlB
V1QqBneBbK10PzVuFV8QtrJhJyxRVwrtbKq38iMNuqUnI4+ijXEUpJFWVvv6nKXo
7McQvEk12dU/JNTX8wJAOlAtSNjp9tVwpMpC0w2St1eKc1L2SknjeohA5ldoBz8sGeZsPhTU3eHSD1neAZXLKN5K68z3zFBr20ubY9nyLw==
-----END RSA PRIVATE KEY-----"
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgHgnvr7O2tiApjJfid1orFnIGm6980fZp+Lpbjo+NC/0whMFga2Biw5b1G2Q/B2u0tpO1Fs/E8z7Lv1nYfr5jx2S8x6BdA4TS2kB9Kf0wn0+7wSlyikHoKhbtzwXHZl17GsyEi6wHnsqNBSauyIWhpha8i+Y+3GyaOY536H47qyXAgMBAAE=\n-----END PUBLIC KEY-----"
JWT_ISSUER="pena-auth-service"
JWT_AUDIENCE="pena"
# SIGN_SECRET="group"
SIGN_SECRET="secret"
@ -36,4 +56,10 @@ SMTP_SENDER="noreply@mailing.pena.digital"
# URL settings
DEFAULT_REDIRECTION_URL = "def.url"
AUTH_EXCHANGE_URL = "http://localhost:8000/auth/exchange"
AUTH_EXCHANGE_URL = "http://localhost:8000/auth/exchange"
DISCOUNT_ADDRESS = "http://CHANGEME:1234"
RECOVERY_URL = "http://127.0.0.1:8080/recover/"
# Kafka settings
KAFKA_BROKERS="localhost:9092"
KAFKA_TOPIC_TARIFF="tariffs"

@ -6,8 +6,9 @@ services:
ports:
- "27020:27017"
environment:
- MONGO_INITDB_ROOT_USERNAME=${MONGO_USER}
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD}
- MONGO_INITDB_ROOT_USERNAME=test
- MONGO_INITDB_ROOT_PASSWORD=test
- MONGO_INITDB_AUTH_MECHANISM=SCRAM-SHA-1
volumes:
- mongo_data:/data/db

@ -2,66 +2,64 @@ openapi: 3.0.0
info:
title: Codeword Recovery Service API
version: 1.0.0
description: API for handling password recovery for the Codeword service.
description: API для обработки восстановления паролей для сервиса Codeword.
tags:
- name: recover
description: Операции связанные с восстановлением пароля
- name: promocode
description: Операции связанные с промокодами
- name: stats
description: Операции связанные со статистикой
paths:
/liveness:
get:
operationId: Liveness
summary: Роут проверки активности
tags:
- recover
responses:
'200':
description: Успех сервис запущен
/readiness:
get:
summary: Роут проверки базы данных
operationId: Readiness
summary: Роут проверки баз данных
tags:
- recover
responses:
'200':
description: Успех — сервис готов и соединение с БД живо
'503':
description: Служба недоступна — не удалось выполнить проверку связи с БД
/recover:
post:
summary: Запустите процесс восстановления пароля
operationId: Recovery
summary: Восстановления пароля
tags:
- recover
requestBody:
required: true
content:
application/x-www-form-urlencoded:
application/json:
schema:
type: object
properties:
email:
type: string
format: email
description: Электронная почта, на которую нужно отправить инструкции по восстановлению
Referrer:
type: string
description: URL-адрес referral, если он доступен
RedirectionURL:
type: string
description: URL-адрес, на который перенаправляется пользователь после отправки электронного письма
$ref: '#/components/schemas/RecoveryReq'
responses:
'200':
description: Запрос на восстановление принят, и возвращен идентификатор записи восстановления
content:
application/json:
schema:
type: object
properties:
id:
type: string
description: Идентификатор запроса на восстановление
description: Запрос на восстановление принят
'404':
description: Пользователь не найден по электронной почте
'500':
description: Внутренняя ошибка сервера разные причины
description: Внутренняя ошибка сервера
/recover/{sign}:
get:
summary: Обработать ссылку восстановления, в которой содержится подпись и обменять ее на токены
operationId: RecoveryLink
summary: Обработать ссылку восстановления и обменять ее на токены
tags:
- recover
parameters:
- in: path
name: sign
@ -71,103 +69,67 @@ paths:
description: Подпись восстановления как часть URL-адреса восстановления
responses:
'200':
description: Восстановление успешно, информация для обмена токенов возвращена
content:
application/json:
schema:
type: object
properties:
accessToken:
type: string
refreshToken:
type: string
description: Восстановление успешно, информация для обмена токенов возвращена в cookie
'406':
description: NotAcceptable - срок действия ссылки для восстановления истек или она недействительна
'500':
description: Внутренняя ошибка сервера разные причины
description: Внутренняя ошибка сервера
/promocode/create:
post:
operationId: CreatePromoCode
summary: Создать новый промокод
tags:
- promocode
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PromoCodeRequest'
$ref: '#/components/schemas/PromoCodeReq'
responses:
'201':
'200':
description: Новый промокод успешно создан
content:
application/json:
schema:
$ref: '#/components/schemas/PromoCodeResponse'
$ref: '#/components/schemas/PromoCode'
'400':
description: Invalid request payload / Duplicate Codeword
content:
application/json:
schema:
type: object
properties:
error:
type: string
description: Неверный формат запроса или дублирующийся codeword
'500':
description: Внутренняя ошибка сервера
content:
application/json:
schema:
type: object
properties:
error:
type: string
/promocode/edit:
put:
operationId: EditPromoCode
summary: Обновить существующий промокод
tags:
- promocode
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ReqEditPromoCode'
$ref: '#/components/schemas/EditPromoCodeReq'
responses:
'200':
description: Промокод успешно обновлен
content:
application/json:
schema:
$ref: '#/components/schemas/PromoCodeResponse'
$ref: '#/components/schemas/PromoCode'
'400':
description: Неверный формат запроса
content:
application/json:
schema:
type: object
properties:
error:
type: string
'404':
description: Промокод не найден
content:
application/json:
schema:
type: object
properties:
error:
type: string
'500':
description: Внутренняя ошибка сервера
content:
application/json:
schema:
type: object
properties:
error:
type: string
/promocode/getList:
post:
summary: Получить список промокодов
operationId: GetList
summary: Получить список промокодов с пагинацией
tags:
- promocode
requestBody:
required: true
content:
@ -183,19 +145,15 @@ paths:
$ref: '#/components/schemas/GetPromoCodesListResp'
'400':
description: Неверный запрос из-за невалидных данных
content:
application/json:
schema:
type: object
properties:
error:
type: string
'500':
description: Внутренняя ошибка сервера
/promocode/activate:
post:
operationId: Activate
summary: Активировать промокод
tags:
- promocode
requestBody:
required: true
content:
@ -210,7 +168,7 @@ paths:
schema:
$ref: '#/components/schemas/ActivateResp'
'400':
description: Невалидный запрос или отсутствует обязательное поле codeword
description: Невалидный запрос или отсутствует обязательное поле codeword или fastLink
'404':
description: Промокод не найден
'500':
@ -218,7 +176,10 @@ paths:
/promocode/{promocodeID}:
delete:
operationId: Delete
summary: Мягко удалить промокод по его id
tags:
- promocode
parameters:
- in: path
name: promocodeID
@ -231,39 +192,81 @@ paths:
description: Промокод успешно помечен как удаленный
'400':
description: Неверный запрос, отсутствует идентификатор промокода
content:
application/json:
schema:
type: object
properties:
error:
type: string
'404':
description: Промокод не найден
content:
application/json:
schema:
type: object
properties:
error:
type: string
'500':
description: Внутренняя ошибка сервера
/promocode/fastlink:
post:
operationId: CreateFastLink
summary: Создать быструю ссылку для промокода
tags:
- promocode
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateFastLinkReq'
responses:
'200':
description: Быстрая ссылка для промокода успешно создана
content:
application/json:
schema:
type: object
properties:
error:
type: string
$ref: '#/components/schemas/CreateFastLinkResp'
'400':
description: Неверный запрос, отсутствует идентификатор промокода
'404':
description: Промокод не найден
'500':
description: Внутренняя ошибка сервера
/promocode/stats:
get:
operationId: GetStats
summary: Получить статистику промокода
tags:
- stats
description: Идентификатор промокода
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PromoCodeStatsReq'
responses:
'200':
description: Статистика промокода успешно получена
content:
application/json:
schema:
$ref: '#/components/schemas/PromoCodeStatsResp'
'400':
description: Неверный запрос
'500':
description: Внутренняя ошибка сервера
components:
schemas:
PromoCodeRequest:
RecoveryReq:
type: object
required:
- email
properties:
email:
type: string
format: email
description: Электронная почта, на которую нужно отправить инструкции по восстановлению
redirectionURL:
type: string
description: URL-адрес, на который перенаправляется пользователь
PromoCodeStatsReq:
type: object
properties:
codeword:
promoCodeID:
type: string
description: Кодовое слово, которое должен ввести пользователь
description:
@ -311,61 +314,30 @@ components:
properties:
id:
type: string
description: ID созданного промокода
codeword:
type: string
description: Кодовое слово промокода
description:
type: string
description: Описание промокода
greetings:
type: string
description: Текст, который будет отправлен пользователю в ответ на активацию кода
dueTo:
description: Идентификатор промокода
usageCount:
type: integer
format: int64
description: Временная метка окончания активации кода
activationCount:
type: integer
format: int64
description: Лимит активации кода
bonus:
description: Количество использований промокода
usageMap:
type: object
properties:
privilege:
type: object
properties:
privilegeID:
type: string
description: Идентификатор привилегии, которую необходимо предоставить
amount:
type: integer
format: uint64
description: Размер привилегии
discount:
type: object
properties:
layer:
type: integer
factor:
type: number
target:
type: string
threshold:
type: integer
description: Информация о бонусах
outdated:
type: boolean
offLimit:
type: boolean
delete:
type: boolean
createdAt:
description: Карта использования промокода
additionalProperties:
type: array
items:
$ref: '#/components/schemas/Usage'
Usage:
type: object
properties:
key:
type: string
description: fastlink или codeword в зависимости от того что применялось
time:
type: string
format: date-time
description: Время создания промокода
description: Время использования промокода
ReqEditPromoCode:
CreateFastLinkReq:
type: object
properties:
id:
@ -430,27 +402,12 @@ components:
filter:
$ref: '#/components/schemas/GetPromoCodesListReqFilter'
GetPromoCodesListReqFilter:
CreateFastLinkResp:
type: object
properties:
text:
fastlink:
type: string
description: Полнотекстовый поиск по полям Codeword, Description, Greetings
active:
type: boolean
description: Если true, выбираются записи, где delete, outdated и offLimit равны false
GetPromoCodesListResp:
type: object
properties:
count:
type: integer
format: int64
description: Общее количество промокодов в выборке
items:
type: array
items:
$ref: '#/components/schemas/PromoCodeResponse'
description: Быстрая ссылка для активации промокода
ActivateReq:
type: object
@ -459,11 +416,188 @@ components:
properties:
codeword:
type: string
description: Кодовое слово промокода, которое требуется активировать
description: Кодовое слово для активации промокода
fastLink:
type: string
description: Быстрая ссылка для активации промокода
ActivateResp:
type: object
properties:
greetings:
type: string
description: Поле из активированного промокода
description: Слово успешной активации промокода
GetPromoCodesListReq:
type: object
required:
- page
- limit
properties:
page:
type: integer
description: Номер страницы
limit:
type: integer
description: Максимальное количество элементов на странице
filter:
$ref: '#/components/schemas/Filter'
Filter:
type: object
properties:
text:
type: string
description: Текстовый фильтр для поиска промокодов
active:
type: boolean
description: Флаг для фильтрации активных промокодов
GetPromoCodesListResp:
type: object
properties:
count:
type: integer
description: Общее количество промокодов
items:
type: array
items:
$ref: '#/components/schemas/PromoCode'
PromoCode:
type: object
properties:
id:
type: string
description: Идентификатор промокода
codeword:
type: string
description: Кодовое слово для активации промокода
description:
type: string
description: Описание промокода
greetings:
type: string
description: Приветственное сообщение после активации промокода
dueTo:
type: integer
description: Дата истечения действия промокода в формате Unix time
activationCount:
type: integer
description: Количество активаций промокода
bonus:
type: object
description: Бонус, предоставляемый с промокодом
items:
$ref: '#/components/schemas/Bonus'
outdated:
type: boolean
description: Флаг
offLimit:
type: boolean
description: Флаг
delete:
type: boolean
description: Флаг
createdAt:
type: string
format: date-time
description: Дата и время создания промокода
fastLinks:
type: array
items:
type: string
description: Список быстрых ссылок для активации промокода
EditPromoCodeReq:
type: object
properties:
ID:
type: string
description: Идентификатор промокода, который требуется обновить
Description:
type: string
nullable: true
description: Описание промокода
Greetings:
type: string
nullable: true
description: Приветственное сообщение после активации промокода
DueTo:
type: integer
nullable: true
description: Дата окончания промокода в формате Unix time
ActivationCount:
type: integer
nullable: true
description: Количество активаций промокода
Delete:
type: boolean
nullable: true
description: Флаг удаления промокода
PromoCodeReq:
type: object
properties:
codeword:
type: string
description: Кодовое слово для активации промокода
description:
type: string
description: Описание промокода
greetings:
type: string
description: Приветственное сообщение после активации промокода
dueTo:
type: integer
description: Дата истечения действия промокода в формате Unix time
activationCount:
type: integer
description: Количество активаций промокода
bonus:
type: object
description: Бонус
items:
$ref: '#/components/schemas/Bonus'
fastLinks:
type: array
items:
type: string
description: Список быстрых ссылок для активации промокода
Bonus:
type: object
description: Бонус
properties:
privilege:
$ref: '#/components/schemas/Privilege'
discount:
$ref: '#/components/schemas/Discount'
Privilege:
type: object
description: Привилегия
properties:
privilegeID:
type: string
description: Идентификатор привилегии
amount:
type: integer
description: Количество привилегии
Discount:
type: object
description: Скидка
properties:
layer:
type: integer
description: Уровень скидки
factor:
type: integer
description: Множитель скидки
target:
type: string
description: Цель скидки
threshold:
type: integer
description: Порог скидки

27
go.mod

@ -6,27 +6,38 @@ require (
github.com/caarlos0/env/v8 v8.0.0
github.com/go-redis/redis/v8 v8.11.5
github.com/gofiber/fiber/v2 v2.51.0
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/joho/godotenv v1.5.1
github.com/pioz/faker v1.7.3
github.com/stretchr/testify v1.8.1
github.com/rs/xid v1.5.0
github.com/stretchr/testify v1.8.4
github.com/twmb/franz-go v1.15.4
go.mongodb.org/mongo-driver v1.13.1
go.uber.org/zap v1.26.0
google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac
google.golang.org/grpc v1.60.1
google.golang.org/protobuf v1.32.0
penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240202120244-c4ef330cfe5d
)
require (
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
github.com/pierrec/lz4/v4 v4.1.19 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/twmb/franz-go/pkg/kmsg v1.7.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.50.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
@ -35,9 +46,13 @@ require (
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sync v0.4.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

72
go.sum

@ -2,8 +2,8 @@ github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/caarlos0/env/v8 v8.0.0 h1:POhxHhSpuxrLMIdvTGARuZqR4Jjm8AYmoi/JKlcScs0=
github.com/caarlos0/env/v8 v8.0.0/go.mod h1:7K4wMY9bH0esiXSSHlfHLX5xKGQMnkH5Fk4TDSSSzfo=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -15,10 +15,17 @@ github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/gofiber/fiber/v2 v2.51.0 h1:JNACcZy5e2tGApWB2QrRpenTWn0fq0hkFm6k0C86gKQ=
github.com/gofiber/fiber/v2 v2.51.0/go.mod h1:xaQRZQJGqnKOQnbQw+ltvku3/h8QxvNi8o6JiJ7Ll0U=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
@ -26,6 +33,12 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@ -41,20 +54,24 @@ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/pierrec/lz4/v4 v4.1.19 h1:tYLzDnjDXh9qIxSTKHwXwOYmm9d887Y7Y1ZkyXYHAN4=
github.com/pierrec/lz4/v4 v4.1.19/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pioz/faker v1.7.3 h1:Tez8Emuq0UN+/d6mo3a9m/9ZZ/zdfJk0c5RtRatrceM=
github.com/pioz/faker v1.7.3/go.mod h1:xSpay5w/oz1a6+ww0M3vfpe40pSIykeUPeWEc3TvVlc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/twmb/franz-go v1.15.4 h1:qBCkHaiutetnrXjAUWA99D9FEcZVMt2AYwkH3vWEQTw=
github.com/twmb/franz-go v1.15.4/go.mod h1:rC18hqNmfo8TMc1kz7CQmHL74PLNF8KVvhflxiiJZCU=
github.com/twmb/franz-go/pkg/kmsg v1.7.0 h1:a457IbvezYfA5UkiBvyV3zj0Is3y1i8EJgqjJYoij2E=
github.com/twmb/franz-go/pkg/kmsg v1.7.0/go.mod h1:se9Mjdt0Nwzc9lnjJ0HyDtLyBnaBDAd7pCje47OhSyw=
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/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M=
@ -81,18 +98,19 @@ 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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -101,8 +119,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -111,16 +129,28 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917 h1:nz5NESFLZbJGPFxDT/HCn+V1mZ8JGNoY4nUpmW/Y2eg=
google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917/go.mod h1:pZqR+glSb11aJ+JQcczCvgf47+duRuzNSKqE8YAQnV0=
google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac h1:OZkkudMUu9LVQMCoRUbI/1p5VCo9BOrlvkqMvWtqa6s=
google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU=
google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
@ -128,3 +158,5 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240202120244-c4ef330cfe5d h1:gbaDt35HMDqOK84WYmDIlXMI7rstUcRqNttaT6Kx1do=
penahub.gitlab.yandexcloud.net/backend/penahub_common v0.0.0-20240202120244-c4ef330cfe5d/go.mod h1:lTmpjry+8evVkXWbEC+WMOELcFkRD1lFMc7J09mOndM=

@ -9,17 +9,16 @@ import (
)
type RecoveryEmailSenderDeps struct {
SmtpApiUrl string
SmtpHost string
SmtpPort string
SmtpSender string
Username string
Password string
ApiKey string
FiberClient *fiber.Client
Logger *zap.Logger
CodewordHost string
CodewordPort string
SmtpApiUrl string
SmtpHost string
SmtpPort string
SmtpSender string
Username string
Password string
ApiKey string
FiberClient *fiber.Client
Logger *zap.Logger
RecoveryUrl string
}
type RecoveryEmailSender struct {
@ -40,7 +39,7 @@ func (r *RecoveryEmailSender) SendRecoveryEmail(email string, signature string)
fmt.Println(email, signature)
message := fmt.Sprintf("http://"+r.deps.CodewordHost+":"+r.deps.CodewordPort+"/recover/%s", signature)
message := r.deps.RecoveryUrl + signature
form := new(bytes.Buffer)
writer := multipart.NewWriter(form)
@ -48,7 +47,7 @@ func (r *RecoveryEmailSender) SendRecoveryEmail(email string, signature string)
fields := map[string]string{
"from": r.deps.SmtpSender,
"to": "pashamullin202@gmail.com",
"to": email,
"subject": "Восстановление доступа",
"html": message,
}

@ -9,13 +9,28 @@ import (
"codeword/internal/services"
"codeword/internal/worker/purge_worker"
"codeword/internal/worker/recovery_worker"
"codeword/pkg/closer"
"codeword/utils"
"context"
"go.mongodb.org/mongo-driver/mongo"
"errors"
"github.com/twmb/franz-go/pkg/kgo"
"go.uber.org/zap"
"time"
)
func Run(ctx context.Context, cfg initialize.Config, logger *zap.Logger) error {
logger.Info("Запуск приложения", zap.String("AppName", cfg.AppName))
defer func() {
if r := recover(); r != nil {
logger.Error("Recovered from a panic", zap.Any("error", r))
}
}()
logger.Info("Starting application", zap.String("AppName", cfg.AppName))
ctx, cancel := context.WithCancel(ctx)
defer cancel()
shutdownGroup := closer.NewCloserGroup()
mdb, err := initialize.MongoDB(ctx, cfg)
if err != nil {
@ -23,10 +38,47 @@ func Run(ctx context.Context, cfg initialize.Config, logger *zap.Logger) error {
return err
}
if err = initialize.InitDatabaseIndexes(ctx, mdb, logger); err != nil {
logger.Error("Failed to initialize db indexes", zap.Error(err))
return err
}
kafkaTariffClient, err := kgo.NewClient(
kgo.SeedBrokers(cfg.KafkaBrokers),
kgo.ConsumeResetOffset(kgo.NewOffset().AtStart()),
kgo.DefaultProduceTopic(cfg.KafkaTopic),
)
if err != nil {
return err
}
err = kafkaTariffClient.Ping(ctx)
if err != nil {
return err
}
discountRpcClient, err := initialize.DiscountGRPCClient(cfg.DiscountServiceAddress)
if err != nil {
logger.Error("failed to connect to discount service", zap.Error(err))
return err
}
brokers := initialize.NewBrokers(initialize.BrokersDeps{
Logger: logger,
TariffClient: kafkaTariffClient,
Topic: cfg.KafkaTopic,
})
rdb, err := initialize.Redis(ctx, cfg)
if err != nil {
logger.Error("failed to connect to redis db", zap.Error(err))
return err
}
encrypt := initialize.Encrypt(cfg)
promoCodeRepo := repository.NewPromoCodeRepository(mdb.Collection("promoCodes"))
statsRepo := repository.NewStatsRepository(repository.Deps{Rdb: nil, Mdb: mdb.Collection("promoStats")})
codewordRepo := repository.NewCodewordRepository(repository.Deps{Rdb: rdb, Mdb: mdb.Collection("codeword")})
userRepo := repository.NewUserRepository(repository.Deps{Rdb: nil, Mdb: mdb.Collection("users")})
@ -42,12 +94,18 @@ func Run(ctx context.Context, cfg initialize.Config, logger *zap.Logger) error {
})
promoService := services.NewPromoCodeService(services.PromoDeps{
Logger: logger,
PromoCodeRepo: promoCodeRepo,
Logger: logger,
PromoCodeRepo: promoCodeRepo,
StatsRepo: statsRepo,
Kafka: brokers.TariffProducer,
DiscountClient: discountRpcClient,
})
jwtUtil := utils.NewJWT(&cfg)
authMiddleware := utils.NewAuthenticator(jwtUtil)
recoveryController := recovery.NewRecoveryController(logger, recoveryService, cfg.DefaultRedirectionURL)
promoCodeController := promocode.NewPromoCodeController(logger, promoService)
promoCodeController := promocode.NewPromoCodeController(promocode.Deps{Logger: logger, PromoCodeService: promoService, AuthMiddleware: authMiddleware})
recoveryWC := recovery_worker.NewRecoveryWC(recovery_worker.Deps{
Logger: logger,
@ -56,7 +114,7 @@ func Run(ctx context.Context, cfg initialize.Config, logger *zap.Logger) error {
Mongo: mdb.Collection("codeword"),
})
purgeWC := purge_worker.NewRecoveryWC(purge_worker.Deps{
purgeWC := purge_worker.NewPurgeWC(purge_worker.Deps{
Logger: logger,
Mongo: mdb.Collection("codeword"),
})
@ -65,52 +123,37 @@ func Run(ctx context.Context, cfg initialize.Config, logger *zap.Logger) error {
go purgeWC.Start(ctx)
server := httpserver.NewServer(httpserver.ServerConfig{
Logger: logger,
RecoveryController: recoveryController,
PromoCodeController: promoCodeController,
Logger: logger,
Controllers: []httpserver.Controller{recoveryController, promoCodeController},
})
go func() {
if err := server.Start(cfg.HTTPHost + ":" + cfg.HTTPPort); err != nil {
logger.Error("Server startup error", zap.Error(err))
cancel()
}
}()
server.ListRoutes()
shutdownGroup.Add(closer.CloserFunc(server.Shutdown))
shutdownGroup.Add(closer.CloserFunc(mdb.Client().Disconnect))
shutdownGroup.Add(closer.CloserFunc(recoveryWC.Stop))
shutdownGroup.Add(closer.CloserFunc(purgeWC.Stop))
<-ctx.Done()
if err := shutdownApp(ctx, server, mdb, logger); err != nil {
return err
}
logger.Info("The application has stopped")
return nil
}
// TODO возможно стоит вынести в отдельные файлы или отказаться от разделения на отдельные методы
func shutdownApp(ctx context.Context, server *httpserver.Server, mdb *mongo.Database, logger *zap.Logger) error {
if err := shutdownHTTPServer(ctx, server, logger); err != nil {
return err
}
if err := shutdownMongoDB(ctx, mdb, logger); err != nil {
return err
}
return nil
}
func shutdownHTTPServer(ctx context.Context, server *httpserver.Server, logger *zap.Logger) error {
if err := server.Shutdown(ctx); err != nil {
logger.Error("Error stopping HTTP server", zap.Error(err))
return err
}
return nil
}
func shutdownMongoDB(ctx context.Context, mdb *mongo.Database, logger *zap.Logger) error {
if err := mdb.Client().Disconnect(ctx); err != nil {
logger.Error("Error when closing MongoDB connection", zap.Error(err))
timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer timeoutCancel()
if err := shutdownGroup.Call(timeoutCtx); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
logger.Error("Shutdown timed out", zap.Error(err))
} else {
logger.Error("Failed to shutdown services gracefully", zap.Error(err))
}
return err
}
logger.Info("Application has stopped")
return nil
}

@ -9,15 +9,23 @@ import (
"go.uber.org/zap"
)
type Deps struct {
Logger *zap.Logger
PromoCodeService *services.PromoCodeService
AuthMiddleware func(*fiber.Ctx) error
}
type PromoCodeController struct {
logger *zap.Logger
promoCodeService *services.PromoCodeService
authMiddleware func(*fiber.Ctx) error
}
func NewPromoCodeController(logger *zap.Logger, promoCodeService *services.PromoCodeService) *PromoCodeController {
func NewPromoCodeController(deps Deps) *PromoCodeController {
return &PromoCodeController{
logger: logger,
promoCodeService: promoCodeService,
logger: deps.Logger,
promoCodeService: deps.PromoCodeService,
authMiddleware: deps.AuthMiddleware,
}
}
@ -27,6 +35,10 @@ func (p *PromoCodeController) CreatePromoCode(c *fiber.Ctx) error {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request payload"})
}
if req.Codeword == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "codeword is required"})
}
createdPromoCode, err := p.promoCodeService.CreatePromoCode(c.Context(), &req)
if err != nil {
p.logger.Error("Failed to create promocode", zap.Error(err))
@ -38,7 +50,7 @@ func (p *PromoCodeController) CreatePromoCode(c *fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"})
}
return c.Status(fiber.StatusCreated).JSON(createdPromoCode)
return c.Status(fiber.StatusOK).JSON(createdPromoCode)
}
func (p *PromoCodeController) EditPromoCode(c *fiber.Ctx) error {
@ -84,30 +96,44 @@ func (p *PromoCodeController) GetList(c *fiber.Ctx) error {
}
func (p *PromoCodeController) Activate(c *fiber.Ctx) error {
err := p.authMiddleware(c)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err})
}
userID := c.Locals(models.AuthJWTDecodedUserIDKey).(string)
if userID == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "failed to get jwt payload"})
}
var req models.ActivateReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request payload"})
}
if req.Codeword == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "codeword is required"})
if req.Codeword == "" && req.FastLink == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "codeword or fastlink is required"})
}
greetings, err := p.promoCodeService.ActivatePromo(c.Context(), &req)
greetings, err := p.promoCodeService.ActivatePromo(c.Context(), &req, userID)
if err != nil {
p.logger.Error("Failed to activate promocode", zap.Error(err))
if errors.Is(err, repository.ErrPromoCodeNotFound) {
switch {
case errors.Is(err, repository.ErrPromoCodeNotFound):
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "PromoCode not found"})
case errors.Is(err, repository.ErrPromoCodeAlreadyActivated):
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "PromoCode already activated"})
case errors.Is(err, repository.ErrPromoCodeExpired):
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
case errors.Is(err, repository.ErrPromoCodeExhausted):
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "PromoCode exhausted"})
default:
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"})
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"})
}
resp := models.ActivateResp{
Greetings: greetings,
}
return c.Status(fiber.StatusOK).JSON(resp)
return c.Status(fiber.StatusOK).JSON(models.ActivateResp{Greetings: greetings})
}
func (p *PromoCodeController) Delete(c *fiber.Ctx) error {
@ -130,3 +156,50 @@ func (p *PromoCodeController) Delete(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
}
func (p *PromoCodeController) CreateFastLink(c *fiber.Ctx) error {
var req struct {
PromoCodeID string `json:"id"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request payload"})
}
if req.PromoCodeID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "PromoCode ID is required"})
}
fastLink, err := p.promoCodeService.CreateFastLink(c.Context(), req.PromoCodeID)
if err != nil {
p.logger.Error("Failed to create fastlink", zap.Error(err))
if errors.Is(err, repository.ErrPromoCodeNotFound) {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "PromoCode not found"})
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"})
}
return c.Status(fiber.StatusCreated).JSON(fiber.Map{"fastlink": fastLink})
}
func (p *PromoCodeController) GetStats(c *fiber.Ctx) error {
var req struct {
PromoCodeID string `json:"id"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request payload"})
}
if req.PromoCodeID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "PromoCode ID is required"})
}
promoStats, err := p.promoCodeService.GetStats(c.Context(), req.PromoCodeID)
if err != nil {
p.logger.Error("Failed getting promo stats", zap.Error(err))
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"})
}
return c.Status(fiber.StatusOK).JSON(promoStats)
}

@ -0,0 +1,18 @@
package promocode
import "github.com/gofiber/fiber/v2"
func (p *PromoCodeController) Register(router fiber.Router) {
router.Post("/create", p.CreatePromoCode)
router.Put("/edit", p.EditPromoCode)
router.Post("/getList", p.GetList)
router.Post("/activate", p.Activate)
router.Delete("/:promocodeID", p.Delete)
router.Post("/fastlink", p.CreateFastLink)
router.Get("/stats", p.GetStats)
}
func (p *PromoCodeController) Name() string {
return "promocode"
}

@ -2,8 +2,11 @@ package recovery
import (
"codeword/internal/models"
"codeword/internal/repository"
"codeword/internal/services"
"encoding/base64"
"errors"
"fmt"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
"time"
@ -23,23 +26,44 @@ func NewRecoveryController(logger *zap.Logger, service *services.RecoveryService
}
}
func (r *RecoveryController) HandlePingDB(c *fiber.Ctx) error {
return r.service.Ping(c.Context())
func (r *RecoveryController) HandleLiveness(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
}
// HandleRecoveryRequest обрабатывает запрос на восстановление пароля
func (r *RecoveryController) HandleRecoveryRequest(c *fiber.Ctx) error {
email := c.FormValue("email")
referralURL := c.Get("Referrer")
redirectionURL := c.FormValue("RedirectionURL")
func (r *RecoveryController) HandlePingDB(c *fiber.Ctx) error {
startTime := time.Now()
if err := r.service.Ping(c.Context()); err != nil {
r.logger.Error("Failed to ping the database", zap.Error(err))
return c.Status(fiber.StatusServiceUnavailable).SendString("DB ping failed")
}
duration := time.Since(startTime)
if redirectionURL == "" && referralURL != "" {
redirectionURL = referralURL
} else if redirectionURL == "" {
redirectionURL = r.defaultURL
durationMillis := duration.Milliseconds()
responseMessage := fmt.Sprintf("DB ping success - Time taken: %d ms", durationMillis)
return c.Status(fiber.StatusOK).SendString(responseMessage)
}
func (r *RecoveryController) HandleRecoveryRequest(c *fiber.Ctx) error {
var req models.RecoveryRequest
if err := c.BodyParser(&req); err != nil {
r.logger.Error("Failed to parse recovery request", zap.Error(err))
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Bad Request"})
}
user, err := r.service.FindUserByEmail(c.Context(), email)
if req.Email == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "email is required"})
}
referralURL := c.Get("Referrer")
if req.RedirectionURL == "" && referralURL != "" {
req.RedirectionURL = referralURL
} else if req.RedirectionURL == "" {
req.RedirectionURL = r.defaultURL
}
user, err := r.service.FindUserByEmail(c.Context(), req.Email)
if err != nil || user == nil {
r.logger.Error("Failed to find user by email", zap.Error(err))
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not found"})
@ -51,7 +75,7 @@ func (r *RecoveryController) HandleRecoveryRequest(c *fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"})
}
signUrl := redirectionURL + base64.URLEncoding.EncodeToString(key)
signUrl := req.RedirectionURL
sign := base64.URLEncoding.EncodeToString(key)
id, err := r.service.StoreRecoveryRecord(c.Context(), models.StoreRecDeps{UserID: user.ID.Hex(), Email: user.Email, Key: sign, Url: signUrl})
@ -62,31 +86,32 @@ func (r *RecoveryController) HandleRecoveryRequest(c *fiber.Ctx) error {
signWithID := sign + id // подпись с id записи
err = r.service.RecoveryEmailTask(c.Context(), models.RecEmailDeps{UserID: user.ID.Hex(), Email: email, SignWithID: signWithID, ID: id})
err = r.service.RecoveryEmailTask(c.Context(), models.RecEmailDeps{UserID: user.ID.Hex(), Email: req.Email, SignWithID: signWithID, ID: id})
if err != nil {
r.logger.Error("Failed to send recovery email", zap.Error(err))
if errors.Is(err, repository.ErrAlreadyReported) {
return c.Status(fiber.StatusAlreadyReported).JSON(fiber.Map{"error": "already reported"})
}
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"})
}
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"id": id,
})
return c.Status(fiber.StatusOK).JSON(fiber.Map{"message": "Recovery email sent successfully"})
}
// todo тут скорее всего помимо подписи будет передаваться еще что-то, например email пользователя от фронта для поиска в бд
// HandleRecoveryLink обрабатывает ссылку восстановления и обменивает ее на токены
func (r *RecoveryController) HandleRecoveryLink(c *fiber.Ctx) error {
key := c.Params("sign")
sign := c.Params("sign")
record, err := r.service.GetRecoveryRecord(c.Context(), key)
record, err := r.service.GetRecoveryRecord(c.Context(), sign)
if err != nil {
r.logger.Error("Failed to get recovery record", zap.Error(err))
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"})
}
if time.Since(record.CreatedAt) > 15*time.Minute {
r.logger.Error("Recovery link expired", zap.String("signature", key))
r.logger.Error("Recovery link expired", zap.String("signature", sign))
return c.Status(fiber.StatusNotAcceptable).JSON(fiber.Map{"error": "Recovery link expired"})
}
@ -96,5 +121,14 @@ func (r *RecoveryController) HandleRecoveryLink(c *fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"})
}
return c.Status(fiber.StatusOK).JSON(tokens)
c.Cookie(&fiber.Cookie{
Name: "refreshToken",
Value: tokens["refreshToken"],
Domain: ".pena.digital",
Expires: time.Now().Add(30 * 24 * time.Hour),
Secure: true,
HTTPOnly: true,
})
return c.Redirect(record.SignUrl + "?auth=" + tokens["accessToken"])
}

@ -0,0 +1,14 @@
package recovery
import "github.com/gofiber/fiber/v2"
func (r *RecoveryController) Register(router fiber.Router) {
router.Get("/liveness", r.HandleLiveness)
router.Get("/readiness", r.HandlePingDB)
router.Post("/recover", r.HandleRecoveryRequest)
router.Get("/recover/:sign", r.HandleRecoveryLink)
}
func (r *RecoveryController) Name() string {
return ""
}

@ -8,17 +8,16 @@ import (
func RecoveryEmailSender(cfg Config, logger *zap.Logger) *client.RecoveryEmailSender {
return client.NewRecoveryEmailSender(client.RecoveryEmailSenderDeps{
SmtpApiUrl: cfg.SmtpApiUrl,
SmtpHost: cfg.SmtpHost,
SmtpPort: cfg.SmtpPort,
SmtpSender: cfg.SmtpSender,
Username: cfg.SmtpUsername,
Password: cfg.SmtpPassword,
ApiKey: cfg.SmtpApiKey,
FiberClient: &fiber.Client{},
Logger: logger,
CodewordHost: cfg.HTTPHost,
CodewordPort: cfg.HTTPPort,
SmtpApiUrl: cfg.SmtpApiUrl,
SmtpHost: cfg.SmtpHost,
SmtpPort: cfg.SmtpPort,
SmtpSender: cfg.SmtpSender,
Username: cfg.SmtpUsername,
Password: cfg.SmtpPassword,
ApiKey: cfg.SmtpApiKey,
FiberClient: &fiber.Client{},
Logger: logger,
RecoveryUrl: cfg.RecoveryUrl,
})
}

@ -7,30 +7,38 @@ import (
)
type Config struct {
AppName string `env:"APP_NAME" envDefault:"codeword"`
HTTPHost string `env:"HTTP_HOST" envDefault:"localhost"`
HTTPPort string `env:"HTTP_PORT" envDefault:"3000"`
MongoHost string `env:"MONGO_HOST" envDefault:"127.0.0.1"`
MongoPort string `env:"MONGO_PORT" envDefault:"27020"`
MongoUser string `env:"MONGO_USER" envDefault:"test"`
MongoPassword string `env:"MONGO_PASSWORD" envDefault:"test"`
MongoDatabase string `env:"MONGO_DB" envDefault:"admin"`
MongoAuth string `env:"MONGO_AUTH" envDefault:"admin"`
PublicCurveKey string `env:"PUBLIC_CURVE_KEY"`
PrivateCurveKey string `env:"PRIVATE_CURVE_KEY"`
SignSecret string `env:"SIGN_SECRET"`
RedisAddr string `env:"REDIS_ADDR" envDefault:"localhost:6379"`
RedisPassword string `env:"REDIS_PASS" envDefault:"admin"`
RedisDB int `env:"REDIS_DB" envDefault:"2"`
SmtpApiUrl string `env:"SMTP_API_URL"`
SmtpHost string `env:"SMTP_HOST"`
SmtpPort string `env:"SMTP_PORT"`
SmtpUsername string `env:"SMTP_UNAME"`
SmtpPassword string `env:"SMTP_PASS"`
SmtpApiKey string `env:"SMTP_API_KEY"`
SmtpSender string `env:"SMTP_SENDER"`
DefaultRedirectionURL string `env:"DEFAULT_REDIRECTION_URL"`
AuthURL string `env:"AUTH_EXCHANGE_URL"`
AppName string `env:"APP_NAME" envDefault:"codeword"`
HTTPHost string `env:"HTTP_HOST" envDefault:"localhost"`
HTTPPort string `env:"HTTP_PORT" envDefault:"3000"`
MongoHost string `env:"MONGO_HOST" envDefault:"127.0.0.1"`
MongoPort string `env:"MONGO_PORT" envDefault:"27020"`
MongoUser string `env:"MONGO_USER" envDefault:"test"`
MongoPassword string `env:"MONGO_PASSWORD" envDefault:"test"`
MongoDatabase string `env:"MONGO_DB" envDefault:"admin"`
MongoAuth string `env:"MONGO_AUTH" envDefault:"admin"`
PublicCurveKey string `env:"PUBLIC_CURVE_KEY"`
PrivateCurveKey string `env:"PRIVATE_CURVE_KEY"`
SignSecret string `env:"SIGN_SECRET"`
RedisAddr string `env:"REDIS_ADDR" envDefault:"localhost:6379"`
RedisPassword string `env:"REDIS_PASS" envDefault:"admin"`
RedisDB int `env:"REDIS_DB" envDefault:"2"`
SmtpApiUrl string `env:"SMTP_API_URL"`
SmtpHost string `env:"SMTP_HOST"`
SmtpPort string `env:"SMTP_PORT"`
SmtpUsername string `env:"SMTP_UNAME"`
SmtpPassword string `env:"SMTP_PASS"`
SmtpApiKey string `env:"SMTP_API_KEY"`
SmtpSender string `env:"SMTP_SENDER"`
DefaultRedirectionURL string `env:"DEFAULT_REDIRECTION_URL"`
AuthURL string `env:"AUTH_EXCHANGE_URL"`
KafkaBrokers string `env:"KAFKA_BROKERS"`
KafkaTopic string `env:"KAFKA_TOPIC_TARIFF"`
DiscountServiceAddress string `env:"DISCOUNT_ADDRESS"`
RecoveryUrl string `env:"RECOVERY_URL"`
PrivateKey string `env:"JWT_PRIVATE_KEY"`
PublicKey string `env:"JWT_PUBLIC_KEY,required"`
Issuer string `env:"JWT_ISSUER,required"`
Audience string `env:"JWT_AUDIENCE,required"`
}
func LoadConfig() (*Config, error) {

@ -0,0 +1,26 @@
package initialize
import (
"codeword/internal/proto/discount"
"context"
"google.golang.org/grpc"
"time"
)
func DiscountGRPCClient(address string) (discount.DiscountServiceClient, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
options := []grpc.DialOption{
grpc.WithInsecure(),
//grpc.WithBlock(),
}
conn, err := grpc.DialContext(ctx, address, options...)
if err != nil {
return nil, err
}
discountClient := discount.NewDiscountServiceClient(conn)
return discountClient, nil
}

@ -0,0 +1,27 @@
package initialize
import (
"codeword/internal/kafka/tariff"
"github.com/twmb/franz-go/pkg/kgo"
"go.uber.org/zap"
)
type BrokersDeps struct {
Logger *zap.Logger
TariffClient *kgo.Client
Topic string
}
type Brokers struct {
TariffProducer *tariff.Producer
}
func NewBrokers(deps BrokersDeps) *Brokers {
return &Brokers{
TariffProducer: tariff.NewProducer(tariff.ProducerDeps{
Logger: deps.Logger,
Client: deps.TariffClient,
Topic: deps.Topic,
}),
}
}

@ -1,36 +1,50 @@
package initialize
import (
mdb "codeword/pkg/mongo"
"codeword/internal/repository"
"context"
"go.mongodb.org/mongo-driver/mongo"
"go.uber.org/zap"
mdb "penahub.gitlab.yandexcloud.net/backend/penahub_common/mongo"
"time"
)
func MongoDB(ctx context.Context, cfg Config) (*mongo.Database, error) {
dbConfig := &mdb.Configuration{
MongoHost: cfg.MongoHost,
MongoPort: cfg.MongoPort,
MongoUser: cfg.MongoUser,
MongoPassword: cfg.MongoPassword,
MongoDatabase: cfg.MongoDatabase,
MongoAuth: cfg.MongoAuth,
Host: cfg.MongoHost,
Port: cfg.MongoPort,
User: cfg.MongoUser,
Password: cfg.MongoPassword,
DatabaseName: cfg.MongoDatabase,
Auth: cfg.MongoAuth,
}
newCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
mongoDeps := &mdb.ConnectDeps{
Configuration: dbConfig,
Timeout: 10 * time.Second,
}
db, err := mdb.Connect(ctx, mongoDeps)
db, err := mdb.Connect(newCtx, mongoDeps)
if err != nil {
return nil, err
}
err = db.Client().Ping(ctx, nil)
err = db.Client().Ping(newCtx, nil)
if err != nil {
return nil, err
}
return db, nil
}
func InitDatabaseIndexes(ctx context.Context, mdb *mongo.Database, logger *zap.Logger) error {
if err := repository.InitPromoCodeIndexes(ctx, mdb.Collection("promoCodes")); err != nil {
logger.Error("Failed to initialize promoCodes indexes", zap.Error(err))
return err
}
return nil
}

@ -0,0 +1,56 @@
package tariff
import (
"codeword/internal/models"
"codeword/internal/utils/transfer"
"context"
"github.com/twmb/franz-go/pkg/kgo"
"go.uber.org/zap"
"google.golang.org/protobuf/proto"
"log"
)
type ProducerDeps struct {
Logger *zap.Logger
Client *kgo.Client
Topic string
}
type Producer struct {
logger *zap.Logger
client *kgo.Client
topic string
}
func NewProducer(deps ProducerDeps) *Producer {
if deps.Logger == nil {
log.Panicln("logger is nil on <NewTariffProducer>")
}
if deps.Client == nil {
log.Panicln("Kafka client is nil on <NewTariffProducer>")
}
return &Producer{
logger: deps.Logger,
client: deps.Client,
topic: deps.Topic,
}
}
func (p *Producer) Send(ctx context.Context, userID string, tariff *models.Tariff) error {
bytes, err := proto.Marshal(transfer.TariffModelToProtoMessage(userID, tariff))
if err != nil {
p.logger.Error("failed to marshal tariff model", zap.Error(err))
return err
}
// упростил, возможно зря, но теперь возвращаем одну ошибку, просто прерываем цикл при первой встретившейся ошибке
err = p.client.ProduceSync(ctx, &kgo.Record{Topic: p.topic, Value: bytes}).FirstErr()
if err != nil {
p.logger.Error("failed to send tariff to Kafka", zap.Error(err))
return err
}
return nil
}

@ -9,3 +9,6 @@ type RefreshResponse struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
}
const AuthJWTDecodedUserIDKey = "userID"
const AuthJWTDecodedAccessTokenKey = "access-token"

@ -18,7 +18,7 @@ type PromoCode struct {
Amount uint64 `json:"amount" bson:"amount"` // количество
} `json:"privilege" bson:"privilege"`
Discount struct {
Layer int `json:"layer" bson:"layer"` // 1|2
Layer uint32 `json:"layer" bson:"layer"` // 1|2
Factor float64 `json:"factor" bson:"factor"` // процент скидки, вернее множитель, при котором достигается этот процент скидки
Target string `json:"target" bson:"target"` // PrivilegeID или ServiceKey в зависимости от слоя
Threshold int64 `json:"threshold" bson:"threshold"` // граничное значение, при пересечении которого применяется эта скидка
@ -28,6 +28,7 @@ type PromoCode struct {
OffLimit bool `json:"offLimit" bson:"offLimit"`
Delete bool `json:"delete" bson:"delete"`
CreatedAt time.Time `json:"createdAt" bson:"createdAt"`
FastLinks []string `json:"fastLinks" bson:"fastLinks"`
}
type ReqEditPromoCode struct {
@ -73,8 +74,20 @@ type GetPromoCodesListResp struct {
type ActivateReq struct {
Codeword string `json:"codeword"`
FastLink string `json:"fastLink"`
}
type ActivateResp struct {
Greetings string `json:"greetings"` // поле из активированного промокода
}
type PromoCodeStats struct {
ID string `bson:"_id,omitempty" json:"id,omitempty"`
UsageCount int `bson:"usageCount" json:"usageCount"`
UsageMap map[string][]Usage `bson:"usageMap" json:"usageMap"`
}
type Usage struct {
Key string `bson:"key" json:"key"`
Time time.Time `bson:"time" json:"time"`
}

46
internal/models/tariff.go Normal file

@ -0,0 +1,46 @@
package models
import (
"codeword/internal/proto/broker"
"time"
)
type Tariff struct {
ID string `json:"_id"`
Name string `json:"name"`
Price uint64 `json:"price,omitempty"`
IsCustom bool `json:"isCustom"`
Privileges []Privilege `json:"privileges"`
Deleted bool `json:"isDeleted"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt *time.Time `json:"deletedAt,omitempty"`
}
type Privilege struct {
ID string `json:"_id"`
Name string `json:"name"`
PrivilegeID string `json:"privilegeId"`
ServiceKey string `json:"serviceKey"`
Description string `json:"description"`
Amount uint64 `json:"amount"`
Type PrivilegeType `json:"type"`
Value string `json:"value"`
Price uint64 `json:"price"`
}
type PrivilegeType string
const (
PrivilegeTypeCount = "count"
PrivilegeTypeDay = "day"
PrivilegeTypeFull = "full"
)
var (
PrivilegeBrokerTypeMap = map[PrivilegeType]broker.PrivilegeType{
PrivilegeTypeFull: broker.PrivilegeType_Full,
PrivilegeTypeDay: broker.PrivilegeType_Day,
PrivilegeTypeCount: broker.PrivilegeType_Count,
}
)

@ -35,3 +35,12 @@ type RecoveryRecord struct {
Email string
Key string
}
type RecoveryRequest struct {
Email string `json:"email"`
RedirectionURL string `json:"redirectionURL"`
}
type RecoveryLinkRequest struct {
Sign string `json:"sign"`
}

@ -0,0 +1,314 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.31.0
// protoc (unknown)
// source: kafka/models.proto
package broker
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type PrivilegeType int32
const (
PrivilegeType_Full PrivilegeType = 0
PrivilegeType_Day PrivilegeType = 1
PrivilegeType_Count PrivilegeType = 2
)
// Enum value maps for PrivilegeType.
var (
PrivilegeType_name = map[int32]string{
0: "Full",
1: "Day",
2: "Count",
}
PrivilegeType_value = map[string]int32{
"Full": 0,
"Day": 1,
"Count": 2,
}
)
func (x PrivilegeType) Enum() *PrivilegeType {
p := new(PrivilegeType)
*p = x
return p
}
func (x PrivilegeType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (PrivilegeType) Descriptor() protoreflect.EnumDescriptor {
return file_broker_models_proto_enumTypes[0].Descriptor()
}
func (PrivilegeType) Type() protoreflect.EnumType {
return &file_broker_models_proto_enumTypes[0]
}
func (x PrivilegeType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use PrivilegeType.Descriptor instead.
func (PrivilegeType) EnumDescriptor() ([]byte, []int) {
return file_broker_models_proto_rawDescGZIP(), []int{0}
}
type PrivilegeMessage struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
PrivilegeID string `protobuf:"bytes,1,opt,name=PrivilegeID,proto3" json:"PrivilegeID,omitempty"`
ServiceKey string `protobuf:"bytes,2,opt,name=ServiceKey,proto3" json:"ServiceKey,omitempty"`
Type PrivilegeType `protobuf:"varint,3,opt,name=Type,proto3,enum=kafka.PrivilegeType" json:"Type,omitempty"`
Value string `protobuf:"bytes,4,opt,name=Value,proto3" json:"Value,omitempty"`
Amount uint64 `protobuf:"varint,5,opt,name=Amount,proto3" json:"Amount,omitempty"`
}
func (x *PrivilegeMessage) Reset() {
*x = PrivilegeMessage{}
if protoimpl.UnsafeEnabled {
mi := &file_broker_models_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *PrivilegeMessage) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*PrivilegeMessage) ProtoMessage() {}
func (x *PrivilegeMessage) ProtoReflect() protoreflect.Message {
mi := &file_broker_models_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use PrivilegeMessage.ProtoReflect.Descriptor instead.
func (*PrivilegeMessage) Descriptor() ([]byte, []int) {
return file_broker_models_proto_rawDescGZIP(), []int{0}
}
func (x *PrivilegeMessage) GetPrivilegeID() string {
if x != nil {
return x.PrivilegeID
}
return ""
}
func (x *PrivilegeMessage) GetServiceKey() string {
if x != nil {
return x.ServiceKey
}
return ""
}
func (x *PrivilegeMessage) GetType() PrivilegeType {
if x != nil {
return x.Type
}
return PrivilegeType_Full
}
func (x *PrivilegeMessage) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
func (x *PrivilegeMessage) GetAmount() uint64 {
if x != nil {
return x.Amount
}
return 0
}
type TariffMessage struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Privileges []*PrivilegeMessage `protobuf:"bytes,1,rep,name=Privileges,proto3" json:"Privileges,omitempty"`
UserID string `protobuf:"bytes,2,opt,name=UserID,proto3" json:"UserID,omitempty"`
}
func (x *TariffMessage) Reset() {
*x = TariffMessage{}
if protoimpl.UnsafeEnabled {
mi := &file_broker_models_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *TariffMessage) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*TariffMessage) ProtoMessage() {}
func (x *TariffMessage) ProtoReflect() protoreflect.Message {
mi := &file_broker_models_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use TariffMessage.ProtoReflect.Descriptor instead.
func (*TariffMessage) Descriptor() ([]byte, []int) {
return file_broker_models_proto_rawDescGZIP(), []int{1}
}
func (x *TariffMessage) GetPrivileges() []*PrivilegeMessage {
if x != nil {
return x.Privileges
}
return nil
}
func (x *TariffMessage) GetUserID() string {
if x != nil {
return x.UserID
}
return ""
}
var File_broker_models_proto protoreflect.FileDescriptor
var file_broker_models_proto_rawDesc = []byte{
0x0a, 0x13, 0x62, 0x72, 0x6f, 0x6b, 0x65, 0x72, 0x2f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x73, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x62, 0x72, 0x6f, 0x6b, 0x65, 0x72, 0x22, 0xad, 0x01,
0x0a, 0x10, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61,
0x67, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x49,
0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65,
0x67, 0x65, 0x49, 0x44, 0x12, 0x1e, 0x0a, 0x0a, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4b,
0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x4b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01,
0x28, 0x0e, 0x32, 0x15, 0x2e, 0x62, 0x72, 0x6f, 0x6b, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x69, 0x76,
0x69, 0x6c, 0x65, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12,
0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18,
0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x61, 0x0a,
0x0d, 0x54, 0x61, 0x72, 0x69, 0x66, 0x66, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x38,
0x0a, 0x0a, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x18, 0x2e, 0x62, 0x72, 0x6f, 0x6b, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x69, 0x76,
0x69, 0x6c, 0x65, 0x67, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x0a, 0x50, 0x72,
0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x55, 0x73, 0x65, 0x72,
0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x55, 0x73, 0x65, 0x72, 0x49, 0x44,
0x2a, 0x2d, 0x0a, 0x0d, 0x50, 0x72, 0x69, 0x76, 0x69, 0x6c, 0x65, 0x67, 0x65, 0x54, 0x79, 0x70,
0x65, 0x12, 0x08, 0x0a, 0x04, 0x46, 0x75, 0x6c, 0x6c, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x44,
0x61, 0x79, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x10, 0x02, 0x42,
0x0a, 0x5a, 0x08, 0x2e, 0x2f, 0x62, 0x72, 0x6f, 0x6b, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x33,
}
var (
file_broker_models_proto_rawDescOnce sync.Once
file_broker_models_proto_rawDescData = file_broker_models_proto_rawDesc
)
func file_broker_models_proto_rawDescGZIP() []byte {
file_broker_models_proto_rawDescOnce.Do(func() {
file_broker_models_proto_rawDescData = protoimpl.X.CompressGZIP(file_broker_models_proto_rawDescData)
})
return file_broker_models_proto_rawDescData
}
var file_broker_models_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_broker_models_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_broker_models_proto_goTypes = []interface{}{
(PrivilegeType)(0), // 0: kafka.PrivilegeType
(*PrivilegeMessage)(nil), // 1: kafka.PrivilegeMessage
(*TariffMessage)(nil), // 2: kafka.TariffMessage
}
var file_broker_models_proto_depIdxs = []int32{
0, // 0: kafka.PrivilegeMessage.Type:type_name -> kafka.PrivilegeType
1, // 1: kafka.TariffMessage.Privileges:type_name -> kafka.PrivilegeMessage
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
}
func init() { file_broker_models_proto_init() }
func file_broker_models_proto_init() {
if File_broker_models_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_broker_models_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*PrivilegeMessage); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_broker_models_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*TariffMessage); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_broker_models_proto_rawDesc,
NumEnums: 1,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_broker_models_proto_goTypes,
DependencyIndexes: file_broker_models_proto_depIdxs,
EnumInfos: file_broker_models_proto_enumTypes,
MessageInfos: file_broker_models_proto_msgTypes,
}.Build()
File_broker_models_proto = out.File
file_broker_models_proto_rawDesc = nil
file_broker_models_proto_goTypes = nil
file_broker_models_proto_depIdxs = nil
}

@ -0,0 +1,192 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.31.0
// protoc (unknown)
// source: discount/audit.model.proto
package discount
import (
_ "google.golang.org/genproto/googleapis/api/annotations"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
_ "google.golang.org/protobuf/types/known/emptypb"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Audit struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=UpdatedAt,proto3" json:"UpdatedAt,omitempty"`
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=CreatedAt,proto3" json:"CreatedAt,omitempty"`
DeletedAt *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=DeletedAt,proto3,oneof" json:"DeletedAt,omitempty"`
Deleted bool `protobuf:"varint,4,opt,name=Deleted,proto3" json:"Deleted,omitempty"`
}
func (x *Audit) Reset() {
*x = Audit{}
if protoimpl.UnsafeEnabled {
mi := &file_discount_audit_model_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Audit) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Audit) ProtoMessage() {}
func (x *Audit) ProtoReflect() protoreflect.Message {
mi := &file_discount_audit_model_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Audit.ProtoReflect.Descriptor instead.
func (*Audit) Descriptor() ([]byte, []int) {
return file_discount_audit_model_proto_rawDescGZIP(), []int{0}
}
func (x *Audit) GetUpdatedAt() *timestamppb.Timestamp {
if x != nil {
return x.UpdatedAt
}
return nil
}
func (x *Audit) GetCreatedAt() *timestamppb.Timestamp {
if x != nil {
return x.CreatedAt
}
return nil
}
func (x *Audit) GetDeletedAt() *timestamppb.Timestamp {
if x != nil {
return x.DeletedAt
}
return nil
}
func (x *Audit) GetDeleted() bool {
if x != nil {
return x.Deleted
}
return false
}
var File_discount_audit_model_proto protoreflect.FileDescriptor
var file_discount_audit_model_proto_rawDesc = []byte{
0x0a, 0x1a, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2f, 0x61, 0x75, 0x64, 0x69, 0x74,
0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x64, 0x69,
0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61,
0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x22, 0xe2, 0x01, 0x0a, 0x05, 0x41, 0x75, 0x64, 0x69, 0x74, 0x12, 0x38, 0x0a, 0x09,
0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x55, 0x70, 0x64,
0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x38, 0x0a, 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
0x64, 0x41, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65,
0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74,
0x12, 0x3d, 0x0a, 0x09, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x03, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48,
0x00, 0x52, 0x09, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x41, 0x74, 0x88, 0x01, 0x01, 0x12,
0x18, 0x0a, 0x07, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08,
0x52, 0x07, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x44, 0x65,
0x6c, 0x65, 0x74, 0x65, 0x64, 0x41, 0x74, 0x42, 0x0c, 0x5a, 0x0a, 0x2e, 0x2f, 0x64, 0x69, 0x73,
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_discount_audit_model_proto_rawDescOnce sync.Once
file_discount_audit_model_proto_rawDescData = file_discount_audit_model_proto_rawDesc
)
func file_discount_audit_model_proto_rawDescGZIP() []byte {
file_discount_audit_model_proto_rawDescOnce.Do(func() {
file_discount_audit_model_proto_rawDescData = protoimpl.X.CompressGZIP(file_discount_audit_model_proto_rawDescData)
})
return file_discount_audit_model_proto_rawDescData
}
var file_discount_audit_model_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_discount_audit_model_proto_goTypes = []interface{}{
(*Audit)(nil), // 0: discount.Audit
(*timestamppb.Timestamp)(nil), // 1: google.protobuf.Timestamp
}
var file_discount_audit_model_proto_depIdxs = []int32{
1, // 0: discount.Audit.UpdatedAt:type_name -> google.protobuf.Timestamp
1, // 1: discount.Audit.CreatedAt:type_name -> google.protobuf.Timestamp
1, // 2: discount.Audit.DeletedAt:type_name -> google.protobuf.Timestamp
3, // [3:3] is the sub-list for method output_type
3, // [3:3] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
}
func init() { file_discount_audit_model_proto_init() }
func file_discount_audit_model_proto_init() {
if File_discount_audit_model_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_discount_audit_model_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Audit); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
file_discount_audit_model_proto_msgTypes[0].OneofWrappers = []interface{}{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_discount_audit_model_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_discount_audit_model_proto_goTypes,
DependencyIndexes: file_discount_audit_model_proto_depIdxs,
MessageInfos: file_discount_audit_model_proto_msgTypes,
}.Build()
File_discount_audit_model_proto = out.File
file_discount_audit_model_proto_rawDesc = nil
file_discount_audit_model_proto_goTypes = nil
file_discount_audit_model_proto_depIdxs = nil
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,524 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.31.0
// protoc (unknown)
// source: discount/service.proto
package discount
import (
_ "google.golang.org/genproto/googleapis/api/annotations"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type GetDiscountByIDRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"`
}
func (x *GetDiscountByIDRequest) Reset() {
*x = GetDiscountByIDRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_discount_service_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetDiscountByIDRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetDiscountByIDRequest) ProtoMessage() {}
func (x *GetDiscountByIDRequest) ProtoReflect() protoreflect.Message {
mi := &file_discount_service_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetDiscountByIDRequest.ProtoReflect.Descriptor instead.
func (*GetDiscountByIDRequest) Descriptor() ([]byte, []int) {
return file_discount_service_proto_rawDescGZIP(), []int{0}
}
func (x *GetDiscountByIDRequest) GetID() string {
if x != nil {
return x.ID
}
return ""
}
type ApplyDiscountRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
UserInformation *UserInformation `protobuf:"bytes,1,opt,name=UserInformation,proto3" json:"UserInformation,omitempty"`
Products []*ProductInformation `protobuf:"bytes,2,rep,name=Products,proto3" json:"Products,omitempty"`
Date *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=Date,proto3" json:"Date,omitempty"`
Coupon *string `protobuf:"bytes,4,opt,name=Coupon,proto3,oneof" json:"Coupon,omitempty"`
}
func (x *ApplyDiscountRequest) Reset() {
*x = ApplyDiscountRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_discount_service_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ApplyDiscountRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ApplyDiscountRequest) ProtoMessage() {}
func (x *ApplyDiscountRequest) ProtoReflect() protoreflect.Message {
mi := &file_discount_service_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ApplyDiscountRequest.ProtoReflect.Descriptor instead.
func (*ApplyDiscountRequest) Descriptor() ([]byte, []int) {
return file_discount_service_proto_rawDescGZIP(), []int{1}
}
func (x *ApplyDiscountRequest) GetUserInformation() *UserInformation {
if x != nil {
return x.UserInformation
}
return nil
}
func (x *ApplyDiscountRequest) GetProducts() []*ProductInformation {
if x != nil {
return x.Products
}
return nil
}
func (x *ApplyDiscountRequest) GetDate() *timestamppb.Timestamp {
if x != nil {
return x.Date
}
return nil
}
func (x *ApplyDiscountRequest) GetCoupon() string {
if x != nil && x.Coupon != nil {
return *x.Coupon
}
return ""
}
type ApplyDiscountResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Price uint64 `protobuf:"varint,1,opt,name=Price,proto3" json:"Price,omitempty"`
AppliedDiscounts []*Discount `protobuf:"bytes,2,rep,name=AppliedDiscounts,proto3" json:"AppliedDiscounts,omitempty"`
}
func (x *ApplyDiscountResponse) Reset() {
*x = ApplyDiscountResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_discount_service_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *ApplyDiscountResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ApplyDiscountResponse) ProtoMessage() {}
func (x *ApplyDiscountResponse) ProtoReflect() protoreflect.Message {
mi := &file_discount_service_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ApplyDiscountResponse.ProtoReflect.Descriptor instead.
func (*ApplyDiscountResponse) Descriptor() ([]byte, []int) {
return file_discount_service_proto_rawDescGZIP(), []int{2}
}
func (x *ApplyDiscountResponse) GetPrice() uint64 {
if x != nil {
return x.Price
}
return 0
}
func (x *ApplyDiscountResponse) GetAppliedDiscounts() []*Discount {
if x != nil {
return x.AppliedDiscounts
}
return nil
}
type CreateDiscountRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=Name,proto3" json:"Name,omitempty"`
Layer uint32 `protobuf:"varint,2,opt,name=Layer,proto3" json:"Layer,omitempty"`
Description string `protobuf:"bytes,3,opt,name=Description,proto3" json:"Description,omitempty"`
Condition *DiscountCondition `protobuf:"bytes,4,opt,name=Condition,proto3" json:"Condition,omitempty"`
Target *DiscountCalculationTarget `protobuf:"bytes,5,opt,name=Target,proto3" json:"Target,omitempty"`
}
func (x *CreateDiscountRequest) Reset() {
*x = CreateDiscountRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_discount_service_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CreateDiscountRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateDiscountRequest) ProtoMessage() {}
func (x *CreateDiscountRequest) ProtoReflect() protoreflect.Message {
mi := &file_discount_service_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateDiscountRequest.ProtoReflect.Descriptor instead.
func (*CreateDiscountRequest) Descriptor() ([]byte, []int) {
return file_discount_service_proto_rawDescGZIP(), []int{3}
}
func (x *CreateDiscountRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *CreateDiscountRequest) GetLayer() uint32 {
if x != nil {
return x.Layer
}
return 0
}
func (x *CreateDiscountRequest) GetDescription() string {
if x != nil {
return x.Description
}
return ""
}
func (x *CreateDiscountRequest) GetCondition() *DiscountCondition {
if x != nil {
return x.Condition
}
return nil
}
func (x *CreateDiscountRequest) GetTarget() *DiscountCalculationTarget {
if x != nil {
return x.Target
}
return nil
}
var File_discount_service_proto protoreflect.FileDescriptor
var file_discount_service_proto_rawDesc = []byte{
0x0a, 0x16, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69,
0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75,
0x6e, 0x74, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61,
0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1d,
0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e,
0x74, 0x2e, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x28, 0x0a,
0x16, 0x47, 0x65, 0x74, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x79, 0x49, 0x44,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x22, 0xed, 0x01, 0x0a, 0x14, 0x41, 0x70, 0x70, 0x6c,
0x79, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x12, 0x43, 0x0a, 0x0f, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x64, 0x69, 0x73, 0x63,
0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0f, 0x55, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x38, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74,
0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75,
0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x73, 0x12,
0x2e, 0x0a, 0x04, 0x44, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x04, 0x44, 0x61, 0x74, 0x65, 0x12,
0x1b, 0x0a, 0x06, 0x43, 0x6f, 0x75, 0x70, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48,
0x00, 0x52, 0x06, 0x43, 0x6f, 0x75, 0x70, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07,
0x5f, 0x43, 0x6f, 0x75, 0x70, 0x6f, 0x6e, 0x22, 0x6d, 0x0a, 0x15, 0x41, 0x70, 0x70, 0x6c, 0x79,
0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x12, 0x14, 0x0a, 0x05, 0x50, 0x72, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52,
0x05, 0x50, 0x72, 0x69, 0x63, 0x65, 0x12, 0x3e, 0x0a, 0x10, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65,
0x64, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b,
0x32, 0x12, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x44, 0x69, 0x73, 0x63,
0x6f, 0x75, 0x6e, 0x74, 0x52, 0x10, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x44, 0x69, 0x73,
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x22, 0xdb, 0x01, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74,
0x65, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x18, 0x02, 0x20,
0x01, 0x28, 0x0d, 0x52, 0x05, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x12, 0x20, 0x0a, 0x0b, 0x44, 0x65,
0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0b, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x09,
0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32,
0x1b, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f,
0x75, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x43, 0x6f,
0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3b, 0x0a, 0x06, 0x54, 0x61, 0x72, 0x67, 0x65,
0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75,
0x6e, 0x74, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x43, 0x61, 0x6c, 0x63, 0x75,
0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x52, 0x06, 0x54, 0x61,
0x72, 0x67, 0x65, 0x74, 0x32, 0x80, 0x07, 0x0a, 0x0f, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e,
0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x52, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x41,
0x6c, 0x6c, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x16, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d,
0x70, 0x74, 0x79, 0x1a, 0x13, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x44,
0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x22, 0x12, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0c,
0x12, 0x0a, 0x2f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x66, 0x0a, 0x10,
0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73,
0x12, 0x20, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x47, 0x65, 0x74, 0x44,
0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x79, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x13, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x44, 0x69,
0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x22, 0x1b, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x15, 0x12,
0x13, 0x2f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x2f,
0x7b, 0x49, 0x44, 0x7d, 0x12, 0x69, 0x0a, 0x12, 0x44, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e,
0x65, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x1e, 0x2e, 0x64, 0x69, 0x73,
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x44, 0x69, 0x73, 0x63, 0x6f,
0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x64, 0x69, 0x73,
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x22,
0x1e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x18, 0x3a, 0x01, 0x2a, 0x22, 0x13, 0x2f, 0x64, 0x69, 0x73,
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2f, 0x64, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x69, 0x6e, 0x65, 0x12,
0x6d, 0x0a, 0x0e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74,
0x73, 0x12, 0x1e, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x41, 0x70, 0x70,
0x6c, 0x79, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x1f, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x41, 0x70, 0x70,
0x6c, 0x79, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x22, 0x1a, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x14, 0x3a, 0x01, 0x2a, 0x22, 0x0f, 0x2f,
0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2f, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x12, 0x5f,
0x0a, 0x0f, 0x47, 0x65, 0x74, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x79, 0x49,
0x44, 0x12, 0x20, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x47, 0x65, 0x74,
0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x79, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x44,
0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x12,
0x0e, 0x2f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2f, 0x7b, 0x49, 0x44, 0x7d, 0x12,
0x5b, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e,
0x74, 0x12, 0x1f, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x43, 0x72, 0x65,
0x61, 0x74, 0x65, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x44, 0x69,
0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x14, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0e, 0x3a, 0x01,
0x2a, 0x22, 0x09, 0x2f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x5c, 0x0a, 0x0f,
0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12,
0x1a, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f,
0x75, 0x6e, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x1a, 0x12, 0x2e, 0x64, 0x69,
0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22,
0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x1a, 0x0e, 0x2f, 0x64, 0x69, 0x73,
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2f, 0x7b, 0x49, 0x44, 0x7d, 0x12, 0x5b, 0x0a, 0x0e, 0x55, 0x70,
0x64, 0x61, 0x74, 0x65, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1a, 0x2e, 0x64,
0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74,
0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x1a, 0x12, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f,
0x75, 0x6e, 0x74, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x19, 0x82, 0xd3,
0xe4, 0x93, 0x02, 0x13, 0x3a, 0x01, 0x2a, 0x32, 0x0e, 0x2f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75,
0x6e, 0x74, 0x2f, 0x7b, 0x49, 0x44, 0x7d, 0x12, 0x5e, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74,
0x65, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x20, 0x2e, 0x64, 0x69, 0x73, 0x63,
0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74,
0x42, 0x79, 0x49, 0x44, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x69,
0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22,
0x16, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x10, 0x2a, 0x0e, 0x2f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75,
0x6e, 0x74, 0x2f, 0x7b, 0x49, 0x44, 0x7d, 0x42, 0x0c, 0x5a, 0x0a, 0x2e, 0x2f, 0x64, 0x69, 0x73,
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_discount_service_proto_rawDescOnce sync.Once
file_discount_service_proto_rawDescData = file_discount_service_proto_rawDesc
)
func file_discount_service_proto_rawDescGZIP() []byte {
file_discount_service_proto_rawDescOnce.Do(func() {
file_discount_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_discount_service_proto_rawDescData)
})
return file_discount_service_proto_rawDescData
}
var file_discount_service_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_discount_service_proto_goTypes = []interface{}{
(*GetDiscountByIDRequest)(nil), // 0: discount.GetDiscountByIDRequest
(*ApplyDiscountRequest)(nil), // 1: discount.ApplyDiscountRequest
(*ApplyDiscountResponse)(nil), // 2: discount.ApplyDiscountResponse
(*CreateDiscountRequest)(nil), // 3: discount.CreateDiscountRequest
(*UserInformation)(nil), // 4: discount.UserInformation
(*ProductInformation)(nil), // 5: discount.ProductInformation
(*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp
(*Discount)(nil), // 7: discount.Discount
(*DiscountCondition)(nil), // 8: discount.DiscountCondition
(*DiscountCalculationTarget)(nil), // 9: discount.DiscountCalculationTarget
(*emptypb.Empty)(nil), // 10: google.protobuf.Empty
(*DiscountOptional)(nil), // 11: discount.DiscountOptional
(*Discounts)(nil), // 12: discount.Discounts
}
var file_discount_service_proto_depIdxs = []int32{
4, // 0: discount.ApplyDiscountRequest.UserInformation:type_name -> discount.UserInformation
5, // 1: discount.ApplyDiscountRequest.Products:type_name -> discount.ProductInformation
6, // 2: discount.ApplyDiscountRequest.Date:type_name -> google.protobuf.Timestamp
7, // 3: discount.ApplyDiscountResponse.AppliedDiscounts:type_name -> discount.Discount
8, // 4: discount.CreateDiscountRequest.Condition:type_name -> discount.DiscountCondition
9, // 5: discount.CreateDiscountRequest.Target:type_name -> discount.DiscountCalculationTarget
10, // 6: discount.DiscountService.GetAllDiscounts:input_type -> google.protobuf.Empty
0, // 7: discount.DiscountService.GetUserDiscounts:input_type -> discount.GetDiscountByIDRequest
1, // 8: discount.DiscountService.DetermineDiscounts:input_type -> discount.ApplyDiscountRequest
1, // 9: discount.DiscountService.ApplyDiscounts:input_type -> discount.ApplyDiscountRequest
0, // 10: discount.DiscountService.GetDiscountByID:input_type -> discount.GetDiscountByIDRequest
3, // 11: discount.DiscountService.CreateDiscount:input_type -> discount.CreateDiscountRequest
11, // 12: discount.DiscountService.ReplaceDiscount:input_type -> discount.DiscountOptional
11, // 13: discount.DiscountService.UpdateDiscount:input_type -> discount.DiscountOptional
0, // 14: discount.DiscountService.DeleteDiscount:input_type -> discount.GetDiscountByIDRequest
12, // 15: discount.DiscountService.GetAllDiscounts:output_type -> discount.Discounts
12, // 16: discount.DiscountService.GetUserDiscounts:output_type -> discount.Discounts
12, // 17: discount.DiscountService.DetermineDiscounts:output_type -> discount.Discounts
2, // 18: discount.DiscountService.ApplyDiscounts:output_type -> discount.ApplyDiscountResponse
7, // 19: discount.DiscountService.GetDiscountByID:output_type -> discount.Discount
7, // 20: discount.DiscountService.CreateDiscount:output_type -> discount.Discount
7, // 21: discount.DiscountService.ReplaceDiscount:output_type -> discount.Discount
7, // 22: discount.DiscountService.UpdateDiscount:output_type -> discount.Discount
7, // 23: discount.DiscountService.DeleteDiscount:output_type -> discount.Discount
15, // [15:24] is the sub-list for method output_type
6, // [6:15] is the sub-list for method input_type
6, // [6:6] is the sub-list for extension type_name
6, // [6:6] is the sub-list for extension extendee
0, // [0:6] is the sub-list for field type_name
}
func init() { file_discount_service_proto_init() }
func file_discount_service_proto_init() {
if File_discount_service_proto != nil {
return
}
file_discount_discount_model_proto_init()
if !protoimpl.UnsafeEnabled {
file_discount_service_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetDiscountByIDRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_discount_service_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ApplyDiscountRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_discount_service_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ApplyDiscountResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_discount_service_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CreateDiscountRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
file_discount_service_proto_msgTypes[1].OneofWrappers = []interface{}{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_discount_service_proto_rawDesc,
NumEnums: 0,
NumMessages: 4,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_discount_service_proto_goTypes,
DependencyIndexes: file_discount_service_proto_depIdxs,
MessageInfos: file_discount_service_proto_msgTypes,
}.Build()
File_discount_service_proto = out.File
file_discount_service_proto_rawDesc = nil
file_discount_service_proto_goTypes = nil
file_discount_service_proto_depIdxs = nil
}

@ -0,0 +1,404 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.3.0
// - protoc (unknown)
// source: discount/service.proto
package discount
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
emptypb "google.golang.org/protobuf/types/known/emptypb"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
const (
DiscountService_GetAllDiscounts_FullMethodName = "/discount.DiscountService/GetAllDiscounts"
DiscountService_GetUserDiscounts_FullMethodName = "/discount.DiscountService/GetUserDiscounts"
DiscountService_DetermineDiscounts_FullMethodName = "/discount.DiscountService/DetermineDiscounts"
DiscountService_ApplyDiscounts_FullMethodName = "/discount.DiscountService/ApplyDiscounts"
DiscountService_GetDiscountByID_FullMethodName = "/discount.DiscountService/GetDiscountByID"
DiscountService_CreateDiscount_FullMethodName = "/discount.DiscountService/CreateDiscount"
DiscountService_ReplaceDiscount_FullMethodName = "/discount.DiscountService/ReplaceDiscount"
DiscountService_UpdateDiscount_FullMethodName = "/discount.DiscountService/UpdateDiscount"
DiscountService_DeleteDiscount_FullMethodName = "/discount.DiscountService/DeleteDiscount"
)
// DiscountServiceClient is the client API for DiscountService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type DiscountServiceClient interface {
GetAllDiscounts(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Discounts, error)
GetUserDiscounts(ctx context.Context, in *GetDiscountByIDRequest, opts ...grpc.CallOption) (*Discounts, error)
DetermineDiscounts(ctx context.Context, in *ApplyDiscountRequest, opts ...grpc.CallOption) (*Discounts, error)
ApplyDiscounts(ctx context.Context, in *ApplyDiscountRequest, opts ...grpc.CallOption) (*ApplyDiscountResponse, error)
GetDiscountByID(ctx context.Context, in *GetDiscountByIDRequest, opts ...grpc.CallOption) (*Discount, error)
CreateDiscount(ctx context.Context, in *CreateDiscountRequest, opts ...grpc.CallOption) (*Discount, error)
ReplaceDiscount(ctx context.Context, in *DiscountOptional, opts ...grpc.CallOption) (*Discount, error)
UpdateDiscount(ctx context.Context, in *DiscountOptional, opts ...grpc.CallOption) (*Discount, error)
DeleteDiscount(ctx context.Context, in *GetDiscountByIDRequest, opts ...grpc.CallOption) (*Discount, error)
}
type discountServiceClient struct {
cc grpc.ClientConnInterface
}
func NewDiscountServiceClient(cc grpc.ClientConnInterface) DiscountServiceClient {
return &discountServiceClient{cc}
}
func (c *discountServiceClient) GetAllDiscounts(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*Discounts, error) {
out := new(Discounts)
err := c.cc.Invoke(ctx, DiscountService_GetAllDiscounts_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *discountServiceClient) GetUserDiscounts(ctx context.Context, in *GetDiscountByIDRequest, opts ...grpc.CallOption) (*Discounts, error) {
out := new(Discounts)
err := c.cc.Invoke(ctx, DiscountService_GetUserDiscounts_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *discountServiceClient) DetermineDiscounts(ctx context.Context, in *ApplyDiscountRequest, opts ...grpc.CallOption) (*Discounts, error) {
out := new(Discounts)
err := c.cc.Invoke(ctx, DiscountService_DetermineDiscounts_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *discountServiceClient) ApplyDiscounts(ctx context.Context, in *ApplyDiscountRequest, opts ...grpc.CallOption) (*ApplyDiscountResponse, error) {
out := new(ApplyDiscountResponse)
err := c.cc.Invoke(ctx, DiscountService_ApplyDiscounts_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *discountServiceClient) GetDiscountByID(ctx context.Context, in *GetDiscountByIDRequest, opts ...grpc.CallOption) (*Discount, error) {
out := new(Discount)
err := c.cc.Invoke(ctx, DiscountService_GetDiscountByID_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *discountServiceClient) CreateDiscount(ctx context.Context, in *CreateDiscountRequest, opts ...grpc.CallOption) (*Discount, error) {
out := new(Discount)
err := c.cc.Invoke(ctx, DiscountService_CreateDiscount_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *discountServiceClient) ReplaceDiscount(ctx context.Context, in *DiscountOptional, opts ...grpc.CallOption) (*Discount, error) {
out := new(Discount)
err := c.cc.Invoke(ctx, DiscountService_ReplaceDiscount_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *discountServiceClient) UpdateDiscount(ctx context.Context, in *DiscountOptional, opts ...grpc.CallOption) (*Discount, error) {
out := new(Discount)
err := c.cc.Invoke(ctx, DiscountService_UpdateDiscount_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *discountServiceClient) DeleteDiscount(ctx context.Context, in *GetDiscountByIDRequest, opts ...grpc.CallOption) (*Discount, error) {
out := new(Discount)
err := c.cc.Invoke(ctx, DiscountService_DeleteDiscount_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// DiscountServiceServer is the server API for DiscountService service.
// All implementations should embed UnimplementedDiscountServiceServer
// for forward compatibility
type DiscountServiceServer interface {
GetAllDiscounts(context.Context, *emptypb.Empty) (*Discounts, error)
GetUserDiscounts(context.Context, *GetDiscountByIDRequest) (*Discounts, error)
DetermineDiscounts(context.Context, *ApplyDiscountRequest) (*Discounts, error)
ApplyDiscounts(context.Context, *ApplyDiscountRequest) (*ApplyDiscountResponse, error)
GetDiscountByID(context.Context, *GetDiscountByIDRequest) (*Discount, error)
CreateDiscount(context.Context, *CreateDiscountRequest) (*Discount, error)
ReplaceDiscount(context.Context, *DiscountOptional) (*Discount, error)
UpdateDiscount(context.Context, *DiscountOptional) (*Discount, error)
DeleteDiscount(context.Context, *GetDiscountByIDRequest) (*Discount, error)
}
// UnimplementedDiscountServiceServer should be embedded to have forward compatible implementations.
type UnimplementedDiscountServiceServer struct {
}
func (UnimplementedDiscountServiceServer) GetAllDiscounts(context.Context, *emptypb.Empty) (*Discounts, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetAllDiscounts not implemented")
}
func (UnimplementedDiscountServiceServer) GetUserDiscounts(context.Context, *GetDiscountByIDRequest) (*Discounts, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetUserDiscounts not implemented")
}
func (UnimplementedDiscountServiceServer) DetermineDiscounts(context.Context, *ApplyDiscountRequest) (*Discounts, error) {
return nil, status.Errorf(codes.Unimplemented, "method DetermineDiscounts not implemented")
}
func (UnimplementedDiscountServiceServer) ApplyDiscounts(context.Context, *ApplyDiscountRequest) (*ApplyDiscountResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ApplyDiscounts not implemented")
}
func (UnimplementedDiscountServiceServer) GetDiscountByID(context.Context, *GetDiscountByIDRequest) (*Discount, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetDiscountByID not implemented")
}
func (UnimplementedDiscountServiceServer) CreateDiscount(context.Context, *CreateDiscountRequest) (*Discount, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateDiscount not implemented")
}
func (UnimplementedDiscountServiceServer) ReplaceDiscount(context.Context, *DiscountOptional) (*Discount, error) {
return nil, status.Errorf(codes.Unimplemented, "method ReplaceDiscount not implemented")
}
func (UnimplementedDiscountServiceServer) UpdateDiscount(context.Context, *DiscountOptional) (*Discount, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateDiscount not implemented")
}
func (UnimplementedDiscountServiceServer) DeleteDiscount(context.Context, *GetDiscountByIDRequest) (*Discount, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteDiscount not implemented")
}
// UnsafeDiscountServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to DiscountServiceServer will
// result in compilation errors.
type UnsafeDiscountServiceServer interface {
mustEmbedUnimplementedDiscountServiceServer()
}
func RegisterDiscountServiceServer(s grpc.ServiceRegistrar, srv DiscountServiceServer) {
s.RegisterService(&DiscountService_ServiceDesc, srv)
}
func _DiscountService_GetAllDiscounts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(emptypb.Empty)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DiscountServiceServer).GetAllDiscounts(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DiscountService_GetAllDiscounts_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DiscountServiceServer).GetAllDiscounts(ctx, req.(*emptypb.Empty))
}
return interceptor(ctx, in, info, handler)
}
func _DiscountService_GetUserDiscounts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetDiscountByIDRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DiscountServiceServer).GetUserDiscounts(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DiscountService_GetUserDiscounts_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DiscountServiceServer).GetUserDiscounts(ctx, req.(*GetDiscountByIDRequest))
}
return interceptor(ctx, in, info, handler)
}
func _DiscountService_DetermineDiscounts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ApplyDiscountRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DiscountServiceServer).DetermineDiscounts(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DiscountService_DetermineDiscounts_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DiscountServiceServer).DetermineDiscounts(ctx, req.(*ApplyDiscountRequest))
}
return interceptor(ctx, in, info, handler)
}
func _DiscountService_ApplyDiscounts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ApplyDiscountRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DiscountServiceServer).ApplyDiscounts(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DiscountService_ApplyDiscounts_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DiscountServiceServer).ApplyDiscounts(ctx, req.(*ApplyDiscountRequest))
}
return interceptor(ctx, in, info, handler)
}
func _DiscountService_GetDiscountByID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetDiscountByIDRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DiscountServiceServer).GetDiscountByID(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DiscountService_GetDiscountByID_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DiscountServiceServer).GetDiscountByID(ctx, req.(*GetDiscountByIDRequest))
}
return interceptor(ctx, in, info, handler)
}
func _DiscountService_CreateDiscount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateDiscountRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DiscountServiceServer).CreateDiscount(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DiscountService_CreateDiscount_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DiscountServiceServer).CreateDiscount(ctx, req.(*CreateDiscountRequest))
}
return interceptor(ctx, in, info, handler)
}
func _DiscountService_ReplaceDiscount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DiscountOptional)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DiscountServiceServer).ReplaceDiscount(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DiscountService_ReplaceDiscount_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DiscountServiceServer).ReplaceDiscount(ctx, req.(*DiscountOptional))
}
return interceptor(ctx, in, info, handler)
}
func _DiscountService_UpdateDiscount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DiscountOptional)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DiscountServiceServer).UpdateDiscount(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DiscountService_UpdateDiscount_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DiscountServiceServer).UpdateDiscount(ctx, req.(*DiscountOptional))
}
return interceptor(ctx, in, info, handler)
}
func _DiscountService_DeleteDiscount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetDiscountByIDRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DiscountServiceServer).DeleteDiscount(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: DiscountService_DeleteDiscount_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DiscountServiceServer).DeleteDiscount(ctx, req.(*GetDiscountByIDRequest))
}
return interceptor(ctx, in, info, handler)
}
// DiscountService_ServiceDesc is the grpc.ServiceDesc for DiscountService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var DiscountService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "discount.DiscountService",
HandlerType: (*DiscountServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetAllDiscounts",
Handler: _DiscountService_GetAllDiscounts_Handler,
},
{
MethodName: "GetUserDiscounts",
Handler: _DiscountService_GetUserDiscounts_Handler,
},
{
MethodName: "DetermineDiscounts",
Handler: _DiscountService_DetermineDiscounts_Handler,
},
{
MethodName: "ApplyDiscounts",
Handler: _DiscountService_ApplyDiscounts_Handler,
},
{
MethodName: "GetDiscountByID",
Handler: _DiscountService_GetDiscountByID_Handler,
},
{
MethodName: "CreateDiscount",
Handler: _DiscountService_CreateDiscount_Handler,
},
{
MethodName: "ReplaceDiscount",
Handler: _DiscountService_ReplaceDiscount_Handler,
},
{
MethodName: "UpdateDiscount",
Handler: _DiscountService_UpdateDiscount_Handler,
},
{
MethodName: "DeleteDiscount",
Handler: _DiscountService_DeleteDiscount_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "discount/service.proto",
}

@ -46,6 +46,17 @@ func (r *CodewordRepository) StoreRecoveryRecord(ctx context.Context, deps model
// добавляем в очередь данные для отправки на почту в редис
func (r *CodewordRepository) InsertToQueue(ctx context.Context, deps models.RecEmailDeps) error {
sendLockKey := "email:sendLock:" + deps.Email
ttl := 5 * time.Minute
lockSuccess, err := r.rdb.SetNX(ctx, sendLockKey, "1", ttl).Result()
if err != nil {
return err
}
if !lockSuccess {
return ErrAlreadyReported
}
task := models.RecoveryRecord{
ID: deps.ID,
UserID: deps.UserID,
@ -58,11 +69,7 @@ func (r *CodewordRepository) InsertToQueue(ctx context.Context, deps models.RecE
return err
}
if err := r.rdb.LPush(ctx, "recoveryQueue", taskBytes).Err(); err != nil {
return err
}
return nil
return r.rdb.Set(ctx, "email:task:"+deps.Email, taskBytes, ttl).Err()
}
// получаем данные юзера по подписи
@ -81,8 +88,13 @@ func (r *CodewordRepository) GetRecoveryRecord(ctx context.Context, key string)
// пингует в монгу чтобы проверить подключение
func (r *CodewordRepository) Ping(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
if err := r.mdb.Database().Client().Ping(ctx, readpref.Primary()); err != nil {
return err
}
if err := r.rdb.Ping(ctx).Err(); err != nil {
return err
}
return nil
}

@ -0,0 +1,13 @@
package repository
import "errors"
var (
ErrPromoUserNotFound = errors.New("user not found")
ErrAlreadyReported = errors.New("already reported")
ErrDuplicateCodeword = errors.New("duplicate codeword")
ErrPromoCodeNotFound = errors.New("promo code not found")
ErrPromoCodeExpired = errors.New("promo code is expired")
ErrPromoCodeExhausted = errors.New("promo code is exhausted")
ErrPromoCodeAlreadyActivated = errors.New("promo code is already activated")
)

@ -3,7 +3,6 @@ package repository
import (
"codeword/internal/models"
"context"
"errors"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
@ -11,11 +10,6 @@ import (
"time"
)
var (
ErrDuplicateCodeword = errors.New("duplicate codeword")
ErrPromoCodeNotFound = errors.New("promo code not found")
)
// структура для горутины чтобы ошибки не пропускать
type countResult struct {
count int64
@ -27,7 +21,10 @@ type PromoCodeRepository struct {
}
func NewPromoCodeRepository(mdb *mongo.Collection) *PromoCodeRepository {
// todo заменить паники вроде как в роде не круто их юзать
return &PromoCodeRepository{mdb: mdb}
}
func InitPromoCodeIndexes(ctx context.Context, mdb *mongo.Collection) error {
uniqueIndexModel := mongo.IndexModel{
Keys: bson.D{
{Key: "codeword", Value: 1},
@ -35,9 +32,9 @@ func NewPromoCodeRepository(mdb *mongo.Collection) *PromoCodeRepository {
},
Options: options.Index().SetUnique(true).SetPartialFilterExpression(bson.M{"delete": false}),
}
_, err := mdb.Indexes().CreateOne(context.Background(), uniqueIndexModel)
_, err := mdb.Indexes().CreateOne(ctx, uniqueIndexModel)
if err != nil {
panic(err)
return err
}
textIndexModel := mongo.IndexModel{
@ -48,17 +45,20 @@ func NewPromoCodeRepository(mdb *mongo.Collection) *PromoCodeRepository {
},
Options: options.Index().SetName("TextIndex"),
}
_, err = mdb.Indexes().CreateOne(context.Background(), textIndexModel)
_, err = mdb.Indexes().CreateOne(ctx, textIndexModel)
if err != nil {
panic(err)
return err
}
return &PromoCodeRepository{mdb: mdb}
return nil
}
func (r *PromoCodeRepository) CreatePromoCode(ctx context.Context, req *models.PromoCode) (*models.PromoCode, error) {
req.CreatedAt = time.Now()
req.ID = primitive.NewObjectID()
if req.FastLinks == nil {
req.FastLinks = []string{}
}
_, err := r.mdb.InsertOne(ctx, req)
if err != nil {
@ -214,48 +214,51 @@ func (r *PromoCodeRepository) GetPromoCodesList(ctx context.Context, req *models
return promoCodes, count, nil
}
func (r *PromoCodeRepository) ActivatePromo(ctx context.Context, req *models.ActivateReq) (string, error) {
session, err := r.mdb.Database().Client().StartSession()
func (r *PromoCodeRepository) ActivatePromo(ctx context.Context, req *models.ActivateReq) (*models.PromoCode, error) {
var promoCode models.PromoCode
var filter bson.M
if req.Codeword != "" {
filter = bson.M{
"codeword": req.Codeword,
}
} else if req.FastLink != "" {
filter = bson.M{
"fastLinks": req.FastLink,
}
}
opts := options.FindOneAndUpdate().SetReturnDocument(options.After)
err := r.mdb.FindOneAndUpdate(ctx, filter, bson.M{"$inc": bson.M{"activationCount": -1}}, opts).Decode(&promoCode)
if err != nil {
return "", err
if err == mongo.ErrNoDocuments {
return nil, ErrPromoCodeNotFound
}
return nil, err
}
defer session.EndSession(ctx)
var greetings string
transactionErr := mongo.WithSession(ctx, session, func(sc mongo.SessionContext) error {
filter := bson.M{
"codeword": req.Codeword,
"delete": false,
"outdated": false,
"offLimit": false,
"activationCount": bson.M{"$gt": 0},
"dueTo": bson.M{"$gt": time.Now().Unix()},
}
update := bson.M{
"$inc": bson.M{"activationCount": -1},
}
opts := options.FindOneAndUpdate().SetReturnDocument(options.After)
var updatedPromoCode models.PromoCode
err := r.mdb.FindOneAndUpdate(sc, filter, update, opts).Decode(&updatedPromoCode)
if err != nil {
if err == mongo.ErrNoDocuments {
return ErrPromoCodeNotFound
if promoCode.ActivationCount <= 0 && promoCode.DueTo > time.Now().Unix() {
if !promoCode.OffLimit {
update := bson.M{"$set": bson.M{"offLimit": true}}
_, err := r.mdb.UpdateOne(ctx, filter, update)
if err != nil {
return nil, err
}
return err
}
}
return &promoCode, nil
}
greetings = updatedPromoCode.Greetings
return nil
})
if transactionErr != nil {
return "", transactionErr
func (r *PromoCodeRepository) IncreaseActivationCount(ctx context.Context, promoCodeID primitive.ObjectID) error {
filter := bson.M{"_id": promoCodeID}
update := bson.M{"$inc": bson.M{"activationCount": 1}}
_, err := r.mdb.UpdateOne(ctx, filter, update)
if err != nil {
return err
}
return greetings, nil
return nil
}
func (r *PromoCodeRepository) DeletePromoCode(ctx context.Context, promoCodeID string) error {
@ -279,3 +282,19 @@ func (r *PromoCodeRepository) DeletePromoCode(ctx context.Context, promoCodeID s
return nil
}
func (r *PromoCodeRepository) AddFastLink(ctx context.Context, promoCodeID primitive.ObjectID, xid string) error {
filter := bson.M{"_id": promoCodeID, "delete": false}
update := bson.M{"$push": bson.M{"fastLinks": xid}}
result, err := r.mdb.UpdateOne(ctx, filter, update)
if err != nil {
return err
}
if result.MatchedCount == 0 {
return ErrPromoCodeNotFound
}
return nil
}

@ -0,0 +1,72 @@
package repository
import (
"codeword/internal/models"
"context"
"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"
"time"
)
type StatsRepository struct {
mdb *mongo.Collection
}
func NewStatsRepository(deps Deps) *StatsRepository {
return &StatsRepository{mdb: deps.Mdb}
}
func (r *StatsRepository) UpdateStatistics(ctx context.Context, req *models.ActivateReq, promoCode *models.PromoCode, userID string) error {
filter := bson.M{"_id": promoCode.ID, "usageMap." + userID: bson.M{"$exists": true}}
count, err := r.mdb.CountDocuments(ctx, filter)
if err != nil {
return err
}
if count >= 1 {
return ErrPromoCodeAlreadyActivated
}
var key string
if req.FastLink != "" {
key = req.FastLink
} else {
key = req.Codeword
}
usage := models.Usage{
Key: key,
Time: time.Now(),
}
update := bson.M{
"$inc": bson.M{"usageCount": 1},
"$push": bson.M{
"usageMap." + userID: usage,
},
}
opts := options.Update().SetUpsert(true)
_, err = r.mdb.UpdateOne(ctx, bson.M{"_id": promoCode.ID}, update, opts)
return err
}
func (r *StatsRepository) GetStatistics(ctx context.Context, promoCodeID string) (*models.PromoCodeStats, error) {
objID, err := primitive.ObjectIDFromHex(promoCodeID)
if err != nil {
return nil, err
}
filter := bson.M{"_id": objID}
var promoCodeStats models.PromoCodeStats
err = r.mdb.FindOne(ctx, filter).Decode(&promoCodeStats)
if err != nil {
return nil, err
}
return &promoCodeStats, nil
}

@ -26,12 +26,12 @@ func NewUserRepository(deps Deps) *UserRepository {
func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*models.User, error) {
var user models.User
err := r.mdb.FindOne(ctx, bson.M{"email": email}).Decode(&user)
err := r.mdb.FindOne(ctx, bson.M{"login": email}).Decode(&user)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, nil
}
return nil, err
return nil, ErrPromoUserNotFound
}
return &user, nil
}

@ -1,36 +1,30 @@
package http
import (
"codeword/internal/controller/promocode"
"codeword/internal/controller/recovery"
"context"
"fmt"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
"time"
)
type ServerConfig struct {
Logger *zap.Logger
RecoveryController *recovery.RecoveryController
PromoCodeController *promocode.PromoCodeController
Logger *zap.Logger
Controllers []Controller
}
type Server struct {
Logger *zap.Logger
RecoveryController *recovery.RecoveryController
PromoCodeController *promocode.PromoCodeController
app *fiber.App
Logger *zap.Logger
Controllers []Controller
app *fiber.App
}
func NewServer(config ServerConfig) *Server {
app := fiber.New()
s := &Server{
Logger: config.Logger,
RecoveryController: config.RecoveryController,
PromoCodeController: config.PromoCodeController,
app: app,
Logger: config.Logger,
Controllers: config.Controllers,
app: app,
}
s.registerRoutes()
@ -51,34 +45,22 @@ func (s *Server) Shutdown(ctx context.Context) error {
}
func (s *Server) registerRoutes() {
s.app.Get("/liveness", s.handleLiveness)
s.app.Get("/readiness", s.handleReadiness)
s.app.Post("/recover", s.RecoveryController.HandleRecoveryRequest)
s.app.Get("/recover/:sign", s.RecoveryController.HandleRecoveryLink)
s.app.Post("/promocode/create", s.PromoCodeController.CreatePromoCode)
s.app.Put("/promocode/edit", s.PromoCodeController.EditPromoCode)
s.app.Post("/promocode/getList", s.PromoCodeController.GetList)
s.app.Post("/promocode/activate", s.PromoCodeController.Activate)
s.app.Delete("/promocode/:promocodeID", s.PromoCodeController.Delete)
//... other
}
func (s *Server) handleLiveness(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
}
func (s *Server) handleReadiness(c *fiber.Ctx) error {
startTime := time.Now()
if err := s.RecoveryController.HandlePingDB(c); err != nil {
s.Logger.Error("Failed to ping the database", zap.Error(err))
return c.Status(fiber.StatusServiceUnavailable).SendString("DB ping failed")
for _, c := range s.Controllers {
router := s.app.Group(c.Name())
c.Register(router)
}
}
type Controller interface {
Register(router fiber.Router)
Name() string
}
func (s *Server) ListRoutes() {
fmt.Println("Registered routes:")
for _, stack := range s.app.Stack() {
for _, route := range stack {
fmt.Printf("%s %s\n", route.Method, route.Path)
}
}
duration := time.Since(startTime)
durationMillis := duration.Milliseconds()
responseMessage := fmt.Sprintf("DB ping success - Time taken: %d ms", durationMillis)
return c.Status(fiber.StatusOK).SendString(responseMessage)
}

@ -1,33 +1,58 @@
package services
import (
"codeword/internal/kafka/tariff"
"codeword/internal/models"
"codeword/internal/proto/discount"
"codeword/internal/repository"
"codeword/internal/utils/genID"
"context"
"errors"
"fmt"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
"time"
)
type PromoCodeRepository interface {
CreatePromoCode(ctx context.Context, req *models.PromoCode) (*models.PromoCode, error)
EditPromoCode(ctx context.Context, req *models.ReqEditPromoCode) (*models.PromoCode, error)
GetPromoCodesList(ctx context.Context, req *models.GetPromoCodesListReq) ([]models.PromoCode, int64, error)
ActivatePromo(ctx context.Context, req *models.ActivateReq) (string, error)
ActivatePromo(ctx context.Context, req *models.ActivateReq) (*models.PromoCode, error)
DeletePromoCode(ctx context.Context, promoCodeID string) error
GetPromoCodeByID(ctx context.Context, promoCodeID primitive.ObjectID) (*models.PromoCode, error)
AddFastLink(ctx context.Context, promoCodeID primitive.ObjectID, xid string) error
IncreaseActivationCount(ctx context.Context, promoCodeID primitive.ObjectID) error
}
type PromoStatsRepository interface {
UpdateStatistics(ctx context.Context, req *models.ActivateReq, promoCode *models.PromoCode, userID string) error
GetStatistics(ctx context.Context, promoCodeID string) (*models.PromoCodeStats, error)
}
type PromoDeps struct {
Logger *zap.Logger
PromoCodeRepo PromoCodeRepository
Logger *zap.Logger
PromoCodeRepo PromoCodeRepository
StatsRepo PromoStatsRepository
Kafka *tariff.Producer
DiscountClient discount.DiscountServiceClient
}
type PromoCodeService struct {
logger *zap.Logger
promoCodeRepo PromoCodeRepository
logger *zap.Logger
promoCodeRepo PromoCodeRepository
statsRepo PromoStatsRepository
kafka *tariff.Producer
discountClient discount.DiscountServiceClient
}
func NewPromoCodeService(deps PromoDeps) *PromoCodeService {
return &PromoCodeService{
logger: deps.Logger,
promoCodeRepo: deps.PromoCodeRepo,
logger: deps.Logger,
promoCodeRepo: deps.PromoCodeRepo,
statsRepo: deps.StatsRepo,
kafka: deps.Kafka,
discountClient: deps.DiscountClient,
}
}
@ -61,14 +86,92 @@ func (s *PromoCodeService) GetPromoCodesList(ctx context.Context, req *models.Ge
return promoCodes, count, nil
}
func (s *PromoCodeService) ActivatePromo(ctx context.Context, req *models.ActivateReq) (string, error) {
greetings, err := s.promoCodeRepo.ActivatePromo(ctx, req)
// todo одумать еще реализацию этого дела, надо уточнить как разделяется ответственность в бонусе между привилегией и скидкой
// разделяется ли она или они всегда вместе, если разделяются то что-то из этого может быть пустым либо все заполеннное,
// соответсвенно надо сделать соответствующие проверки до записи в кафку и до отправки в дискаунт сервис
func (s *PromoCodeService) ActivatePromo(ctx context.Context, req *models.ActivateReq, userID string) (string, error) {
promoCode, err := s.promoCodeRepo.ActivatePromo(ctx, req)
if err != nil {
s.logger.Error("Failed to activate promocode", zap.Error(err))
return "", err
}
//todo такая реализация проверок кажется довольно массивной, думаю как то это стоит сделать параллельно обхаживая все условия
if promoCode.DueTo < time.Now().Unix() && promoCode.DueTo > 0 {
err := s.promoCodeRepo.IncreaseActivationCount(ctx, promoCode.ID)
if err != nil {
return "", err
}
return "", fmt.Errorf("%w: expired on %s", repository.ErrPromoCodeExpired, time.Unix(promoCode.DueTo, 0).Format(time.RFC3339))
}
return greetings, nil
if promoCode.DueTo == 0 && promoCode.ActivationCount < 0 {
err := s.promoCodeRepo.IncreaseActivationCount(ctx, promoCode.ID)
if err != nil {
return "", err
}
return "", repository.ErrPromoCodeExhausted
}
err = s.statsRepo.UpdateStatistics(ctx, req, promoCode, userID)
if err != nil {
if errors.Is(err, repository.ErrPromoCodeAlreadyActivated) {
err := s.promoCodeRepo.IncreaseActivationCount(ctx, promoCode.ID)
if err != nil {
return "", err
}
return "", repository.ErrPromoCodeAlreadyActivated
}
s.logger.Error("Failed add in stats", zap.Error(err))
return "", err
}
var postfix string
if req.FastLink != "" {
postfix = fmt.Sprintf(":(%s)", req.FastLink)
}
var privileges []models.Privilege
privilege := models.Privilege{
PrivilegeID: promoCode.Bonus.Privilege.PrivilegeID,
Amount: promoCode.Bonus.Privilege.Amount,
}
privileges = append(privileges, privilege)
fakeTariff := &models.Tariff{
Name: promoCode.Codeword + postfix,
Privileges: privileges,
Deleted: promoCode.Delete,
CreatedAt: promoCode.CreatedAt,
}
if err := s.kafka.Send(ctx, userID, fakeTariff); err != nil {
s.logger.Error("Failed to send fake tariff to Kafka", zap.Error(err))
return "", err
}
disOverHelm := true
discountRequest := &discount.CreateDiscountRequest{
Name: promoCode.Codeword + postfix,
Layer: promoCode.Bonus.Discount.Layer,
Description: "",
Condition: &discount.DiscountCondition{
Coupon: &promoCode.Codeword,
User: &userID,
},
Target: &discount.DiscountCalculationTarget{
Factor: promoCode.Bonus.Discount.Factor,
Overhelm: &disOverHelm,
},
}
_, err = s.discountClient.CreateDiscount(ctx, discountRequest)
if err != nil {
s.logger.Error("Failed to create discount", zap.Error(err))
return "", err
}
return promoCode.Greetings, nil
}
func (s *PromoCodeService) DeletePromoCode(ctx context.Context, promoCodeID string) error {
@ -80,3 +183,29 @@ func (s *PromoCodeService) DeletePromoCode(ctx context.Context, promoCodeID stri
return nil
}
func (s *PromoCodeService) CreateFastLink(ctx context.Context, promoCodeID string) (string, error) {
xid := genID.GenerateXID()
promoID, err := primitive.ObjectIDFromHex(promoCodeID)
if err != nil {
s.logger.Error("Failed conversion promoCodeID to ObjectID", zap.Error(err))
return "", err
}
err = s.promoCodeRepo.AddFastLink(ctx, promoID, xid)
if err != nil {
s.logger.Error("Failed to add fastlink for promocode by promocode id", zap.Error(err))
return "", err
}
return xid, nil
}
func (s *PromoCodeService) GetStats(ctx context.Context, promoCodeID string) (*models.PromoCodeStats, error) {
promoStats, err := s.statsRepo.GetStatistics(ctx, promoCodeID)
if err != nil {
s.logger.Error("Failed getting promo stats", zap.Error(err))
return nil, err
}
return promoStats, nil
}

@ -73,10 +73,6 @@ func (s *RecoveryService) FindUserByEmail(ctx context.Context, email string) (*m
s.logger.Error("Failed to find user by email", zap.String("email", email), zap.Error(err))
return nil, err
}
if user == nil {
s.logger.Info("No user found with email", zap.String("email", email))
return nil, nil
}
return user, nil
}

@ -0,0 +1,8 @@
package genID
import "github.com/rs/xid"
func GenerateXID() string {
id := xid.New()
return id.String()
}

@ -0,0 +1,31 @@
package transfer
import (
"codeword/internal/models"
"codeword/internal/proto/broker"
)
func PrivilegeModelToProto(privilege *models.Privilege) *broker.PrivilegeMessage {
if privilege == nil {
return &broker.PrivilegeMessage{}
}
return &broker.PrivilegeMessage{
PrivilegeID: privilege.PrivilegeID,
ServiceKey: privilege.ServiceKey,
Type: models.PrivilegeBrokerTypeMap[privilege.Type],
Value: privilege.Value,
Amount: privilege.Amount,
}
}
func PrivilegeArrayModelToProto(privileges []models.Privilege) []*broker.PrivilegeMessage {
privilegesProto := make([]*broker.PrivilegeMessage, len(privileges))
for index, privilege := range privileges {
privilegeCopy := privilege
privilegesProto[index] = PrivilegeModelToProto(&privilegeCopy)
}
return privilegesProto
}

@ -0,0 +1,17 @@
package transfer
import (
"codeword/internal/models"
"codeword/internal/proto/broker"
)
func TariffModelToProtoMessage(userID string, tariffModel *models.Tariff) *broker.TariffMessage {
if tariffModel == nil {
return &broker.TariffMessage{}
}
return &broker.TariffMessage{
UserID: userID,
Privileges: PrivilegeArrayModelToProto(tariffModel.Privileges),
}
}

@ -18,7 +18,7 @@ type PurgeWorker struct {
mongo *mongo.Collection
}
func NewRecoveryWC(deps Deps) *PurgeWorker {
func NewPurgeWC(deps Deps) *PurgeWorker {
return &PurgeWorker{
logger: deps.Logger,
mongo: deps.Mongo,
@ -54,3 +54,7 @@ func (wc *PurgeWorker) processTasks(ctx context.Context) {
wc.logger.Info("Deleted documents", zap.Int64("count", result.DeletedCount))
}
}
func (wc *PurgeWorker) Stop(ctx context.Context) error {
return nil
}

@ -37,14 +37,13 @@ func NewRecoveryWC(deps Deps) *RecoveryWorker {
}
func (wc *RecoveryWorker) Start(ctx context.Context) {
ticker := time.NewTicker(1 * time.Second)
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
wc.processTasks(ctx)
case <-ctx.Done():
return
}
@ -52,29 +51,39 @@ func (wc *RecoveryWorker) Start(ctx context.Context) {
}
func (wc *RecoveryWorker) processTasks(ctx context.Context) {
result, err := wc.redis.BRPop(ctx, 1*time.Second, "recoveryQueue").Result()
if err != nil {
if err != redis.Nil {
wc.logger.Error("Failed to BRPop from the recovery queue", zap.Error(err))
var cursor uint64
for {
var keys []string
var err error
keys, cursor, err = wc.redis.Scan(ctx, cursor, "email:task:*", 0).Result()
if err != nil {
wc.logger.Error("Failed to scan for email tasks", zap.Error(err))
break
}
return
}
if len(result) < 2 {
wc.logger.Error("Received unexpected number of elements from BRPop", zap.Strings("result", result))
return
}
for _, key := range keys {
taskBytes, err := wc.redis.GetDel(ctx, key).Result()
if err == redis.Nil {
continue
} else if err != nil {
wc.logger.Error("Failed to getdel recovery task", zap.String("key", key), zap.Error(err))
continue
}
var task models.RecoveryRecord
if err = json.Unmarshal([]byte(result[1]), &task); err != nil {
wc.logger.Error("Failed to unmarshal recovery task", zap.String("key", result[0]), zap.Error(err))
return
}
var task models.RecoveryRecord
if json.Unmarshal([]byte(taskBytes), &task) != nil {
wc.logger.Error("Failed to unmarshal recovery task", zap.String("key", key), zap.String("task", taskBytes))
continue
}
err = wc.sendRecoveryTask(ctx, task)
if err != nil {
wc.logger.Error("Failed to send recovery task", zap.String("key", result[0]), zap.Error(err))
return
err = wc.sendRecoveryTask(ctx, task)
if err != nil {
wc.logger.Error("Failed to send recovery task", zap.String("key", key), zap.Error(err))
}
}
if cursor == 0 {
break
}
}
}
@ -114,3 +123,7 @@ func (wc *RecoveryWorker) sendRecoveryTask(ctx context.Context, task models.Reco
//wc.logger.Info("Recovery email sent and restore request updated successfully", zap.String("email", task.Email))
return nil
}
func (wc *RecoveryWorker) Stop(ctx context.Context) error {
return nil
}

37
pkg/closer/closer.go Normal file

@ -0,0 +1,37 @@
package closer
import (
"context"
)
type Closer interface {
Close(ctx context.Context) error
}
type CloserFunc func(ctx context.Context) error
func (cf CloserFunc) Close(ctx context.Context) error {
return cf(ctx)
}
type CloserGroup struct {
closers []Closer
}
func NewCloserGroup() *CloserGroup {
return &CloserGroup{}
}
func (cg *CloserGroup) Add(c Closer) {
cg.closers = append(cg.closers, c)
}
func (cg *CloserGroup) Call(ctx context.Context) error {
var closeErr error
for i := len(cg.closers) - 1; i >= 0; i-- {
if err := cg.closers[i].Close(ctx); err != nil && closeErr == nil {
closeErr = err
}
}
return closeErr
}

@ -1,22 +0,0 @@
package mongo
import (
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type Configuration struct {
MongoHost string `env:"MONGO_HOST" envDefault:"127.0.0.1"`
MongoPort string `env:"MONGO_PORT" envDefault:"27020"`
MongoUser string `env:"MONGO_USER" envDefault:"test"`
MongoPassword string `env:"MONGO_PASSWORD" envDefault:"test"`
MongoDatabase string `env:"MONGO_DB" envDefault:"admin"`
MongoAuth string `env:"MONGO_AUTH" envDefault:"admin"`
}
type RequestSettings struct {
Driver *mongo.Collection
Options *options.FindOptions
Filter primitive.M
}

@ -1,59 +0,0 @@
package mongo
import (
"context"
"fmt"
"log"
"net"
"net/url"
"time"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type ConnectDeps struct {
Configuration *Configuration
Timeout time.Duration
}
func Connect(ctx context.Context, deps *ConnectDeps) (*mongo.Database, error) {
if deps == nil {
return nil, ErrEmptyArgs
}
mongoURI := &url.URL{
Scheme: "mongodb",
Host: net.JoinHostPort(deps.Configuration.MongoHost, deps.Configuration.MongoPort),
}
connectionOptions := options.Client().
ApplyURI(mongoURI.String()).
SetAuth(options.Credential{
AuthMechanism: "SCRAM-SHA-256",
AuthSource: deps.Configuration.MongoAuth,
Username: deps.Configuration.MongoUser,
Password: deps.Configuration.MongoPassword,
})
ticker := time.NewTicker(1 * time.Second)
timeoutExceeded := time.After(deps.Timeout)
defer ticker.Stop()
for {
select {
case <-ticker.C:
connection, err := mongo.Connect(ctx, connectionOptions)
if err == nil {
return connection.Database(deps.Configuration.MongoDatabase), nil
}
log.Printf("failed to connect to db <%s>: %s", mongoURI.String(), err.Error())
case <-timeoutExceeded:
return nil, fmt.Errorf("db connection <%s> failed after %d timeout", mongoURI.String(), deps.Timeout)
default:
time.Sleep(1 * time.Second)
}
}
}

@ -1,7 +0,0 @@
package mongo
import "errors"
var (
ErrEmptyArgs = errors.New("arguments are empty")
)

599
tests/e2e/promo_test.go Normal file

@ -0,0 +1,599 @@
package e2e
import (
"codeword/internal/models"
"codeword/tests/helpers"
"encoding/json"
"fmt"
"github.com/gofiber/fiber/v2"
"github.com/pioz/faker"
"github.com/stretchr/testify/assert"
"go.mongodb.org/mongo-driver/bson/primitive"
"strconv"
"testing"
)
var promoID string
var fastLink string
// CreatePromoCode
func TestCreatePromoCode(t *testing.T) {
client := fiber.AcquireClient()
t.Run("CreatePromoCode-success", func(t *testing.T) {
for i := 0; i < 10; i++ {
jsonString := `{
"codeword": "example",
"description": "Example description",
"greetings": "Example greetings",
"dueTo": 1734429225,
"activationCount": 100,
"bonus": {
"privilege": {
"privilegeID": "examplePrivilegeID",
"amount": 50
},
"discount": {
"layer": 1,
"factor": 0.2,
"target": "exampleTarget",
"threshold": 500
}
},
"outdated": false,
"offLimit": false,
"delete": false
}`
var reqBody models.PromoCode
err := json.Unmarshal([]byte(jsonString), &reqBody)
assert.NoError(t, err)
if i != 0 {
reqBody.Codeword = reqBody.Codeword + faker.String() + strconv.Itoa(i)
}
reqJSON, _ := json.Marshal(reqBody)
req := client.Post(BaseUrl+"/promocode/create").Set("Content-Type", "application/json").Body(reqJSON)
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.NoError(t, errs[0])
}
assert.Equal(t, fiber.StatusCreated, statusCode)
var response models.PromoCode
err = json.Unmarshal(resBody, &response)
assert.NoError(t, err)
promoID = response.ID.Hex()
fmt.Println(response)
}
})
t.Run("CreatePromoCode-duplicate", func(t *testing.T) {
jsonString := `{
"codeword": "example",
"description": "Example description",
"greetings": "Example greetings",
"dueTo": 1734429225,
"activationCount": 100,
"bonus": {
"privilege": {
"privilegeID": "examplePrivilegeID",
"amount": 50
},
"discount": {
"layer": 1,
"factor": 0.2,
"target": "exampleTarget",
"threshold": 500
}
},
"outdated": false,
"offLimit": false,
"delete": false
}`
var reqBody models.PromoCode
err := json.Unmarshal([]byte(jsonString), &reqBody)
assert.NoError(t, err)
reqJSON, _ := json.Marshal(reqBody)
req := client.Post(BaseUrl+"/promocode/create").Set("Content-Type", "application/json").Body(reqJSON)
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.Error(t, errs[0])
}
assert.Equal(t, fiber.StatusBadRequest, statusCode)
var response map[string]interface{}
err = json.Unmarshal(resBody, &response)
assert.NoError(t, err)
fmt.Println(response["error"])
})
t.Run("CreatePromoCode-invalid request payload", func(t *testing.T) {
jsonString := `{
"example": "example",
"description": "Example description",
"greetings": "Example greetings",
"dueTo": 1734429225,
"activationCount": 100,
"bonus": {
"privilege": {
"privilegeID": "examplePrivilegeID",
"amount": 50
},
"discount": {
"layer": 1,
"factor": 0.2,
"target": "exampleTarget",
"threshold": 500
}
},
"outdated": false,
"offLimit": false,
"delete": false
}`
req := client.Post(BaseUrl+"/promocode/create").Set("Content-Type", "application/json").Body([]byte(jsonString))
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.Error(t, errs[0])
}
assert.Equal(t, fiber.StatusBadRequest, statusCode)
var response map[string]interface{}
err := json.Unmarshal(resBody, &response)
assert.NoError(t, err)
fmt.Println(response["error"])
})
t.Run("CreatePromoCode-nil codeword", func(t *testing.T) {
jsonString := `{
"description": "Example description",
"greetings": "Example greetings",
"dueTo": 1734429225,
"activationCount": 100,
"bonus": {
"privilege": {
"privilegeID": "examplePrivilegeID",
"amount": 50
},
"discount": {
"layer": 1,
"factor": 0.2,
"target": "exampleTarget",
"threshold": 500
}
},
"outdated": false,
"offLimit": false,
"delete": false
}`
var reqBody models.PromoCode
err := json.Unmarshal([]byte(jsonString), &reqBody)
assert.NoError(t, err)
reqJSON, _ := json.Marshal(reqBody)
req := client.Post(BaseUrl+"/promocode/create").Set("Content-Type", "application/json").Body(reqJSON)
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.Error(t, errs[0])
}
assert.Equal(t, fiber.StatusBadRequest, statusCode)
var response map[string]interface{}
err = json.Unmarshal(resBody, &response)
assert.NoError(t, err)
fmt.Println(response["error"])
})
}
// EditPromoCode
func TestEditPromoCode(t *testing.T) {
client := fiber.AcquireClient()
t.Run("EditPromoCode-success", func(t *testing.T) {
reqBody := models.ReqEditPromoCode{
ID: promoID,
Description: toString("Updated description"),
Greetings: toString("Updated greetings"),
DueTo: toInt64(1734429225),
ActivationCount: toInt64(150),
Delete: toBool(false),
}
reqJSON, _ := json.Marshal(reqBody)
req := client.Put(BaseUrl+"/promocode/edit").Set("Content-Type", "application/json").Body(reqJSON)
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.NoError(t, errs[0])
}
assert.Equal(t, fiber.StatusOK, statusCode)
var response models.PromoCode
err := json.Unmarshal(resBody, &response)
assert.NoError(t, err)
fmt.Println(response)
})
t.Run("EditPromoCode-success one column", func(t *testing.T) {
reqBody := models.ReqEditPromoCode{
ID: promoID,
Greetings: toString("Updated greetings one"),
}
reqJSON, _ := json.Marshal(reqBody)
req := client.Put(BaseUrl+"/promocode/edit").Set("Content-Type", "application/json").Body(reqJSON)
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.NoError(t, errs[0])
}
assert.Equal(t, fiber.StatusOK, statusCode)
var response models.PromoCode
err := json.Unmarshal(resBody, &response)
assert.NoError(t, err)
fmt.Println(response)
})
t.Run("EditPromoCode-promocod not found", func(t *testing.T) {
reqBody := models.ReqEditPromoCode{
ID: primitive.NewObjectID().Hex(),
Description: toString("Updated description"),
Greetings: toString("Updated greetings"),
DueTo: toInt64(1734429225),
ActivationCount: toInt64(150),
Delete: toBool(false),
}
reqJSON, _ := json.Marshal(reqBody)
req := client.Put(BaseUrl+"/promocode/edit").Set("Content-Type", "application/json").Body(reqJSON)
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.NoError(t, errs[0])
}
assert.Equal(t, fiber.StatusNotFound, statusCode)
var response map[string]interface{}
err := json.Unmarshal(resBody, &response)
assert.NoError(t, err)
fmt.Println(response["error"])
})
t.Run("EditPromoCode-invalid request payload", func(t *testing.T) {
reqBody := map[string]interface{}{
"invalid_field": "example",
"description": "Updated description",
"greetings": "Updated greetings",
"dueTo": 1734429225,
"activationCount": 150,
"delete": false,
}
reqJSON, _ := json.Marshal(reqBody)
req := client.Put(BaseUrl+"/promocode/edit").Set("Content-Type", "application/json").Body(reqJSON)
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.NoError(t, errs[0])
}
assert.Equal(t, fiber.StatusBadRequest, statusCode)
var response map[string]interface{}
err := json.Unmarshal(resBody, &response)
assert.NoError(t, err)
fmt.Println(response["error"])
})
}
func toString(s string) *string {
return &s
}
func toInt64(i int64) *int64 {
return &i
}
func toBool(b bool) *bool {
return &b
}
// CreateFastLink
func TestCreateFastLink(t *testing.T) {
client := fiber.AcquireClient()
t.Run("CreateFastLink-success", func(t *testing.T) {
reqBody := struct {
PromoCodeID string `json:"id"`
}{
PromoCodeID: promoID,
}
reqJSON, _ := json.Marshal(reqBody)
req := client.Post(BaseUrl+"/promocode/fastlink").Set("Content-Type", "application/json").Body(reqJSON)
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.NoError(t, errs[0])
}
fmt.Println(string(resBody))
assert.Equal(t, fiber.StatusCreated, statusCode)
var response map[string]string
err := json.Unmarshal(resBody, &response)
assert.NoError(t, err)
fastLink = response["fastlink"]
fmt.Println(response["fastlink"])
})
t.Run("CreateFastLink-missing promoCodeID", func(t *testing.T) {
req := client.Post(BaseUrl+"/promocode/fastlink").Set("Content-Type", "application/json").Body([]byte(`{}`))
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.Error(t, errs[0])
}
assert.Equal(t, fiber.StatusBadRequest, statusCode)
var response map[string]interface{}
err := json.Unmarshal(resBody, &response)
assert.NoError(t, err)
fmt.Println(response["error"])
})
t.Run("CreateFastLink-promocode not found", func(t *testing.T) {
reqBody := map[string]string{"id": primitive.NewObjectID().Hex()}
reqJSON, _ := json.Marshal(reqBody)
req := client.Post(BaseUrl+"/promocode/fastlink").Set("Content-Type", "application/json").Body(reqJSON)
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.Error(t, errs[0])
}
assert.Equal(t, fiber.StatusNotFound, statusCode)
var response map[string]interface{}
err := json.Unmarshal(resBody, &response)
assert.NoError(t, err)
fmt.Println(response["error"])
})
}
// GetPromoCodesList
func TestGetPromoCodesList(t *testing.T) {
client := fiber.AcquireClient()
t.Run("GetPromoCodesList-success", func(t *testing.T) {
reqBody := models.GetPromoCodesListReq{
Page: 0,
Limit: 10,
Filter: models.GetPromoCodesListReqFilter{
Text: "example",
Active: true,
},
}
reqJSON, _ := json.Marshal(reqBody)
req := client.Post(BaseUrl+"/promocode/getList").Set("Content-Type", "application/json").Body(reqJSON)
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.NoError(t, errs[0])
}
assert.Equal(t, fiber.StatusOK, statusCode)
var response models.GetPromoCodesListResp
err := json.Unmarshal(resBody, &response)
assert.NoError(t, err)
fmt.Println(response)
})
t.Run("GetPromoCodesList-invalid request payload", func(t *testing.T) {
req := client.Post(BaseUrl+"/promocode/getList").Set("Content-Type", "application/json").Body([]byte("invalid json"))
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.NoError(t, errs[0])
}
assert.Equal(t, fiber.StatusBadRequest, statusCode)
var response map[string]interface{}
err := json.Unmarshal(resBody, &response)
assert.NoError(t, err)
fmt.Println(response["error"])
})
}
// ActivatePromoCode
func TestActivatePromoCode(t *testing.T) {
client := fiber.AcquireClient()
jwtUtil := helpers.InitializeJWT()
token, tokenErr := jwtUtil.Create(ExampleUserID)
fmt.Println(token)
if isNoError := assert.NoError(t, tokenErr); !isNoError {
return
}
t.Run("ActivatePromoCode-success codeword", func(t *testing.T) {
reqBody := models.ActivateReq{
Codeword: "example",
}
reqJSON, _ := json.Marshal(reqBody)
req := client.Post(BaseUrl+"/promocode/activate").Set("Content-Type", "application/json").Set("Authorization", "Bearer "+token).Body(reqJSON)
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.NoError(t, errs[0])
}
fmt.Println(string(resBody))
assert.Equal(t, fiber.StatusOK, statusCode)
fmt.Println(statusCode)
var response models.ActivateResp
err := json.Unmarshal(resBody, &response)
assert.NoError(t, err)
fmt.Println(response)
})
t.Run("ActivatePromoCode-success fastLink", func(t *testing.T) {
reqBody := models.ActivateReq{
FastLink: fastLink,
}
reqJSON, _ := json.Marshal(reqBody)
req := client.Post(BaseUrl+"/promocode/activate").Set("Content-Type", "application/json").Set("Authorization", "Bearer "+token).Body(reqJSON)
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.NoError(t, errs[0])
}
assert.Equal(t, fiber.StatusOK, statusCode)
var response models.ActivateResp
err := json.Unmarshal(resBody, &response)
assert.NoError(t, err)
fmt.Println(response)
})
t.Run("ActivatePromoCode-missing userid", func(t *testing.T) {
reqBody := models.ActivateReq{
Codeword: "example",
}
reqJSON, _ := json.Marshal(reqBody)
req := client.Post(BaseUrl+"/promocode/activate").Set("Content-Type", "application/json").Set("Authorization", "Bearer "+token).Body(reqJSON)
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.Error(t, errs[0])
}
assert.Equal(t, fiber.StatusBadRequest, statusCode)
var response map[string]interface{}
err := json.Unmarshal(resBody, &response)
assert.NoError(t, err)
fmt.Println(response["error"])
})
t.Run("ActivatePromoCode-missing codeword and fastlink", func(t *testing.T) {
req := client.Post(BaseUrl+"/promocode/activate").Set("Content-Type", "application/json").Set("Authorization", "Bearer "+token).Body(nil)
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.Error(t, errs[0])
}
assert.Equal(t, fiber.StatusBadRequest, statusCode)
var response map[string]interface{}
err := json.Unmarshal(resBody, &response)
assert.NoError(t, err)
fmt.Println(response["error"])
})
t.Run("ActivatePromoCode-promocode not found", func(t *testing.T) {
reqBody := models.ActivateReq{
Codeword: "none",
}
reqJSON, _ := json.Marshal(reqBody)
req := client.Post(BaseUrl+"/promocode/activate").Set("Content-Type", "application/json").Set("Authorization", "Bearer "+token).Body(reqJSON)
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.Error(t, errs[0])
}
assert.Equal(t, fiber.StatusNotFound, statusCode)
var response map[string]interface{}
err := json.Unmarshal(resBody, &response)
assert.NoError(t, err)
fmt.Println(response["error"])
})
}
// GetPromoStats
func TestGetPromoStats(t *testing.T) {
client := fiber.AcquireClient()
t.Run("GetAllStats", func(t *testing.T) {
reqBody := struct {
PromoCodeID string `json:"id"`
}{
PromoCodeID: promoID,
}
reqJSON, _ := json.Marshal(reqBody)
req := client.Get(BaseUrl+"/promocode/stats").Set("Content-Type", "application/json").Body(reqJSON)
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.NoError(t, errs[0])
}
assert.Equal(t, fiber.StatusOK, statusCode)
var response []models.PromoCodeStats
err := json.Unmarshal(resBody, &response)
assert.NoError(t, err)
fmt.Println(response)
})
}
// DeletePromoCode
func TestDeletePromoCode(t *testing.T) {
client := fiber.AcquireClient()
t.Run("DeletePromoCode-success", func(t *testing.T) {
req := client.Delete(BaseUrl + "/promocode/" + promoID)
statusCode, _, errs := req.Bytes()
if len(errs) != 0 {
assert.NoError(t, errs[0])
}
assert.Equal(t, fiber.StatusOK, statusCode)
})
t.Run("DeletePromoCode-promocode not found", func(t *testing.T) {
req := client.Delete(BaseUrl + "/promocode/" + primitive.NewObjectID().Hex())
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.Error(t, errs[0])
}
assert.Equal(t, fiber.StatusNotFound, statusCode)
var response map[string]interface{}
err := json.Unmarshal(resBody, &response)
assert.NoError(t, err)
fmt.Println(response["error"])
})
}

122
tests/e2e/recover_test.go Normal file

@ -0,0 +1,122 @@
package e2e
import (
"codeword/internal/models"
"encoding/json"
"fmt"
"github.com/gofiber/fiber/v2"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
// todo добавить другие константы такие как exampleUserID
const (
BaseUrl = "http://localhost:8080"
ValidSign = "GSiyv5zBITGshqnvYLHKtXE3e4yZjKGvruOVFWuUuj9Nvaps28-Zt6RDq9n47eaNUlay1-nUVld61I3xoAAgCA==65b286c2f13095d96792079d"
ExampleUserID = "6597babdd1ba7e2dbd32d7e3"
)
// post handler
func TestRecoveryHandler(t *testing.T) {
client := fiber.AcquireClient()
t.Run("HandleRecoveryRequest", func(t *testing.T) {
reqBody := models.RecoveryRequest{
Email: "adminSOLO",
RedirectionURL: "http://redirect.com",
}
reqJSON, _ := json.Marshal(reqBody)
req := client.Post(BaseUrl+"/recover").Set("Content-Type", "application/json").Body(reqJSON)
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.NoError(t, errs[0])
}
assert.Equal(t, fiber.StatusOK, statusCode)
var responseMap map[string]interface{}
err := json.Unmarshal(resBody, &responseMap)
assert.NoError(t, err)
fmt.Println(responseMap)
})
t.Run("HandleRecoveryRequest-AlreadyReported", func(t *testing.T) {
reqBody := models.RecoveryRequest{
Email: "adminSOLO",
RedirectionURL: "http://redirect.com",
}
reqJSON, _ := json.Marshal(reqBody)
req := client.Post(BaseUrl+"/recover").Set("Content-Type", "application/json").Body(reqJSON)
statusCode, _, errs := req.Bytes()
if len(errs) != 0 {
assert.NoError(t, errs[0])
}
assert.Equal(t, fiber.StatusAlreadyReported, statusCode)
})
t.Run("HandleRecoveryRequest_MissingEmail", func(t *testing.T) {
reqBody := models.RecoveryRequest{
RedirectionURL: "http://redirect.com",
}
reqJSON, _ := json.Marshal(reqBody)
req := client.Post(BaseUrl+"/recover").Set("Content-Type", "application/json").Body(reqJSON)
statusCode, _, errs := req.Bytes()
if len(errs) != 0 {
assert.NoError(t, errs[0])
}
assert.Equal(t, fiber.StatusBadRequest, statusCode)
})
t.Run("HandleRecoveryRequest_UserNotFound", func(t *testing.T) {
reqBody := models.RecoveryRequest{
Email: "nonexistent@example.com",
RedirectionURL: "http://redirect.com",
}
reqJSON, _ := json.Marshal(reqBody)
req := client.Post(BaseUrl+"/recover").Set("Content-Type", "application/json").Body(reqJSON)
statusCode, _, errs := req.Bytes()
if len(errs) != 0 {
assert.NoError(t, errs[0])
}
assert.Equal(t, fiber.StatusNotFound, statusCode)
})
}
// get handler
func TestRecoveryLinkHandler(t *testing.T) {
client := fiber.AcquireClient()
t.Run("HandleRecoveryLink_ValidSign", func(t *testing.T) {
req := client.Get(BaseUrl + "/recover/" + ValidSign)
statusCode, _, errs := req.Bytes()
if len(errs) != 0 {
assert.NoError(t, errs[0])
}
assert.Equal(t, fiber.StatusOK, statusCode)
fmt.Println("Recovery link handled successfully")
})
time.Sleep(15 * time.Minute)
t.Run("HandleRecoveryLink_ExpiredSign", func(t *testing.T) {
req := client.Get(BaseUrl + "/recover/" + ValidSign)
statusCode, _, errs := req.Bytes()
if len(errs) != 0 {
assert.NoError(t, errs[0])
}
assert.Equal(t, fiber.StatusNotAcceptable, statusCode)
fmt.Println("Recovery link with expired sign handled correctly")
})
}

38
tests/helpers/jwt.go Normal file

@ -0,0 +1,38 @@
package helpers
import (
"codeword/internal/initialize"
"codeword/utils"
"strings"
)
func InitializeJWT() *utils.JWT {
publicKey := strings.Replace(`-----BEGIN PUBLIC KEY-----
MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgHgnvr7O2tiApjJfid1orFnIGm69
80fZp+Lpbjo+NC/0whMFga2Biw5b1G2Q/B2u0tpO1Fs/E8z7Lv1nYfr5jx2S8x6B
dA4TS2kB9Kf0wn0+7wSlyikHoKhbtzwXHZl17GsyEi6wHnsqNBSauyIWhpha8i+Y
+3GyaOY536H47qyXAgMBAAE=
-----END PUBLIC KEY-----`, "\t", "", -1)
privateKey := strings.Replace(`-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgHgnvr7O2tiApjJfid1orFnIGm6980fZp+Lpbjo+NC/0whMFga2B
iw5b1G2Q/B2u0tpO1Fs/E8z7Lv1nYfr5jx2S8x6BdA4TS2kB9Kf0wn0+7wSlyikH
oKhbtzwXHZl17GsyEi6wHnsqNBSauyIWhpha8i+Y+3GyaOY536H47qyXAgMBAAEC
gYAOphnVPXbk6lpYzdkLC1Xn5EOEuNfOLLURLxBnPWozZo26r/Mtahu/9mYhrYlv
PP8r6mxta3VIil8iOdZyOLa/4d1LPd+UehgEXIJEiYXLtn7RS5eUnoPuQxssfs1k
OWjdN8p6SzppleegFTvGRX4KM3cDLfSphOk8JuBCrpSSYQJBAOdqizTSrdKMTuVe
c7Jk1JOJkyFuFs+N5zeryyeFGH7IpRdWy0rkWMxIUAi8Ap1vYVBPHv4tDOo3sy5X
VLc/knkCQQCE62pg+0TmsrhO/2Pgog6MLBkzlzXYMRp/01HbmznwYF+ejfPnzLkz
hnUlxRUNK3lhXM/7H6oAjvqF2R72u/OPAkEAterkmdbQfEZ+MwNoEiH/lie9OLdx
SSI1VGdBYcTYN7qFRW6eizYstBJYkDU0HQ0Uw+we4hMKJwk4W0KdvxxDiQJAeqlB
V1QqBneBbK10PzVuFV8QtrJhJyxRVwrtbKq38iMNuqUnI4+ijXEUpJFWVvv6nKXo
7McQvEk12dU/JNTX8wJAOlAtSNjp9tVwpMpC0w2St1eKc1L2SknjeohA5ldoBz8sGeZsPhTU3eHSD1neAZXLKN5K68z3zFBr20ubY9nyLw==
-----END RSA PRIVATE KEY-----`, "\t", "", -1)
return utils.NewJWT(&initialize.Config{
PrivateKey: privateKey,
PublicKey: publicKey,
Audience: "pena",
Issuer: "pena-auth-service",
})
}

@ -7,9 +7,11 @@ import (
"fmt"
"github.com/stretchr/testify/require"
"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"
"log"
"strconv"
"testing"
"time"
@ -17,10 +19,9 @@ import (
"github.com/stretchr/testify/assert"
)
// todo add another tests
const mongoURI = "mongodb://test:test@127.0.0.1:27020/?authMechanism=SCRAM-SHA-256&authSource=admin&directConnection=true"
// codeword unit tests
func TestFindByEmail(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
@ -45,14 +46,15 @@ func TestFindByEmail(t *testing.T) {
userRepo := repository.NewUserRepository(repository.Deps{Rdb: nil, Mdb: db.Collection("users")})
t.Run("FindByEmail - existing user", func(t *testing.T) {
user, err := userRepo.FindByEmail(ctx, "email@mail.ru")
user, err := userRepo.FindByEmail(ctx, "admin")
assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, "email@mail.ru", user.Email)
fmt.Println(user.Email)
assert.Equal(t, "admin", user.Login)
})
t.Run("FindByEmail - non-existing user", func(t *testing.T) {
user, err := userRepo.FindByEmail(ctx, "nonexisting@example.com")
user, err := userRepo.FindByEmail(ctx, "neadmin")
assert.NoError(t, err)
assert.Nil(t, user)
})
@ -60,7 +62,8 @@ func TestFindByEmail(t *testing.T) {
}
func TestStoreRecoveryRecord(t *testing.T) {
ctx := context.Background()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
require.NoError(t, err)
@ -93,3 +96,423 @@ func TestStoreRecoveryRecord(t *testing.T) {
_ = database.Drop(ctx)
}
func TestGetRecoveryRecord(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
require.NoError(t, err)
defer func() {
_ = mongoClient.Disconnect(ctx)
}()
database := mongoClient.Database("admin")
codeword := database.Collection("codeword")
_ = codeword.Drop(ctx)
userRepo := repository.NewCodewordRepository(repository.Deps{Rdb: nil, Mdb: codeword})
ID := primitive.NewObjectID()
userID := "6597babdd1ba7e2dbd32d7e3"
email := "test@mail.ru"
key := "test_recovery_key"
record := models.RestoreRequest{
ID: ID,
UserID: userID,
Email: email,
Sign: key,
SignUrl: "def.url",
SignID: key + userID,
CreatedAt: time.Now(),
}
_, err = codeword.InsertOne(ctx, record)
assert.NoError(t, err)
result, err := userRepo.GetRecoveryRecord(ctx, key+userID)
assert.NoError(t, err)
assert.Equal(t, userID, result.UserID)
assert.Equal(t, email, result.Email)
assert.Equal(t, key, result.Sign)
_ = database.Drop(ctx)
}
// promoCode unit tests
func TestInitPromoCodeIndexes(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
require.NoError(t, err)
defer func() {
_ = mongoClient.Disconnect(ctx)
}()
database := mongoClient.Database("admin")
promoCode := database.Collection("promoCode")
err = repository.InitPromoCodeIndexes(ctx, promoCode)
assert.NoError(t, err)
}
func TestCreatePromoCode(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
require.NoError(t, err)
defer func() {
_ = mongoClient.Disconnect(ctx)
}()
database := mongoClient.Database("admin")
promoCode := database.Collection("promoCode")
_ = promoCode.Drop(ctx)
userRepo := repository.NewPromoCodeRepository(promoCode)
err = repository.InitPromoCodeIndexes(ctx, promoCode)
assert.NoError(t, err)
t.Run("CreatePromoCode - success", func(t *testing.T) {
req := &models.PromoCode{
Codeword: "test_codeword",
Description: faker.String(),
Greetings: faker.String(),
DueTo: 1737280065,
ActivationCount: 100,
}
createdPromoCode, err := userRepo.CreatePromoCode(ctx, req)
assert.NoError(t, err)
assert.NotNil(t, createdPromoCode)
assert.Equal(t, "test_codeword", createdPromoCode.Codeword)
})
t.Run("CreatePromoCode - duplicate codeword", func(t *testing.T) {
req := &models.PromoCode{
Codeword: "test_codeword",
Description: faker.String(),
Greetings: faker.String(),
DueTo: 1737280065,
ActivationCount: 100,
}
_, err := userRepo.CreatePromoCode(ctx, req)
assert.Error(t, err)
})
}
func TestEditPromoCode(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
require.NoError(t, err)
defer func() {
_ = mongoClient.Disconnect(ctx)
}()
database := mongoClient.Database("admin")
promoCode := database.Collection("promoCode")
_ = promoCode.Drop(ctx)
userRepo := repository.NewPromoCodeRepository(promoCode)
err = repository.InitPromoCodeIndexes(ctx, promoCode)
assert.NoError(t, err)
req := &models.PromoCode{
Codeword: "test_codeword",
Description: faker.String(),
Greetings: faker.String(),
DueTo: 1737280065,
ActivationCount: 100,
}
createdPromoCode, err := userRepo.CreatePromoCode(ctx, req)
require.NoError(t, err)
newDescription := "New Description"
t.Run("EditPromoCode - success", func(t *testing.T) {
editReq := &models.ReqEditPromoCode{
ID: createdPromoCode.ID.Hex(),
Description: &newDescription,
}
editedPromoCode, err := userRepo.EditPromoCode(ctx, editReq)
assert.NoError(t, err)
assert.NotNil(t, editedPromoCode)
assert.Equal(t, "New Description", editedPromoCode.Description)
})
t.Run("EditPromoCode - promo code not found", func(t *testing.T) {
nonExistingID := primitive.NewObjectID().Hex()
editReq := &models.ReqEditPromoCode{
ID: nonExistingID,
}
_, err := userRepo.EditPromoCode(ctx, editReq)
assert.Error(t, err)
})
}
func TestGetPromoCodeByID(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
require.NoError(t, err)
defer func() {
_ = mongoClient.Disconnect(ctx)
}()
database := mongoClient.Database("admin")
promoCode := database.Collection("promoCode")
_ = promoCode.Drop(ctx)
userRepo := repository.NewPromoCodeRepository(promoCode)
err = repository.InitPromoCodeIndexes(ctx, promoCode)
assert.NoError(t, err)
req := &models.PromoCode{
Codeword: "test_codeword",
Description: faker.String(),
Greetings: faker.String(),
DueTo: 1737280065,
ActivationCount: 100,
}
createdPromoCode, err := userRepo.CreatePromoCode(ctx, req)
require.NoError(t, err)
t.Run("GetPromoCodeByID - success", func(t *testing.T) {
result, err := userRepo.GetPromoCodeByID(ctx, createdPromoCode.ID)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, createdPromoCode.Codeword, result.Codeword)
})
t.Run("GetPromoCodeByID - promo code not found", func(t *testing.T) {
nonExistingID := primitive.NewObjectID()
_, err := userRepo.GetPromoCodeByID(ctx, nonExistingID)
assert.Error(t, err)
})
}
func TestGetPromoCodesList(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
require.NoError(t, err)
defer func() {
_ = mongoClient.Disconnect(ctx)
}()
database := mongoClient.Database("admin")
promoCode := database.Collection("promoCode")
_ = promoCode.Drop(ctx)
userRepo := repository.NewPromoCodeRepository(promoCode)
err = repository.InitPromoCodeIndexes(ctx, promoCode)
assert.NoError(t, err)
for i := 0; i < 1111; i++ {
req := &models.PromoCode{
Codeword: "test" + faker.String() + strconv.Itoa(i),
Description: faker.String(),
Greetings: faker.String(),
DueTo: 1737280065,
ActivationCount: 100,
Delete: faker.Bool(),
Outdated: faker.Bool(),
OffLimit: faker.Bool(),
}
createdPromoCode, err := userRepo.CreatePromoCode(ctx, req)
assert.NoError(t, err)
assert.NotNil(t, createdPromoCode)
}
t.Run("GetPromoCodesList - true", func(t *testing.T) {
filter := models.GetPromoCodesListReqFilter{
Text: "test",
Active: true,
}
req := &models.GetPromoCodesListReq{
Page: 0,
Limit: 10,
Filter: filter,
}
promoCodes, count, err := userRepo.GetPromoCodesList(ctx, req)
assert.NoError(t, err)
assert.NotNil(t, promoCodes)
assert.True(t, count >= 0)
})
t.Run("GetPromoCodesList - false", func(t *testing.T) {
filter := models.GetPromoCodesListReqFilter{
Text: "test",
Active: false,
}
req := &models.GetPromoCodesListReq{
Page: 0,
Limit: 10,
Filter: filter,
}
promoCodes, count, err := userRepo.GetPromoCodesList(ctx, req)
assert.NoError(t, err)
assert.NotNil(t, promoCodes)
assert.True(t, count >= 0)
})
}
func TestActivatePromo(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
require.NoError(t, err)
defer func() {
_ = mongoClient.Disconnect(ctx)
}()
database := mongoClient.Database("admin")
promoCode := database.Collection("promoCode")
_ = promoCode.Drop(ctx)
userRepo := repository.NewPromoCodeRepository(promoCode)
err = repository.InitPromoCodeIndexes(ctx, promoCode)
assert.NoError(t, err)
req := &models.PromoCode{
Codeword: "test_codeword",
Description: faker.String(),
Greetings: faker.String(),
DueTo: 1737280065,
ActivationCount: 100,
}
createdPromoCode, err := userRepo.CreatePromoCode(ctx, req)
assert.NoError(t, err)
assert.NotNil(t, createdPromoCode)
xid := "test_xid"
err = userRepo.AddFastLink(ctx, createdPromoCode.ID, xid)
assert.NoError(t, err)
t.Run("ActivatePromo Codeword - success", func(t *testing.T) {
req := &models.ActivateReq{
UserID: "6597babdd1ba7e2dbd32d7e3",
Codeword: "test_codeword",
}
activatedPromoCode, err := userRepo.ActivatePromo(ctx, req)
assert.NoError(t, err)
assert.NotNil(t, activatedPromoCode)
})
t.Run("ActivatePromo FastLink - success", func(t *testing.T) {
req := &models.ActivateReq{
UserID: "6597babdd1ba7e2dbd32d7e3",
FastLink: "test_xid",
}
activatedPromoCode, err := userRepo.ActivatePromo(ctx, req)
assert.NoError(t, err)
assert.NotNil(t, activatedPromoCode)
})
t.Run("ActivatePromo - promo code not found", func(t *testing.T) {
req := &models.ActivateReq{
UserID: "6597babdd1ba7e2dbd32d7e3",
Codeword: "non_existing_codeword",
}
_, err := userRepo.ActivatePromo(ctx, req)
assert.Error(t, err)
})
}
func TestDeletePromoCode(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
require.NoError(t, err)
defer func() {
_ = mongoClient.Disconnect(ctx)
}()
database := mongoClient.Database("admin")
promoCode := database.Collection("promoCode")
_ = promoCode.Drop(ctx)
userRepo := repository.NewPromoCodeRepository(promoCode)
err = repository.InitPromoCodeIndexes(ctx, promoCode)
assert.NoError(t, err)
req := &models.PromoCode{
Codeword: "test_codeword",
Description: faker.String(),
Greetings: faker.String(),
DueTo: 1737280065,
ActivationCount: 100,
}
createdPromoCode, err := userRepo.CreatePromoCode(ctx, req)
assert.NoError(t, err)
assert.NotNil(t, createdPromoCode)
t.Run("DeletePromoCode - success", func(t *testing.T) {
err := userRepo.DeletePromoCode(ctx, createdPromoCode.ID.Hex())
assert.NoError(t, err)
})
t.Run("DeletePromoCode - promo code not found", func(t *testing.T) {
nonExistingID := primitive.NewObjectID().Hex()
err := userRepo.DeletePromoCode(ctx, nonExistingID)
assert.Error(t, err)
})
}
func TestAddFastLink(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
mongoClient, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
require.NoError(t, err)
defer func() {
_ = mongoClient.Disconnect(ctx)
}()
database := mongoClient.Database("admin")
promoCode := database.Collection("promoCode")
_ = promoCode.Drop(ctx)
userRepo := repository.NewPromoCodeRepository(promoCode)
err = repository.InitPromoCodeIndexes(ctx, promoCode)
assert.NoError(t, err)
req := &models.PromoCode{
Codeword: "test_codeword",
Description: faker.String(),
Greetings: faker.String(),
DueTo: 1737280065,
ActivationCount: 100,
}
createdPromoCode, err := userRepo.CreatePromoCode(ctx, req)
assert.NoError(t, err)
assert.NotNil(t, createdPromoCode)
t.Run("AddFastLink - success", func(t *testing.T) {
xid := "test_xid"
err := userRepo.AddFastLink(ctx, createdPromoCode.ID, xid)
assert.NoError(t, err)
})
t.Run("AddFastLink - promo code not found", func(t *testing.T) {
nonExistingID := primitive.NewObjectID()
xid := "test_xid"
err := userRepo.AddFastLink(ctx, nonExistingID, xid)
assert.Error(t, err)
})
}

46
utils/authenticator.go Normal file

@ -0,0 +1,46 @@
package utils
import (
"codeword/internal/models"
"fmt"
"strings"
"github.com/gofiber/fiber/v2"
)
const (
prefix = "Bearer "
)
func NewAuthenticator(jwtUtil *JWT) func(*fiber.Ctx) error {
return func(c *fiber.Ctx) error {
if jwtUtil == nil {
return fmt.Errorf("jwt util is nil")
}
if err := authenticate(c, jwtUtil); err != nil {
return fmt.Errorf("authentication error:%d", err)
}
return nil
}
}
func authenticate(c *fiber.Ctx, jwtUtil *JWT) error {
authHeader := c.Get("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, prefix) {
return fmt.Errorf("failed to parse jws from request header: %s", authHeader)
}
jws := strings.TrimPrefix(authHeader, prefix)
userID, validateErr := jwtUtil.Validate(jws)
if validateErr != nil {
return validateErr
}
c.Locals(models.AuthJWTDecodedUserIDKey, userID)
c.Locals(models.AuthJWTDecodedAccessTokenKey, jws)
return nil
}

89
utils/jwt.go Normal file

@ -0,0 +1,89 @@
package utils
import (
"codeword/internal/initialize"
"errors"
"fmt"
"github.com/golang-jwt/jwt/v5"
"time"
)
type JWT struct {
privateKey []byte
publicKey []byte
algorithm *jwt.SigningMethodRSA
expiresIn time.Duration
issuer string
audience string
}
func NewJWT(configuration *initialize.Config) *JWT {
return &JWT{
privateKey: []byte(configuration.PrivateKey),
publicKey: []byte(configuration.PublicKey),
issuer: configuration.Issuer,
audience: configuration.Audience,
algorithm: jwt.SigningMethodRS256,
expiresIn: 15 * time.Minute,
}
}
func (j *JWT) Create(id string) (string, error) {
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(j.privateKey)
if err != nil {
return "", fmt.Errorf("failed to parse private key on <Create> of <JWT>: %w", err)
}
now := time.Now().UTC()
claims := jwt.MapClaims{
"id": id,
"exp": now.Add(j.expiresIn).Unix(),
"aud": j.audience,
"iss": j.issuer,
}
token, err := jwt.NewWithClaims(j.algorithm, claims).SignedString(privateKey)
if err != nil {
return "", fmt.Errorf("failed to sing on <Create> of <JWT>: %w", err)
}
return token, nil
}
func (j *JWT) Validate(tokenString string) (string, error) {
key, err := jwt.ParseRSAPublicKeyFromPEM(j.publicKey)
if err != nil {
return "", fmt.Errorf("failed to parse rsa public key on <Validate> of <JWT>: %w", err)
}
parseCallback := func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %s", token.Header["alg"])
}
return key, nil
}
token, err := jwt.Parse(
tokenString,
parseCallback,
jwt.WithAudience(j.audience),
jwt.WithIssuer(j.issuer),
)
if err != nil {
return "", fmt.Errorf("failed to parse jwt token on <Validate> of <JWT>: %w", err)
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
return "", errors.New("token is invalid on <Validate> of <JWT>")
}
data, ok := claims["id"].(string)
if !ok {
return "", errors.New("data is empty or not a string on <Validate> of <JWT>")
}
return data, nil
}