// ============================================================================= // Notification Service // ============================================================================= import pool from '../config/database'; import logger from '../utils/logger'; import { Notification, CreateNotificationData } from '../models/notification.model'; function rowToNotification(row: any): Notification { return { id: row.id, user_id: row.user_id, typ: row.typ, titel: row.titel, nachricht: row.nachricht, schwere: row.schwere, gelesen: row.gelesen, gelesen_am: row.gelesen_am ? new Date(row.gelesen_am) : null, link: row.link ?? null, quell_id: row.quell_id ?? null, quell_typ: row.quell_typ ?? null, erstellt_am: new Date(row.erstellt_am), }; } class NotificationService { /** Returns all notifications for a user (newest first, max 100). */ async getByUser(userId: string): Promise { try { const result = await pool.query( `SELECT * FROM notifications WHERE user_id = $1 ORDER BY erstellt_am DESC LIMIT 100`, [userId] ); return result.rows.map(rowToNotification); } catch (error) { logger.error('NotificationService.getByUser failed', { error, userId }); throw new Error('Notifications konnten nicht geladen werden'); } } /** Returns the count of unread notifications for a user. */ async getUnreadCount(userId: string): Promise { try { const result = await pool.query( `SELECT COUNT(*) AS cnt FROM notifications WHERE user_id = $1 AND gelesen = FALSE`, [userId] ); return parseInt(result.rows[0].cnt, 10); } catch (error) { logger.error('NotificationService.getUnreadCount failed', { error, userId }); throw new Error('Ungelesene Notifications konnten nicht gezählt werden'); } } /** Marks a single notification as read. Returns false if not found or not owned by user. */ async markAsRead(id: string, userId: string): Promise { try { const result = await pool.query( `UPDATE notifications SET gelesen = TRUE, gelesen_am = NOW() WHERE id = $1 AND user_id = $2 AND gelesen = FALSE RETURNING id`, [id, userId] ); return (result.rowCount ?? 0) > 0; } catch (error) { logger.error('NotificationService.markAsRead failed', { error, id, userId }); throw new Error('Notification konnte nicht als gelesen markiert werden'); } } /** Marks all notifications as read for a user. */ async markAllRead(userId: string): Promise { try { await pool.query( `UPDATE notifications SET gelesen = TRUE, gelesen_am = NOW() WHERE user_id = $1 AND gelesen = FALSE`, [userId] ); } catch (error) { logger.error('NotificationService.markAllRead failed', { error, userId }); throw new Error('Notifications konnten nicht als gelesen markiert werden'); } } /** * Creates a notification. If quell_typ + quell_id are provided the insert is * silently ignored when an identical unread notification already exists * (dedup via unique index). */ async createNotification(data: CreateNotificationData): Promise { try { await pool.query( `INSERT INTO notifications (user_id, typ, titel, nachricht, schwere, link, quell_id, quell_typ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (user_id, quell_typ, quell_id) WHERE NOT gelesen AND quell_typ IS NOT NULL AND quell_id IS NOT NULL DO NOTHING`, [ data.user_id, data.typ, data.titel, data.nachricht, data.schwere ?? 'info', data.link ?? null, data.quell_id ?? null, data.quell_typ ?? null, ] ); } catch (error) { logger.error('NotificationService.createNotification failed', { error }); // Non-fatal — don't propagate to callers } } /** Marks all unread notifications of a given quell_typ as read for a user. */ async dismissByType(userId: string, quellTyp: string): Promise { try { await pool.query( `UPDATE notifications SET gelesen = TRUE, gelesen_am = NOW() WHERE user_id = $1 AND quell_typ = $2 AND gelesen = FALSE`, [userId, quellTyp] ); } catch (error) { logger.error('NotificationService.dismissByType failed', { error, userId, quellTyp }); throw new Error('Notifications konnten nicht als gelesen markiert werden'); } } /** Deletes all read notifications for a user. */ async deleteAllRead(userId: string): Promise { try { await pool.query( `DELETE FROM notifications WHERE user_id = $1 AND gelesen = TRUE`, [userId] ); } catch (error) { logger.error('NotificationService.deleteAllRead failed', { error, userId }); throw new Error('Gelesene Notifications konnten nicht gelöscht werden'); } } /** Deletes read notifications older than 90 days for all users. */ async deleteOldRead(): Promise { try { const result = await pool.query( `DELETE FROM notifications WHERE gelesen = TRUE AND gelesen_am < NOW() - INTERVAL '90 days'` ); if ((result.rowCount ?? 0) > 0) { logger.info(`NotificationService.deleteOldRead: removed ${result.rowCount} old notifications`); } } catch (error) { logger.error('NotificationService.deleteOldRead failed', { error }); } } } export default new NotificationService();