diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..56cc0e8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + push: + branches-ignore: + - master + pull_request: + branches: + - master + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.25" + - uses: golangci/golangci-lint-action@v6 + with: + version: latest + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.25" + - name: Test + run: go test ./tests/... diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..d74f350 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,12 @@ +version: "2" + +linters: + default: none + enable: + - govet + - staticcheck + - unused + exclusions: + rules: + - linters: [staticcheck] + text: "SA1029" diff --git a/cmd/main.go b/cmd/main.go index 847ebb6..7766d65 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,7 +2,9 @@ package main import ( "context" + "errors" "log/slog" + "net/http" "os" "os/signal" "syscall" @@ -54,7 +56,11 @@ func main() { slog.ErrorContext(ctx, "Ошибка подключения к redis", slog.String("error", err.Error())) os.Exit(1) } - defer redis.Close() + defer func() { + if err := redis.Close(); err != nil { + slog.Warn("redis close error", slog.String("error", err.Error())) + } + }() if err := database.RunMigrations(ctx, cfg.DatabaseURL()); err != nil { slog.ErrorContext(ctx, "Ошибка миграций", slog.String("error", err.Error())) @@ -78,5 +84,9 @@ func main() { } cancel() + if err := <-serverErr; err != nil && !errors.Is(err, http.ErrServerClosed) { + slog.Error("Ошибка при остановке сервера", "error", err.Error()) + } + slog.Info("Приложение остановлено") } diff --git a/internal/database/postgres.go b/internal/database/postgres.go index f0f75ac..7b1456e 100644 --- a/internal/database/postgres.go +++ b/internal/database/postgres.go @@ -37,9 +37,11 @@ func RunMigrations(ctx context.Context, dbURL string) error { if err != nil { return fmt.Errorf("не удалось открыть соединение для миграций: %w", err) } - defer db.Close() + defer db.Close() //nolint:errcheck - goose.SetDialect(gooseDriverName) + if err := goose.SetDialect(gooseDriverName); err != nil { + return fmt.Errorf("goose dialect: %w", err) + } statusErr := goose.Status(db, migrationsDir) if statusErr != nil { diff --git a/internal/handler/driver.go b/internal/handler/driver.go index 3d8dc50..06eaa82 100644 --- a/internal/handler/driver.go +++ b/internal/handler/driver.go @@ -13,6 +13,7 @@ import ( "github.com/google/uuid" "github.com/gosimple/slug" "github.com/huandu/go-sqlbuilder" + "github.com/jackc/pgx/v5" "golang.org/x/crypto/bcrypt" ) @@ -34,7 +35,12 @@ func (s *Server) ListDrivers(w http.ResponseWriter, r *http.Request, params api. func (s *Server) CreateDriver(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - claims := ctx.Value("user").(*Claims) + claims, ok := ctx.Value(UserKey).(*Claims) + if !ok { + slog.ErrorContext(ctx, "Error while casting claims") + s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError) + return + } if claims.Role != "admin" { slog.WarnContext(ctx, "unusual try from not allowed role", slog.String("Role", claims.Role), slog.String("id", claims.ID.String())) s.JSON(w, r, http.StatusForbidden, MsgForbidden, RespError) @@ -72,7 +78,11 @@ func (s *Server) CreateDriver(w http.ResponseWriter, r *http.Request) { s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError) return } - defer tx.Rollback(ctx) + defer func() { + if err := tx.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.ErrorContext(ctx, "tx rollback failed", slog.String("error", err.Error())) + } + }() if err := storage.Create(ctx, "users", user, tx); err != nil { slog.ErrorContext(ctx, "Unable to create user", slog.String("error", err.Error()), slog.Any("user", user)) @@ -111,9 +121,9 @@ func (s *Server) CreateDriver(w http.ResponseWriter, r *http.Request) { func (s *Server) UpdateMyDriverStatus(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - claims, ok := ctx.Value("user").(*Claims) + claims, ok := ctx.Value(UserKey).(*Claims) if !ok || claims == nil { - slog.ErrorContext(ctx, "Unable to convert claims", slog.Any("claims", ctx.Value("user"))) + slog.ErrorContext(ctx, "Unable to convert claims", slog.Any("claims", ctx.Value(UserKey))) s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError) return } diff --git a/internal/handler/hub.go b/internal/handler/hub.go index c497946..3c761c0 100644 --- a/internal/handler/hub.go +++ b/internal/handler/hub.go @@ -2,6 +2,7 @@ package handler import ( "context" + "log/slog" "sync" "github.com/google/uuid" @@ -43,7 +44,9 @@ func (h *Hub) Broadcast(orderID uuid.UUID, msg any) { h.mu.RLock() defer h.mu.RUnlock() for _, conn := range h.connections[orderID] { - conn.WriteJSON(msg) + if err := conn.WriteJSON(msg); err != nil { + slog.Error("error while writing json to clients", slog.String("error", err.Error())) + } } } diff --git a/internal/handler/manager.go b/internal/handler/manager.go index ef1c3fa..9cfcffe 100644 --- a/internal/handler/manager.go +++ b/internal/handler/manager.go @@ -12,6 +12,7 @@ import ( storage "github.com/anxi0uz/logiflow/pkg" "github.com/google/uuid" "github.com/huandu/go-sqlbuilder" + "github.com/jackc/pgx/v5" "golang.org/x/crypto/bcrypt" ) @@ -65,12 +66,18 @@ func (s *Server) CreateManager(w http.ResponseWriter, r *http.Request) { s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError) return } - defer tx.Rollback(ctx) + defer func() { + if err := tx.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.ErrorContext(ctx, "tx rollback failed", slog.String("error", err.Error())) + } + }() if err := storage.Create(ctx, "users", userModel, tx); err != nil { slog.ErrorContext(ctx, "Error while creating user", slog.String("error", err.Error())) s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError) - tx.Rollback(ctx) + if err := tx.Rollback(ctx); err != nil { + slog.ErrorContext(ctx, "tx rollback failed", slog.String("error", err.Error())) + } return } @@ -84,7 +91,9 @@ func (s *Server) CreateManager(w http.ResponseWriter, r *http.Request) { if err := storage.Create(ctx, "managers", managerModel, tx); err != nil { slog.ErrorContext(ctx, "Error while creating manager", slog.String("error", err.Error())) s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError) - tx.Rollback(ctx) + if err := tx.Rollback(ctx); err != nil { + slog.ErrorContext(ctx, "tx rollback failed", slog.String("error", err.Error())) + } return } diff --git a/internal/handler/notification.go b/internal/handler/notification.go index 48a9592..b7ca5c9 100644 --- a/internal/handler/notification.go +++ b/internal/handler/notification.go @@ -13,7 +13,7 @@ import ( func (s *Server) ListNotifications(w http.ResponseWriter, r *http.Request, params api.ListNotificationsParams) { ctx := r.Context() - claims, ok := ctx.Value("user").(*Claims) + claims, ok := ctx.Value(UserKey).(*Claims) if !ok { slog.ErrorContext(ctx, "Error while casting claims") s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError) @@ -36,7 +36,7 @@ func (s *Server) ListNotifications(w http.ResponseWriter, r *http.Request, param func (s *Server) MarkNotificationRead(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) { ctx := r.Context() - claims, ok := ctx.Value("user").(*Claims) + claims, ok := ctx.Value(UserKey).(*Claims) if !ok { slog.ErrorContext(ctx, "Error while casting claims") s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError) @@ -51,6 +51,10 @@ func (s *Server) MarkNotificationRead(w http.ResponseWriter, r *http.Request, id s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError) return } + if notification == nil { + s.JSON(w, r, http.StatusNotFound, MsgNotFound, RespNotFound) + return + } if notification.UserID != claims.ID { s.JSON(w, r, http.StatusForbidden, MsgForbidden, RespError) return diff --git a/internal/handler/order.go b/internal/handler/order.go index cb4e71c..7e0e90f 100644 --- a/internal/handler/order.go +++ b/internal/handler/order.go @@ -15,7 +15,7 @@ import ( func (s *Server) ListOrders(w http.ResponseWriter, r *http.Request, params api.ListOrdersParams) { ctx := r.Context() - claims, ok := ctx.Value("user").(*Claims) + claims, ok := ctx.Value(UserKey).(*Claims) if !ok { slog.ErrorContext(ctx, "Error while casting claims") s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError) @@ -32,7 +32,7 @@ func (s *Server) ListOrders(w http.ResponseWriter, r *http.Request, params api.L func (s *Server) CreateOrder(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - claims, ok := ctx.Value("user").(*Claims) + claims, ok := ctx.Value(UserKey).(*Claims) if !ok { slog.ErrorContext(ctx, "error while casting claims") s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError) @@ -60,7 +60,7 @@ func (s *Server) CreateOrder(w http.ResponseWriter, r *http.Request) { func (s *Server) GetOrder(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) { ctx := r.Context() - claims, ok := ctx.Value("user").(*Claims) + claims, ok := ctx.Value(UserKey).(*Claims) if !ok { slog.ErrorContext(ctx, "Error while casting claims") s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError) @@ -86,7 +86,7 @@ func (s *Server) GetOrder(w http.ResponseWriter, r *http.Request, id openapi_typ func (s *Server) CancelOrder(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) { ctx := r.Context() - claims, ok := ctx.Value("user").(*Claims) + claims, ok := ctx.Value(UserKey).(*Claims) if !ok { slog.ErrorContext(ctx, "Error while casting claims") s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError) @@ -111,7 +111,7 @@ func (s *Server) CancelOrder(w http.ResponseWriter, r *http.Request, id openapi_ func (s *Server) UpdateOrderStatus(w http.ResponseWriter, r *http.Request, id openapi_types.UUID) { ctx := r.Context() - claims, ok := ctx.Value("user").(*Claims) + claims, ok := ctx.Value(UserKey).(*Claims) if !ok { slog.ErrorContext(ctx, "Error while casting claims") s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError) @@ -123,6 +123,7 @@ func (s *Server) UpdateOrderStatus(w http.ResponseWriter, r *http.Request, id op s.JSON(w, r, http.StatusBadRequest, MsgInvalidBody, RespError) return } + order, err := s.OrderSerice.UpdateOrderStatus(ctx, id, claims.ID, claims.Role, req) if err != nil { switch { @@ -136,15 +137,17 @@ func (s *Server) UpdateOrderStatus(w http.ResponseWriter, r *http.Request, id op } return } + if req.Status == api.OrderStatusUpdateStatusInTransit { go s.startRouteTracker(id) } + s.JSON(w, r, http.StatusOK, order, RespSuccess) } func (s *Server) GetOrdersReport(w http.ResponseWriter, r *http.Request, params api.GetOrdersReportParams) { ctx := r.Context() - claims, ok := ctx.Value("user").(*Claims) + claims, ok := ctx.Value(UserKey).(*Claims) if !ok { slog.ErrorContext(ctx, "Error while casting claims") s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError) @@ -164,12 +167,12 @@ func (s *Server) GetOrdersReport(w http.ResponseWriter, r *http.Request, params f := excelize.NewFile() sheet := "Orders" - f.SetSheetName("Sheet1", sheet) + f.SetSheetName("Sheet1", sheet) //nolint:errcheck headers := []string{"ID", "Status", "Origin", "Destination", "Weight", "Volume", "Price", "Created At"} for i, h := range headers { cell, _ := excelize.CoordinatesToCellName(i+1, 1) - f.SetCellValue(sheet, cell, h) + f.SetCellValue(sheet, cell, h) //nolint:errcheck } for row, o := range orders { @@ -185,7 +188,7 @@ func (s *Server) GetOrdersReport(w http.ResponseWriter, r *http.Request, params } for col, v := range values { cell, _ := excelize.CoordinatesToCellName(col+1, row+2) - f.SetCellValue(sheet, cell, v) + f.SetCellValue(sheet, cell, v) //nolint:errcheck } } buf, err := f.WriteToBuffer() @@ -198,12 +201,12 @@ func (s *Server) GetOrdersReport(w http.ResponseWriter, r *http.Request, params w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") w.Header().Set("Content-Disposition", "attachment; filename=orders_report.xlsx") w.WriteHeader(http.StatusOK) - w.Write(buf.Bytes()) + w.Write(buf.Bytes()) //nolint:errcheck } func (s *Server) GetDashboard(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - claims, ok := ctx.Value("user").(*Claims) + claims, ok := ctx.Value(UserKey).(*Claims) if !ok { slog.ErrorContext(ctx, "error while casting claims") s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError) diff --git a/internal/handler/route.go b/internal/handler/route.go index bb3137a..051f0ca 100644 --- a/internal/handler/route.go +++ b/internal/handler/route.go @@ -39,7 +39,11 @@ func (s *Server) RouteWebSocket(w http.ResponseWriter, r *http.Request, id opena s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError) return } - defer conn.Close() + defer func() { + if err := conn.Close(); err != nil { + slog.Warn("ws conn close error", slog.String("error", err.Error())) + } + }() orderID := id s.Hub.Register(id, conn) @@ -58,10 +62,12 @@ func (s *Server) RouteWebSocket(w http.ResponseWriter, r *http.Request, id opena if err == nil { coords, err := route.ParseCoordinates() if err == nil && route.CurrentIndex < len(coords) { - conn.WriteJSON(map[string]any{ + if err := conn.WriteJSON(map[string]any{ "current_index": route.CurrentIndex, "coordinate": coords[route.CurrentIndex], - }) + }); err != nil { + slog.ErrorContext(ctx, "error while writing date to clients", slog.String("error", err.Error())) + } } } @@ -102,9 +108,11 @@ func (s *Server) startRouteTracker(orderID uuid.UUID) { "coordinate": coords[route.CurrentIndex], }) route.CurrentIndex++ - storage.Update(ctx, "routes", *route, s.DB, func(sb *sqlbuilder.UpdateBuilder) { + if err := storage.Update(ctx, "routes", *route, s.DB, func(sb *sqlbuilder.UpdateBuilder) { sb.Where(sb.EQ("order_id", orderID)) - }) + }); err != nil { + slog.ErrorContext(ctx, "error while updating route", slog.String("id", route.ID.String()), slog.String("error", err.Error())) + } } } }) diff --git a/internal/handler/server_impl.go b/internal/handler/server_impl.go index 304bc3c..5d6ec18 100644 --- a/internal/handler/server_impl.go +++ b/internal/handler/server_impl.go @@ -28,6 +28,7 @@ type ctxKey string const ( requestIDKey ctxKey = "X-Request-ID" tokenKey ctxKey = "Authorization" + UserKey ctxKey = "user" // response messages MsgInternalError = "Internal server error" @@ -145,7 +146,7 @@ func (s *Server) AuthMiddleware(next http.Handler) http.Handler { return } - ctx := context.WithValue(r.Context(), "user", claims) + ctx := context.WithValue(r.Context(), UserKey, claims) next.ServeHTTP(w, r.WithContext(ctx)) }) } diff --git a/internal/handler/user.go b/internal/handler/user.go index ad74732..4cf8779 100644 --- a/internal/handler/user.go +++ b/internal/handler/user.go @@ -16,6 +16,7 @@ import ( "github.com/google/uuid" "github.com/gosimple/slug" "github.com/huandu/go-sqlbuilder" + "github.com/jackc/pgx/v5" "github.com/redis/go-redis/v9" "golang.org/x/crypto/bcrypt" ) @@ -54,9 +55,13 @@ func (s *Server) AuthLogin(w http.ResponseWriter, r *http.Request) { now := time.Now() user.LastLoginAt = &now user.UpdatedAt = now - err = storage.Update(ctx, "users", user, s.DB, func(sb *sqlbuilder.UpdateBuilder) { + if err := storage.Update(ctx, "users", user, s.DB, func(sb *sqlbuilder.UpdateBuilder) { sb.Where(sb.Equal("id", user.ID)) - }) + }); err != nil { + slog.ErrorContext(ctx, "Failed to update user", slog.String("error", err.Error())) + s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError) + return + } s.issueTokens(w, r, user) } @@ -93,6 +98,8 @@ func (s *Server) AuthLogout(w http.ResponseWriter, r *http.Request) { s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError) return } + s.deleteRefreshCookie(w) + s.JSON(w, r, http.StatusOK, "Logged out", RespSuccess) } func (s *Server) AuthRefresh(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -113,7 +120,7 @@ func (s *Server) AuthRefresh(w http.ResponseWriter, r *http.Request) { return } - claimsValue := ctx.Value("user") + claimsValue := ctx.Value(UserKey) claims, ok := claimsValue.(*Claims) @@ -181,7 +188,7 @@ func (s *Server) AuthRegister(w http.ResponseWriter, r *http.Request) { func (s *Server) DeleteMe(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - claimsValue := ctx.Value("user") + claimsValue := ctx.Value(UserKey) claims, ok := claimsValue.(*Claims) if !ok { slog.ErrorContext(ctx, "Error while converting claims", slog.Any("claims", claimsValue)) @@ -195,7 +202,11 @@ func (s *Server) DeleteMe(w http.ResponseWriter, r *http.Request) { s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError) return } - defer tx.Rollback(ctx) + defer func() { + if err := tx.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.ErrorContext(ctx, "tx rollback failed", slog.String("error", err.Error())) + } + }() err = storage.Delete[models.User](ctx, "users", tx, func(sb *sqlbuilder.DeleteBuilder) { sb.Where(sb.Equal("id", userID)) @@ -242,7 +253,7 @@ func (s *Server) DeleteMe(w http.ResponseWriter, r *http.Request) { func (s *Server) GetMe(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - claimsValue := ctx.Value("user") + claimsValue := ctx.Value(UserKey) claims, ok := claimsValue.(*Claims) if !ok { slog.ErrorContext(ctx, "Error while converting claims", slog.Any("claims", claimsValue)) @@ -265,7 +276,7 @@ func (s *Server) GetMe(w http.ResponseWriter, r *http.Request) { } func (s *Server) UpdateMe(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - claims, ok := ctx.Value("user").(*Claims) + claims, ok := ctx.Value(UserKey).(*Claims) if !ok { slog.ErrorContext(ctx, "Error while casting claims") s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError) @@ -325,7 +336,7 @@ func (s *Server) UpdateMe(w http.ResponseWriter, r *http.Request) { func (s *Server) GetMyTrips(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - claims, ok := ctx.Value("user").(*Claims) + claims, ok := ctx.Value(UserKey).(*Claims) if !ok { slog.ErrorContext(ctx, "Error while casting claims") s.JSON(w, r, http.StatusInternalServerError, MsgInternalError, RespError) diff --git a/internal/models/dashboard.go b/internal/models/dashboard.go index 08daa6e..fd0ba8a 100644 --- a/internal/models/dashboard.go +++ b/internal/models/dashboard.go @@ -8,9 +8,9 @@ type DashboardRevenue struct { } type DashboardOrderStatus struct { - Total int `json:"total` + Total int `json:"total"` Delivered int `json:"delivered"` - InTransit int `json:"inTransit` + InTransit int `json:"inTransit"` Pending int `json:"pending"` Cancelled int `json:"cancelled"` } diff --git a/internal/services/order.go b/internal/services/order.go index d917eb3..37eb984 100644 --- a/internal/services/order.go +++ b/internal/services/order.go @@ -16,6 +16,7 @@ import ( "github.com/anxi0uz/logiflow/pkg/geocode" "github.com/google/uuid" "github.com/huandu/go-sqlbuilder" + "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" "golang.org/x/sync/errgroup" ) @@ -102,7 +103,11 @@ func (s *OrderService) CreateOrder(ctx context.Context, req api.OrderCreate, use if err != nil { return nil, fmt.Errorf("osrm request: %w", err) } - defer osrmResp.Body.Close() + defer func() { + if err := osrmResp.Body.Close(); err != nil { + slog.Warn("failed to close response body", slog.String("error", err.Error())) + } + }() var osrmResult geocode.OsrmResult if err := json.NewDecoder(osrmResp.Body).Decode(&osrmResult); err != nil || len(osrmResult.Routes) == 0 { @@ -154,6 +159,7 @@ func (s *OrderService) CreateOrder(ctx context.Context, req api.OrderCreate, use OrderID: orderID, Coordinates: coordsJSON, DurationSec: int(route.Duration), + DistanceKm: distanceKm, Status: "pending", } @@ -161,7 +167,11 @@ func (s *OrderService) CreateOrder(ctx context.Context, req api.OrderCreate, use if err != nil { return nil, fmt.Errorf("begin tx: %w", err) } - defer tx.Rollback(ctx) + defer func() { + if err := tx.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) { + slog.ErrorContext(ctx, "tx rollback failed", slog.String("error", err.Error())) + } + }() if err := storage.Create(ctx, "orders", order, tx); err != nil { return nil, fmt.Errorf("create order: %w", err) @@ -284,20 +294,28 @@ func (s *OrderService) UpdateOrderStatus(ctx context.Context, id uuid.UUID, user } order.DriverID = req.DriverId order.AssignedAt = &now - driver, _ := storage.GetOne[models.Driver](ctx, s.db, "drivers", func(sb *sqlbuilder.SelectBuilder) { + driver, err := storage.GetOne[models.Driver](ctx, s.db, "drivers", func(sb *sqlbuilder.SelectBuilder) { sb.Where(sb.EQ("id", order.DriverID)) }) + if err != nil { + slog.ErrorContext(ctx, "error while getting driver", slog.String("error", err.Error())) + return nil, fmt.Errorf("update order status: %w", err) + } s.createNotification(ctx, driver.UserID, "Новый заказ", "Вам назначен новый заказ") } if req.Status == api.OrderStatusUpdateStatusInTransit { if order.CreatedByID == nil { return nil, fmt.Errorf("created by id needed") } - s.createNotification(ctx, *order.CreatedByID, "Заказ в пути", "Ваш заказ передан водителю") + if order.CreatedByID != nil { + s.createNotification(ctx, *order.CreatedByID, "Заказ в пути", "Ваш заказ передан водителю") + } } if req.Status == api.OrderStatusUpdateStatusDelivered { order.DeliveredAt = &now - s.createNotification(ctx, *order.CreatedByID, "Заказ доставлен", "Ваш заказ успешно доставлен") + if order.CreatedByID != nil { + s.createNotification(ctx, *order.CreatedByID, "Заказ доставлен", "Ваш заказ успешно доставлен") + } } if err := storage.Update(ctx, "orders", *order, s.db, func(sb *sqlbuilder.UpdateBuilder) { sb.Where(sb.EQ("id", id)) diff --git a/pkg/geocode/geocode.go b/pkg/geocode/geocode.go index 4194019..f162d75 100644 --- a/pkg/geocode/geocode.go +++ b/pkg/geocode/geocode.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "log/slog" "net/http" neturl "net/url" "strconv" @@ -35,7 +36,11 @@ func Geocode(ctx context.Context, address string) (lat, lon float64, err error) if err != nil { return 0, 0, err } - defer resp.Body.Close() + defer func() { + if err := resp.Body.Close(); err != nil { + slog.Warn("failed to close response body", slog.String("error", err.Error())) + } + }() var results []struct { Lat string `json:"lat"` diff --git a/tests/order_handler_test.go b/tests/order_handler_test.go index d74d90d..acc583d 100644 --- a/tests/order_handler_test.go +++ b/tests/order_handler_test.go @@ -60,7 +60,7 @@ func newTestServer(svc services.OrderServicer) *handler.Server { func withClaims(r *http.Request, id uuid.UUID, role string) *http.Request { claims := &handler.Claims{ID: id, Role: role} - ctx := context.WithValue(r.Context(), "user", claims) + ctx := context.WithValue(r.Context(), handler.UserKey, claims) return r.WithContext(ctx) }