- Rename module path from github.com/patbritton/sojorn-backend to gitlab.com/patrickbritton3/sojorn/go-backend - Updated 78 references across 41 files - Matches new GitLab repository structure
597 lines
19 KiB
Go
597 lines
19 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"gitlab.com/patrickbritton3/sojorn/go-backend/internal/models"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
type NotificationRepository struct {
|
|
pool *pgxpool.Pool
|
|
}
|
|
|
|
func NewNotificationRepository(pool *pgxpool.Pool) *NotificationRepository {
|
|
return &NotificationRepository{pool: pool}
|
|
}
|
|
|
|
// ============================================================================
|
|
// FCM Token Management
|
|
// ============================================================================
|
|
|
|
func (r *NotificationRepository) UpsertFCMToken(ctx context.Context, token *models.UserFCMToken) error {
|
|
query := `
|
|
INSERT INTO public.user_fcm_tokens (user_id, token, device_type, created_at, last_updated)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
ON CONFLICT (user_id, token)
|
|
DO UPDATE SET
|
|
device_type = EXCLUDED.device_type,
|
|
last_updated = EXCLUDED.last_updated
|
|
`
|
|
_, err := r.pool.Exec(ctx, query,
|
|
token.UserID,
|
|
token.FCMToken,
|
|
token.Platform,
|
|
time.Now(),
|
|
time.Now(),
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (r *NotificationRepository) GetFCMTokensForUser(ctx context.Context, userID string) ([]string, error) {
|
|
query := `
|
|
SELECT token
|
|
FROM public.user_fcm_tokens
|
|
WHERE user_id = $1::uuid
|
|
`
|
|
rows, err := r.pool.Query(ctx, query, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var tokens []string
|
|
for rows.Next() {
|
|
var token string
|
|
if err := rows.Scan(&token); err != nil {
|
|
return nil, err
|
|
}
|
|
tokens = append(tokens, token)
|
|
}
|
|
|
|
return tokens, nil
|
|
}
|
|
|
|
func (r *NotificationRepository) DeleteFCMToken(ctx context.Context, userID string, token string) error {
|
|
_, err := r.pool.Exec(ctx, `
|
|
DELETE FROM public.user_fcm_tokens
|
|
WHERE user_id = $1::uuid AND token = $2
|
|
`, userID, token)
|
|
return err
|
|
}
|
|
|
|
func (r *NotificationRepository) DeleteAllFCMTokensForUser(ctx context.Context, userID string) error {
|
|
_, err := r.pool.Exec(ctx, `
|
|
DELETE FROM public.user_fcm_tokens WHERE user_id = $1::uuid
|
|
`, userID)
|
|
return err
|
|
}
|
|
|
|
// ============================================================================
|
|
// Notification CRUD
|
|
// ============================================================================
|
|
|
|
func (r *NotificationRepository) CreateNotification(ctx context.Context, notif *models.Notification) error {
|
|
query := `
|
|
INSERT INTO public.notifications (user_id, type, actor_id, post_id, comment_id, is_read, metadata, group_key, priority)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
RETURNING id, created_at
|
|
`
|
|
|
|
priority := notif.Priority
|
|
if priority == "" {
|
|
priority = models.PriorityNormal
|
|
}
|
|
|
|
err := r.pool.QueryRow(ctx, query,
|
|
notif.UserID, notif.Type, notif.ActorID, notif.PostID, notif.CommentID, notif.IsRead, notif.Metadata, notif.GroupKey, priority,
|
|
).Scan(¬if.ID, ¬if.CreatedAt)
|
|
|
|
return err
|
|
}
|
|
|
|
func (r *NotificationRepository) GetNotifications(ctx context.Context, userID string, limit, offset int, includeArchived bool) ([]models.Notification, error) {
|
|
whereClause := "WHERE n.user_id = $1::uuid AND n.archived_at IS NULL AND n.type != 'message'"
|
|
if includeArchived {
|
|
whereClause = "WHERE n.user_id = $1::uuid AND n.archived_at IS NOT NULL AND n.type != 'message'"
|
|
}
|
|
|
|
query := `
|
|
SELECT
|
|
n.id, n.user_id, n.type, n.actor_id, n.post_id, n.comment_id, n.is_read, n.created_at, n.archived_at, n.metadata,
|
|
COALESCE(n.group_key, '') as group_key,
|
|
COALESCE(n.priority, 'normal') as priority,
|
|
pr.handle, pr.display_name, COALESCE(pr.avatar_url, ''),
|
|
po.image_url,
|
|
LEFT(po.body, 100) as post_body
|
|
FROM public.notifications n
|
|
JOIN public.profiles pr ON n.actor_id = pr.id
|
|
LEFT JOIN public.posts po ON n.post_id = po.id
|
|
` + whereClause + `
|
|
ORDER BY n.created_at DESC
|
|
LIMIT $2 OFFSET $3
|
|
`
|
|
rows, err := r.pool.Query(ctx, query, userID, limit, offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
notifications := []models.Notification{}
|
|
for rows.Next() {
|
|
var n models.Notification
|
|
var groupKey string
|
|
var postImageURL *string
|
|
var postBody *string
|
|
err := rows.Scan(
|
|
&n.ID, &n.UserID, &n.Type, &n.ActorID, &n.PostID, &n.CommentID, &n.IsRead, &n.CreatedAt, &n.ArchivedAt, &n.Metadata,
|
|
&groupKey, &n.Priority,
|
|
&n.ActorHandle, &n.ActorDisplayName, &n.ActorAvatarURL,
|
|
&postImageURL, &postBody,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if groupKey != "" {
|
|
n.GroupKey = &groupKey
|
|
}
|
|
n.PostImageURL = postImageURL
|
|
n.PostBody = postBody
|
|
notifications = append(notifications, n)
|
|
}
|
|
return notifications, nil
|
|
}
|
|
|
|
// GetGroupedNotifications returns notifications with grouping (e.g., "5 people liked your post")
|
|
func (r *NotificationRepository) GetGroupedNotifications(ctx context.Context, userID string, limit, offset int, includeArchived bool) ([]models.Notification, error) {
|
|
whereClause := "WHERE n.user_id = $1::uuid AND n.archived_at IS NULL AND n.type != 'message'"
|
|
if includeArchived {
|
|
whereClause = "WHERE n.user_id = $1::uuid AND n.archived_at IS NOT NULL AND n.type != 'message'"
|
|
}
|
|
|
|
query := `
|
|
WITH ranked AS (
|
|
SELECT
|
|
n.*,
|
|
pr.handle as actor_handle,
|
|
pr.display_name as actor_display_name,
|
|
COALESCE(pr.avatar_url, '') as actor_avatar_url,
|
|
po.image_url as post_image_url,
|
|
LEFT(po.body, 100) as post_body,
|
|
COUNT(*) OVER (PARTITION BY n.group_key) as group_count,
|
|
ROW_NUMBER() OVER (PARTITION BY n.group_key ORDER BY n.created_at DESC) as rn
|
|
FROM public.notifications n
|
|
JOIN public.profiles pr ON n.actor_id = pr.id
|
|
LEFT JOIN public.posts po ON n.post_id = po.id
|
|
` + whereClause + `
|
|
)
|
|
SELECT
|
|
id, user_id, type, actor_id, post_id, comment_id, is_read, created_at, archived_at, metadata,
|
|
COALESCE(group_key, '') as group_key,
|
|
COALESCE(priority, 'normal') as priority,
|
|
actor_handle, actor_display_name, actor_avatar_url,
|
|
post_image_url, post_body,
|
|
group_count
|
|
FROM ranked
|
|
WHERE rn = 1 OR group_key IS NULL
|
|
ORDER BY created_at DESC
|
|
LIMIT $2 OFFSET $3
|
|
`
|
|
|
|
rows, err := r.pool.Query(ctx, query, userID, limit, offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
notifications := []models.Notification{}
|
|
for rows.Next() {
|
|
var n models.Notification
|
|
var groupKey string
|
|
err := rows.Scan(
|
|
&n.ID, &n.UserID, &n.Type, &n.ActorID, &n.PostID, &n.CommentID, &n.IsRead, &n.CreatedAt, &n.ArchivedAt, &n.Metadata,
|
|
&groupKey, &n.Priority,
|
|
&n.ActorHandle, &n.ActorDisplayName, &n.ActorAvatarURL,
|
|
&n.PostImageURL, &n.PostBody,
|
|
&n.GroupCount,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if groupKey != "" {
|
|
n.GroupKey = &groupKey
|
|
}
|
|
notifications = append(notifications, n)
|
|
}
|
|
return notifications, nil
|
|
}
|
|
|
|
func (r *NotificationRepository) MarkAsRead(ctx context.Context, notificationID, userID string) error {
|
|
_, err := r.pool.Exec(ctx, `
|
|
UPDATE public.notifications SET is_read = TRUE
|
|
WHERE id = $1::uuid AND user_id = $2::uuid
|
|
`, notificationID, userID)
|
|
return err
|
|
}
|
|
|
|
func (r *NotificationRepository) MarkAllAsRead(ctx context.Context, userID string) error {
|
|
_, err := r.pool.Exec(ctx, `
|
|
UPDATE public.notifications SET is_read = TRUE
|
|
WHERE user_id = $1::uuid AND is_read = FALSE
|
|
`, userID)
|
|
return err
|
|
}
|
|
|
|
func (r *NotificationRepository) DeleteNotification(ctx context.Context, notificationID, userID string) error {
|
|
_, err := r.pool.Exec(ctx, `
|
|
DELETE FROM public.notifications
|
|
WHERE id = $1::uuid AND user_id = $2::uuid
|
|
`, notificationID, userID)
|
|
return err
|
|
}
|
|
|
|
func (r *NotificationRepository) MarkNotificationsAsRead(ctx context.Context, ids []string, userID string) error {
|
|
_, err := r.pool.Exec(ctx, `
|
|
UPDATE public.notifications SET is_read = TRUE
|
|
WHERE id = ANY($1::uuid[]) AND user_id = $2::uuid
|
|
`, ids, userID)
|
|
return err
|
|
}
|
|
|
|
func (r *NotificationRepository) ArchiveNotifications(ctx context.Context, ids []string, userID string) error {
|
|
_, err := r.pool.Exec(ctx, `
|
|
UPDATE public.notifications SET archived_at = NOW(), is_read = TRUE
|
|
WHERE id = ANY($1::uuid[]) AND user_id = $2::uuid
|
|
`, ids, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Recalculate badge count excluding archived and message types
|
|
_, err = r.pool.Exec(ctx, `
|
|
UPDATE profiles SET unread_notification_count = (
|
|
SELECT COUNT(*) FROM notifications WHERE user_id = $1::uuid AND is_read = FALSE AND archived_at IS NULL AND type != 'message'
|
|
) WHERE id = $1::uuid
|
|
`, userID)
|
|
return err
|
|
}
|
|
|
|
func (r *NotificationRepository) ArchiveAllNotifications(ctx context.Context, userID string) error {
|
|
_, err := r.pool.Exec(ctx, `
|
|
UPDATE public.notifications SET archived_at = NOW(), is_read = TRUE
|
|
WHERE user_id = $1::uuid AND archived_at IS NULL
|
|
`, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Reset badge count — all notifications are now archived and read
|
|
_, err = r.pool.Exec(ctx, `
|
|
UPDATE profiles SET unread_notification_count = 0 WHERE id = $1::uuid
|
|
`, userID)
|
|
return err
|
|
}
|
|
|
|
func (r *NotificationRepository) GetUnreadCount(ctx context.Context, userID string) (int, error) {
|
|
var count int
|
|
err := r.pool.QueryRow(ctx, `
|
|
SELECT COUNT(*) FROM public.notifications
|
|
WHERE user_id = $1::uuid AND is_read = FALSE AND archived_at IS NULL AND type != 'message'
|
|
`, userID).Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
func (r *NotificationRepository) GetUnreadBadge(ctx context.Context, userID string) (*models.UnreadBadge, error) {
|
|
badge := &models.UnreadBadge{}
|
|
|
|
// Get notification count (live query, excludes archived and chat messages)
|
|
err := r.pool.QueryRow(ctx, `
|
|
SELECT COUNT(*) FROM public.notifications
|
|
WHERE user_id = $1::uuid AND is_read = FALSE AND archived_at IS NULL AND type != 'message'
|
|
`, userID).Scan(&badge.NotificationCount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get unread message count
|
|
err = r.pool.QueryRow(ctx, `
|
|
SELECT COUNT(*) FROM encrypted_messages
|
|
WHERE receiver_id = $1::uuid AND is_read = FALSE
|
|
`, userID).Scan(&badge.MessageCount)
|
|
if err != nil {
|
|
// Table might not exist or column missing, ignore
|
|
badge.MessageCount = 0
|
|
}
|
|
|
|
badge.TotalCount = badge.NotificationCount + badge.MessageCount
|
|
return badge, nil
|
|
}
|
|
|
|
// ============================================================================
|
|
// Notification Preferences
|
|
// ============================================================================
|
|
|
|
func (r *NotificationRepository) GetNotificationPreferences(ctx context.Context, userID string) (*models.NotificationPreferences, error) {
|
|
prefs := &models.NotificationPreferences{}
|
|
|
|
err := r.pool.QueryRow(ctx, `
|
|
SELECT
|
|
user_id, push_enabled, push_likes, push_comments, push_replies, push_mentions,
|
|
push_follows, push_follow_requests, push_messages, push_saves, push_beacons,
|
|
email_enabled, email_digest_frequency, quiet_hours_enabled,
|
|
quiet_hours_start::text, quiet_hours_end::text, show_badge_count,
|
|
created_at, updated_at
|
|
FROM notification_preferences
|
|
WHERE user_id = $1::uuid
|
|
`, userID).Scan(
|
|
&prefs.UserID, &prefs.PushEnabled, &prefs.PushLikes, &prefs.PushComments, &prefs.PushReplies, &prefs.PushMentions,
|
|
&prefs.PushFollows, &prefs.PushFollowRequests, &prefs.PushMessages, &prefs.PushSaves, &prefs.PushBeacons,
|
|
&prefs.EmailEnabled, &prefs.EmailDigestFrequency, &prefs.QuietHoursEnabled,
|
|
&prefs.QuietHoursStart, &prefs.QuietHoursEnd, &prefs.ShowBadgeCount,
|
|
&prefs.CreatedAt, &prefs.UpdatedAt,
|
|
)
|
|
|
|
if err != nil {
|
|
// Return defaults if not found
|
|
return r.createDefaultPreferences(ctx, userID)
|
|
}
|
|
|
|
return prefs, nil
|
|
}
|
|
|
|
func (r *NotificationRepository) createDefaultPreferences(ctx context.Context, userID string) (*models.NotificationPreferences, error) {
|
|
userUUID, err := uuid.Parse(userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
prefs := &models.NotificationPreferences{
|
|
UserID: userUUID,
|
|
PushEnabled: true,
|
|
PushLikes: true,
|
|
PushComments: true,
|
|
PushReplies: true,
|
|
PushMentions: true,
|
|
PushFollows: true,
|
|
PushFollowRequests: true,
|
|
PushMessages: true,
|
|
PushSaves: true,
|
|
PushBeacons: true,
|
|
EmailEnabled: false,
|
|
EmailDigestFrequency: "never",
|
|
QuietHoursEnabled: false,
|
|
ShowBadgeCount: true,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
_, err = r.pool.Exec(ctx, `
|
|
INSERT INTO notification_preferences (user_id, push_enabled, push_likes, push_comments, push_replies, push_mentions,
|
|
push_follows, push_follow_requests, push_messages, push_saves, push_beacons,
|
|
email_enabled, email_digest_frequency, quiet_hours_enabled, show_badge_count)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
|
ON CONFLICT (user_id) DO NOTHING
|
|
`,
|
|
prefs.UserID, prefs.PushEnabled, prefs.PushLikes, prefs.PushComments, prefs.PushReplies, prefs.PushMentions,
|
|
prefs.PushFollows, prefs.PushFollowRequests, prefs.PushMessages, prefs.PushSaves, prefs.PushBeacons,
|
|
prefs.EmailEnabled, prefs.EmailDigestFrequency, prefs.QuietHoursEnabled, prefs.ShowBadgeCount,
|
|
)
|
|
|
|
return prefs, err
|
|
}
|
|
|
|
func (r *NotificationRepository) UpdateNotificationPreferences(ctx context.Context, prefs *models.NotificationPreferences) error {
|
|
_, err := r.pool.Exec(ctx, `
|
|
INSERT INTO notification_preferences (
|
|
user_id, push_enabled, push_likes, push_comments, push_replies, push_mentions,
|
|
push_follows, push_follow_requests, push_messages, push_saves, push_beacons,
|
|
email_enabled, email_digest_frequency, quiet_hours_enabled, quiet_hours_start, quiet_hours_end,
|
|
show_badge_count, updated_at
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15::time, $16::time, $17, NOW())
|
|
ON CONFLICT (user_id) DO UPDATE SET
|
|
push_enabled = EXCLUDED.push_enabled,
|
|
push_likes = EXCLUDED.push_likes,
|
|
push_comments = EXCLUDED.push_comments,
|
|
push_replies = EXCLUDED.push_replies,
|
|
push_mentions = EXCLUDED.push_mentions,
|
|
push_follows = EXCLUDED.push_follows,
|
|
push_follow_requests = EXCLUDED.push_follow_requests,
|
|
push_messages = EXCLUDED.push_messages,
|
|
push_saves = EXCLUDED.push_saves,
|
|
push_beacons = EXCLUDED.push_beacons,
|
|
email_enabled = EXCLUDED.email_enabled,
|
|
email_digest_frequency = EXCLUDED.email_digest_frequency,
|
|
quiet_hours_enabled = EXCLUDED.quiet_hours_enabled,
|
|
quiet_hours_start = EXCLUDED.quiet_hours_start,
|
|
quiet_hours_end = EXCLUDED.quiet_hours_end,
|
|
show_badge_count = EXCLUDED.show_badge_count,
|
|
updated_at = NOW()
|
|
`,
|
|
prefs.UserID, prefs.PushEnabled, prefs.PushLikes, prefs.PushComments, prefs.PushReplies, prefs.PushMentions,
|
|
prefs.PushFollows, prefs.PushFollowRequests, prefs.PushMessages, prefs.PushSaves, prefs.PushBeacons,
|
|
prefs.EmailEnabled, prefs.EmailDigestFrequency, prefs.QuietHoursEnabled, prefs.QuietHoursStart, prefs.QuietHoursEnd,
|
|
prefs.ShowBadgeCount,
|
|
)
|
|
return err
|
|
}
|
|
|
|
// ShouldSendPush checks user preferences and quiet hours to determine if push should be sent
|
|
func (r *NotificationRepository) ShouldSendPush(ctx context.Context, userID, notificationType string) (bool, error) {
|
|
prefs, err := r.GetNotificationPreferences(ctx, userID)
|
|
if err != nil {
|
|
log.Warn().Err(err).Str("user_id", userID).Msg("Failed to get notification preferences, defaulting to send")
|
|
return true, nil
|
|
}
|
|
|
|
if !prefs.PushEnabled {
|
|
return false, nil
|
|
}
|
|
|
|
// Check quiet hours
|
|
if prefs.QuietHoursEnabled && prefs.QuietHoursStart != nil && prefs.QuietHoursEnd != nil {
|
|
if r.isInQuietHours(*prefs.QuietHoursStart, *prefs.QuietHoursEnd) {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
// Check specific notification type
|
|
switch notificationType {
|
|
case models.NotificationTypeLike:
|
|
return prefs.PushLikes, nil
|
|
case models.NotificationTypeComment:
|
|
return prefs.PushComments, nil
|
|
case models.NotificationTypeReply:
|
|
return prefs.PushReplies, nil
|
|
case models.NotificationTypeMention:
|
|
return prefs.PushMentions, nil
|
|
case models.NotificationTypeFollow:
|
|
return prefs.PushFollows, nil
|
|
case models.NotificationTypeFollowRequest:
|
|
return prefs.PushFollowRequests, nil
|
|
case models.NotificationTypeMessage:
|
|
return prefs.PushMessages, nil
|
|
case models.NotificationTypeSave:
|
|
return prefs.PushSaves, nil
|
|
case models.NotificationTypeBeaconVouch, models.NotificationTypeBeaconReport:
|
|
return prefs.PushBeacons, nil
|
|
default:
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
func (r *NotificationRepository) isInQuietHours(start, end string) bool {
|
|
now := time.Now().UTC()
|
|
currentTime := now.Format("15:04:05")
|
|
|
|
// Simple string comparison for time ranges
|
|
// Handle cases where quiet hours span midnight
|
|
if start > end {
|
|
// Spans midnight: 22:00 -> 08:00
|
|
return currentTime >= start || currentTime <= end
|
|
}
|
|
// Same day: 23:00 -> 23:59
|
|
return currentTime >= start && currentTime <= end
|
|
}
|
|
|
|
// ============================================================================
|
|
// Mention Extraction
|
|
// ============================================================================
|
|
|
|
// ExtractMentions finds @username patterns in text and returns user IDs
|
|
func (r *NotificationRepository) ExtractMentions(ctx context.Context, text string) ([]uuid.UUID, error) {
|
|
// Extract @mentions using regex
|
|
mentions := extractMentionHandles(text)
|
|
if len(mentions) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Look up user IDs for handles
|
|
query := `SELECT id FROM profiles WHERE handle = ANY($1)`
|
|
rows, err := r.pool.Query(ctx, query, mentions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var userIDs []uuid.UUID
|
|
for rows.Next() {
|
|
var id uuid.UUID
|
|
if err := rows.Scan(&id); err != nil {
|
|
continue
|
|
}
|
|
userIDs = append(userIDs, id)
|
|
}
|
|
|
|
return userIDs, nil
|
|
}
|
|
|
|
func extractMentionHandles(text string) []string {
|
|
var mentions []string
|
|
inMention := false
|
|
current := ""
|
|
|
|
for i, r := range text {
|
|
if r == '@' {
|
|
inMention = true
|
|
current = ""
|
|
continue
|
|
}
|
|
|
|
if inMention {
|
|
// Valid handle characters: alphanumeric and underscore
|
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {
|
|
current += string(r)
|
|
} else {
|
|
if len(current) > 0 {
|
|
mentions = append(mentions, current)
|
|
}
|
|
inMention = false
|
|
current = ""
|
|
}
|
|
}
|
|
|
|
// Check end of string
|
|
if i == len(text)-1 && inMention && len(current) > 0 {
|
|
mentions = append(mentions, current)
|
|
}
|
|
}
|
|
|
|
return mentions
|
|
}
|
|
|
|
// ============================================================================
|
|
// Notification Cleanup
|
|
// ============================================================================
|
|
|
|
// DeleteOldNotifications removes notifications older than the specified days
|
|
func (r *NotificationRepository) DeleteOldNotifications(ctx context.Context, daysOld int) (int64, error) {
|
|
result, err := r.pool.Exec(ctx, `
|
|
DELETE FROM notifications
|
|
WHERE created_at < NOW() - INTERVAL '1 day' * $1
|
|
`, daysOld)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
// ArchiveOldNotifications marks old notifications as archived instead of deleting
|
|
func (r *NotificationRepository) ArchiveOldNotifications(ctx context.Context, daysOld int) (int64, error) {
|
|
result, err := r.pool.Exec(ctx, `
|
|
UPDATE notifications
|
|
SET archived_at = NOW()
|
|
WHERE created_at < NOW() - INTERVAL '1 day' * $1 AND archived_at IS NULL
|
|
`, daysOld)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helper for building metadata JSON
|
|
// ============================================================================
|
|
|
|
func BuildNotificationMetadata(data map[string]interface{}) json.RawMessage {
|
|
if data == nil {
|
|
return json.RawMessage("{}")
|
|
}
|
|
bytes, err := json.Marshal(data)
|
|
if err != nil {
|
|
return json.RawMessage("{}")
|
|
}
|
|
return bytes
|
|
}
|