# README
Накопительная система лояльности Gophermart
Система предназначена для расчета бонусных начислений и ведения накопительного бонусного счета пользователя и состоит из следующих сервисов:
- Сервис лояльности
Gophermart
- Сервис начисления баллов лояльности
Accrual/Mockaccrual
Краткий сценарий использования:
- Пользователь регистрируется в системе лояльности
Gophermart
. - Совершая покупки пользователь загружает в систему
Gophermart
номера заказов. - Система
Gophermart
направляет номера заказов в систему начисления баллов лояльностиAccrual
. Accrual
производит расчет баллов лояльности.Accrual
может вернуть ответ без баллов лояльности со статусомPROCESSING
.- Полученные ответы от
Accrual
обрабатываются в зависимости от статуса ответа. - У каждого пользователя ведётся баланс его баллов лояльности.
- Пользователь при наличии баллов лояльности может потратить их на покупки (списав на определенный заказ)
Спецификация проекта Specification
Для взаимодействия с сервисами предоставляется HTTP API.
Используемые технологии:
- PostgreSQL/pgx
- Docker/Docker compose
- Swagger
- Echo
- golang-migrate/migrate
- golang/mock, testify
- go-playground/validator
- avito/go-transaction-manager
Запуск проекта
Запуск возможен в т.ч. с использованием
Скопировать проект git clone
и выполнить команду из корня docker compose up
Для тестирования приложения можно воспользоваться коллекцией postman
запросов
Схема базы данных (в т.ч. скрипт создания бд).
Особенности проекта в т.ч. сложности, возникшие в процессе
Проект прежде всего учебный, поэтому считаю возможным уточнить некоторые решенные нюансы:
-
Система
accrual
была представлена "черным ящиком", внутреннее устройство неизвестно (известны только форматы ответов). Для тестирования системы был написан сервисmockaccrual
, имитирующий работу системыaccrual
. Имитация позволила обнаружить и исправить баги, которые невозможно было обнаружить с помощью встроенных тестов Яндекс Практикума -
Потребовалось отслеживать какие номера заказов отправлялись в
accrual
систему, чтобы избежать отправки дублирующих запросов. Для этого в таблицеorder
были добавлены поля для отслеживания состояния (готовность к отправке вaccrual
и т.д.) Первоначально была идея использовать транзакции для выборки заказов, возможных к отправке (с определенным статусом) и простановка статуса в отдельном запросе. В конечно итоге было принято решение использовать такой запрос к БД:
update gophermart.order
set
accrual_readiness = false,
accrual_started_at = now()
where id in
(select id
from gophermart."order"
where status not in ('INVALID', 'PROCESSED') and accrual_readiness = true
order by uploaded_at desc
for update skip locked
limit 10)
returning
id, number, user_login, uploaded_at, coalesce(accrual, 0), status;
В рамках одного запросы происходит выборка заказов, у которых НЕ окончательный статус и имеется статус true
для возможности отправки в accrual
.
В свою очередь при получении ответа от accrual
происходит обновление заказа в соответствии с полученным статусом:
update gophermart.order
set
accrual = $1,
status = $2,
accrual_readiness =
case
when $2::gophermart.order_status in ('PROCESSED', 'INVALID') then false
else true
end,
accrual_finished_at = now(),
accrual_count = accrual_count + 1
where number = $3;
Т.е. если у заказа статус не являются окончательным - флаг возможности отправки в accrual
снова меняется на true
, таким образом заказ будет повторно обработан.
В итоге gophermart
не отправляет в accrual
уже отправленные заказы.
-
Попался стандартный баг с запуском горутин в цикле и использованием переменной цикла.
-
Для таблицы
balance
первоначально были созданы неименованныеconstraint
:current >= 0
иwithdrawn >= 0
. Однако был обнаружен баг, при котором общая проверкаCheckViolation
одинаково понимала и нарушение для баланса и для списаний. Соответственно указанныеconstraints
были заменены на именованные, при этом добавлена проверка конкретно дляconstraint
баланса:
var e *pgconn.PgError
if errors.As(err, &e) && e.Code == pgerrcode.CheckViolation {
if e.ConstraintName == "not_negative_balance" {
return apperrors.ErrInsufficientFunds
}
}
В свою очередь была добавлена валидация на сумму списаний:
type BalanceWithdrawRequest struct {
OrderNumber string `json:"order" validate:"required"`
Amount decimal.Decimal `json:"sum" validate:"required,positive_withdraw"`
}
func PositiveWithdraw(fl validator.FieldLevel) bool {
data, ok := fl.Field().Interface().(decimal.Decimal)
if !ok {
return false
}
return data.GreaterThan(decimal.Zero)
}
Таким образом запросы с отрицательным списанием в принципе не обрабатываются дальше уровня хендлера.
-
В проекте старался обработать все возможные ошибки и залогировать их (в т.ч. через собственный
caller
, показывающий полный стек трейс вызова ошибки). Убедился, насколько это упростило отладку приложения. -
Весь
sql
код был вынесен в отдельные файлы и использован через механизмembed
. Предполагаю, что в реальности это позволило бы отделить код приложения от кода работа с БД, что упростило бы отладку. Также измененияsql
кода не приводили бы к изменениям файловgo
(при условии сохранения "сигнатур"). -
В проекте использовал триггер для создания записи баланса в таблице
balance
при регистрации пользователя. Мелочь, но приятно (понимаю, что подход зависит от принятых положений в команде). -
Также использовал avito/go-transaction-manager, т.к. необходимо было использовать ряд методов из разных репозиториев в рамках одной транзакции. Цель была такая - не создавать лишний репозиторий для сервиса взаимодействия с
accrual
системой и не дублировать в нем методы работы с БД из других репозиториев. При этом общий смысл транзакций "бизнесовый" обновить заказ и баланс пользователя. Предполагаю, что не стоит выносить эту транзакцию на уровень репозитория и оставить её в слое сервиса. -
Так как в проекте предполагаются финансовые операции был использован пакет shopspring/decimal
TODO
- Интеграционные тесты (testcontainers)
- Godoc