Files
gochat/internal/repository/notification_repo.go
T

292 lines
10 KiB
Go

package repository
import (
"context"
"encoding/json"
"time"
"gorm.io/gorm"
"github.com/gochat/gochat/internal/model"
)
// NotificationRepo implements GORM repository for Notification.
// Reference: Chatwoot app/models/notification.rb
type NotificationRepo struct {
db *gorm.DB
}
// NotificationListFilter mirrors Chatwoot NotificationFinder filters used by
// the dashboard notifications API.
type NotificationListFilter struct {
IncludeRead bool
IncludeSnoozed bool
SortOrder string
}
// NewNotificationRepo creates a new Notification repository.
func NewNotificationRepo(db *gorm.DB) *NotificationRepo {
return &NotificationRepo{db: db}
}
// FindByID retrieves a notification by primary key.
func (r *NotificationRepo) FindByID(ctx context.Context, id uint) (*model.Notification, error) {
var n model.Notification
err := r.db.WithContext(ctx).First(&n, id).Error
if err != nil {
return nil, err
}
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
var total int64
db := r.db.WithContext(ctx).Where("user_id = ?", userID)
if err := db.Model(&model.Notification{}).Count(&total).Error; err != nil {
return nil, 0, err
}
if err := db.Order("created_at DESC").Offset(offset).Limit(limit).Find(&notifications).Error; err != nil {
return nil, 0, err
}
return notifications, total, nil
}
// ListByUserAndAccount retrieves notifications for a user within an account.
func (r *NotificationRepo) ListByUserAndAccount(ctx context.Context, userID uint, accountID uint, offset, limit int) ([]model.Notification, int64, error) {
var notifications []model.Notification
var total int64
db := r.db.WithContext(ctx).Where("user_id = ? AND account_id = ?", userID, accountID)
if err := db.Model(&model.Notification{}).Count(&total).Error; err != nil {
return nil, 0, err
}
if err := db.Order("created_at DESC").Offset(offset).Limit(limit).Find(&notifications).Error; err != nil {
return nil, 0, err
}
return notifications, total, nil
}
func (r *NotificationRepo) ListByUserAndAccountFiltered(ctx context.Context, userID uint, accountID uint, offset, limit int, filter NotificationListFilter) ([]model.Notification, int64, int64, error) {
var notifications []model.Notification
var total int64
var unreadCount int64
db := r.applyNotificationListFilter(r.db.WithContext(ctx).Model(&model.Notification{}).Where("user_id = ? AND account_id = ?", userID, accountID), filter)
if err := db.Count(&total).Error; err != nil {
return nil, 0, 0, err
}
if err := db.Session(&gorm.Session{}).Where("read_at IS NULL").Count(&unreadCount).Error; err != nil {
return nil, 0, 0, err
}
sortOrder := "DESC"
if filter.SortOrder == "asc" {
sortOrder = "ASC"
}
if err := db.Session(&gorm.Session{}).
Order("updated_at " + sortOrder).
Offset(offset).
Limit(limit).
Find(&notifications).Error; err != nil {
return nil, 0, 0, err
}
return notifications, total, unreadCount, nil
}
func (r *NotificationRepo) applyNotificationListFilter(db *gorm.DB, filter NotificationListFilter) *gorm.DB {
if !filter.IncludeSnoozed {
db = db.Where("snoozed_until IS NULL")
}
if !filter.IncludeRead {
db = db.Where("read_at IS NULL")
}
return db
}
// Create inserts a new notification.
func (r *NotificationRepo) Create(ctx context.Context, notification *model.Notification) error {
return r.db.WithContext(ctx).Create(notification).Error
}
// Update modifies an existing notification.
func (r *NotificationRepo) Update(ctx context.Context, notification *model.Notification) error {
return r.db.WithContext(ctx).Save(notification).Error
}
// MarkRead marks a single notification as read.
func (r *NotificationRepo) MarkRead(ctx context.Context, id uint) error {
now := time.Now()
result := r.db.WithContext(ctx).Model(&model.Notification{}).
Where("id = ?", id).
Update("read_at", now)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
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()
return r.db.WithContext(ctx).Model(&model.Notification{}).
Where("user_id = ? AND read_at IS NULL", userID).
Update("read_at", now).Error
}
// MarkAllReadByUserAndAccount marks all unread notifications for a user within an account as read.
func (r *NotificationRepo) MarkAllReadByUserAndAccount(ctx context.Context, userID uint, accountID uint) error {
now := time.Now()
return r.db.WithContext(ctx).Model(&model.Notification{}).
Where("user_id = ? AND account_id = ? AND read_at IS NULL", userID, accountID).
Update("read_at", now).Error
}
func (r *NotificationRepo) MarkPrimaryActorReadByUserAndAccount(ctx context.Context, userID, accountID uint, actorType string, actorID uint) error {
now := time.Now()
return r.db.WithContext(ctx).Model(&model.Notification{}).
Where("user_id = ? AND account_id = ? AND primary_actor_type = ? AND primary_actor_id = ? AND read_at IS NULL", userID, accountID, actorType, actorID).
Update("read_at", now).Error
}
// Delete soft-deletes a notification.
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
err := r.db.WithContext(ctx).Model(&model.Notification{}).
Where("user_id = ? AND read_at IS NULL", userID).
Count(&count).Error
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(&notification).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.UTC(), "read_at": nil, "additional_attributes": encodedMeta})
if result.Error != nil {
return nil, result.Error
}
if result.RowsAffected == 0 {
return nil, gorm.ErrRecordNotFound
}
var notif model.Notification
if err := r.db.WithContext(ctx).First(&notif, id).Error; err != nil {
return nil, err
}
return &notif, nil
}
// MarkUnread clears read_at for a notification, marking it as unread.
// Returns the updated notification.
// Reference: Chatwoot notifications_controller.rb#unread
func (r *NotificationRepo) MarkUnread(ctx context.Context, id, userID, accountID uint) (*model.Notification, error) {
result := r.db.WithContext(ctx).Model(&model.Notification{}).
Where("id = ? AND user_id = ? AND account_id = ?", id, userID, accountID).
Update("read_at", nil)
if result.Error != nil {
return nil, result.Error
}
if result.RowsAffected == 0 {
return nil, gorm.ErrRecordNotFound
}
var notif model.Notification
if err := r.db.WithContext(ctx).First(&notif, id).Error; err != nil {
return nil, err
}
return &notif, nil
}
// DeleteAllByUser soft-deletes all notifications for a user within an account.
// Reference: Chatwoot notifications_controller.rb#destroy_all
func (r *NotificationRepo) DeleteAllByUser(ctx context.Context, userID, accountID uint) error {
return r.db.WithContext(ctx).
Where("user_id = ? AND account_id = ?", userID, accountID).
Delete(&model.Notification{}).Error
}
// DeleteReadByUser soft-deletes only read notifications for a user/account.
func (r *NotificationRepo) DeleteReadByUser(ctx context.Context, userID, accountID uint) error {
return r.db.WithContext(ctx).
Where("user_id = ? AND account_id = ? AND read_at IS NOT NULL", userID, accountID).
Delete(&model.Notification{}).Error
}