From b690a1c933838b5917525c7b946084a4c4ef9bfa Mon Sep 17 00:00:00 2001 From: dawitel Date: Wed, 2 Apr 2025 21:50:52 +0300 Subject: [PATCH] fix: refactored the hadler for notification and added mark as read --- README.md | 69 +++++++++++++++- db/migrations/000002_notification.up.sql | 4 +- gen/db/models.go | 2 +- gen/db/notification.sql.go | 4 +- internal/domain/notification.go | 2 +- internal/repository/notification.go | 4 +- internal/services/notfication/port.go | 12 +-- internal/services/notfication/service.go | 28 ++----- internal/web_server/handlers/handlers.go | 22 +++++ .../handlers/notification_handler.go | 80 ++++++++++++++----- internal/web_server/routes.go | 5 +- 11 files changed, 178 insertions(+), 54 deletions(-) create mode 100644 internal/web_server/handlers/handlers.go diff --git a/README.md b/README.md index 8614c23..353769b 100644 --- a/README.md +++ b/README.md @@ -7,38 +7,105 @@ │ ├── migrations │ │ ├── 000001_fortune.down.sql │ │ ├── 000001_fortune.up.sql +│ │ ├── 000002_notification.down.sql +│ │ ├── 000002_notification.up.sql │ └── query +│ ├── auth.sql +│ ├── bet.sql +│ ├── notification.sql +│ ├── otp.sql +│ ├── ticket.sql │ ├── user.sql +├── docs +│ ├── docs.go +│ ├── swagger.json +│ ├── swagger.yaml ├── gen │ └── db +│ ├── auth.sql.go +│ ├── bet.sql.go │ ├── db.go │ ├── models.go +│ ├── notification.sql.go +│ ├── otp.sql.go +│ ├── ticket.sql.go │ ├── user.sql.go └── internal ├── config │ ├── config.go ├── domain │ ├── auth.go + │ ├── bet.go + │ ├── branch.go + │ ├── common.go + │ ├── event.go │ ├── notification.go + │ ├── otp.go + │ ├── role.go + │ ├── ticket.go │ ├── user.go ├── logger │ ├── logger.go + ├── mocks + │ ├── mock_email + │ │ ├── email.go + │ └── mock_sms + │ ├── sms.go + ├── pkgs + │ └── helpers + │ ├── helpers.go ├── repository + │ ├── auth.go + │ ├── bet.go + │ ├── notification.go + │ ├── otp.go │ ├── store.go + │ ├── ticket.go │ ├── user.go ├── services + │ ├── authentication + │ │ ├── impl.go + │ │ ├── port.go + │ │ ├── service.go + │ ├── bet + │ │ ├── port.go + │ │ ├── service.go │ ├── notfication │ │ ├── port.go │ │ ├── service.go + │ ├── sportsbook + │ │ ├── events.go + │ │ ├── odds.go + │ │ ├── service.go + │ ├── ticket + │ │ ├── port.go + │ │ ├── service.go │ └── user + │ ├── common.go │ ├── port.go + │ ├── register.go + │ ├── reset.go │ ├── service.go + │ ├── user.go └── web_server + ├── handlers + │ ├── auth_handler.go + │ ├── bet_handler.go + │ ├── notification_handler.go + │ ├── ticket_handler.go + │ ├── user.go + ├── jwt + │ ├── jwt.go + │ ├── jwt_test.go + ├── response + │ ├── res.go └── validator ├── validatord.go ├── app.go - ├── app_routes.go + ├── middleware.go + ├── routes.go ├── .air.toml +├── .env ├── .gitignore ├── README.md ├── compose.db.yaml diff --git a/db/migrations/000002_notification.up.sql b/db/migrations/000002_notification.up.sql index 860004e..134fe94 100644 --- a/db/migrations/000002_notification.up.sql +++ b/db/migrations/000002_notification.up.sql @@ -1,6 +1,6 @@ CREATE TABLE IF NOT EXISTS notifications ( - id VARCHAR(255) PRIMARY KEY, - recipient_id VARCHAR(255) NOT NULL, + id VARCHAR(255) NOT NULL PRIMARY KEY, + recipient_id BIGSERIAL NOT NULL, type TEXT NOT NULL CHECK ( type IN ( 'cash_out_success', diff --git a/gen/db/models.go b/gen/db/models.go index 8703486..3bac40f 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -25,7 +25,7 @@ type Bet struct { type Notification struct { ID string - RecipientID string + RecipientID int64 Type string Level string ErrorSeverity pgtype.Text diff --git a/gen/db/notification.sql.go b/gen/db/notification.sql.go index 0df4dd1..d784202 100644 --- a/gen/db/notification.sql.go +++ b/gen/db/notification.sql.go @@ -21,7 +21,7 @@ INSERT INTO notifications ( type CreateNotificationParams struct { ID string - RecipientID string + RecipientID int64 Type string Level string ErrorSeverity pgtype.Text @@ -141,7 +141,7 @@ SELECT id, recipient_id, type, level, error_severity, reciever, is_read, deliver ` type ListNotificationsParams struct { - RecipientID string + RecipientID int64 Limit int32 Offset int32 } diff --git a/internal/domain/notification.go b/internal/domain/notification.go index df2a04c..e39ee16 100644 --- a/internal/domain/notification.go +++ b/internal/domain/notification.go @@ -58,7 +58,7 @@ type NotificationPayload struct { type Notification struct { ID string `json:"id"` - RecipientID string `json:"recipient_id"` + RecipientID int64 `json:"recipient_id"` Type NotificationType `json:"type"` Level NotificationLevel `json:"level"` ErrorSeverity *NotificationErrorSeverity `json:"error_severity"` diff --git a/internal/repository/notification.go b/internal/repository/notification.go index 5c62849..215209c 100644 --- a/internal/repository/notification.go +++ b/internal/repository/notification.go @@ -12,7 +12,7 @@ import ( type NotificationRepository interface { CreateNotification(ctx context.Context, notification *domain.Notification) (*domain.Notification, error) UpdateNotificationStatus(ctx context.Context, id, status string, isRead bool, metadata []byte) (*domain.Notification, error) - ListNotifications(ctx context.Context, recipientID string, limit, offset int) ([]domain.Notification, error) + ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error) ListFailedNotifications(ctx context.Context, limit int) ([]domain.Notification, error) } @@ -83,7 +83,7 @@ func (r *Repository) UpdateNotificationStatus(ctx context.Context, id, status st return r.mapDBToDomain(&dbNotification), nil } -func (r *Repository) ListNotifications(ctx context.Context, recipientID string, limit, offset int) ([]domain.Notification, error) { +func (r *Repository) ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error) { params := dbgen.ListNotificationsParams{ RecipientID: recipientID, Limit: int32(limit), diff --git a/internal/services/notfication/port.go b/internal/services/notfication/port.go index 4bca486..9ddff29 100644 --- a/internal/services/notfication/port.go +++ b/internal/services/notfication/port.go @@ -9,10 +9,10 @@ import ( type NotificationStore interface { SendNotification(ctx context.Context, notification *domain.Notification) error - MarkAsRead(ctx context.Context, notificationID, recipientID string) error - ListNotifications(ctx context.Context, recipientID string, limit, offset int) ([]domain.Notification, error) - ConnectWebSocket(ctx context.Context, recipientID string, c *websocket.Conn) error - DisconnectWebSocket(recipientID string) - SendSMS(ctx context.Context, recipientID, message string) error - SendEmail(ctx context.Context, recipientID, subject, message string) error + MarkAsRead(ctx context.Context, notificationID string, recipientID int64) error + ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error) + ConnectWebSocket(ctx context.Context, recipientID int64, c *websocket.Conn) error + DisconnectWebSocket(recipientID int64) + SendSMS(ctx context.Context, recipientID int64, message string) error + SendEmail(ctx context.Context, recipientID int64, subject, message string) error } diff --git a/internal/services/notfication/service.go b/internal/services/notfication/service.go index 8b6f696..70a5d11 100644 --- a/internal/services/notfication/service.go +++ b/internal/services/notfication/service.go @@ -35,7 +35,7 @@ func New(repo repository.NotificationRepository, logger *slog.Logger) Notificati return svc } -func (s *Service) addConnection(ctx context.Context, recipientID string, c *websocket.Conn) { +func (s *Service) addConnection(ctx context.Context, recipientID int64, c *websocket.Conn) { if c == nil { s.logger.Warn("Attempted to add nil WebSocket connection", "recipientID", recipientID) return @@ -66,7 +66,7 @@ func (s *Service) SendNotification(ctx context.Context, notification *domain.Not return nil } -func (s *Service) MarkAsRead(ctx context.Context, notificationID, recipientID string) error { +func (s *Service) MarkAsRead(ctx context.Context, notificationID string, recipientID int64) error { _, err := s.repo.UpdateNotificationStatus(ctx, notificationID, string(domain.DeliveryStatusSent), true, nil) if err != nil { return err @@ -74,28 +74,16 @@ func (s *Service) MarkAsRead(ctx context.Context, notificationID, recipientID st return nil } -func (s *Service) ListNotifications(ctx context.Context, recipientID string, limit, offset int) ([]domain.Notification, error) { +func (s *Service) ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error) { return s.repo.ListNotifications(ctx, recipientID, limit, offset) } -func (s *Service) ConnectWebSocket(ctx context.Context, recipientID string, c *websocket.Conn) error { +func (s *Service) ConnectWebSocket(ctx context.Context, recipientID int64, c *websocket.Conn) error { s.addConnection(ctx, recipientID, c) - defer func() { - s.DisconnectWebSocket(recipientID) - }() - - for { - _, _, err := c.ReadMessage() - if err != nil { - if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { - s.logger.Error("WebSocket error", "recipientID", recipientID, "error", err) - } - return nil - } - } + return nil } -func (s *Service) DisconnectWebSocket(recipientID string) { +func (s *Service) DisconnectWebSocket(recipientID int64) { s.connections.Delete(recipientID) if conn, loaded := s.connections.LoadAndDelete(recipientID); loaded { conn.(*websocket.Conn).Close() @@ -103,12 +91,12 @@ func (s *Service) DisconnectWebSocket(recipientID string) { } } -func (s *Service) SendSMS(ctx context.Context, recipientID, message string) error { +func (s *Service) SendSMS(ctx context.Context, recipientID int64, message string) error { s.logger.Info("SMS notification requested", "recipientID", recipientID, "message", message) return nil } -func (s *Service) SendEmail(ctx context.Context, recipientID, subject, message string) error { +func (s *Service) SendEmail(ctx context.Context, recipientID int64, subject, message string) error { s.logger.Info("Email notification requested", "recipientID", recipientID, "subject", subject) return nil } diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go new file mode 100644 index 0000000..4c3c770 --- /dev/null +++ b/internal/web_server/handlers/handlers.go @@ -0,0 +1,22 @@ +package handlers + +import ( + "log/slog" + + notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" + customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" +) + +type Handler struct { + logger *slog.Logger + notificationSvc notificationservice.NotificationStore + validator *customvalidator.CustomValidator +} + +func New(logger *slog.Logger, notificationSvc notificationservice.NotificationStore, validator *customvalidator.CustomValidator) *Handler { + return &Handler{ + logger: logger, + notificationSvc: notificationSvc, + validator: validator, + } +} diff --git a/internal/web_server/handlers/notification_handler.go b/internal/web_server/handlers/notification_handler.go index cd13517..9e41b0e 100644 --- a/internal/web_server/handlers/notification_handler.go +++ b/internal/web_server/handlers/notification_handler.go @@ -2,31 +2,75 @@ package handlers import ( "context" - "log/slog" - notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" - customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" "github.com/gofiber/fiber/v2" "github.com/gofiber/websocket/v2" ) -func ConnectSocket(logger slog.Logger, NotidicationSvc notificationservice.NotificationStore, v *customvalidator.CustomValidator) fiber.Handler { - return func(c *fiber.Ctx) error { - if !websocket.IsWebSocketUpgrade(c) { - return fiber.ErrUpgradeRequired +func (h *Handler) ConnectSocket(c *fiber.Ctx) error { + if !websocket.IsWebSocketUpgrade(c) { + h.logger.Warn("WebSocket upgrade required") + return fiber.ErrUpgradeRequired + } + + userID, ok := c.Locals("userID").(int64) + if !ok || userID == 0 { + h.logger.Error("Invalid user ID in context") + return fiber.NewError(fiber.StatusUnauthorized, "invalid user identification") + } + + c.Locals("allowed", true) + + return websocket.New(func(conn *websocket.Conn) { + ctx := context.Background() + logger := h.logger.With("userID", userID, "remoteAddr", conn.RemoteAddr()) + + if err := h.notificationSvc.ConnectWebSocket(ctx, userID, conn); err != nil { + logger.Error("Failed to connect WebSocket", "error", err) + _ = conn.Close() + return } - c.Locals("allowed", true) + logger.Info("WebSocket connection established") - return websocket.New(func(conn *websocket.Conn) { - // TODO: get the recipientID from the token - recipientID := c.Params("recipientID") - NotidicationSvc.ConnectWebSocket(context.Background(), recipientID, conn) + defer func() { + h.notificationSvc.DisconnectWebSocket(userID) + logger.Info("WebSocket connection closed") + _ = conn.Close() + }() - defer func() { - NotidicationSvc.DisconnectWebSocket(recipientID) - conn.Close() - }() - })(c) - } + for { + if _, _, err := conn.ReadMessage(); err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + logger.Warn("WebSocket unexpected close", "error", err) + } + break + } + } + })(c) +} + +func (h *Handler) MarkNotificationAsRead(c *fiber.Ctx) error { + type Request struct { + NotificationID string `json:"notification_id" validate:"required"` + } + + var req Request + if err := c.BodyParser(&req); err != nil { + h.logger.Error("Failed to parse request body", "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + userID, ok := c.Locals("userID").(int64) + if !ok || userID == 0 { + h.logger.Error("Invalid user ID in context") + return fiber.NewError(fiber.StatusUnauthorized, "invalid user identification") + } + + if err := h.notificationSvc.MarkAsRead(context.Background(), req.NotificationID, userID); err != nil { + h.logger.Error("Failed to mark notification as read", "notificationID", req.NotificationID, "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update notification status") + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{"message": "Notification marked as read"}) } diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 2456d3b..3c36302 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -8,6 +8,8 @@ import ( ) func (a *App) initAppRoutes() { + handler := handlers.New(a.logger, a.NotidicationStore, a.validator) + a.fiber.Post("/auth/login", handlers.LoginCustomer(a.logger, a.authSvc, a.validator, a.JwtConfig)) a.fiber.Post("/auth/refresh", a.authMiddleware, handlers.RefreshToken(a.logger, a.authSvc, a.validator, a.JwtConfig)) a.fiber.Post("/auth/logout", a.authMiddleware, handlers.LogOutCustomer(a.logger, a.authSvc, a.validator)) @@ -41,7 +43,8 @@ func (a *App) initAppRoutes() { a.fiber.Patch("/bet/:id", handlers.UpdateCashOut(a.logger, a.betSvc, a.validator)) a.fiber.Delete("/bet/:id", handlers.DeleteBet(a.logger, a.betSvc, a.validator)) - a.fiber.Get("/ws/:recipientID", handlers.ConnectSocket(*a.logger, a.NotidicationStore, a.validator)) + a.fiber.Get("/notifications/ws/connect/:recipientID", handler.ConnectSocket) + a.fiber.Post("/notifications/mark-as-read", handler.MarkNotificationAsRead) } ///user/profile get