Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

increment 1 #12

Open
wants to merge 91 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
c63aab9
initial project
Alexsatter Dec 4, 2022
64f9d38
initial project
iRootPro Dec 4, 2022
c3fc184
add doc
Alexsatter Dec 8, 2022
1ecf72b
server up
Alexsatter Dec 8, 2022
9e3ade8
server up
Alexsatter Dec 9, 2022
79eba82
fix
Alexsatter Dec 9, 2022
6f7034a
register and login
iRootPro Dec 9, 2022
c55a51c
Merge branch 'master' into increment1
iRootPro Dec 9, 2022
4ea8df8
register and login
iRootPro Dec 9, 2022
e89d1eb
fix
iRootPro Dec 9, 2022
23064bd
change username to login
iRootPro Dec 9, 2022
aac7611
post order
iRootPro Dec 10, 2022
5f160e3
fix lint
iRootPro Dec 10, 2022
dd6d885
fix lint
iRootPro Dec 10, 2022
392e46e
add list orders
iRootPro Dec 11, 2022
14697a5
change status
iRootPro Dec 11, 2022
98d145c
add rows.Err check
iRootPro Dec 11, 2022
e2ce4c6
fix
iRootPro Dec 11, 2022
5672554
fix
iRootPro Dec 11, 2022
f18b068
fix
iRootPro Dec 11, 2022
a9716d1
fix
iRootPro Dec 11, 2022
332399e
add balance endpoint
iRootPro Dec 11, 2022
2f28a08
add err for db.exec
iRootPro Dec 11, 2022
a7f071b
for test
iRootPro Dec 12, 2022
57effc1
fix lint
iRootPro Dec 12, 2022
c6f0329
response
iRootPro Dec 12, 2022
3b246a4
update status
iRootPro Dec 12, 2022
35549f7
update status
iRootPro Dec 12, 2022
3883ba3
update status
iRootPro Dec 12, 2022
54f2a7b
update status
iRootPro Dec 12, 2022
d6742f9
update status
iRootPro Dec 12, 2022
5fcb888
update status
iRootPro Dec 12, 2022
d4e9905
update status
iRootPro Dec 12, 2022
b478f76
update status
iRootPro Dec 12, 2022
2df4671
update status
iRootPro Dec 12, 2022
1a46c2b
update status
iRootPro Dec 12, 2022
658e4f8
update status
iRootPro Dec 12, 2022
a82910e
update status
iRootPro Dec 12, 2022
5613fac
update status
iRootPro Dec 12, 2022
82f2736
update status
iRootPro Dec 12, 2022
3bfbe44
update status
iRootPro Dec 12, 2022
4cc0e92
update status
iRootPro Dec 12, 2022
646382c
fix
iRootPro Dec 12, 2022
794fcf3
fix
iRootPro Dec 12, 2022
9cf2244
fix
iRootPro Dec 12, 2022
2a7c898
fix
iRootPro Dec 12, 2022
d472e9f
fix
iRootPro Dec 12, 2022
164543f
fix
iRootPro Dec 12, 2022
1857396
fix
iRootPro Dec 12, 2022
0aef4ee
fix
iRootPro Dec 12, 2022
15fa685
fix
iRootPro Dec 12, 2022
3959140
fix
iRootPro Dec 12, 2022
6520093
fix
iRootPro Dec 12, 2022
4b8bfa7
fix
iRootPro Dec 12, 2022
61b57ff
fix
iRootPro Dec 12, 2022
fb52592
fix
iRootPro Dec 12, 2022
a8a7e9b
fix
iRootPro Dec 12, 2022
d458e22
fix
iRootPro Dec 12, 2022
8e284d4
fix
iRootPro Dec 12, 2022
c7217de
fix
iRootPro Dec 12, 2022
ca72de9
fix
iRootPro Dec 12, 2022
a236efb
fix
iRootPro Dec 12, 2022
f7ae623
fix
iRootPro Dec 12, 2022
3819d89
fix
iRootPro Dec 12, 2022
6d2dafe
fix
iRootPro Dec 12, 2022
64187ac
fix
iRootPro Dec 12, 2022
2206427
fix
iRootPro Dec 12, 2022
0211403
fix
iRootPro Dec 12, 2022
d051979
fix
iRootPro Dec 12, 2022
4a673f0
fix
iRootPro Dec 12, 2022
eade109
fix
iRootPro Dec 12, 2022
a1c9086
fix
iRootPro Dec 12, 2022
03acc86
fix
iRootPro Dec 12, 2022
927d2bf
fix
iRootPro Dec 12, 2022
5b1633f
fix
iRootPro Dec 12, 2022
988ec65
fix
iRootPro Dec 12, 2022
09f78ca
fix
iRootPro Dec 13, 2022
67d1d0c
fix
iRootPro Dec 13, 2022
83269f9
fix
iRootPro Dec 13, 2022
e01c451
add withdrown endpoint
iRootPro Dec 13, 2022
3573fe8
add withdrown endpoint
iRootPro Dec 13, 2022
f011772
add withdrown endpoint
iRootPro Dec 13, 2022
523ccf1
add list withdown
iRootPro Dec 13, 2022
3245bbb
debug order list
iRootPro Dec 13, 2022
a48bc9b
debug order list
iRootPro Dec 13, 2022
7a34c3f
debug order list
iRootPro Dec 13, 2022
27d17bf
add gracefull shutdown
iRootPro Dec 14, 2022
ee2a2ac
add group for routers
iRootPro Dec 14, 2022
5ef750e
add group for routers
iRootPro Dec 14, 2022
a65e3b8
add group for routers
iRootPro Dec 14, 2022
d9fa306
add group for routers
iRootPro Dec 14, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
build:
go build ./cmd/apiServer

start:
./apiServer
test:
go test -v --race --timeout 30s ./...
50 changes: 49 additions & 1 deletion cmd/gophermart/main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,51 @@
package main

func main() {}
import (
"context"
"github.com/gorilla/sessions"
"github.com/iRootPro/gophermart/extservice/accrual"
"github.com/iRootPro/gophermart/internal/apiserver"
"github.com/iRootPro/gophermart/internal/store/sqlstore"
"log"
"os"
"os/signal"
"time"
)

const sessionKey = "SECRET_KEY"

func main() {
config := apiserver.NewConfig()

store := sqlstore.New()
if err := store.Open(config.DatabaseURI); err != nil {
log.Fatal(err)
}

if err := store.CreateTables(); err != nil {
log.Fatal(err)
}

accrualConfig := accrual.NewAccrualConfig(config.AccrualSystemAddress, config.LogLevel)
accrualService := accrual.NewAccrual(accrualConfig, store)

go accrualService.Run()

sessionsStore := sessions.NewCookieStore([]byte(sessionKey))
s := apiserver.NewAPIServer(config, store, sessionsStore)

go func() {
if err := s.Start(); err != nil {
log.Fatal(err)
}
}()

quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := s.Shutdown(ctx); err != nil {
log.Fatal(err)
}
}
79 changes: 79 additions & 0 deletions doc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Накопительная система лояльности «Гофермарт»

**Система представляет собой HTTP API со следующими требованиями к бизнес-логике:**

+ регистрация, аутентификация и авторизация пользователей;
+ приём номеров заказов от зарегистрированных пользователей;
+ учёт и ведение списка переданных номеров заказов зарегистрированного пользователя;
+ учёт и ведение накопительного счёта зарегистрированного пользователя;
+ проверка принятых номеров заказов через систему расчёта баллов лояльности;
+ начисление за каждый подходящий номер заказа положенного вознаграждения на счёт лояльности пользователя.

**Абстрактная схема взаимодействия с системой**
+ Пользователь регистрируется в системе лояльности «Гофермарт».
+ Пользователь совершает покупку в интернет-магазине «Гофермарт».
+ Заказ попадает в систему расчёта баллов лояльности.
+ Пользователь передаёт номер совершённого заказа в систему лояльности.
+ Система связывает номер заказа с пользователем и сверяет номер с системой расчёта баллов лояльности.
+ При наличии положительного расчёта баллов лояльности производится начисление баллов лояльности на счёт пользователя.
+ Пользователь списывает доступные баллы лояльности для частичной или полной оплаты последующих заказов в интернет-магазине «Гофермарт».

Примечания:
+ пункт 2 представлен как гипотетический и не требует реализации в данной работе;
+ пункт 3 реализован в системе расчёта баллов лояльности и не требует реализации в данной работе.

### Архитектура

Система состоит из следующих компонентов:

![Architecture](images/architecture.png)

**Система расчета баллов лояльности**

Система расчета баллов лояльности является внешним сервисом в доверенном контуре. Он работает по принципу чёрного ящика и недоступен для инспекции внешними клиентами. Система рассчитывает положенные баллы лояльности за совершённый заказ по сложным алгоритмам, которые могут меняться в любой момент времени.

Внешнему потребителю доступна только информация о количестве положенных за конкретный заказ баллов лояльности. Причины наличия или отсутствия начислений внешнему потребителю неизвестны.

#### Сущности:

![Entities](images/entities.png)

#### База данных:

![Database](images/db.png)


### Ошибки

Все ошибки, возникающие в процессе работы системы, должны быть обработаны и корректно обработаны. В случае возникновения ошибки, система должна вернуть соответствующий код ошибки и описание ошибки.

```json
{
"code": 400,
"message": "неверный формат запрпоса"
}
```
Ошибка | Status Code | Описание
:---: |:-----------:| :---:
ErrBadRequest | 400 |неверный формат запрпоса
ErrInternal | 500 | внутренняя ошибка сервера
ErrUserOrPasswordBad | 401 | неверный логин или пароль
ErrUserBusy | 409 | логин уже занят
ErrOrderAlreadyLoaded | 409 | номер заказа уже был загружен другим пользователем;
ErrWrongOrderFormat | 422 | неверный формат заказа;
ErrLowBalance | 402 | недостаточно баллов лояльности для оплаты заказа;
ErrCountRequest | 429 | превышено количество запросов в секунду;

### Конфигурация приложения

Переменная окружения | Флаг командной стройки | Описание
--------------------- |:----------------------:| :--------:
RUN_ADDRESS | -a | Адрес и порт для запуска приложения
DATABASE_URI | -d | URI для подключения к базе данных
ACCRUAL_SYSTEM_ADDRESS| -r | Адрес системы расчёта баллов лояльности

user (-balance, -withdrawn)
order (user_id, order_num, sum, status, type) = operations
balance (user_id, balance) как кэш баланса

Таблица взаимодействия (хендлеры) - диаграмма сервисов
24 changes: 24 additions & 0 deletions doc/diagrams/login.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
@startuml
title
Авторизация пользователя
end title
User -> Handler: POST /api/user/login
note right
{
"login": "<login>",
"password": "<password>"
}
end note
Handler --> User: StatusCode: 400, bad request)
Handler --> User: StatusCode: 500, internal error)
Handler -> Usecases: login(username,password)

Usecases -> Repository: login(username, password)
Repository -> Repository: getPasswordByUser(username)
Repository -> Usecases: password
Usecases -> Usecases: comparePasswords(pass1, pass2)
Usecases --> User: StatusCode: 401
Usecases -> Usecases: setCookie
Usecases --> User: StatusCode: 200, session: cookie

@enduml
29 changes: 29 additions & 0 deletions doc/diagrams/order.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@startuml
'https://plantuml.com/sequence-diagram

title
Загрузка номера заказа
end title
User -> Handler: POST /api/user/orders
note right
12345678903
end note
Handler --> User: StatusCode: 400, bad request)
Handler --> User: StatusCode: 500, internal error)

Handler -> Usecases: checkCookie(cookie)
Usecases --> Handler: ErrUserOrPasswordBad
Handler --> User: StatusCode: 401

Usecases -> Usecases: checkOrder(number)
Usecases -> Handler: ErrWrongOrderFormat
Handler --> User: StatusCode: 422
Usecases -> Repository: loadOrder(num)
Repository -> Repository: checkExistOrderNum(num)
Repository -> Usecases: ErrOrderAlreadyLoaded
Usecases --> Handler: ErrOrderAlreadyLoaded
Handler --> User: StatusCode: 200
Usecases --> External_Service: putOrder(num)
Usecases --> User: StatusCode: 202

@enduml
35 changes: 35 additions & 0 deletions doc/diagrams/register.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@startuml
'https://plantuml.com/sequence-diagram

title
Регистрация пользователя
end title

User -> Handler: POST /api/user/register
note right
{
"login": "<login>",
"password": "<password>"
}
end note
Handler --> User: StatusCode: 500, bad request)

Handler -> Usecases: register(username,password)

