erp-core-backend/src/modules/system/notifications.service.ts
2025-12-12 14:39:29 -06:00

228 lines
6.1 KiB
TypeScript

import { query, queryOne } from '../../config/database.js';
import { NotFoundError } from '../../shared/errors/index.js';
export interface Notification {
id: string;
tenant_id: string;
user_id: string;
title: string;
message: string;
url?: string;
model?: string;
record_id?: string;
status: 'pending' | 'sent' | 'read' | 'failed';
read_at?: Date;
created_at: Date;
sent_at?: Date;
}
export interface CreateNotificationDto {
user_id: string;
title: string;
message: string;
url?: string;
model?: string;
record_id?: string;
}
export interface NotificationFilters {
user_id?: string;
status?: string;
unread_only?: boolean;
model?: string;
search?: string;
page?: number;
limit?: number;
}
class NotificationsService {
async findAll(tenantId: string, filters: NotificationFilters = {}): Promise<{ data: Notification[]; total: number }> {
const { user_id, status, unread_only, model, search, page = 1, limit = 50 } = filters;
const offset = (page - 1) * limit;
let whereClause = 'WHERE n.tenant_id = $1';
const params: any[] = [tenantId];
let paramIndex = 2;
if (user_id) {
whereClause += ` AND n.user_id = $${paramIndex++}`;
params.push(user_id);
}
if (status) {
whereClause += ` AND n.status = $${paramIndex++}`;
params.push(status);
}
if (unread_only) {
whereClause += ` AND n.read_at IS NULL`;
}
if (model) {
whereClause += ` AND n.model = $${paramIndex++}`;
params.push(model);
}
if (search) {
whereClause += ` AND (n.title ILIKE $${paramIndex} OR n.message ILIKE $${paramIndex})`;
params.push(`%${search}%`);
paramIndex++;
}
const countResult = await queryOne<{ count: string }>(
`SELECT COUNT(*) as count FROM system.notifications n ${whereClause}`,
params
);
params.push(limit, offset);
const data = await query<Notification>(
`SELECT n.*
FROM system.notifications n
${whereClause}
ORDER BY n.created_at DESC
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
params
);
return {
data,
total: parseInt(countResult?.count || '0', 10),
};
}
async findByUser(userId: string, tenantId: string, unreadOnly: boolean = false): Promise<Notification[]> {
let whereClause = 'WHERE n.user_id = $1 AND n.tenant_id = $2';
if (unreadOnly) {
whereClause += ' AND n.read_at IS NULL';
}
const notifications = await query<Notification>(
`SELECT n.*
FROM system.notifications n
${whereClause}
ORDER BY n.created_at DESC
LIMIT 100`,
[userId, tenantId]
);
return notifications;
}
async getUnreadCount(userId: string, tenantId: string): Promise<number> {
const result = await queryOne<{ count: string }>(
`SELECT COUNT(*) as count
FROM system.notifications
WHERE user_id = $1 AND tenant_id = $2 AND read_at IS NULL`,
[userId, tenantId]
);
return parseInt(result?.count || '0', 10);
}
async findById(id: string, tenantId: string): Promise<Notification> {
const notification = await queryOne<Notification>(
`SELECT n.*
FROM system.notifications n
WHERE n.id = $1 AND n.tenant_id = $2`,
[id, tenantId]
);
if (!notification) {
throw new NotFoundError('Notificación no encontrada');
}
return notification;
}
async create(dto: CreateNotificationDto, tenantId: string): Promise<Notification> {
const notification = await queryOne<Notification>(
`INSERT INTO system.notifications (
tenant_id, user_id, title, message, url, model, record_id, status, sent_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, 'sent', CURRENT_TIMESTAMP)
RETURNING *`,
[tenantId, dto.user_id, dto.title, dto.message, dto.url, dto.model, dto.record_id]
);
return notification!;
}
async createBulk(notifications: CreateNotificationDto[], tenantId: string): Promise<number> {
if (notifications.length === 0) return 0;
const values = notifications.map((n, i) => {
const base = i * 7;
return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}, $${base + 5}, $${base + 6}, $${base + 7}, 'sent', CURRENT_TIMESTAMP)`;
}).join(', ');
const params = notifications.flatMap(n => [
tenantId, n.user_id, n.title, n.message, n.url, n.model, n.record_id
]);
const result = await query(
`INSERT INTO system.notifications (
tenant_id, user_id, title, message, url, model, record_id, status, sent_at
)
VALUES ${values}`,
params
);
return notifications.length;
}
async markAsRead(id: string, tenantId: string): Promise<Notification> {
await this.findById(id, tenantId);
const notification = await queryOne<Notification>(
`UPDATE system.notifications SET
status = 'read',
read_at = CURRENT_TIMESTAMP
WHERE id = $1 AND tenant_id = $2
RETURNING *`,
[id, tenantId]
);
return notification!;
}
async markAllAsRead(userId: string, tenantId: string): Promise<number> {
const result = await query(
`UPDATE system.notifications SET
status = 'read',
read_at = CURRENT_TIMESTAMP
WHERE user_id = $1 AND tenant_id = $2 AND read_at IS NULL`,
[userId, tenantId]
);
return result.length;
}
async delete(id: string, tenantId: string): Promise<void> {
await this.findById(id, tenantId);
await query(
`DELETE FROM system.notifications WHERE id = $1 AND tenant_id = $2`,
[id, tenantId]
);
}
async deleteOld(daysToKeep: number = 30, tenantId?: string): Promise<number> {
let whereClause = `WHERE read_at IS NOT NULL AND created_at < CURRENT_TIMESTAMP - INTERVAL '${daysToKeep} days'`;
const params: any[] = [];
if (tenantId) {
whereClause += ' AND tenant_id = $1';
params.push(tenantId);
}
const result = await query(
`DELETE FROM system.notifications ${whereClause}`,
params
);
return result.length;
}
}
export const notificationsService = new NotificationsService();