package v1 import ( "net/http" "time" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/service" applogger "github.com/gochat/gochat/pkg/logger" "github.com/gochat/gochat/pkg/pagination" "github.com/gochat/gochat/pkg/response" ) // NotificationHandler handles Notification CRUD. // Reference: Chatwoot app/controllers/api/v1/accounts/notifications_controller.rb type NotificationHandler struct { notificationService *service.NotificationService } // NewNotificationHandler creates a new Notification handler with injected service. func NewNotificationHandler(notificationService *service.NotificationService) *NotificationHandler { return &NotificationHandler{notificationService: notificationService} } // List returns all notifications for the current user in an account. // GET /api/v1/accounts/:account_id/notifications // Reference: Chatwoot index — NotificationFinder with pagination func (h *NotificationHandler) List(c *gin.Context) { if h.notificationService == nil { response.AbortWithStatusError(c, http.StatusServiceUnavailable, response.ErrInternal, "Notification service not available") return } accountID := getAccountID(c) userID := getUserID(c) p := pagination.Parse(c) notifications, total, err := h.notificationService.ListNotificationsByAccount(c.Request.Context(), userID, accountID, p.Page, p.PerPage) if err != nil { applogger.L().Errorf("List notifications: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "Failed to fetch notifications") return } response.OK(c, gin.H{ "notifications": notifications, "page": p.Page, "per_page": p.PerPage, "total": total, }) } // Get returns a single notification by ID. // GET /api/v1/accounts/:account_id/notifications/:id func (h *NotificationHandler) Get(c *gin.Context) { if h.notificationService == nil { response.AbortWithStatusError(c, http.StatusServiceUnavailable, response.ErrInternal, "Notification service not available") return } notificationID, err := parseUintParam(c, "notification_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid notification id") return } notification, svcErr := h.notificationService.GetNotification(c.Request.Context(), notificationID) if svcErr != nil { handleServiceError(c, svcErr) return } response.OK(c, notification) } // Update marks a notification as read. // PUT /api/v1/accounts/:account_id/notifications/:id // Reference: Chatwoot update — @notification.update(read_at: DateTime.now.utc); render json: @notification func (h *NotificationHandler) Update(c *gin.Context) { notificationID, err := parseUintParam(c, "notification_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid notification id") return } // MarkRead only returns error; need to fetch updated notification for response if svcErr := h.notificationService.MarkRead(c.Request.Context(), notificationID); svcErr != nil { handleServiceError(c, svcErr) return } // Return the updated notification (Chatwoot renders json: @notification) notification, svcErr := h.notificationService.GetNotification(c.Request.Context(), notificationID) if svcErr != nil { handleServiceError(c, svcErr) return } response.OK(c, notification) } // MarkAllRead marks all unread notifications as read for the current user in account. // POST /api/v1/accounts/:account_id/notifications/read_all // Reference: Chatwoot read_all — update_all(read_at: ...); head :ok func (h *NotificationHandler) MarkAllRead(c *gin.Context) { accountID := getAccountID(c) userID := getUserID(c) if err := h.notificationService.MarkAllReadByAccount(c.Request.Context(), userID, accountID); err != nil { applogger.L().Errorf("MarkAllRead notifications: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "Failed to mark notifications as read") return } c.Status(http.StatusOK) } // UnreadCount returns the unread notification count. // GET /api/v1/accounts/:account_id/notifications/unread_count // Reference: Chatwoot unread_count — render json: @unread_count func (h *NotificationHandler) UnreadCount(c *gin.Context) { userID := getUserID(c) count, err := h.notificationService.GetUnreadCount(c.Request.Context(), userID) if err != nil { applogger.L().Errorf("UnreadCount notifications: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "Failed to count unread notifications") return } response.OK(c, gin.H{"unread_count": count}) } // Snooze snoozes a notification until a specified time. // POST /api/v1/accounts/:account_id/notifications/:id/snooze // Reference: Chatwoot snooze — update(snoozed_until: ..., meta: ...) func (h *NotificationHandler) Snooze(c *gin.Context) { notificationID, err := parseUintParam(c, "notification_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid notification id") return } accountID := getAccountID(c) userID := getUserID(c) var req struct { SnoozedUntil string `json:"snoozed_until"` } if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid request body") return } snoozedUntil, parseErr := time.Parse(time.RFC3339, req.SnoozedUntil) if parseErr != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid snoozed_until format") return } notification, svcErr := h.notificationService.SnoozeNotification(c.Request.Context(), notificationID, userID, accountID, snoozedUntil) if svcErr != nil { handleServiceError(c, svcErr) return } response.OK(c, notification) } // Unread marks a notification as unread. // POST /api/v1/accounts/:account_id/notifications/:id/unread // Reference: Chatwoot unread — @notification.update(read_at: nil); render json: @notification func (h *NotificationHandler) Unread(c *gin.Context) { notificationID, err := parseUintParam(c, "notification_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid notification id") return } accountID := getAccountID(c) userID := getUserID(c) notification, svcErr := h.notificationService.MarkNotificationUnread(c.Request.Context(), notificationID, userID, accountID) if svcErr != nil { handleServiceError(c, svcErr) return } response.OK(c, notification) } // Destroy deletes a single notification. // DELETE /api/v1/accounts/:account_id/notifications/:id // Reference: Chatwoot destroy — @notification.destroy; head :ok func (h *NotificationHandler) Destroy(c *gin.Context) { notificationID, err := parseUintParam(c, "notification_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid notification id") return } if svcErr := h.notificationService.DeleteNotification(c.Request.Context(), notificationID); svcErr != nil { handleServiceError(c, svcErr) return } c.Status(http.StatusOK) } // DestroyAll deletes all (or all read) notifications. // DELETE /api/v1/accounts/:account_id/notifications/destroy_all // Reference: Chatwoot destroy_all — DeleteNotificationJob; head :ok // Query params: type=read → only read; otherwise all // Note: Chatwoot uses async DeleteNotificationJob; GoChat currently does synchronous deletion func (h *NotificationHandler) DestroyAll(c *gin.Context) { accountID := getAccountID(c) userID := getUserID(c) // Chatwoot supports type=read filter; GoChat currently deletes all regardless // TODO: implement type=read filter when async job system is available _ = c.Query("type") if err := h.notificationService.DeleteAllNotifications(c.Request.Context(), userID, accountID); err != nil { applogger.L().Errorf("DestroyAll notifications: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "Failed to delete notifications") return } c.Status(http.StatusOK) }