Skip to content

Commit 99f7d8c

Browse files
authored
feat: user balance and withdrawals API (#3)
* feat(gophermart): add balance API * feat(gophermart): add withdraw API * feat(gophermart): add withdrawals API
1 parent c3ad18c commit 99f7d8c

File tree

15 files changed

+297
-43
lines changed

15 files changed

+297
-43
lines changed

internal/gophermart/bootstrap/bootstrap.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package bootstrap
22

33
import (
4-
"github.com/go-chi/chi/v5"
5-
"go.uber.org/zap"
64
httpServer "net/http"
75
"ya41-56/cmd"
86
"ya41-56/internal/gophermart/customerror"
@@ -15,6 +13,9 @@ import (
1513
"ya41-56/internal/shared/db"
1614
"ya41-56/internal/shared/logger"
1715
"ya41-56/internal/shared/repositories"
16+
17+
"github.com/go-chi/chi/v5"
18+
"go.uber.org/zap"
1819
)
1920

2021
func Run() {
@@ -30,9 +31,10 @@ func Run() {
3031
logger.L().Fatal(customerror.ErrInitDB.Error())
3132
}
3233

34+
orderRepo := repositories.NewGormRepository[models.Order](dbConn)
3335
userRepo := repositories.NewGormRepository[models.User](dbConn)
36+
withdrawalRepo := repositories.NewGormRepository[models.Withdrawal](dbConn)
3437
tokenService := services.NewTokenService(cfg.JWTSecretKey, cfg.JWTLifetime)
35-
orderRepo := repositories.NewGormRepository[models.Order](dbConn)
3638

3739
if cfg.JWTSecretKey == "" {
3840
logger.L().Info(customerror.ErrEmptySecretKey.Error())
@@ -48,13 +50,14 @@ func Run() {
4850
defer fetchPool.Stop() //TODO давай добавим тут шатдаун из accrual
4951

5052
r := router.RegisterRoutes(&di.AppContainer{
51-
UserRepo: userRepo,
52-
OrderRepo: orderRepo,
53-
Auth: services.NewAuthService(userRepo, tokenService),
54-
Router: chi.NewRouter(),
55-
Cfg: cfg,
56-
Gorm: dbConn,
57-
FetchPool: fetchPool,
53+
UserRepo: userRepo,
54+
OrderRepo: orderRepo,
55+
WithdrawalRepo: withdrawalRepo,
56+
Auth: services.NewAuthService(userRepo, tokenService),
57+
Router: chi.NewRouter(),
58+
Cfg: cfg,
59+
Gorm: dbConn,
60+
FetchPool: fetchPool,
5861
})
5962

6063
logger.L().Info("starting HTTP server", zap.String("addr", cfg.Address))

internal/gophermart/customerror/customerror.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ var (
1111
ErrUserExists = errors.New("user already exists")
1212
ErrJWTToken = errors.New("invalid JWT token")
1313
ErrGenerateRandomString = errors.New("failed to generate random string")
14+
15+
ErrNotEnoughFunds = errors.New("not enough funds")
1416
)

internal/gophermart/db/migrate.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
package db
22

33
import (
4-
"gorm.io/gorm"
54
"ya41-56/internal/gophermart/models"
65
"ya41-56/internal/shared/logger"
6+
7+
"gorm.io/gorm"
78
)
89

910
func Migrate(db *gorm.DB) error {
1011
if err := db.AutoMigrate(
1112
models.User{},
1213
models.Order{},
14+
models.Withdrawal{},
1315
); err != nil {
1416
logger.L().Info("auto migration to postgres failed")
1517
return err

internal/gophermart/di/container.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
package di
22

33
import (
4-
"github.com/go-chi/chi/v5"
5-
"gorm.io/gorm"
64
"ya41-56/cmd"
75
"ya41-56/internal/gophermart/models"
86
"ya41-56/internal/gophermart/services"
97
"ya41-56/internal/gophermart/worker"
108
"ya41-56/internal/shared/repositories"
9+
10+
"github.com/go-chi/chi/v5"
11+
"gorm.io/gorm"
1112
)
1213

1314
type AppContainer struct {
14-
UserRepo repositories.Repository[models.User]
15-
OrderRepo repositories.Repository[models.Order]
16-
Auth *services.AuthService
17-
Router chi.Router
18-
Cfg cmd.Config
19-
FetchPool *worker.FetchPool
20-
Gorm *gorm.DB
15+
UserRepo repositories.Repository[models.User]
16+
OrderRepo repositories.Repository[models.Order]
17+
WithdrawalRepo repositories.Repository[models.Withdrawal]
18+
Auth *services.AuthService
19+
Router chi.Router
20+
Cfg cmd.Config
21+
FetchPool *worker.FetchPool
22+
Gorm *gorm.DB
2123
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package handlers
2+
3+
import (
4+
"net/http"
5+
"strings"
6+
"ya41-56/internal/gophermart/customerror"
7+
"ya41-56/internal/gophermart/models"
8+
"ya41-56/internal/shared/contextutil"
9+
"ya41-56/internal/shared/customstrings"
10+
"ya41-56/internal/shared/httputil"
11+
"ya41-56/internal/shared/logger"
12+
"ya41-56/internal/shared/luhn"
13+
"ya41-56/internal/shared/repositories"
14+
"ya41-56/internal/shared/response"
15+
16+
"go.uber.org/zap"
17+
)
18+
19+
type BalanceHandler struct {
20+
Orders repositories.Repository[models.Order]
21+
Withdrawal repositories.Repository[models.Withdrawal]
22+
}
23+
24+
func NewBalanceHandler(orderRepo repositories.Repository[models.Order], withdrawalRepo repositories.Repository[models.Withdrawal]) *BalanceHandler {
25+
return &BalanceHandler{
26+
Orders: orderRepo,
27+
Withdrawal: withdrawalRepo,
28+
}
29+
}
30+
31+
// Withdraw
32+
33+
type withdrawRequest struct {
34+
Order string `json:"order"`
35+
Sum float64 `json:"sum"`
36+
}
37+
38+
// TODO: accumulate the results of calculations (use Balance model)
39+
func (h *BalanceHandler) Withdraw(w http.ResponseWriter, r *http.Request) {
40+
var req withdrawRequest
41+
42+
userIDStr, ok := contextutil.GetUserID(r.Context())
43+
if !ok {
44+
response.Error(w, http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized))
45+
return
46+
}
47+
48+
if err := httputil.ParseJSON(r, &req); err != nil {
49+
response.Error(w, http.StatusBadRequest, http.StatusText(http.StatusBadRequest))
50+
return
51+
}
52+
53+
if req.Sum <= 0 {
54+
response.Error(w, http.StatusBadRequest, http.StatusText(http.StatusBadRequest))
55+
logger.L().Error("bad value", zap.Float64("sum", req.Sum))
56+
return
57+
}
58+
59+
orders, err := h.Orders.FindManyByField(r.Context(), "user_id", customstrings.ParseID(userIDStr))
60+
if err != nil {
61+
response.Error(w, http.StatusInternalServerError, err.Error())
62+
return
63+
}
64+
65+
withdrawals, err := h.Withdrawal.FindManyByField(r.Context(), "user_id", customstrings.ParseID(userIDStr))
66+
if err != nil {
67+
response.Error(w, http.StatusInternalServerError, err.Error())
68+
return
69+
}
70+
71+
sumOfAccruals := float64(0.0)
72+
for _, order := range orders {
73+
if order.Status == models.OrderStatusProcessed {
74+
sumOfAccruals += float64(order.Accrual)
75+
}
76+
}
77+
78+
sumOfWithdrawals := float64(0.0)
79+
for _, withdrawal := range withdrawals {
80+
sumOfWithdrawals += float64(withdrawal.Value)
81+
}
82+
83+
if req.Sum > sumOfAccruals-sumOfWithdrawals {
84+
response.Error(w, http.StatusPaymentRequired, customerror.ErrNotEnoughFunds.Error())
85+
return
86+
}
87+
88+
number := strings.TrimSpace(req.Order)
89+
if !luhn.IsValidLuhn(number) {
90+
response.Error(w, http.StatusUnprocessableEntity, http.StatusText(http.StatusUnprocessableEntity))
91+
return
92+
}
93+
94+
withdrawal := &models.Withdrawal{
95+
UserID: customstrings.ParseID(userIDStr),
96+
Order: number,
97+
Value: float64(req.Sum),
98+
}
99+
100+
err = h.Withdrawal.Create(r.Context(), withdrawal)
101+
if err != nil {
102+
response.JSON(w, http.StatusInternalServerError, err.Error())
103+
return
104+
}
105+
106+
// TODO: Commit the transaction
107+
108+
response.JSON(w, http.StatusOK, nil)
109+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package handlers
2+
3+
import "testing"
4+
5+
func TestWithdraw(t *testing.T) {
6+
t.Logf("TODO")
7+
}

internal/gophermart/handlers/orders.go

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ package handlers
22

33
import (
44
"errors"
5-
"fmt"
65
"gorm.io/gorm"
76
"io"
87
"net/http"
98
"strings"
109
"ya41-56/internal/gophermart/models"
1110
"ya41-56/internal/gophermart/worker"
1211
"ya41-56/internal/shared/contextutil"
12+
"ya41-56/internal/shared/customstrings"
1313
"ya41-56/internal/shared/luhn"
1414
"ya41-56/internal/shared/repositories"
1515
"ya41-56/internal/shared/response"
@@ -52,7 +52,7 @@ func (h *OrdersHandler) Upload(w http.ResponseWriter, r *http.Request) {
5252
}
5353
}
5454
if existed.ID > 0 {
55-
if existed.UserID == parseID(userIDStr) {
55+
if existed.UserID == customstrings.ParseID(userIDStr) {
5656
response.Error(w, http.StatusOK, "order already exists")
5757
return
5858
}
@@ -61,7 +61,7 @@ func (h *OrdersHandler) Upload(w http.ResponseWriter, r *http.Request) {
6161
}
6262

6363
order := &models.Order{
64-
UserID: parseID(userIDStr),
64+
UserID: customstrings.ParseID(userIDStr),
6565
Number: number,
6666
Status: models.OrderStatusNew,
6767
}
@@ -83,7 +83,7 @@ func (h *OrdersHandler) List(w http.ResponseWriter, r *http.Request) {
8383
return
8484
}
8585

86-
orders, err := h.Orders.FindManyByField(r.Context(), "user_id", parseID(userIDStr))
86+
orders, err := h.Orders.FindManyByField(r.Context(), "user_id", customstrings.ParseID(userIDStr))
8787
if err != nil {
8888
response.Error(w, http.StatusInternalServerError, err.Error())
8989
return
@@ -96,12 +96,3 @@ func (h *OrdersHandler) List(w http.ResponseWriter, r *http.Request) {
9696

9797
response.JSON(w, http.StatusOK, orders)
9898
}
99-
100-
func parseID(id string) uint {
101-
var uid uint
102-
_, err := fmt.Sscanf(id, "%d", &uid)
103-
if err != nil {
104-
return 0
105-
}
106-
return uid
107-
}

0 commit comments

Comments
 (0)