diff --git a/db/query/notification.sql b/db/query/notification.sql index 170f46b..22bae8d 100644 --- a/db/query/notification.sql +++ b/db/query/notification.sql @@ -16,3 +16,6 @@ UPDATE notifications SET delivery_status = $2, is_read = $3, metadata = $4 WHERE -- name: ListFailedNotifications :many 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; diff --git a/gen/db/notification.sql.go b/gen/db/notification.sql.go index d784202..3735d72 100644 --- a/gen/db/notification.sql.go +++ b/gen/db/notification.sql.go @@ -181,6 +181,30 @@ func (q *Queries) ListNotifications(ctx context.Context, arg ListNotificationsPa return items, nil } +const ListRecipientIDsByReceiver = `-- name: ListRecipientIDsByReceiver :many +SELECT recipient_id FROM notifications WHERE reciever = $1 +` + +func (q *Queries) ListRecipientIDsByReceiver(ctx context.Context, reciever string) ([]int64, error) { + rows, err := q.db.Query(ctx, ListRecipientIDsByReceiver, reciever) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int64 + for rows.Next() { + var recipient_id int64 + if err := rows.Scan(&recipient_id); err != nil { + return nil, err + } + items = append(items, recipient_id) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + 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 ` diff --git a/internal/repository/notification.go b/internal/repository/notification.go index 215209c..eb922a9 100644 --- a/internal/repository/notification.go +++ b/internal/repository/notification.go @@ -14,6 +14,7 @@ type NotificationRepository interface { UpdateNotificationStatus(ctx context.Context, id, status string, isRead bool, metadata []byte) (*domain.Notification, error) 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) } type Repository struct { @@ -119,6 +120,10 @@ func (r *Repository) ListFailedNotifications(ctx context.Context, limit int) ([] return result, nil } +func (r *Repository) ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) { + return r.store.queries.ListRecipientIDsByReceiver(ctx, string(receiver)) +} + func (r *Repository) mapDBToDomain(dbNotif *dbgen.Notification) *domain.Notification { var errorSeverity *domain.NotificationErrorSeverity if dbNotif.ErrorSeverity.Valid { diff --git a/internal/services/notfication/port.go b/internal/services/notfication/port.go index 9ddff29..9fa2f72 100644 --- a/internal/services/notfication/port.go +++ b/internal/services/notfication/port.go @@ -15,4 +15,5 @@ type NotificationStore interface { DisconnectWebSocket(recipientID int64) 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 } diff --git a/internal/services/notfication/service.go b/internal/services/notfication/service.go index d1aa52b..1ca03a3 100644 --- a/internal/services/notfication/service.go +++ b/internal/services/notfication/service.go @@ -153,6 +153,10 @@ func (s *Service) startWorker() { } } +func (s *Service) ListRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) { + return s.repo.ListRecipientIDs(ctx, receiver) +} + func (s *Service) handleNotification(notification *domain.Notification) { ctx := context.Background() diff --git a/internal/web_server/handlers/notification_handler.go b/internal/web_server/handlers/notification_handler.go index 9e41b0e..9d8ca1a 100644 --- a/internal/web_server/handlers/notification_handler.go +++ b/internal/web_server/handlers/notification_handler.go @@ -2,7 +2,9 @@ package handlers import ( "context" + "encoding/json" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/gofiber/fiber/v2" "github.com/gofiber/websocket/v2" ) @@ -74,3 +76,107 @@ func (h *Handler) MarkNotificationAsRead(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(fiber.Map{"message": "Notification marked as read"}) } + +func (h *Handler) CreateAndSendNotification(c *fiber.Ctx) error { + type Request struct { + RecipientID int64 `json:"recipient_id" validate:"required_if=DeliveryScheme single"` + Type domain.NotificationType `json:"type" validate:"required"` + Level domain.NotificationLevel `json:"level" validate:"required"` + ErrorSeverity *domain.NotificationErrorSeverity `json:"error_severity"` + Reciever domain.NotificationRecieverSide `json:"reciever" validate:"required"` + DeliveryScheme domain.NotificationDeliveryScheme `json:"delivery_scheme" validate:"required"` + DeliveryChannel domain.DeliveryChannel `json:"delivery_channel" validate:"required"` + Payload domain.NotificationPayload `json:"payload" validate:"required"` + Priority int `json:"priority"` + Metadata json.RawMessage `json:"metadata,omitempty"` + } + + var req Request + if err := c.BodyParser(&req); err != nil { + h.logger.Error("[NotificationSvc.CreateAndSendNotification] 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("[NotificationSvc.CreateAndSendNotification] Invalid user ID in context") + return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification") + } + + switch req.DeliveryScheme { + case domain.NotificationDeliverySchemeSingle: + if req.Reciever == domain.NotificationRecieverSideCustomer && req.RecipientID != userID { + h.logger.Warn("[NotificationSvc.CreateAndSendNotification] Unauthorized attempt to send notification", "userID", userID, "recipientID", req.RecipientID) + return fiber.NewError(fiber.StatusForbidden, "Unauthorized to send notification to this recipient") + } + + notification := &domain.Notification{ + ID: "", + RecipientID: req.RecipientID, + Type: req.Type, + Level: req.Level, + ErrorSeverity: req.ErrorSeverity, + Reciever: req.Reciever, + IsRead: false, + DeliveryStatus: domain.DeliveryStatusPending, + DeliveryChannel: req.DeliveryChannel, + Payload: req.Payload, + Priority: req.Priority, + Metadata: req.Metadata, + } + + if err := h.notificationSvc.SendNotification(context.Background(), notification); err != nil { + h.logger.Error("[NotificationSvc.CreateAndSendNotification] Failed to send single notification", "recipientID", req.RecipientID, "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to send notification") + } + + h.logger.Info("[NotificationSvc.CreateAndSendNotification] Single notification sent successfully", "recipientID", req.RecipientID, "type", req.Type) + 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) + 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") + } + + notificationIDs := make([]string, 0, len(recipients)) + for _, recipientID := range recipients { + notification := &domain.Notification{ + ID: "", + RecipientID: recipientID, + Type: req.Type, + Level: req.Level, + ErrorSeverity: req.ErrorSeverity, + Reciever: req.Reciever, + IsRead: false, + DeliveryStatus: domain.DeliveryStatusPending, + DeliveryChannel: req.DeliveryChannel, + Payload: req.Payload, + Priority: req.Priority, + Metadata: req.Metadata, + } + + if err := h.notificationSvc.SendNotification(context.Background(), notification); err != nil { + h.logger.Error("[NotificationSvc.CreateAndSendNotification] Failed to send bulk notification", "recipientID", recipientID, "error", err) + continue + } + notificationIDs = append(notificationIDs, notification.ID) + } + + h.logger.Info("[NotificationSvc.CreateAndSendNotification] Bulk notification sent successfully", "recipient_count", len(recipients), "type", req.Type) + return c.Status(fiber.StatusCreated).JSON(fiber.Map{ + "message": "Bulk notification sent successfully", + "recipient_count": len(recipients), + "notification_ids": notificationIDs, + }) + + default: + h.logger.Error("[NotificationSvc.CreateAndSendNotification] Invalid delivery scheme", "delivery_scheme", req.DeliveryScheme) + return fiber.NewError(fiber.StatusBadRequest, "Invalid delivery scheme") + } +} + +func (h *Handler) getAllRecipientIDs(ctx context.Context, receiver domain.NotificationRecieverSide) ([]int64, error) { + return h.notificationSvc.ListRecipientIDs(ctx, receiver) +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 3c36302..9142afe 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -45,6 +45,7 @@ func (a *App) initAppRoutes() { a.fiber.Get("/notifications/ws/connect/:recipientID", handler.ConnectSocket) a.fiber.Post("/notifications/mark-as-read", handler.MarkNotificationAsRead) + a.fiber.Post("/notifications/create", handler.CreateAndSendNotification) } ///user/profile get