From 498c1a8c31b0dca7e937404342c3e62b1bb58c7c Mon Sep 17 00:00:00 2001 From: oleg Date: Tue, 9 Jan 2024 23:12:57 +0300 Subject: [PATCH] add database --- cmd/gophermart/main.go | 28 ++- go.mod | 8 + go.sum | 26 +++ internal/accrual/accrual.go | 49 ----- internal/handler/accrual/accrual.go | 52 +++++ internal/handler/checkauth.go | 7 +- internal/handler/handler.go | 177 +++++++++-------- internal/model/models.go | 27 +++ internal/repository/postgres/user.go | 282 +++++++++++++++++++++++++++ internal/repository/repository.go | 20 ++ internal/util/jwt.go | 4 +- 11 files changed, 530 insertions(+), 150 deletions(-) delete mode 100644 internal/accrual/accrual.go create mode 100644 internal/handler/accrual/accrual.go create mode 100644 internal/model/models.go create mode 100644 internal/repository/postgres/user.go create mode 100644 internal/repository/repository.go diff --git a/cmd/gophermart/main.go b/cmd/gophermart/main.go index 38acd37..eed23fd 100644 --- a/cmd/gophermart/main.go +++ b/cmd/gophermart/main.go @@ -2,25 +2,28 @@ package main import ( "flag" - "fmt" + "log" "net/http" "os" "github.com/go-chi/chi/v5" + _ "github.com/jackc/pgx/v5/stdlib" - "github.com/OlegVankov/fantastic-engine/internal/accrual" "github.com/OlegVankov/fantastic-engine/internal/handler" + "github.com/OlegVankov/fantastic-engine/internal/handler/accrual" ) -func main() { +var ( + serverAddr string + accrualAddr string + databaseURI string +) - var ( - serverAddr string - accrualAddr string - ) +func main() { flag.StringVar(&serverAddr, "a", "localhost:8080", "адрес и порт запуска сервиса") - flag.StringVar(&accrualAddr, "r", "localhost:34567", "адрес системы расчёта начислений") + flag.StringVar(&accrualAddr, "r", "http://localhost:34567", "адрес системы расчёта начислений") + flag.StringVar(&databaseURI, "d", "", "адрес подключения к базе данных") flag.Parse() @@ -30,6 +33,9 @@ func main() { if envAccrualAddr := os.Getenv("ACCRUAL_SYSTEM_ADDRESS"); envAccrualAddr != "" { accrualAddr = envAccrualAddr } + if envDatabaseURI := os.Getenv("DATABASE_URI"); envDatabaseURI != "" { + databaseURI = envDatabaseURI + } router := chi.NewRouter() @@ -49,8 +55,12 @@ func main() { }) }) + err := handler.SetRepository(databaseURI) + if err != nil { + log.Fatal(err) + } + go accrual.SendAccrual(accrualAddr) - fmt.Println("start server:", serverAddr) http.ListenAndServe(serverAddr, router) } diff --git a/go.mod b/go.mod index 4c67e2d..0e0d161 100644 --- a/go.mod +++ b/go.mod @@ -6,5 +6,13 @@ require ( github.com/go-chi/chi/v5 v5.0.11 // indirect github.com/go-resty/resty/v2 v2.11.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect + github.com/jackc/pgx/v5 v5.5.1 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jmoiron/sqlx v1.3.5 // indirect + golang.org/x/crypto v0.18.0 // indirect golang.org/x/net v0.19.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index 9360743..8bd9c19 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,33 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-resty/resty/v2 v2.11.0 h1:i7jMfNOJYMp69lq7qozJP+bjgzfAzeOhuGlyDrqxT/8= github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI= +github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -21,6 +41,8 @@ golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -40,9 +62,13 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +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/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/accrual/accrual.go b/internal/accrual/accrual.go deleted file mode 100644 index 572d2a5..0000000 --- a/internal/accrual/accrual.go +++ /dev/null @@ -1,49 +0,0 @@ -package accrual - -import ( - "fmt" - "net/http" - "time" - - "github.com/go-resty/resty/v2" - - "github.com/OlegVankov/fantastic-engine/internal/handler" -) - -func SendAccrual(addr string) { - client := resty.New() - url := addr + "/api/orders/" - ball := struct { - Order string `json:"order"` - Status string `json:"status"` - Accrual float64 `json:"accrual"` - }{} - for { - for k := range handler.Orders2 { - resp, err := client.R().SetResult(&ball).Get(url + k) - if err != nil { - fmt.Printf("[ERROR] %s\n", err.Error()) - } - - if resp.StatusCode() == http.StatusOK { - - username := handler.Orders2[ball.Order] - user := handler.Users2[username] - order := handler.Users2[username].Order[ball.Order] - order.Status = ball.Status - - if ball.Status == "PROCESSED" { - order.Accrual = ball.Accrual - user.Balance += ball.Accrual - } - - handler.Users2[username] = user - handler.Users2[username].Order[ball.Order] = order - - } - - } - - <-time.After(time.Second) - } -} diff --git a/internal/handler/accrual/accrual.go b/internal/handler/accrual/accrual.go new file mode 100644 index 0000000..6c4f68d --- /dev/null +++ b/internal/handler/accrual/accrual.go @@ -0,0 +1,52 @@ +package accrual + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/go-resty/resty/v2" + + "github.com/OlegVankov/fantastic-engine/internal/handler" +) + +func SendAccrual(addr string) { + client := resty.New() + url := addr + "/api/orders/" + ball := struct { + Order string `json:"order"` + Status string `json:"status"` + Accrual float64 `json:"accrual"` + }{} + for { + + orders, err := handler.Repository.GetOrders(context.Background()) + if err != nil { + continue + } + + for _, k := range orders { + url := url + k.Number + resp, err := client.R(). + SetResult(&ball). + Get(url) + if err != nil { + fmt.Printf("[ERROR] %s\n", err.Error()) + } + + if resp.StatusCode() == http.StatusOK && ball.Status == "PROCESSED" { + + err := handler.Repository.UpdateOrder(context.Background(), ball.Order, ball.Status, ball.Accrual) + if err != nil { + fmt.Printf("[ERROR] %s\n", err.Error()) + continue + } + + } + + } + + <-time.After(time.Second * time.Duration(5)) + } +} diff --git a/internal/handler/checkauth.go b/internal/handler/checkauth.go index a014ca9..760f099 100644 --- a/internal/handler/checkauth.go +++ b/internal/handler/checkauth.go @@ -1,6 +1,7 @@ package handler import ( + "fmt" "net/http" "strings" @@ -30,11 +31,7 @@ func Auth(h http.Handler) http.Handler { } if !token.Valid { - w.WriteHeader(http.StatusUnauthorized) - return - } - - if _, ok := Users2[userClaim.Username]; !ok { + fmt.Println("token not valid", userClaim.UserID, userClaim.Username) w.WriteHeader(http.StatusUnauthorized) return } diff --git a/internal/handler/handler.go b/internal/handler/handler.go index cf3186b..09b1ef2 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -1,87 +1,98 @@ package handler import ( + "context" "encoding/json" + "errors" "fmt" "io" "net/http" - "sort" - "time" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + + "github.com/OlegVankov/fantastic-engine/internal/repository" + "github.com/OlegVankov/fantastic-engine/internal/repository/postgres" "github.com/OlegVankov/fantastic-engine/internal/util" ) -type User struct { +type credential struct { Login string `json:"login"` Password string `json:"password"` - Token string - Balance float64 - Withdraw float64 - Order map[string]Order -} - -type Order struct { - Number string - Status string - Accrual float64 - Uploaded time.Time } -// [login] -var Users2 = map[string]User{} +var ( + Repository repository.Repository + // = postgres.NewUserRepository("postgresql://postgres:postgres@localhost:5432/gophermart?sslmode=disable") +) -// [login] -var Orders2 = map[string]string{} +func SetRepository(dsn string) error { + Repository = postgres.NewUserRepository(dsn) + return nil +} func Register(w http.ResponseWriter, r *http.Request) { - user := User{} + c := credential{} - err := json.NewDecoder(r.Body).Decode(&user) + err := json.NewDecoder(r.Body).Decode(&c) if err != nil { w.WriteHeader(http.StatusBadRequest) return } - if _, ok := Users2[user.Login]; ok { - w.WriteHeader(http.StatusConflict) + user, err := Repository.AddUser(context.Background(), c.Login, c.Password) + if err != nil { + var e *pgconn.PgError + if errors.As(err, &e) && e.Code == "23505" { + w.WriteHeader(http.StatusConflict) + return + } + fmt.Printf("%v\n", err) + w.WriteHeader(http.StatusInternalServerError) return } - tkn, _ := util.CreateToken(user.Login) + tkn, err := util.CreateToken(user.Login, user.ID) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } authorization := fmt.Sprintf("Bearer %s", tkn) - user.Token = tkn - user.Order = map[string]Order{} - Users2[user.Login] = user - w.Header().Add("Authorization", authorization) w.WriteHeader(http.StatusOK) } func Login(w http.ResponseWriter, r *http.Request) { - user := User{} + c := credential{} - err := json.NewDecoder(r.Body).Decode(&user) + err := json.NewDecoder(r.Body).Decode(&c) if err != nil { w.WriteHeader(http.StatusBadRequest) return } - if Users2[user.Login].Password != user.Password { + user, err := Repository.GetUser(context.Background(), c.Login) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + w.WriteHeader(http.StatusUnauthorized) + return + } + w.WriteHeader(http.StatusInternalServerError) + return + } + + if user.Password != c.Password { w.WriteHeader(http.StatusUnauthorized) return } - tkn, _ := util.CreateToken(user.Login) + tkn, _ := util.CreateToken(user.Login, user.ID) authorization := fmt.Sprintf("Bearer %s", tkn) - user.Token = tkn - user.Order = map[string]Order{} - Users2[user.Login] = user - w.Header().Add("Authorization", authorization) w.WriteHeader(http.StatusOK) } @@ -108,35 +119,45 @@ func Orders(w http.ResponseWriter, r *http.Request) { username := r.Header.Get("username") - if _, ok := Orders2[number]; ok { - if Orders2[number] == username { - w.WriteHeader(http.StatusOK) - return + _, err = Repository.AddOrder(context.Background(), username, number) + if err != nil { + var e *pgconn.PgError + if errors.As(err, &e) && e.Code == "23505" { + order, err := Repository.GetOrderByNumber(context.Background(), number) + // fmt.Printf("username: %s number %s %v\n", username, number, order) + if err == nil { + if order.UserLogin == username { + w.WriteHeader(http.StatusOK) + return + } + w.WriteHeader(http.StatusConflict) + return + } } - w.WriteHeader(http.StatusConflict) + w.WriteHeader(http.StatusInternalServerError) return } - Orders2[number] = username - - Users2[username].Order[number] = Order{Number: number, Status: "NEW", Uploaded: time.Now()} - w.WriteHeader(http.StatusAccepted) } func GetOrders(w http.ResponseWriter, r *http.Request) { username := r.Header.Get("username") - o := []Order{} - for _, order := range Users2[username].Order { - o = append(o, order) + + orders, err := Repository.GetOrdersByLogin(context.Background(), username) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return } - if len(o) == 0 { + + if len(orders) == 0 { w.WriteHeader(http.StatusNoContent) return } + w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(o) + json.NewEncoder(w).Encode(orders) } func Withdraw(w http.ResponseWriter, r *http.Request) { @@ -151,26 +172,21 @@ func Withdraw(w http.ResponseWriter, r *http.Request) { return } - user := Users2[username] - if !util.CheckLun(withdraw.Order) { w.WriteHeader(http.StatusUnprocessableEntity) return } - Orders2[withdraw.Order] = username - - if withdraw.Sum > user.Balance { - w.WriteHeader(http.StatusPaymentRequired) + err = Repository.UpdateWithdraw(context.Background(), username, withdraw.Order, withdraw.Sum) + if err != nil { + if err.Error() == "balance error" { + w.WriteHeader(http.StatusPaymentRequired) + return + } + w.WriteHeader(http.StatusInternalServerError) return } - user.Balance -= withdraw.Sum - user.Withdraw += withdraw.Sum - user.Order[withdraw.Order] = Order{Number: withdraw.Order, Status: "NEW", Uploaded: time.Now()} - - Users2[username] = user - w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusOK) } @@ -178,12 +194,18 @@ func Withdraw(w http.ResponseWriter, r *http.Request) { func Balance(w http.ResponseWriter, r *http.Request) { username := r.Header.Get("username") + user, err := Repository.GetBalance(context.Background(), username) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + balance := struct { Current float64 Withdrawn float64 }{ - Current: Users2[username].Balance, - Withdrawn: Users2[username].Withdraw, + user.Balance, + user.Withdraw, } w.Header().Add("Content-Type", "application/json") @@ -194,34 +216,17 @@ func Balance(w http.ResponseWriter, r *http.Request) { func Withdrawals(w http.ResponseWriter, r *http.Request) { username := r.Header.Get("username") - type Wd struct { - Order string - Sum float64 - Proccessed_At time.Time - uploaded time.Time - } - withdrawals := []Wd{} - - for _, v := range Users2[username].Order { - withdrawals = append(withdrawals, - Wd{ - Order: v.Number, - Sum: Users2[username].Withdraw, - Proccessed_At: time.Now(), - uploaded: v.Uploaded, - }) + wd, err := Repository.GetWithdrawals(r.Context(), username) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return } - - sort.Slice(withdrawals, func(i, j int) bool { - return withdrawals[j].uploaded.Before(withdrawals[i].uploaded) - }) - - if len(withdrawals) == 0 { + if len(wd) == 0 { w.WriteHeader(http.StatusNoContent) return } w.Header().Add("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(withdrawals) + json.NewEncoder(w).Encode(wd) } diff --git a/internal/model/models.go b/internal/model/models.go new file mode 100644 index 0000000..2287039 --- /dev/null +++ b/internal/model/models.go @@ -0,0 +1,27 @@ +package model + +import "time" + +type User struct { + ID uint64 `db:"id" json:"-"` + Login string `db:"login" json:"login"` + Password string `db:"password" json:"-"` + Balance float64 `db:"balance"` + Withdraw float64 `db:"withdraw"` +} + +type Order struct { + Number string + Status string `json:"status"` + Accrual float64 `json:"accrual"` + UserLogin string `json:"-"` + Uploaded time.Time `db:"uploaded" json:"uploaded_at"` +} + +type Withdraw struct { + ID uint64 `db:"id" json:"-"` + Number string `db:"number" json:"order"` + Amount float64 `db:"amount" json:"sum"` + UserLogin string `db:"userlogin" json:"-"` + ProcessedAt time.Time `db:"processed" json:"processed_at"` +} diff --git a/internal/repository/postgres/user.go b/internal/repository/postgres/user.go new file mode 100644 index 0000000..eb8eb47 --- /dev/null +++ b/internal/repository/postgres/user.go @@ -0,0 +1,282 @@ +package postgres + +import ( + "context" + "errors" + "fmt" + "log" + + "github.com/jmoiron/sqlx" + + "github.com/OlegVankov/fantastic-engine/internal/model" +) + +type UserRepository struct { + db *sqlx.DB +} + +func NewUserRepository(dsn string) *UserRepository { + db, _ := sqlx.Open("pgx", dsn) + repo := &UserRepository{ + db: db, + } + err := repo.Bootstrap(context.Background()) + if err != nil { + log.Fatal(err) + } + return repo +} + +func (r *UserRepository) Bootstrap(ctx context.Context) error { + users := `CREATE TABLE IF NOT EXISTS users +( + id bigserial PRIMARY KEY, + login varchar not null, + password varchar not null, + balance decimal(10, 2) default 0, + withdraw decimal(10, 2) default 0, + constraint users_unique_login unique (login) +); +` + orders := `CREATE TABLE IF NOT EXISTS orders +( + number varchar PRIMARY KEY not null, + status varchar, + accrual decimal(10, 2) default 0, + userlogin varchar not null, + uploaded timestamp with time zone not null default now(), + constraint orders_unique_number unique (number) +); +` + withdraw := `CREATE TABLE IF NOT EXISTS withdraw +( + id bigserial PRIMARY KEY, + number varchar not null, + amount decimal(10, 2) default 0, + userlogin varchar not null, + processed timestamp with time zone not null default now() +); +` + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + _, err = r.db.ExecContext(ctx, users) + if err != nil { + return err + } + + _, err = r.db.ExecContext(ctx, orders) + if err != nil { + return err + } + + _, err = r.db.ExecContext(ctx, withdraw) + if err != nil { + return err + } + + return tx.Commit() +} + +func (r *UserRepository) AddUser(ctx context.Context, login, password string) (*model.User, error) { + user := &model.User{} + err := r.db.QueryRowContext(ctx, + "insert into users(login, password) values($1, $2) returning *;", + login, + password, + ).Scan( + &user.ID, + &user.Login, + &user.Password, + &user.Balance, + &user.Withdraw, + ) + if err != nil { + return nil, err + } + return user, nil +} + +func (r *UserRepository) GetUser(ctx context.Context, login string) (*model.User, error) { + user := &model.User{} + err := r.db.QueryRowContext(ctx, + "select * from users where login = $1", + login, + ).Scan( + &user.ID, + &user.Login, + &user.Password, + &user.Balance, + &user.Withdraw, + ) + if err != nil { + return nil, err + } + return user, nil +} + +func (r *UserRepository) AddOrder(ctx context.Context, login, number string) (*model.Order, error) { + order := &model.Order{} + err := r.db.QueryRowContext(ctx, + "insert into orders(number, userlogin, status) values($1, $2, $3) returning *;", + number, + login, + "NEW", + ).Scan( + &order.Number, + &order.Status, + &order.Accrual, + &order.UserLogin, + &order.Uploaded, + ) + if err != nil { + return nil, err + } + return order, nil +} + +func (r *UserRepository) GetOrderByNumber(ctx context.Context, number string) (*model.Order, error) { + order := &model.Order{} + err := r.db.QueryRowContext(ctx, + "select number, userlogin from orders where number = $1", + number, + ).Scan( + &order.Number, + &order.UserLogin, + ) + if err != nil { + return nil, err + } + return order, nil +} + +func (r *UserRepository) UpdateOrder(ctx context.Context, number, status string, accrual float64) error { + order := &model.Order{} + + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + err = tx.QueryRow("update orders set status = $1, accrual = $2 where number = $3 returning number, userlogin", + status, + accrual, + number, + ).Scan( + &order.Number, + &order.UserLogin, + ) + + if err != nil { + return err + } + + _, err = tx.Exec("update users set balance = balance + $1 where login = $2", + accrual, + order.UserLogin, + ) + + if err != nil { + return err + } + + return tx.Commit() +} + +func (r *UserRepository) GetOrdersByLogin(ctx context.Context, username string) ([]model.Order, error) { + orders := []model.Order{} + err := r.db.SelectContext(ctx, &orders, "select * from orders where userlogin = $1", username) + if err != nil { + return nil, err + } + return orders, nil +} + +func (r *UserRepository) GetBalance(ctx context.Context, username string) (*model.User, error) { + user := &model.User{} + err := r.db.QueryRowContext(ctx, + "select balance, withdraw from users where login = $1", + username, + ).Scan( + &user.Balance, + &user.Withdraw, + ) + if err != nil { + return nil, err + } + return user, nil +} + +func (r *UserRepository) GetOrders(ctx context.Context) ([]model.Order, error) { + orders := []model.Order{} + err := r.db.SelectContext(ctx, &orders, "select * from orders") + if err != nil { + return nil, err + } + return orders, nil +} + +func (r *UserRepository) UpdateWithdraw(ctx context.Context, login, number string, sum float64) error { + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + order := &model.Order{} + balance := 0.0 + tx.QueryRow("select balance from users where login = $1", login).Scan(&balance) + + if balance < sum { + return errors.New("balance error") + } + + _, err = tx.Exec("update users set balance = balance - $1, withdraw = withdraw + $1 where login = $2", + sum, + login, + ) + if err != nil { + return err + } + + err = tx.QueryRowContext(ctx, + "insert into orders(number, userlogin, status) values($1, $2, $3) returning *;", + number, + login, + "NEW", + ).Scan( + &order.Number, + &order.Status, + &order.Accrual, + &order.UserLogin, + &order.Uploaded, + ) + if err != nil { + return err + } + + _, err = tx.Exec("insert into withdraw(number, amount, userlogin) values($1, $2, $3);", + number, + sum, + login, + ) + if err != nil { + fmt.Println(err) + return err + } + + return tx.Commit() +} + +func (r *UserRepository) GetWithdrawals(ctx context.Context, login string) ([]model.Withdraw, error) { + withdrawals := []model.Withdraw{} + err := r.db.SelectContext(ctx, &withdrawals, "select * from withdraw where userlogin = $1 order by processed;", login) + if err != nil { + return nil, err + } + return withdrawals, nil +} diff --git a/internal/repository/repository.go b/internal/repository/repository.go new file mode 100644 index 0000000..0062505 --- /dev/null +++ b/internal/repository/repository.go @@ -0,0 +1,20 @@ +package repository + +import ( + "context" + + "github.com/OlegVankov/fantastic-engine/internal/model" +) + +type Repository interface { + AddUser(ctx context.Context, login, password string) (*model.User, error) + GetUser(ctx context.Context, login string) (*model.User, error) + AddOrder(ctx context.Context, login, number string) (*model.Order, error) + GetOrdersByLogin(ctx context.Context, number string) ([]model.Order, error) + GetOrders(ctx context.Context) ([]model.Order, error) + GetOrderByNumber(ctx context.Context, number string) (*model.Order, error) + UpdateOrder(ctx context.Context, number, status string, accrual float64) error + GetBalance(ctx context.Context, username string) (*model.User, error) + UpdateWithdraw(ctx context.Context, login, number string, sum float64) error + GetWithdrawals(ctx context.Context, login string) ([]model.Withdraw, error) +} diff --git a/internal/util/jwt.go b/internal/util/jwt.go index c66752d..b6017f0 100644 --- a/internal/util/jwt.go +++ b/internal/util/jwt.go @@ -9,11 +9,13 @@ import ( type UserClaim struct { jwt.RegisteredClaims Username string + UserID uint64 } -func CreateToken(username string) (string, error) { +func CreateToken(username string, userid uint64) (string, error) { userClaim := &UserClaim{ Username: username, + UserID: userid, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)), },