feat(notifications): tighten chatwoot scoping
This commit is contained in:
@@ -2,7 +2,9 @@ package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -81,7 +83,9 @@ func (h *NotificationHandler) Get(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
notification, svcErr := h.notificationService.GetNotification(c.Request.Context(), notificationID)
|
||||
accountID := getAccountID(c)
|
||||
userID := getUserID(c)
|
||||
notification, svcErr := h.notificationService.GetNotificationByAccount(c.Request.Context(), notificationID, userID, accountID)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
@@ -99,15 +103,10 @@ func (h *NotificationHandler) Update(c *gin.Context) {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid notification id")
|
||||
return
|
||||
}
|
||||
accountID := getAccountID(c)
|
||||
userID := getUserID(c)
|
||||
|
||||
// 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)
|
||||
notification, svcErr := h.notificationService.MarkReadByAccount(c.Request.Context(), notificationID, userID, accountID)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
@@ -147,9 +146,10 @@ func (h *NotificationHandler) MarkAllRead(c *gin.Context) {
|
||||
// GET /api/v1/accounts/:account_id/notifications/unread_count
|
||||
// Reference: Chatwoot unread_count — render json: @unread_count
|
||||
func (h *NotificationHandler) UnreadCount(c *gin.Context) {
|
||||
accountID := getAccountID(c)
|
||||
userID := getUserID(c)
|
||||
|
||||
count, err := h.notificationService.GetUnreadCount(c.Request.Context(), userID)
|
||||
count, err := h.notificationService.GetUnreadCountByAccount(c.Request.Context(), userID, accountID)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("UnreadCount notifications: %v", err)
|
||||
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "Failed to count unread notifications")
|
||||
@@ -173,14 +173,23 @@ func (h *NotificationHandler) Snooze(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
|
||||
var req struct {
|
||||
SnoozedUntil string `json:"snoozed_until"`
|
||||
SnoozedUntil interface{} `json:"snoozed_until"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
if err := bindOptionalNotificationJSON(c, &req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
if req.SnoozedUntil == nil || fmt.Sprint(req.SnoozedUntil) == "" {
|
||||
notification, svcErr := h.notificationService.GetNotificationByAccount(c.Request.Context(), notificationID, userID, accountID)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, serializeNotification(notification))
|
||||
return
|
||||
}
|
||||
|
||||
snoozedUntil, parseErr := time.Parse(time.RFC3339, req.SnoozedUntil)
|
||||
snoozedUntil, parseErr := parseNotificationUnixTime(req.SnoozedUntil)
|
||||
if parseErr != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid snoozed_until format")
|
||||
return
|
||||
@@ -227,7 +236,9 @@ func (h *NotificationHandler) Destroy(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if svcErr := h.notificationService.DeleteNotification(c.Request.Context(), notificationID); svcErr != nil {
|
||||
accountID := getAccountID(c)
|
||||
userID := getUserID(c)
|
||||
if svcErr := h.notificationService.DeleteNotificationByAccount(c.Request.Context(), notificationID, userID, accountID); svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
@@ -341,3 +352,26 @@ func bindOptionalNotificationJSON(c *gin.Context, target interface{}) error {
|
||||
}
|
||||
return c.ShouldBindJSON(target)
|
||||
}
|
||||
|
||||
func parseNotificationUnixTime(value interface{}) (time.Time, error) {
|
||||
switch v := value.(type) {
|
||||
case float64:
|
||||
return time.Unix(int64(v), 0).UTC(), nil
|
||||
case int64:
|
||||
return time.Unix(v, 0).UTC(), nil
|
||||
case int:
|
||||
return time.Unix(int64(v), 0).UTC(), nil
|
||||
case string:
|
||||
seconds, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return time.Unix(seconds, 0).UTC(), nil
|
||||
default:
|
||||
seconds, err := strconv.ParseInt(fmt.Sprint(v), 10, 64)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return time.Unix(seconds, 0).UTC(), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,14 +54,16 @@ func setupNotificationRouter(handler *NotificationHandler) *gin.Engine {
|
||||
c.Next()
|
||||
})
|
||||
router.GET("/api/v1/accounts/:account_id/notifications", handler.List)
|
||||
router.GET("/api/v1/accounts/:account_id/notifications/:notification_id", handler.Get)
|
||||
router.POST("/api/v1/accounts/:account_id/notifications/read_all", handler.MarkAllRead)
|
||||
router.GET("/api/v1/accounts/:account_id/notifications/unread_count", handler.UnreadCount)
|
||||
router.POST("/api/v1/accounts/:account_id/notifications/destroy_all", handler.DestroyAll)
|
||||
router.DELETE("/api/v1/accounts/:account_id/notifications/destroy_all", handler.DestroyAll)
|
||||
router.GET("/api/v1/accounts/:account_id/notifications/:notification_id", handler.Get)
|
||||
router.PUT("/api/v1/accounts/:account_id/notifications/:notification_id", handler.Update)
|
||||
router.DELETE("/api/v1/accounts/:account_id/notifications/:notification_id", handler.Destroy)
|
||||
// G8 extension routes
|
||||
router.POST("/api/v1/accounts/:account_id/notifications/:notification_id/snooze", handler.Snooze)
|
||||
router.POST("/api/v1/accounts/:account_id/notifications/:notification_id/unread", handler.Unread)
|
||||
router.POST("/api/v1/accounts/:account_id/notifications/destroy_all", handler.DestroyAll)
|
||||
router.DELETE("/api/v1/accounts/:account_id/notifications/destroy_all", handler.DestroyAll)
|
||||
return router
|
||||
}
|
||||
|
||||
@@ -174,6 +176,87 @@ func TestNotificationGetDifferentID(t *testing.T) {
|
||||
sqlDB.Close()
|
||||
}
|
||||
|
||||
func TestNotificationMutationsAreScopedToCurrentUserAndAccount(t *testing.T) {
|
||||
db := setupNotificationDB(t)
|
||||
handler := setupNotificationHandler(t, db)
|
||||
router := setupNotificationRouter(handler)
|
||||
|
||||
user := &model.User{Name: "Scoped User", Email: "scoped@example.com", Password: "pass", AccountID: 1}
|
||||
otherUser := &model.User{Name: "Other User", Email: "scoped-other@example.com", Password: "pass", AccountID: 2}
|
||||
require.NoError(t, db.Create(user).Error)
|
||||
require.NoError(t, db.Create(otherUser).Error)
|
||||
accountID := uint(1)
|
||||
otherAccountID := uint(2)
|
||||
own := &model.Notification{UserID: user.ID, AccountID: &accountID, NotificationType: "message_created", PrimaryActorType: "Conversation", PrimaryActorID: 1}
|
||||
otherAccount := &model.Notification{UserID: user.ID, AccountID: &otherAccountID, NotificationType: "message_created", PrimaryActorType: "Conversation", PrimaryActorID: 2}
|
||||
otherOwner := &model.Notification{UserID: otherUser.ID, AccountID: &accountID, NotificationType: "message_created", PrimaryActorType: "Conversation", PrimaryActorID: 3}
|
||||
require.NoError(t, db.Create(own).Error)
|
||||
require.NoError(t, db.Create(otherAccount).Error)
|
||||
require.NoError(t, db.Create(otherOwner).Error)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/1/notifications/%d", otherAccount.ID), nil)
|
||||
req.Header.Set("X-User-ID", strconv.FormatUint(uint64(user.ID), 10))
|
||||
router.ServeHTTP(w, req)
|
||||
require.Equal(t, http.StatusNotFound, w.Code)
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("PUT", fmt.Sprintf("/api/v1/accounts/1/notifications/%d", otherOwner.ID), nil)
|
||||
req.Header.Set("X-User-ID", strconv.FormatUint(uint64(user.ID), 10))
|
||||
router.ServeHTTP(w, req)
|
||||
require.Equal(t, http.StatusNotFound, w.Code)
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("PUT", fmt.Sprintf("/api/v1/accounts/1/notifications/%d", own.ID), nil)
|
||||
req.Header.Set("X-User-ID", strconv.FormatUint(uint64(user.ID), 10))
|
||||
router.ServeHTTP(w, req)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
var reloaded model.Notification
|
||||
require.NoError(t, db.First(&reloaded, own.ID).Error)
|
||||
assert.NotNil(t, reloaded.ReadAt)
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("DELETE", fmt.Sprintf("/api/v1/accounts/1/notifications/%d", otherAccount.ID), nil)
|
||||
req.Header.Set("X-User-ID", strconv.FormatUint(uint64(user.ID), 10))
|
||||
router.ServeHTTP(w, req)
|
||||
require.Equal(t, http.StatusNotFound, w.Code)
|
||||
var otherAccountReloaded model.Notification
|
||||
require.NoError(t, db.First(&otherAccountReloaded, otherAccount.ID).Error)
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("DELETE", fmt.Sprintf("/api/v1/accounts/1/notifications/%d", own.ID), nil)
|
||||
req.Header.Set("X-User-ID", strconv.FormatUint(uint64(user.ID), 10))
|
||||
router.ServeHTTP(w, req)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
require.Error(t, db.First(&reloaded, own.ID).Error)
|
||||
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.Close()
|
||||
}
|
||||
|
||||
func TestNotificationUnreadCountIsAccountScoped(t *testing.T) {
|
||||
db := setupNotificationDB(t)
|
||||
handler := setupNotificationHandler(t, db)
|
||||
router := setupNotificationRouter(handler)
|
||||
|
||||
user := &model.User{Name: "Unread Scoped User", Email: "unread-scoped@example.com", Password: "pass", AccountID: 1}
|
||||
require.NoError(t, db.Create(user).Error)
|
||||
accountID := uint(1)
|
||||
otherAccountID := uint(2)
|
||||
require.NoError(t, db.Create(&model.Notification{UserID: user.ID, AccountID: &accountID, NotificationType: "message_created"}).Error)
|
||||
require.NoError(t, db.Create(&model.Notification{UserID: user.ID, AccountID: &otherAccountID, NotificationType: "message_created"}).Error)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/notifications/unread_count", nil)
|
||||
req.Header.Set("X-User-ID", strconv.FormatUint(uint64(user.ID), 10))
|
||||
router.ServeHTTP(w, req)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Equal(t, "1", strings.TrimSpace(w.Body.String()))
|
||||
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.Close()
|
||||
}
|
||||
|
||||
func TestNotificationReadAll(t *testing.T) {
|
||||
db := setupNotificationDB(t)
|
||||
handler := setupNotificationHandler(t, db)
|
||||
@@ -461,9 +544,9 @@ func TestNotificationHandler_SnoozeWithDB(t *testing.T) {
|
||||
notif := &model.Notification{UserID: user.ID, AccountID: &accountID, NotificationType: "conversation_assignment", PrimaryActorType: "conversation", PrimaryActorID: 1}
|
||||
db.Create(notif)
|
||||
|
||||
// Snooze the notification
|
||||
snoozeTime := time.Now().Add(2 * time.Hour).Format(time.RFC3339)
|
||||
body := fmt.Sprintf(`{"snoozed_until":"%s"}`, snoozeTime)
|
||||
// Chatwoot DateRangeHelper parses snoozed_until as Unix seconds.
|
||||
snoozeUnix := time.Now().Add(2 * time.Hour).Unix()
|
||||
body := fmt.Sprintf(`{"snoozed_until":%d}`, snoozeUnix)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/1/notifications/%d/snooze", notif.ID), strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
@@ -476,6 +559,10 @@ func TestNotificationHandler_SnoozeWithDB(t *testing.T) {
|
||||
var updated model.Notification
|
||||
require.NoError(t, db.First(&updated, notif.ID).Error)
|
||||
assert.NotNil(t, updated.SnoozedUntil)
|
||||
assert.Equal(t, snoozeUnix, updated.SnoozedUntil.Unix())
|
||||
var meta map[string]any
|
||||
require.NoError(t, json.Unmarshal(updated.AdditionalAttributes, &meta))
|
||||
assert.Contains(t, meta, "last_snoozed_at")
|
||||
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.Close()
|
||||
@@ -486,8 +573,8 @@ func TestNotificationHandler_Snooze_InvalidID(t *testing.T) {
|
||||
handler := setupNotificationHandler(t, db)
|
||||
router := setupNotificationRouter(handler)
|
||||
|
||||
snoozeTime := time.Now().Add(2 * time.Hour).Format(time.RFC3339)
|
||||
body := fmt.Sprintf(`{"snoozed_until":"%s"}`, snoozeTime)
|
||||
snoozeTime := time.Now().Add(2 * time.Hour).Unix()
|
||||
body := fmt.Sprintf(`{"snoozed_until":%d}`, snoozeTime)
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/notifications/abc/snooze", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
@@ -517,7 +604,7 @@ func TestNotificationHandler_Snooze_MissingBody(t *testing.T) {
|
||||
req.Header.Set("X-User-ID", strconv.FormatUint(uint64(user.ID), 10))
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.Close()
|
||||
|
||||
@@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -38,6 +39,16 @@ func (r *NotificationRepo) FindByID(ctx context.Context, id uint) (*model.Notifi
|
||||
return &n, nil
|
||||
}
|
||||
|
||||
// FindByUserAndAccount retrieves a notification using Chatwoot's current_user/current_account scope.
|
||||
func (r *NotificationRepo) FindByUserAndAccount(ctx context.Context, id, userID, accountID uint) (*model.Notification, error) {
|
||||
var n model.Notification
|
||||
err := r.db.WithContext(ctx).Where("id = ? AND user_id = ? AND account_id = ?", id, userID, accountID).First(&n).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &n, nil
|
||||
}
|
||||
|
||||
// ListByUser retrieves all notifications for a user with pagination.
|
||||
func (r *NotificationRepo) ListByUser(ctx context.Context, userID uint, offset, limit int) ([]model.Notification, int64, error) {
|
||||
var notifications []model.Notification
|
||||
@@ -135,6 +146,21 @@ func (r *NotificationRepo) MarkRead(ctx context.Context, id uint) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkReadByUserAndAccount marks a single scoped notification as read.
|
||||
func (r *NotificationRepo) MarkReadByUserAndAccount(ctx context.Context, id, userID, accountID uint) (*model.Notification, error) {
|
||||
now := time.Now().UTC()
|
||||
result := r.db.WithContext(ctx).Model(&model.Notification{}).
|
||||
Where("id = ? AND user_id = ? AND account_id = ?", id, userID, accountID).
|
||||
Update("read_at", &now)
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
return r.FindByUserAndAccount(ctx, id, userID, accountID)
|
||||
}
|
||||
|
||||
// MarkAllReadByUser marks all unread notifications for a user as read.
|
||||
func (r *NotificationRepo) MarkAllReadByUser(ctx context.Context, userID uint) error {
|
||||
now := time.Now()
|
||||
@@ -163,6 +189,20 @@ func (r *NotificationRepo) Delete(ctx context.Context, id uint) error {
|
||||
return r.db.WithContext(ctx).Delete(&model.Notification{}, id).Error
|
||||
}
|
||||
|
||||
// DeleteByUserAndAccount deletes a notification using Chatwoot's current user/account scope.
|
||||
func (r *NotificationRepo) DeleteByUserAndAccount(ctx context.Context, id, userID, accountID uint) error {
|
||||
result := r.db.WithContext(ctx).
|
||||
Where("id = ? AND user_id = ? AND account_id = ?", id, userID, accountID).
|
||||
Delete(&model.Notification{})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CountUnreadByUser counts unread notifications for a user.
|
||||
func (r *NotificationRepo) CountUnreadByUser(ctx context.Context, userID uint) (int64, error) {
|
||||
var count int64
|
||||
@@ -172,13 +212,36 @@ func (r *NotificationRepo) CountUnreadByUser(ctx context.Context, userID uint) (
|
||||
return count, err
|
||||
}
|
||||
|
||||
// CountUnreadByUserAndAccount counts unread notifications for the current account only.
|
||||
func (r *NotificationRepo) CountUnreadByUserAndAccount(ctx context.Context, userID, accountID uint) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).Model(&model.Notification{}).
|
||||
Where("user_id = ? AND account_id = ? AND read_at IS NULL", userID, accountID).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// Snooze sets the snoozed_until timestamp and clears read_at for a notification.
|
||||
// Returns the updated notification.
|
||||
// Reference: Chatwoot notifications_controller.rb#snooze
|
||||
func (r *NotificationRepo) Snooze(ctx context.Context, id, userID, accountID uint, snoozedUntil time.Time) (*model.Notification, error) {
|
||||
var notification model.Notification
|
||||
if err := r.db.WithContext(ctx).Where("id = ? AND user_id = ? AND account_id = ?", id, userID, accountID).First(¬ification).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
meta := map[string]interface{}{}
|
||||
if len(notification.AdditionalAttributes) > 0 {
|
||||
_ = json.Unmarshal(notification.AdditionalAttributes, &meta)
|
||||
}
|
||||
meta["last_snoozed_at"] = nil
|
||||
encodedMeta, err := json.Marshal(meta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := r.db.WithContext(ctx).Model(&model.Notification{}).
|
||||
Where("id = ? AND user_id = ? AND account_id = ?", id, userID, accountID).
|
||||
Updates(map[string]interface{}{"snoozed_until": snoozedUntil, "read_at": nil})
|
||||
Updates(map[string]interface{}{"snoozed_until": snoozedUntil.UTC(), "read_at": nil, "additional_attributes": encodedMeta})
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
@@ -50,6 +50,11 @@ func (s *NotificationService) GetNotification(ctx context.Context, id uint) (*mo
|
||||
return s.notifRepo.FindByID(ctx, id)
|
||||
}
|
||||
|
||||
// GetNotificationByAccount retrieves a notification scoped like Chatwoot fetch_notification.
|
||||
func (s *NotificationService) GetNotificationByAccount(ctx context.Context, id, userID, accountID uint) (*model.Notification, error) {
|
||||
return s.notifRepo.FindByUserAndAccount(ctx, id, userID, accountID)
|
||||
}
|
||||
|
||||
// ListNotifications retrieves all notifications for a user with pagination.
|
||||
func (s *NotificationService) ListNotifications(ctx context.Context, userID uint, page, perPage int) ([]model.Notification, int64, error) {
|
||||
offset := (page - 1) * perPage
|
||||
@@ -85,6 +90,11 @@ func (s *NotificationService) MarkRead(ctx context.Context, id uint) error {
|
||||
return s.notifRepo.MarkRead(ctx, id)
|
||||
}
|
||||
|
||||
// MarkReadByAccount marks a single notification as read within current user/account scope.
|
||||
func (s *NotificationService) MarkReadByAccount(ctx context.Context, id, userID, accountID uint) (*model.Notification, error) {
|
||||
return s.notifRepo.MarkReadByUserAndAccount(ctx, id, userID, accountID)
|
||||
}
|
||||
|
||||
// MarkAllRead marks all unread notifications for a user as read.
|
||||
func (s *NotificationService) MarkAllRead(ctx context.Context, userID uint) error {
|
||||
return s.notifRepo.MarkAllReadByUser(ctx, userID)
|
||||
@@ -104,11 +114,21 @@ func (s *NotificationService) DeleteNotification(ctx context.Context, id uint) e
|
||||
return s.notifRepo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// DeleteNotificationByAccount deletes a scoped notification.
|
||||
func (s *NotificationService) DeleteNotificationByAccount(ctx context.Context, id, userID, accountID uint) error {
|
||||
return s.notifRepo.DeleteByUserAndAccount(ctx, id, userID, accountID)
|
||||
}
|
||||
|
||||
// GetUnreadCount returns the number of unread notifications for a user.
|
||||
func (s *NotificationService) GetUnreadCount(ctx context.Context, userID uint) (int64, error) {
|
||||
return s.notifRepo.CountUnreadByUser(ctx, userID)
|
||||
}
|
||||
|
||||
// GetUnreadCountByAccount returns the unread count for the current account only.
|
||||
func (s *NotificationService) GetUnreadCountByAccount(ctx context.Context, userID, accountID uint) (int64, error) {
|
||||
return s.notifRepo.CountUnreadByUserAndAccount(ctx, userID, accountID)
|
||||
}
|
||||
|
||||
// --- Notification Preference operations ---
|
||||
|
||||
// GetPreferences retrieves all notification preferences for a user within an account.
|
||||
|
||||
Reference in New Issue
Block a user