From 6a06b399c7d1c36b5a2599cae6caca5fd4a58d7d Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Thu, 22 May 2025 00:15:30 +0300 Subject: [PATCH] fix: notification integration issues --- db/query/notification.sql | 78 +++++++++-- db/query/user.sql | 3 +- gen/db/events.sql.go | 1 - gen/db/notification.sql.go | 124 ++++++++++++++++-- gen/db/user.sql.go | 8 +- internal/domain/common.go | 4 + internal/repository/notification.go | 24 ++++ internal/repository/user.go | 10 +- internal/services/notfication/port.go | 2 + internal/services/notfication/service.go | 56 +++++--- internal/services/user/direct.go | 4 +- internal/web_server/handlers/admin.go | 13 +- internal/web_server/handlers/manager.go | 12 +- .../handlers/notification_handler.go | 75 +++++++++-- internal/web_server/routes.go | 4 +- makefile | 4 +- 16 files changed, 352 insertions(+), 70 deletions(-) diff --git a/db/query/notification.sql b/db/query/notification.sql index 22bae8d..8a1c51f 100644 --- a/db/query/notification.sql +++ b/db/query/notification.sql @@ -1,21 +1,71 @@ -- name: CreateNotification :one INSERT INTO notifications ( - id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, timestamp, metadata -) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13 -) RETURNING *; - + id, + recipient_id, + type, + level, + error_severity, + reciever, + is_read, + delivery_status, + delivery_channel, + payload, + priority, + timestamp, + metadata + ) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13 + ) +RETURNING *; -- name: GetNotification :one -SELECT * FROM notifications WHERE id = $1 LIMIT 1; - +SELECT * +FROM notifications +WHERE id = $1 +LIMIT 1; +-- name: GetAllNotifications :many +SELECT * +FROM notifications +ORDER BY timestamp DESC +LIMIT $1 OFFSET $2; -- name: ListNotifications :many -SELECT * FROM notifications WHERE recipient_id = $1 ORDER BY timestamp DESC LIMIT $2 OFFSET $3; - +SELECT * +FROM notifications +WHERE recipient_id = $1 +ORDER BY timestamp DESC +LIMIT $2 OFFSET $3; +-- name: CountUnreadNotifications :one +SELECT count(id) +FROM notifications +WHERE recipient_id = $1 + AND is_read = false; -- name: UpdateNotificationStatus :one -UPDATE notifications SET delivery_status = $2, is_read = $3, metadata = $4 WHERE id = $1 RETURNING *; - +UPDATE notifications +SET delivery_status = $2, + is_read = $3, + metadata = $4 +WHERE id = $1 +RETURNING *; -- name: ListFailedNotifications :many -SELECT * FROM notifications WHERE delivery_status = 'failed' AND timestamp < NOW() - INTERVAL '1 hour' ORDER BY timestamp ASC LIMIT $1; - +SELECT * +FROM notifications +WHERE delivery_status = 'failed' + AND timestamp < NOW() - INTERVAL '1 hour' +ORDER BY timestamp ASC +LIMIT $1; -- name: ListRecipientIDsByReceiver :many -SELECT recipient_id FROM notifications WHERE reciever = $1; +SELECT recipient_id +FROM notifications +WHERE reciever = $1; \ No newline at end of file diff --git a/db/query/user.sql b/db/query/user.sql index c5799e8..84cfe4a 100644 --- a/db/query/user.sql +++ b/db/query/user.sql @@ -66,7 +66,7 @@ wHERE ( company_id = $2 OR $2 IS NULL ) -LIMIT $3 OFFSET $4; +LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- name: GetTotalUsers :one SELECT COUNT(*) FROM users @@ -109,7 +109,6 @@ WHERE id = $7; UPDATE users SET company_id = $1 WHERE id = $2; - -- name: SuspendUser :exec UPDATE users SET suspended = $1, diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index 37bdad1..d95a9db 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -1,4 +1,3 @@ - // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.28.0 diff --git a/gen/db/notification.sql.go b/gen/db/notification.sql.go index 5bfedd6..d30b3d1 100644 --- a/gen/db/notification.sql.go +++ b/gen/db/notification.sql.go @@ -11,12 +11,52 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const CountUnreadNotifications = `-- name: CountUnreadNotifications :one +SELECT count(id) +FROM notifications +WHERE recipient_id = $1 + AND is_read = false +` + +func (q *Queries) CountUnreadNotifications(ctx context.Context, recipientID int64) (int64, error) { + row := q.db.QueryRow(ctx, CountUnreadNotifications, recipientID) + var count int64 + err := row.Scan(&count) + return count, err +} + const CreateNotification = `-- name: CreateNotification :one INSERT INTO notifications ( - id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, timestamp, metadata -) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13 -) RETURNING id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata + id, + recipient_id, + type, + level, + error_severity, + reciever, + is_read, + delivery_status, + delivery_channel, + payload, + priority, + timestamp, + metadata + ) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13 + ) +RETURNING id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata ` type CreateNotificationParams struct { @@ -71,8 +111,58 @@ func (q *Queries) CreateNotification(ctx context.Context, arg CreateNotification return i, err } +const GetAllNotifications = `-- name: GetAllNotifications :many +SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata +FROM notifications +ORDER BY timestamp DESC +LIMIT $1 OFFSET $2 +` + +type GetAllNotificationsParams struct { + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) GetAllNotifications(ctx context.Context, arg GetAllNotificationsParams) ([]Notification, error) { + rows, err := q.db.Query(ctx, GetAllNotifications, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Notification + for rows.Next() { + var i Notification + if err := rows.Scan( + &i.ID, + &i.RecipientID, + &i.Type, + &i.Level, + &i.ErrorSeverity, + &i.Reciever, + &i.IsRead, + &i.DeliveryStatus, + &i.DeliveryChannel, + &i.Payload, + &i.Priority, + &i.Version, + &i.Timestamp, + &i.Metadata, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const GetNotification = `-- name: GetNotification :one -SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata FROM notifications WHERE id = $1 LIMIT 1 +SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata +FROM notifications +WHERE id = $1 +LIMIT 1 ` func (q *Queries) GetNotification(ctx context.Context, id string) (Notification, error) { @@ -98,7 +188,12 @@ func (q *Queries) GetNotification(ctx context.Context, id string) (Notification, } const ListFailedNotifications = `-- name: ListFailedNotifications :many -SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata FROM notifications WHERE delivery_status = 'failed' AND timestamp < NOW() - INTERVAL '1 hour' ORDER BY timestamp ASC LIMIT $1 +SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata +FROM notifications +WHERE delivery_status = 'failed' + AND timestamp < NOW() - INTERVAL '1 hour' +ORDER BY timestamp ASC +LIMIT $1 ` func (q *Queries) ListFailedNotifications(ctx context.Context, limit int32) ([]Notification, error) { @@ -137,7 +232,11 @@ func (q *Queries) ListFailedNotifications(ctx context.Context, limit int32) ([]N } const ListNotifications = `-- name: ListNotifications :many -SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata FROM notifications WHERE recipient_id = $1 ORDER BY timestamp DESC LIMIT $2 OFFSET $3 +SELECT id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata +FROM notifications +WHERE recipient_id = $1 +ORDER BY timestamp DESC +LIMIT $2 OFFSET $3 ` type ListNotificationsParams struct { @@ -182,7 +281,9 @@ func (q *Queries) ListNotifications(ctx context.Context, arg ListNotificationsPa } const ListRecipientIDsByReceiver = `-- name: ListRecipientIDsByReceiver :many -SELECT recipient_id FROM notifications WHERE reciever = $1 +SELECT recipient_id +FROM notifications +WHERE reciever = $1 ` func (q *Queries) ListRecipientIDsByReceiver(ctx context.Context, reciever string) ([]int64, error) { @@ -206,7 +307,12 @@ func (q *Queries) ListRecipientIDsByReceiver(ctx context.Context, reciever strin } const UpdateNotificationStatus = `-- name: UpdateNotificationStatus :one -UPDATE notifications SET delivery_status = $2, is_read = $3, metadata = $4 WHERE id = $1 RETURNING id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata +UPDATE notifications +SET delivery_status = $2, + is_read = $3, + metadata = $4 +WHERE id = $1 +RETURNING id, recipient_id, type, level, error_severity, reciever, is_read, delivery_status, delivery_channel, payload, priority, version, timestamp, metadata ` type UpdateNotificationStatusParams struct { diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index a595372..f66aff0 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -182,14 +182,14 @@ wHERE ( company_id = $2 OR $2 IS NULL ) -LIMIT $3 OFFSET $4 +LIMIT $4 OFFSET $3 ` type GetAllUsersParams struct { Role string `json:"role"` CompanyID pgtype.Int8 `json:"company_id"` - Limit int32 `json:"limit"` - Offset int32 `json:"offset"` + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` } type GetAllUsersRow struct { @@ -212,8 +212,8 @@ func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]Get rows, err := q.db.Query(ctx, GetAllUsers, arg.Role, arg.CompanyID, - arg.Limit, arg.Offset, + arg.Limit, ) if err != nil { return nil, err diff --git a/internal/domain/common.go b/internal/domain/common.go index 14323a4..fc652d1 100644 --- a/internal/domain/common.go +++ b/internal/domain/common.go @@ -9,6 +9,10 @@ type ValidInt64 struct { Value int64 Valid bool } +type ValidInt struct { + Value int + Valid bool +} type ValidString struct { Value string diff --git a/internal/repository/notification.go b/internal/repository/notification.go index eb922a9..b189ccf 100644 --- a/internal/repository/notification.go +++ b/internal/repository/notification.go @@ -15,6 +15,8 @@ type NotificationRepository interface { ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error) ListFailedNotifications(ctx context.Context, limit int) ([]domain.Notification, error) ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) + CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) + GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) } type Repository struct { @@ -105,6 +107,24 @@ func (r *Repository) ListNotifications(ctx context.Context, recipientID int64, l return result, nil } +func (r *Repository) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) { + + dbNotifications, err := r.store.queries.GetAllNotifications(ctx, dbgen.GetAllNotificationsParams{ + Limit: int32(limit), + Offset: int32(offset), + }) + if err != nil { + return nil, err + } + + var result []domain.Notification = make([]domain.Notification, 0, len(dbNotifications)) + for _, dbNotif := range dbNotifications { + domainNotif := r.mapDBToDomain(&dbNotif) + result = append(result, *domainNotif) + } + return result, nil +} + func (r *Repository) ListFailedNotifications(ctx context.Context, limit int) ([]domain.Notification, error) { dbNotifications, err := r.store.queries.ListFailedNotifications(ctx, int32(limit)) if err != nil { @@ -177,3 +197,7 @@ func unmarshalPayload(data []byte) (domain.NotificationPayload, error) { } return payload, nil } + +func (r *Repository) CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) { + return r.store.queries.CountUnreadNotifications(ctx, recipient_id) +} diff --git a/internal/repository/user.go b/internal/repository/user.go index c2aa930..88a320b 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -90,8 +90,14 @@ func (s *Store) GetAllUsers(ctx context.Context, filter user.Filter) ([]domain.U Int64: filter.CompanyID.Value, Valid: filter.CompanyID.Valid, }, - Limit: int32(filter.PageSize), - Offset: int32(filter.Page), + Limit: pgtype.Int4{ + Int32: int32(filter.PageSize.Value), + Valid: filter.PageSize.Valid, + }, + Offset: pgtype.Int4{ + Int32: int32(filter.Page.Value), + Valid: filter.Page.Valid, + }, }) if err != nil { return nil, 0, err diff --git a/internal/services/notfication/port.go b/internal/services/notfication/port.go index 23120ee..ec82c03 100644 --- a/internal/services/notfication/port.go +++ b/internal/services/notfication/port.go @@ -16,4 +16,6 @@ type NotificationStore interface { SendSMS(ctx context.Context, recipientID int64, message string) error SendEmail(ctx context.Context, recipientID int64, subject, message string) error ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) // New method + CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) + GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) } diff --git a/internal/services/notfication/service.go b/internal/services/notfication/service.go index 368e637..5d5760c 100644 --- a/internal/services/notfication/service.go +++ b/internal/services/notfication/service.go @@ -85,28 +85,31 @@ func (s *Service) SendNotification(ctx context.Context, notification *domain.Not return nil } -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 { - s.logger.Error("[NotificationSvc.MarkAsRead] Failed to mark notification as read", "notificationID", notificationID, "recipientID", recipientID, "error", err) - return err +func (s *Service) MarkAsRead(ctx context.Context, notificationIDs []string, recipientID int64) error { + for _, notificationID := range notificationIDs { + _, err := s.repo.UpdateNotificationStatus(ctx, notificationID, string(domain.DeliveryStatusSent), true, nil) + if err != nil { + s.logger.Error("[NotificationSvc.MarkAsRead] Failed to mark notification as read", "notificationID", notificationID, "recipientID", recipientID, "error", err) + return err + } + + // count, err := s.repo.CountUnreadNotifications(ctx, recipientID) + // if err != nil { + // s.logger.Error("[NotificationSvc.MarkAsRead] Failed to count unread notifications", "recipientID", recipientID, "error", err) + // return err + // } + + // s.Hub.Broadcast <- map[string]interface{}{ + // "type": "COUNT_NOT_OPENED_NOTIFICATION", + // "recipient_id": recipientID, + // "payload": map[string]int{ + // "not_opened_notifications_count": int(count), + // }, + // } + + s.logger.Info("[NotificationSvc.MarkAsRead] Notification marked as read", "notificationID", notificationID, "recipientID", recipientID) } - // count, err := s.repo.CountUnreadNotifications(ctx, recipientID) - // if err != nil { - // s.logger.Error("[NotificationSvc.MarkAsRead] Failed to count unread notifications", "recipientID", recipientID, "error", err) - // return err - // } - - // s.Hub.Broadcast <- map[string]interface{}{ - // "type": "COUNT_NOT_OPENED_NOTIFICATION", - // "recipient_id": recipientID, - // "payload": map[string]int{ - // "not_opened_notifications_count": int(count), - // }, - // } - - s.logger.Info("[NotificationSvc.MarkAsRead] Notification marked as read", "notificationID", notificationID, "recipientID", recipientID) return nil } @@ -120,6 +123,16 @@ func (s *Service) ListNotifications(ctx context.Context, recipientID int64, limi return notifications, nil } +func (s *Service) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) { + notifications, err := s.repo.GetAllNotifications(ctx, limit, offset) + if err != nil { + s.logger.Error("[NotificationSvc.ListNotifications] Failed to get all notifications") + return nil, err + } + s.logger.Info("[NotificationSvc.ListNotifications] Successfully retrieved all notifications", "count", len(notifications)) + return notifications, nil +} + func (s *Service) ConnectWebSocket(ctx context.Context, recipientID int64, c *websocket.Conn) error { s.addConnection(ctx, recipientID, c) s.logger.Info("[NotificationSvc.ConnectWebSocket] WebSocket connection established", "recipientID", recipientID) @@ -267,3 +280,6 @@ func (s *Service) retryFailedNotifications() { } } +func (s *Service) CountUnreadNotifications(ctx context.Context, recipient_id int64) (int64, error) { + return s.repo.CountUnreadNotifications(ctx, recipient_id) +} diff --git a/internal/services/user/direct.go b/internal/services/user/direct.go index 8181822..04b8a65 100644 --- a/internal/services/user/direct.go +++ b/internal/services/user/direct.go @@ -46,8 +46,8 @@ func (s *Service) DeleteUser(ctx context.Context, id int64) error { type Filter struct { Role string CompanyID domain.ValidInt64 - Page int - PageSize int + Page domain.ValidInt + PageSize domain.ValidInt } type ValidRole struct { Value domain.Role diff --git a/internal/web_server/handlers/admin.go b/internal/web_server/handlers/admin.go index 2b8cdf0..3b5bbb6 100644 --- a/internal/web_server/handlers/admin.go +++ b/internal/web_server/handlers/admin.go @@ -124,14 +124,21 @@ type AdminRes struct { // @Failure 500 {object} response.APIResponse // @Router /admin [get] func (h *Handler) GetAllAdmins(c *fiber.Ctx) error { + filter := user.Filter{ Role: string(domain.RoleAdmin), CompanyID: domain.ValidInt64{ Value: int64(c.QueryInt("company_id")), Valid: true, }, - Page: c.QueryInt("page", 1) - 1, - PageSize: c.QueryInt("page_size", 10), + Page: domain.ValidInt{ + Value: c.QueryInt("page", 1) - 1, + Valid: true, + }, + PageSize: domain.ValidInt{ + Value: c.QueryInt("page_size", 10), + Valid: true, + }, } valErrs, ok := h.validator.Validate(c, filter) if !ok { @@ -171,6 +178,6 @@ func (h *Handler) GetAllAdmins(c *fiber.Ctx) error { } } - return response.WritePaginatedJSON(c, fiber.StatusOK, "Admins retrieved successfully", result, nil, filter.Page, int(total)) + return response.WritePaginatedJSON(c, fiber.StatusOK, "Admins retrieved successfully", result, nil, filter.Page.Value, int(total)) } diff --git a/internal/web_server/handlers/manager.go b/internal/web_server/handlers/manager.go index 9edfbb6..0c3a980 100644 --- a/internal/web_server/handlers/manager.go +++ b/internal/web_server/handlers/manager.go @@ -115,8 +115,14 @@ func (h *Handler) GetAllManagers(c *fiber.Ctx) error { Value: int64(c.QueryInt("company_id")), Valid: true, }, - Page: c.QueryInt("page", 1) - 1, - PageSize: c.QueryInt("page_size", 10), + Page: domain.ValidInt{ + Value: c.QueryInt("page", 1) - 1, + Valid: true, + }, + PageSize: domain.ValidInt{ + Value: c.QueryInt("page_size", 10), + Valid: true, + }, } valErrs, ok := h.validator.Validate(c, filter) if !ok { @@ -156,7 +162,7 @@ func (h *Handler) GetAllManagers(c *fiber.Ctx) error { } } - return response.WritePaginatedJSON(c, fiber.StatusOK, "Managers retrieved successfully", result, nil, filter.Page, int(total)) + return response.WritePaginatedJSON(c, fiber.StatusOK, "Managers retrieved successfully", result, nil, filter.Page.Value, int(total)) } diff --git a/internal/web_server/handlers/notification_handler.go b/internal/web_server/handlers/notification_handler.go index 3d61451..1e28543 100644 --- a/internal/web_server/handlers/notification_handler.go +++ b/internal/web_server/handlers/notification_handler.go @@ -3,11 +3,13 @@ package handlers import ( "context" "encoding/json" + "fmt" "net" "net/http" "strconv" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/ws" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/adaptor" @@ -101,7 +103,7 @@ func (h *Handler) ConnectSocket(c *fiber.Ctx) error { func (h *Handler) MarkNotificationAsRead(c *fiber.Ctx) error { type Request struct { - NotificationID string `json:"notification_id" validate:"required"` + NotificationIDs []string `json:"notification_ids" validate:"required"` } var req Request @@ -110,14 +112,15 @@ func (h *Handler) MarkNotificationAsRead(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } - userID, ok := c.Locals("userID").(int64) + userID, ok := c.Locals("user_id").(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) + fmt.Printf("Notification IDs: %v \n", req.NotificationIDs) + if err := h.notificationSvc.MarkAsRead(context.Background(), req.NotificationIDs, userID); err != nil { + h.logger.Error("Failed to mark notifications as read", "notificationID", req.NotificationIDs, "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update notification status") } @@ -181,17 +184,21 @@ func (h *Handler) CreateAndSendNotification(c *fiber.Ctx) error { return c.Status(fiber.StatusCreated).JSON(fiber.Map{"message": "Single notification sent successfully", "notification_id": notification.ID}) case domain.NotificationDeliverySchemeBulk: - recipients, err := h.getAllRecipientIDs(context.Background(), req.Reciever) + recipients, _, err := h.userSvc.GetAllUsers(context.Background(), user.Filter{ + Role: string(req.Reciever), + }) if err != nil { h.logger.Error("[NotificationSvc.CreateAndSendNotification] Failed to fetch recipients for bulk notification", "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch recipients") } + fmt.Printf("Number of Recipients %d \n", len(recipients)) + notificationIDs := make([]string, 0, len(recipients)) - for _, recipientID := range recipients { + for _, user := range recipients { notification := &domain.Notification{ ID: "", - RecipientID: recipientID, + RecipientID: user.ID, Type: req.Type, Level: req.Level, ErrorSeverity: req.ErrorSeverity, @@ -205,7 +212,7 @@ func (h *Handler) CreateAndSendNotification(c *fiber.Ctx) error { } if err := h.notificationSvc.SendNotification(context.Background(), notification); err != nil { - h.logger.Error("[NotificationSvc.CreateAndSendNotification] Failed to send bulk notification", "recipientID", recipientID, "error", err) + h.logger.Error("[NotificationSvc.CreateAndSendNotification] Failed to send bulk notification", "UserID", user.ID, "error", err) continue } notificationIDs = append(notificationIDs, notification.ID) @@ -258,8 +265,60 @@ func (h *Handler) GetNotifications(c *fiber.Ctx) error { "limit": limit, "offset": offset, }) + } func (h *Handler) getAllRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) { return h.notificationSvc.ListRecipientIDs(ctx, receiver) } + +func (h *Handler) CountUnreadNotifications(c *fiber.Ctx) error { + + userID, ok := c.Locals("user_id").(int64) + if !ok || userID == 0 { + h.logger.Error("[NotificationSvc.GetNotifications] Invalid user ID in context") + return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification") + } + + total, err := h.notificationSvc.CountUnreadNotifications(c.Context(), userID) + + if err != nil { + h.logger.Error("[NotificationSvc.CountUnreadNotifications] Failed to fetch unread notification count", "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notifications") + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "unread": total, + }) +} + +func (h *Handler) GetAllNotifications(c *fiber.Ctx) error { + limitStr := c.Query("limit", "10") + pageStr := c.Query("page", "1") + + // Convert limit and offset to integers + limit, err := strconv.Atoi(limitStr) + if err != nil || limit <= 0 { + h.logger.Error("[NotificationSvc.GetNotifications] Invalid limit value", "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid limit value") + } + page, err := strconv.Atoi(pageStr) + if err != nil || page <= 0 { + h.logger.Error("[NotificationSvc.GetNotifications] Invalid page value", "error", err) + return fiber.NewError(fiber.StatusBadRequest, "Invalid page value") + } + + notifications, err := h.notificationSvc.GetAllNotifications(context.Background(), limit, ((page - 1) * limit)) + if err != nil { + h.logger.Error("[NotificationSvc.GetNotifications] Failed to fetch notifications", "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notifications") + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "notifications": notifications, + "total_count": len(notifications), + "limit": limit, + "page": page, + }) + +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index e631246..06611c9 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -177,7 +177,9 @@ func (a *App) initAppRoutes() { // Notification Routes a.fiber.Get("/ws/connect", a.WebsocketAuthMiddleware, h.ConnectSocket) a.fiber.Get("/notifications", a.authMiddleware, h.GetNotifications) - a.fiber.Post("/notifications/mark-as-read", h.MarkNotificationAsRead) + a.fiber.Get("/notifications/all", a.authMiddleware, h.GetAllNotifications) + a.fiber.Post("/notifications/mark-as-read", a.authMiddleware, h.MarkNotificationAsRead) + a.fiber.Get("/notifications/unread", a.authMiddleware, h.CountUnreadNotifications) a.fiber.Post("/notifications/create", h.CreateAndSendNotification) // Virtual Game Routes diff --git a/makefile b/makefile index 5b62eb3..10f54f7 100644 --- a/makefile +++ b/makefile @@ -46,11 +46,13 @@ swagger: .PHONY: db-up db-up: - @docker compose up -d postgres + @docker compose up -d postgres migrate .PHONY: db-down db-down: @docker compose down +postgres: + @docker exec -it fortunebet-backend-postgres-1 psql -U root -d gh .PHONY: sqlc-gen sqlc-gen: