package handlers import ( "Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/web_server/ws" "bufio" "context" "encoding/json" "fmt" "io" "net" "net/http" "strconv" "time" "github.com/gofiber/fiber/v2" "github.com/gorilla/websocket" "github.com/resend/resend-go/v2" "go.uber.org/zap" ) func (h *Handler) ConnectSocket(c *fiber.Ctx) error { userID, ok := c.Locals("userID").(int64) if !ok || userID == 0 { h.mongoLoggerSvc.Info("Invalid user ID in context", zap.Int64("userID", userID), zap.Int("status_code", fiber.StatusUnauthorized), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification") } // Convert fasthttp request to net/http request for gorilla upgrader stdReq := &http.Request{ Method: http.MethodGet, Header: make(http.Header), } c.Context().Request.Header.VisitAll(func(key, value []byte) { stdReq.Header.Set(string(key), string(value)) }) stdReq.Host = string(c.Context().Host()) stdReq.RequestURI = string(c.Context().RequestURI()) // Hijack the underlying net.Conn from fasthttp // The hijack callback runs after the handler returns, so we must not block. c.Context().HijackSetNoResponse(true) c.Context().Hijack(func(netConn net.Conn) { // Create a hijackable response writer around the raw connection hjRW := &hijackResponseWriter{ conn: netConn, brw: bufio.NewReadWriter(bufio.NewReader(netConn), bufio.NewWriter(netConn)), h: make(http.Header), } wsConn, err := ws.Upgrader.Upgrade(hjRW, stdReq, nil) if err != nil { h.mongoLoggerSvc.Error("WebSocket upgrade failed", zap.Int64("userID", userID), zap.Error(err), zap.Time("timestamp", time.Now()), ) netConn.Close() return } client := &ws.Client{ Conn: wsConn, RecipientID: userID, } h.notificationSvc.Hub.Register <- client defer func() { h.notificationSvc.Hub.Unregister <- client wsConn.Close() }() h.mongoLoggerSvc.Info("WebSocket connection established", zap.Int64("userID", userID), zap.Time("timestamp", time.Now()), ) for { _, _, err := wsConn.ReadMessage() if err != nil { if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) { h.mongoLoggerSvc.Info("WebSocket closed normally", zap.Int64("userID", userID), zap.Time("timestamp", time.Now()), ) } else { h.mongoLoggerSvc.Info("Unexpected WebSocket closure", zap.Int64("userID", userID), zap.Error(err), zap.Time("timestamp", time.Now()), ) } break } } }) return nil } // hijackResponseWriter implements http.ResponseWriter and http.Hijacker // so gorilla/websocket can upgrade over a raw net.Conn. type hijackResponseWriter struct { conn net.Conn brw *bufio.ReadWriter h http.Header } func (w *hijackResponseWriter) Header() http.Header { return w.h } func (w *hijackResponseWriter) WriteHeader(statusCode int) {} func (w *hijackResponseWriter) Write(b []byte) (int, error) { return w.conn.Write(b) } func (w *hijackResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { return w.conn, w.brw, nil } func (h *Handler) MarkNotificationAsRead(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid notification ID", Error: err.Error(), }) } notification, err := h.notificationSvc.MarkNotificationAsRead(context.Background(), id) if err != nil { h.mongoLoggerSvc.Error("[NotificationHandler.MarkNotificationAsRead] Failed to mark notification as read", zap.Int64("notificationID", id), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to mark notification as read", Error: err.Error(), }) } return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Notification marked as read", Success: true, StatusCode: fiber.StatusOK, Data: notification, }) } func (h *Handler) MarkAllNotificationsAsRead(c *fiber.Ctx) error { userID, ok := c.Locals("user_id").(int64) if !ok || userID == 0 { return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ Message: "Invalid user identification", Error: "User ID not found in context", }) } if err := h.notificationSvc.MarkAllUserNotificationsAsRead(context.Background(), userID); err != nil { h.mongoLoggerSvc.Error("[NotificationHandler.MarkAllNotificationsAsRead] Failed to mark all notifications as read", zap.Int64("userID", userID), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to mark all notifications as read", Error: err.Error(), }) } return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "All notifications marked as read", Success: true, StatusCode: fiber.StatusOK, }) } func (h *Handler) MarkNotificationAsUnread(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid notification ID", Error: err.Error(), }) } notification, err := h.notificationSvc.MarkNotificationAsUnread(context.Background(), id) if err != nil { h.mongoLoggerSvc.Error("[NotificationHandler.MarkNotificationAsUnread] Failed to mark notification as unread", zap.Int64("notificationID", id), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to mark notification as unread", Error: err.Error(), }) } return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Notification marked as unread", Success: true, StatusCode: fiber.StatusOK, Data: notification, }) } func (h *Handler) MarkAllNotificationsAsUnread(c *fiber.Ctx) error { userID, ok := c.Locals("user_id").(int64) if !ok || userID == 0 { return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ Message: "Invalid user identification", Error: "User ID not found in context", }) } if err := h.notificationSvc.MarkAllUserNotificationsAsUnread(context.Background(), userID); err != nil { h.mongoLoggerSvc.Error("[NotificationHandler.MarkAllNotificationsAsUnread] Failed to mark all notifications as unread", zap.Int64("userID", userID), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to mark all notifications as unread", Error: err.Error(), }) } return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "All notifications marked as unread", Success: true, StatusCode: fiber.StatusOK, }) } func (h *Handler) DeleteUserNotifications(c *fiber.Ctx) error { userID, ok := c.Locals("user_id").(int64) if !ok || userID == 0 { return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ Message: "Invalid user identification", Error: "User ID not found in context", }) } if err := h.notificationSvc.DeleteUserNotifications(context.Background(), userID); err != nil { h.mongoLoggerSvc.Error("[NotificationHandler.DeleteUserNotifications] Failed to delete notifications", zap.Int64("userID", userID), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to delete notifications", Error: err.Error(), }) } return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Notifications deleted successfully", Success: true, StatusCode: fiber.StatusOK, }) } 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.mongoLoggerSvc.Info("[NotificationSvc.CreateAndSendNotification] Failed to parse request body", zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) 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 { // h.logger.Warn("[NotificationSvc.CreateAndSendNotification] Unauthorized attempt to send notification", "recipientID", req.RecipientID) // return fiber.NewError(fiber.StatusForbidden, "Unauthorized to send notification to this recipient") // } errorSeverity := domain.NotificationErrorSeverityMedium if req.ErrorSeverity != nil { errorSeverity = *req.ErrorSeverity } notification := &domain.Notification{ ID: "", RecipientID: req.RecipientID, ReceiverType: domain.ReceiverTypeFromReciever(req.Reciever), Type: req.Type, Level: req.Level, ErrorSeverity: 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.mongoLoggerSvc.Error("[NotificationSvc.CreateAndSendNotification] Failed to send single notification", zap.Int64("recipientID", req.RecipientID), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to send notification:"+err.Error()) } h.mongoLoggerSvc.Info("[NotificationSvc.CreateAndSendNotification] Single notification sent successfully", zap.Int64("recipientID", req.RecipientID), zap.String("type", string(req.Type)), zap.Time("timestamp", time.Now()), ) return c.Status(fiber.StatusCreated).JSON(fiber.Map{"message": "Single notification sent successfully", "notification_id": notification.ID}) case domain.NotificationDeliverySchemeBulk: recipients, _, err := h.userSvc.GetAllUsers(context.Background(), domain.UserFilter{ Role: string(req.Reciever), }) if err != nil { h.mongoLoggerSvc.Error("[NotificationSvc.CreateAndSendNotification] Failed to fetch recipients for bulk notification", zap.Int64("RecipientID", req.RecipientID), zap.String("Reciever", string(req.Reciever)), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch recipients:"+err.Error()) } notificationIDs := make([]string, 0, len(recipients)) for _, user := range recipients { errorSeverity := domain.NotificationErrorSeverityMedium if req.ErrorSeverity != nil { errorSeverity = *req.ErrorSeverity } notification := &domain.Notification{ ID: "", RecipientID: user.ID, ReceiverType: domain.ReceiverTypeFromReciever(req.Reciever), Type: req.Type, Level: req.Level, ErrorSeverity: 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.mongoLoggerSvc.Error("[NotificationSvc.CreateAndSendNotification] Failed to send bulk notification", zap.Int64("UserID", user.ID), zap.Error(err), zap.Time("timestamp", time.Now()), ) continue } notificationIDs = append(notificationIDs, notification.ID) } h.mongoLoggerSvc.Error("[NotificationSvc.CreateAndSendNotification] Bulk notification sent successfully", zap.Int("recipient_count", len(recipients)), zap.String("type", string(req.Type)), zap.Int("status_code", fiber.StatusCreated), zap.Error(err), zap.Time("timestamp", time.Now()), ) return c.Status(fiber.StatusCreated).JSON(fiber.Map{ "message": "Bulk notification sent successfully", "recipient_count": len(recipients), "notification_ids": notificationIDs, }) default: h.mongoLoggerSvc.Info("[NotificationSvc.CreateAndSendNotification] Invalid delivery scheme", zap.String("delivery_scheme", string(req.DeliveryScheme)), zap.Int("status_code", fiber.StatusBadRequest), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid delivery scheme") } } func (h *Handler) GetUserNotification(c *fiber.Ctx) error { limitStr := c.Query("limit", "10") offsetStr := c.Query("offset", "0") // Convert limit and offset to integers limit, err := strconv.Atoi(limitStr) if err != nil || limit <= 0 { h.mongoLoggerSvc.Info("[NotificationSvc.GetUserNotification] Invalid limit value", zap.String("limit", limitStr), zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid limit value") } offset, err := strconv.Atoi(offsetStr) if err != nil || offset < 0 { h.mongoLoggerSvc.Info("[NotificationSvc.GetUserNotification] Invalid offset value", zap.String("offset", offsetStr), zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid offset value") } userID, ok := c.Locals("user_id").(int64) if !ok || userID == 0 { h.mongoLoggerSvc.Error("[NotificationSvc.GetUserNotification] Invalid user ID in context", zap.Int64("userID", userID), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification") } notifications, total, err := h.notificationSvc.GetUserNotifications(context.Background(), userID, limit, offset) if err != nil { h.mongoLoggerSvc.Error("[NotificationSvc.GetUserNotification] Failed to fetch notifications", zap.Int64("userID", userID), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notifications:"+err.Error()) } return c.Status(fiber.StatusOK).JSON(fiber.Map{ "notifications": notifications, "total_count": total, "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.mongoLoggerSvc.Error("NotificationSvc.GetNotifications] Invalid user ID in context", zap.Int64("userID", userID), zap.Int("status_code", fiber.StatusInternalServerError), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification") } total, err := h.notificationSvc.CountUnreadNotifications(c.Context(), userID) if err != nil { h.mongoLoggerSvc.Error("[NotificationSvc.CountUnreadNotifications] Failed to fetch unread notification count", zap.Int64("userID", userID), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notifications:"+err.Error()) } 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") limit, err := strconv.Atoi(limitStr) if err != nil || limit <= 0 { return fiber.NewError(fiber.StatusBadRequest, "Invalid limit value") } page, err := strconv.Atoi(pageStr) if err != nil || page <= 0 { return fiber.NewError(fiber.StatusBadRequest, "Invalid page value") } filter := domain.NotificationFilter{ Channel: c.Query("channel"), Type: c.Query("type"), Limit: limit, Offset: (page - 1) * limit, } if uid := c.Query("user_id"); uid != "" { id, err := strconv.ParseInt(uid, 10, 64) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid user_id value") } filter.UserID = &id } if isRead := c.Query("is_read"); isRead != "" { val := isRead == "true" filter.IsRead = &val } if after := c.Query("after"); after != "" { t, err := time.Parse(time.RFC3339, after) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid after date, use RFC3339 format") } filter.After = &t } if before := c.Query("before"); before != "" { t, err := time.Parse(time.RFC3339, before) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid before date, use RFC3339 format") } filter.Before = &t } notifications, total, err := h.notificationSvc.GetFilteredNotifications(context.Background(), filter) if err != nil { h.mongoLoggerSvc.Error("[NotificationSvc.GetNotifications] Failed to fetch notifications", zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch notifications") } return c.Status(fiber.StatusOK).JSON(fiber.Map{ "notifications": notifications, "total_count": total, "limit": limit, "page": page, }) } type SendSingleAfroSMSReq struct { Recipient string `json:"recipient" validate:"required" example:"+251912345678"` Message string `json:"message" validate:"required" example:"Hello world"` } // SendSingleAfroSMS godoc // @Summary Send single SMS via AfroMessage // @Description Sends an SMS message to a single phone number using AfroMessage // @Tags user // @Accept json // @Produce json // @Param sendSMS body SendSingleAfroSMSReq true "Send SMS request" // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/sendSMS [post] func (h *Handler) SendSingleAfroSMS(c *fiber.Ctx) error { var req SendSingleAfroSMSReq if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Info("Failed to parse SendSingleAfroSMS request", zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Failed to send SMS", Error: "Invalid request body: " + err.Error(), }) } // Validate request if valErrs, ok := h.validator.Validate(c, req); !ok { var errMsg string for field, msg := range valErrs { errMsg += fmt.Sprintf("%s: %s; ", field, msg) } return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Failed to send SMS", Error: errMsg, }) } // Send SMS via service if err := h.notificationSvc.SendAfroMessageSMSTemp( c.Context(), req.Recipient, req.Message, nil, ); err != nil { h.mongoLoggerSvc.Error("Failed to send AfroMessage SMS", zap.String("phone_number", req.Recipient), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to send SMS", Error: err.Error(), }) } return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "SMS sent successfully", Success: true, StatusCode: fiber.StatusOK, Data: req, }) } func (h *Handler) RegisterDeviceToken(c *fiber.Ctx) error { type Request struct { DeviceToken string `json:"device_token" validate:"required"` Platform string `json:"platform" validate:"required,oneof=android ios web"` } var req Request if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Info("[NotificationHandler.RegisterDeviceToken] Failed to parse request body", zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } userID, ok := c.Locals("user_id").(int64) if !ok || userID == 0 { h.mongoLoggerSvc.Error("[NotificationHandler.RegisterDeviceToken] Invalid user ID in context", zap.Int("status_code", fiber.StatusUnauthorized), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification") } if err := h.userSvc.RegisterDevice(c.Context(), userID, req.DeviceToken, req.Platform); err != nil { h.mongoLoggerSvc.Error("[NotificationHandler.RegisterDeviceToken] Failed to register device token", zap.Int64("userID", userID), zap.String("platform", req.Platform), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to register device token: "+err.Error()) } h.mongoLoggerSvc.Info("[NotificationHandler.RegisterDeviceToken] Device token registered successfully", zap.Int64("userID", userID), zap.String("platform", req.Platform), zap.Time("timestamp", time.Now()), ) return c.Status(fiber.StatusCreated).JSON(domain.Response{ Message: "Device token registered successfully", Success: true, StatusCode: fiber.StatusCreated, }) } // SendTestPushNotification sends a test push notification to the authenticated user // @Summary Send test push notification // @Description Sends a test push notification to all registered devices of the current user // @Tags notifications // @Accept json // @Produce json // @Param body body object{title=string,message=string} true "Test notification content" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 401 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/notifications/test-push [post] func (h *Handler) SendTestPushNotification(c *fiber.Ctx) error { title := c.FormValue("title", "Test Push Notification") message := c.FormValue("message", "This is a test push notification from Yimaru Backend") userID, ok := c.Locals("user_id").(int64) if !ok || userID == 0 { return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{ Message: "Invalid user identification", Error: "User ID not found in context", }) } // Get user's device tokens first to provide feedback tokens, err := h.userSvc.GetUserDeviceTokens(c.Context(), userID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to get device tokens", Error: err.Error(), }) } if len(tokens) == 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "No registered devices found", Error: "Please register a device token first using POST /devices/register", }) } // Handle optional image upload var imageURL string if _, err := c.FormFile("file"); err == nil { savedPath, saveErr := h.processAndSaveThumbnail(c, "notification_images") if saveErr != nil { return saveErr } imageURL = c.BaseURL() + savedPath } // Create test notification notification := &domain.Notification{ RecipientID: userID, Type: "system_alert", DeliveryChannel: domain.DeliveryChannelPush, Image: imageURL, Payload: domain.NotificationPayload{ Headline: title, Message: message, }, } // Send push notification err = h.notificationSvc.SendPushNotification(c.Context(), notification) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to send push notification", Error: err.Error(), }) } // Record in DB for history h.notificationSvc.RecordNotification(c.Context(), userID, domain.NOTIFICATION_TYPE_SYSTEM_ALERT, domain.DeliveryChannelPush, domain.NotificationLevelInfo, title, message) h.mongoLoggerSvc.Info("[NotificationHandler.SendTestPushNotification] Test push sent", zap.Int64("userID", userID), zap.Int("deviceCount", len(tokens)), zap.Time("timestamp", time.Now()), ) return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Test push notification sent successfully", Success: true, StatusCode: fiber.StatusOK, Data: map[string]interface{}{ "devices_count": len(tokens), "title": title, "message": message, "image": imageURL, }, }) } // SendBulkPushNotification sends a push notification to multiple users or all users of a given role. // @Summary Send bulk push notification // @Description Sends a push notification to specified user IDs or all users matching a role. Optionally schedule for later with scheduled_at (RFC3339). // @Tags notifications // @Accept json // @Produce json // @Param body body object{title=string,message=string,image=string,user_ids=[]int64,role=string,scheduled_at=string} true "Bulk push content" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/notifications/bulk-push [post] func (h *Handler) SendBulkPushNotification(c *fiber.Ctx) error { title := c.FormValue("title") message := c.FormValue("message") role := c.FormValue("role") userIDsRaw := c.FormValue("user_ids") scheduledAtRaw := c.FormValue("scheduled_at") if title == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Title is required", }) } if message == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Message is required", }) } // Parse user_ids from JSON array string e.g. "[1,2,3]" var userIDs []int64 if userIDsRaw != "" { if err := json.Unmarshal([]byte(userIDsRaw), &userIDs); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid user_ids format", Error: "user_ids must be a JSON array of integers, e.g. [1,2,3]", }) } } if len(userIDs) == 0 && role == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "No target users specified", Error: "Provide either user_ids or role", }) } // Schedule for later if scheduled_at is provided if scheduledAtRaw != "" { scheduledAt, err := time.Parse(time.RFC3339, scheduledAtRaw) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid scheduled_at format, use RFC3339 (e.g. 2025-06-01T10:00:00Z)", Error: err.Error(), }) } if scheduledAt.Before(time.Now()) { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "scheduled_at must be in the future", }) } creatorID, _ := c.Locals("user_id").(int64) sn := &domain.ScheduledNotification{ Channel: domain.DeliveryChannelPush, Title: title, Message: message, ScheduledAt: scheduledAt, TargetUserIDs: userIDs, TargetRole: role, CreatedBy: creatorID, } created, err := h.notificationSvc.CreateScheduledNotification(c.Context(), sn) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to schedule push notification", Error: err.Error(), }) } return c.Status(fiber.StatusCreated).JSON(domain.Response{ Message: "Push notification scheduled", Success: true, StatusCode: fiber.StatusCreated, Data: created, }) } // Determine target user IDs by role if no specific IDs given if len(userIDs) == 0 && role != "" { users, _, err := h.userSvc.GetAllUsers(context.Background(), domain.UserFilter{Role: role}) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to fetch users for role", Error: err.Error(), }) } for _, u := range users { userIDs = append(userIDs, u.ID) } } if len(userIDs) == 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "No target users found", }) } // Handle optional image upload var imageURL string if _, err := c.FormFile("file"); err == nil { savedPath, saveErr := h.processAndSaveThumbnail(c, "notification_images") if saveErr != nil { return saveErr } imageURL = c.BaseURL() + savedPath } notification := &domain.Notification{ Type: "system_alert", DeliveryChannel: domain.DeliveryChannelPush, Image: imageURL, Payload: domain.NotificationPayload{ Headline: title, Message: message, }, } sent, failed, err := h.notificationSvc.SendBulkPushNotification(c.Context(), userIDs, notification) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to send bulk push notification", Error: err.Error(), }) } // Record in DB for history for _, uid := range userIDs { h.notificationSvc.RecordNotification(c.Context(), uid, domain.NOTIFICATION_TYPE_SYSTEM_ALERT, domain.DeliveryChannelPush, domain.NotificationLevelInfo, title, message) } h.mongoLoggerSvc.Info("[NotificationHandler.SendBulkPushNotification] Bulk push sent", zap.Int("targetUsers", len(userIDs)), zap.Int("sent", sent), zap.Int("failed", failed), zap.Time("timestamp", time.Now()), ) return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Bulk push notification sent", Success: true, StatusCode: fiber.StatusOK, Data: map[string]interface{}{ "target_users": len(userIDs), "sent": sent, "failed": failed, "image": imageURL, }, }) } // SendBulkSMS sends an SMS to multiple users by user IDs, role, or direct phone numbers. // @Summary Send bulk SMS // @Description Sends an SMS to specified user IDs, all users of a role, or direct phone numbers. Optionally schedule for later with scheduled_at (RFC3339). // @Tags notifications // @Accept json // @Produce json // @Param body body object{message=string,user_ids=[]int64,role=string,phone_numbers=[]string,scheduled_at=string} true "Bulk SMS content" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/notifications/bulk-sms [post] func (h *Handler) SendBulkSMS(c *fiber.Ctx) error { type Request struct { Message string `json:"message" validate:"required"` UserIDs []int64 `json:"user_ids"` Role string `json:"role"` PhoneNumbers []string `json:"phone_numbers"` ScheduledAt string `json:"scheduled_at"` } var req Request if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } if req.Message == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Message is required", }) } if len(req.UserIDs) == 0 && req.Role == "" && len(req.PhoneNumbers) == 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "No recipients specified", Error: "Provide user_ids, role, or phone_numbers", }) } // Schedule for later if scheduled_at is provided if req.ScheduledAt != "" { scheduledAt, err := time.Parse(time.RFC3339, req.ScheduledAt) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid scheduled_at format, use RFC3339 (e.g. 2025-06-01T10:00:00Z)", Error: err.Error(), }) } if scheduledAt.Before(time.Now()) { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "scheduled_at must be in the future", }) } creatorID, _ := c.Locals("user_id").(int64) var targetRaw json.RawMessage if len(req.PhoneNumbers) > 0 { raw := domain.ScheduledNotificationTargetRaw{Phones: req.PhoneNumbers} targetRaw, _ = json.Marshal(raw) } sn := &domain.ScheduledNotification{ Channel: domain.DeliveryChannelSMS, Message: req.Message, ScheduledAt: scheduledAt, TargetUserIDs: req.UserIDs, TargetRole: req.Role, TargetRaw: targetRaw, CreatedBy: creatorID, } created, err := h.notificationSvc.CreateScheduledNotification(c.Context(), sn) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to schedule SMS", Error: err.Error(), }) } return c.Status(fiber.StatusCreated).JSON(domain.Response{ Message: "SMS scheduled", Success: true, StatusCode: fiber.StatusCreated, Data: created, }) } // Collect phone numbers from all sources phoneNumbers := make(map[string]struct{}) // Add directly provided phone numbers for _, p := range req.PhoneNumbers { phoneNumbers[p] = struct{}{} } // Collect user IDs from role if needed userIDs := req.UserIDs if len(userIDs) == 0 && req.Role != "" { users, _, err := h.userSvc.GetAllUsers(context.Background(), domain.UserFilter{Role: req.Role}) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to fetch users for role", Error: err.Error(), }) } for _, u := range users { userIDs = append(userIDs, u.ID) } } // Resolve user IDs to phone numbers for _, uid := range userIDs { user, err := h.userSvc.GetUserByID(context.Background(), uid) if err != nil { h.mongoLoggerSvc.Warn("[NotificationHandler.SendBulkSMS] Failed to get user", zap.Int64("userID", uid), zap.Error(err), ) continue } if user.PhoneNumber != "" { phoneNumbers[user.PhoneNumber] = struct{}{} } } if len(phoneNumbers) == 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "No recipients found", Error: "Provide user_ids, role, or phone_numbers", }) } // Flatten to slice recipients := make([]string, 0, len(phoneNumbers)) for p := range phoneNumbers { recipients = append(recipients, p) } sent, failed := h.notificationSvc.SendBulkSMS(c.Context(), recipients, req.Message) // Record in DB for history (only for known users) for _, uid := range userIDs { h.notificationSvc.RecordNotification(c.Context(), uid, domain.NOTIFICATION_TYPE_SYSTEM_ALERT, domain.DeliveryChannelSMS, domain.NotificationLevelInfo, "SMS Notification", req.Message) } h.mongoLoggerSvc.Info("[NotificationHandler.SendBulkSMS] Bulk SMS sent", zap.Int("totalRecipients", len(recipients)), zap.Int("sent", sent), zap.Int("failed", failed), zap.Time("timestamp", time.Now()), ) return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Bulk SMS sent", Success: true, StatusCode: fiber.StatusOK, Data: map[string]interface{}{ "total_recipients": len(recipients), "sent": sent, "failed": failed, }, }) } // GetScheduledNotification retrieves a single scheduled notification by ID. // @Summary Get scheduled notification // @Description Returns a single scheduled notification by its ID // @Tags notifications // @Produce json // @Param id path int true "Scheduled Notification ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/notifications/scheduled/{id} [get] func (h *Handler) GetScheduledNotification(c *fiber.Ctx) error { id, err := strconv.ParseInt(c.Params("id"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid ID", Error: err.Error(), }) } sn, err := h.notificationSvc.GetScheduledNotification(c.Context(), id) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to get scheduled notification", Error: err.Error(), }) } return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Scheduled notification retrieved", Success: true, StatusCode: fiber.StatusOK, Data: sn, }) } // ListScheduledNotifications lists scheduled notifications with optional filters. // @Summary List scheduled notifications // @Description Returns paginated scheduled notifications with optional status, channel, and date filters // @Tags notifications // @Produce json // @Param status query string false "Filter by status" // @Param channel query string false "Filter by channel" // @Param after query string false "Filter after date (RFC3339)" // @Param before query string false "Filter before date (RFC3339)" // @Param limit query int false "Page size" default(20) // @Param page query int false "Page number" default(1) // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/notifications/scheduled [get] func (h *Handler) ListScheduledNotifications(c *fiber.Ctx) error { limit, err := strconv.Atoi(c.Query("limit", "20")) if err != nil || limit <= 0 { limit = 20 } page, err := strconv.Atoi(c.Query("page", "1")) if err != nil || page <= 0 { page = 1 } filter := domain.ScheduledNotificationFilter{ Status: c.Query("status"), Channel: c.Query("channel"), Limit: limit, Offset: (page - 1) * limit, } if after := c.Query("after"); after != "" { t, err := time.Parse(time.RFC3339, after) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid after date, use RFC3339 format", Error: err.Error(), }) } filter.After = &t } if before := c.Query("before"); before != "" { t, err := time.Parse(time.RFC3339, before) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid before date, use RFC3339 format", Error: err.Error(), }) } filter.Before = &t } notifications, total, err := h.notificationSvc.ListScheduledNotifications(c.Context(), filter) if err != nil { h.mongoLoggerSvc.Error("[NotificationHandler.ListScheduledNotifications] Failed", zap.Error(err), zap.Time("timestamp", time.Now()), ) return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to list scheduled notifications", Error: err.Error(), }) } return c.Status(fiber.StatusOK).JSON(fiber.Map{ "scheduled_notifications": notifications, "total_count": total, "limit": limit, "page": page, }) } // CancelScheduledNotification cancels a pending or processing scheduled notification. // @Summary Cancel scheduled notification // @Description Cancels a scheduled notification if it is still pending or processing // @Tags notifications // @Produce json // @Param id path int true "Scheduled Notification ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/notifications/scheduled/{id}/cancel [post] func (h *Handler) CancelScheduledNotification(c *fiber.Ctx) error { id, err := strconv.ParseInt(c.Params("id"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid ID", Error: err.Error(), }) } cancelled, err := h.notificationSvc.CancelScheduledNotification(c.Context(), id) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to cancel scheduled notification", Error: err.Error(), }) } return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Scheduled notification cancelled", Success: true, StatusCode: fiber.StatusOK, Data: cancelled, }) } // parseEmailAttachment reads an optional "file" from the multipart form and returns a Resend attachment. func (h *Handler) parseEmailAttachment(c *fiber.Ctx) []*resend.Attachment { fileHeader, err := c.FormFile("file") if err != nil { return nil } fh, err := fileHeader.Open() if err != nil { return nil } defer fh.Close() data, err := io.ReadAll(fh) if err != nil { return nil } return []*resend.Attachment{ { Content: data, Filename: fileHeader.Filename, }, } } // SendSingleEmail sends an email to a single recipient with an optional image attachment. // @Summary Send single email // @Description Sends an email to a single email address with optional image attachment // @Tags notifications // @Accept multipart/form-data // @Produce json // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/notifications/send-email [post] func (h *Handler) SendSingleEmail(c *fiber.Ctx) error { recipient := c.FormValue("recipient") subject := c.FormValue("subject") message := c.FormValue("message") html := c.FormValue("html") if recipient == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Recipient is required", }) } if subject == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Subject is required", }) } if message == "" && html == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Either message or html is required", }) } attachments := h.parseEmailAttachment(c) if err := h.notificationSvc.MessengerSvc().SendEmailWithAttachments( c.Context(), recipient, message, html, subject, attachments, ); err != nil { h.mongoLoggerSvc.Error("[NotificationHandler.SendSingleEmail] Failed to send email", zap.String("recipient", recipient), zap.Error(err), zap.Time("timestamp", time.Now()), ) return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to send email", Error: err.Error(), }) } return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Email sent successfully", Success: true, StatusCode: fiber.StatusOK, }) } // SendBulkEmail sends an email to multiple users by user IDs, role, or direct email addresses. // @Summary Send bulk email // @Description Sends an email to specified user IDs, all users of a role, or direct email addresses with optional image attachment. Optionally schedule for later with scheduled_at (RFC3339). // @Tags notifications // @Accept multipart/form-data // @Produce json // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/notifications/bulk-email [post] func (h *Handler) SendBulkEmail(c *fiber.Ctx) error { subject := c.FormValue("subject") message := c.FormValue("message") html := c.FormValue("html") role := c.FormValue("role") userIDsRaw := c.FormValue("user_ids") emailsRaw := c.FormValue("emails") scheduledAtRaw := c.FormValue("scheduled_at") if subject == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Subject is required", }) } if message == "" && html == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Either message or html is required", }) } // Parse direct emails var directEmails []string if emailsRaw != "" { if err := json.Unmarshal([]byte(emailsRaw), &directEmails); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid emails format", Error: "emails must be a JSON array of strings", }) } } // Parse user_ids var userIDs []int64 if userIDsRaw != "" { if err := json.Unmarshal([]byte(userIDsRaw), &userIDs); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid user_ids format", Error: "user_ids must be a JSON array of integers", }) } } if len(userIDs) == 0 && role == "" && len(directEmails) == 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "No recipients specified", Error: "Provide user_ids, role, or emails", }) } // Schedule for later if scheduled_at is provided if scheduledAtRaw != "" { scheduledAt, err := time.Parse(time.RFC3339, scheduledAtRaw) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid scheduled_at format, use RFC3339 (e.g. 2025-06-01T10:00:00Z)", Error: err.Error(), }) } if scheduledAt.Before(time.Now()) { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "scheduled_at must be in the future", }) } creatorID, _ := c.Locals("user_id").(int64) var targetRaw json.RawMessage if len(directEmails) > 0 { raw := domain.ScheduledNotificationTargetRaw{Emails: directEmails} targetRaw, _ = json.Marshal(raw) } sn := &domain.ScheduledNotification{ Channel: domain.DeliveryChannelEmail, Title: subject, Message: message, HTML: html, ScheduledAt: scheduledAt, TargetUserIDs: userIDs, TargetRole: role, TargetRaw: targetRaw, CreatedBy: creatorID, } created, err := h.notificationSvc.CreateScheduledNotification(c.Context(), sn) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to schedule email", Error: err.Error(), }) } return c.Status(fiber.StatusCreated).JSON(domain.Response{ Message: "Email scheduled", Success: true, StatusCode: fiber.StatusCreated, Data: created, }) } // Immediate send: collect emails from all sources emailSet := make(map[string]struct{}) for _, e := range directEmails { emailSet[e] = struct{}{} } if len(userIDs) == 0 && role != "" { users, _, err := h.userSvc.GetAllUsers(context.Background(), domain.UserFilter{Role: role}) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to fetch users for role", Error: err.Error(), }) } for _, u := range users { userIDs = append(userIDs, u.ID) } } for _, uid := range userIDs { user, err := h.userSvc.GetUserByID(context.Background(), uid) if err != nil { h.mongoLoggerSvc.Warn("[NotificationHandler.SendBulkEmail] Failed to get user", zap.Int64("userID", uid), zap.Error(err), ) continue } if user.Email != "" { emailSet[user.Email] = struct{}{} } } if len(emailSet) == 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "No recipients found", Error: "Provide user_ids, role, or emails", }) } recipients := make([]string, 0, len(emailSet)) for e := range emailSet { recipients = append(recipients, e) } attachments := h.parseEmailAttachment(c) sent, failed := h.notificationSvc.SendBulkEmail(c.Context(), recipients, subject, message, html, attachments) // Record in DB for history (only for known users) for _, uid := range userIDs { h.notificationSvc.RecordNotification(c.Context(), uid, domain.NOTIFICATION_TYPE_SYSTEM_ALERT, domain.DeliveryChannelEmail, domain.NotificationLevelInfo, subject, message) } h.mongoLoggerSvc.Info("[NotificationHandler.SendBulkEmail] Bulk email sent", zap.Int("totalRecipients", len(recipients)), zap.Int("sent", sent), zap.Int("failed", failed), zap.Time("timestamp", time.Now()), ) return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Bulk email sent", Success: true, StatusCode: fiber.StatusOK, Data: map[string]interface{}{ "total_recipients": len(recipients), "sent": sent, "failed": failed, }, }) } func (h *Handler) UnregisterDeviceToken(c *fiber.Ctx) error { type Request struct { DeviceToken string `json:"device_token" validate:"required"` } var req Request if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Info("[NotificationHandler.UnregisterDeviceToken] Failed to parse request body", zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } userID, ok := c.Locals("user_id").(int64) if !ok || userID == 0 { h.mongoLoggerSvc.Error("[NotificationHandler.UnregisterDeviceToken] Invalid user ID in context", zap.Int("status_code", fiber.StatusUnauthorized), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification") } if err := h.userSvc.DeactivateDevice(c.Context(), userID, req.DeviceToken); err != nil { h.mongoLoggerSvc.Error("[NotificationHandler.UnregisterDeviceToken] Failed to unregister device token", zap.Int64("userID", userID), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to unregister device token: "+err.Error()) } h.mongoLoggerSvc.Info("[NotificationHandler.UnregisterDeviceToken] Device token unregistered successfully", zap.Int64("userID", userID), zap.Time("timestamp", time.Now()), ) return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Device token unregistered successfully", Success: true, StatusCode: fiber.StatusOK, }) }