Usecases -> Repository: register(username, password)
Repository -> Repository: getUser
Repository --> Usecases: ErrUserBusy

Usecases --> Handler: ErrUserBusy

Handler --> User: StatusCode: 409
Repository -> Repository: createUser
Repository -> Usecases: user_id
Usecases -> UseCases: setCookie()

Usecases -> Handler: user_id

Handler -> User: StatusCode: 200, session: cookie

@enduml

Binary file added doc/images/architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/images/db.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/images/entities.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions doc/register.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
title Регистрация пользователя

participant Client
participant handler
participant usecase
participant repository

Client->handler: POST /api/user/register (register)
note over Client,handler:{\n "login": "<login>",\n "password": "<password>"\n}
handler-->Client: Status Code 500
handler->usecase: register
usecase-->handler: user already exist
handler-->Client:Status Code 409

usecase->repository: register new user (register)
repository->Client: Status Code 200
13 changes: 13 additions & 0 deletions doc/register.zen
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@Starter(Client)
Client->handlers: POST /api/users/register
handlers.register(username, password) {
usecases.register(username, password) {
repository.register(username) {
repository.findOne(username)
if ("user exist") {
return "exist"
}

}
}
}
98 changes: 98 additions & 0 deletions extservice/accrual/accrual.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package accrual

import (
"encoding/json"
"fmt"
"github.com/iRootPro/gophermart/internal/store/sqlstore"
"github.com/sirupsen/logrus"
"io"
"net/http"
"time"
)

type Accrual struct {
Config *Config
logger *logrus.Logger
store *sqlstore.Store
}

type ResponseAccrual struct {
Order string `json:"order"`
Status string `json:"status"`
Accrual float64 `json:"accrual"`
}

func NewAccrual(config *Config, store *sqlstore.Store) *Accrual {
return &Accrual{
Config: config,
logger: logrus.New(),
store: store,
}
}

func (a *Accrual) configureLogger() error {
level, err := logrus.ParseLevel(a.Config.LogLevel)
if err != nil {
return err
}
a.logger.SetLevel(level)
return nil
}

func (a *Accrual) Run() {
a.logger.Infoln("starting accrual service on address: ", a.Config.RunAddress)
for {
orders := a.store.Order().GetOrdersForUpgradeStatus()
if len(orders) == 0 {
continue
}

fmt.Println("ORDERS", orders)

for _, order := range orders {
response := a.GetOrder(order)
err := a.store.Order().UpdateStatus(response.Order, response.Accrual, response.Status)
if err != nil {
a.logger.Errorln("update status", err)
}

if response.Status == "PROCESSED" {
a.logger.Infoln("order: ", response.Order, " status: ", response.Status, " accrual: ", response.Accrual)
userID, err := a.store.Order().FindUserIDByOrder(response.Order)
if err != nil {
a.logger.Errorln("find user_id by order number", err)
}

err = a.store.Balance().UpdateCurrentByUserID(userID, response.Accrual)
if err != nil {
a.logger.Errorln("update balance", err)
}
a.logger.Infoln("update current balance for user_id: ", userID, " accrual: ", response.Accrual)
}

}
time.Sleep(a.Config.PoolingTimeout)
}
}

func (a *Accrual) GetOrder(orderNum string) ResponseAccrual {
resp, err := http.Get(a.Config.RunAddress + "/api/orders/" + orderNum)
if err != nil {
a.logger.Errorln("get /api/orders/{number}", err)
}

resBody, err := io.ReadAll(resp.Body)
if err != nil {
a.logger.Errorln("read from body", err)
}

defer resp.Body.Close()

response := ResponseAccrual{}
err = json.Unmarshal(resBody, &response)
if err != nil {
a.logger.Errorln("unmarshaling response from accrual", err)
}

return response
}
21 changes: 21 additions & 0 deletions extservice/accrual/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package accrual

import (
"github.com/iRootPro/gophermart/internal/store/sqlstore"
"time"
)

type Config struct {
RunAddress string
LogLevel string
PoolingTimeout time.Duration
store *sqlstore.Store
}

func NewAccrualConfig(address string, loglevel string) *Config {
return &Config{
RunAddress: address,
LogLevel: loglevel,
PoolingTimeout: 5 * time.Second,
}
}
Loading