174 lines
4.9 KiB
TypeScript
174 lines
4.9 KiB
TypeScript
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
import { PrismaService } from '../infra/prisma.service';
|
|
import { NotificationPublisherService } from '../infra/notification-publisher.service';
|
|
|
|
@Injectable()
|
|
export class NotificationsService {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly publisher: NotificationPublisherService
|
|
) {}
|
|
|
|
async create(
|
|
userId: string,
|
|
type: string,
|
|
title: string,
|
|
message: string,
|
|
payload?: Record<string, unknown>,
|
|
options?: { skipPublish?: boolean }
|
|
) {
|
|
const notification = await this.prisma.notification.create({
|
|
data: {
|
|
userId,
|
|
type,
|
|
title,
|
|
message,
|
|
payload: (payload ?? undefined) as never
|
|
}
|
|
});
|
|
|
|
if (!options?.skipPublish) {
|
|
await this.publisher.publish({
|
|
userId,
|
|
type,
|
|
title,
|
|
message,
|
|
payload: {
|
|
notificationId: notification.id,
|
|
...(payload ?? {})
|
|
}
|
|
});
|
|
}
|
|
|
|
return this.toResponse(notification);
|
|
}
|
|
|
|
async publishRealtime(userId: string, type: string, title: string, message: string, payload?: Record<string, unknown>) {
|
|
await this.publisher.publish({ userId, type, title, message, payload });
|
|
}
|
|
|
|
async list(userId: string, limit = 30, unreadOnly = false) {
|
|
const notifications = await this.prisma.notification.findMany({
|
|
where: {
|
|
userId,
|
|
...(unreadOnly ? { isRead: false } : {})
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
take: Math.min(Math.max(limit, 1), 100)
|
|
});
|
|
|
|
const inviteIds = notifications
|
|
.filter((item) => item.type === 'family_invite')
|
|
.map((item) => (item.payload as { inviteId?: string } | null)?.inviteId)
|
|
.filter((inviteId): inviteId is string => Boolean(inviteId));
|
|
|
|
const pendingInviteIds = new Set<string>();
|
|
if (inviteIds.length) {
|
|
const invites = await this.prisma.familyInvite.findMany({
|
|
where: { id: { in: inviteIds }, status: 'PENDING' }
|
|
});
|
|
invites.forEach((invite) => pendingInviteIds.add(invite.id));
|
|
}
|
|
|
|
const staleIds: string[] = [];
|
|
const filtered = notifications.filter((item) => {
|
|
if (item.type !== 'family_invite') return true;
|
|
const inviteId = (item.payload as { inviteId?: string } | null)?.inviteId;
|
|
if (!inviteId || !pendingInviteIds.has(inviteId)) {
|
|
staleIds.push(item.id);
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
if (staleIds.length) {
|
|
await this.prisma.notification.deleteMany({
|
|
where: { id: { in: staleIds }, userId }
|
|
});
|
|
}
|
|
|
|
return { notifications: filtered.map((item) => this.toResponse(item)) };
|
|
}
|
|
|
|
async unreadCount(userId: string) {
|
|
const count = await this.prisma.notification.count({
|
|
where: { userId, isRead: false }
|
|
});
|
|
return { count };
|
|
}
|
|
|
|
async markRead(userId: string, notificationId: string) {
|
|
return this.delete(userId, notificationId);
|
|
}
|
|
|
|
async markAllRead(userId: string) {
|
|
return this.deleteAll(userId);
|
|
}
|
|
|
|
async delete(userId: string, notificationId: string) {
|
|
const existing = await this.prisma.notification.findFirst({
|
|
where: { id: notificationId, userId }
|
|
});
|
|
if (!existing) {
|
|
throw new NotFoundException('Уведомление не найдено');
|
|
}
|
|
await this.prisma.notification.delete({ where: { id: notificationId } });
|
|
return { count: 1 };
|
|
}
|
|
|
|
async deleteAll(userId: string) {
|
|
const result = await this.prisma.notification.deleteMany({ where: { userId } });
|
|
return { count: result.count };
|
|
}
|
|
|
|
async dismissOtpCodeNotifications(userId: string) {
|
|
const result = await this.prisma.notification.deleteMany({
|
|
where: { userId, type: 'otp_code' }
|
|
});
|
|
return { count: result.count };
|
|
}
|
|
|
|
async dismissFamilyInvite(userId: string, inviteId: string) {
|
|
const items = await this.prisma.notification.findMany({
|
|
where: { userId, type: 'family_invite' }
|
|
});
|
|
const ids = items
|
|
.filter((item) => {
|
|
const payload = item.payload as { inviteId?: string } | null;
|
|
return payload?.inviteId === inviteId;
|
|
})
|
|
.map((item) => item.id);
|
|
|
|
if (!ids.length) {
|
|
return { count: 0 };
|
|
}
|
|
|
|
await this.prisma.notification.deleteMany({
|
|
where: { id: { in: ids }, userId }
|
|
});
|
|
return { count: ids.length };
|
|
}
|
|
|
|
private toResponse(notification: {
|
|
id: string;
|
|
userId: string;
|
|
type: string;
|
|
title: string;
|
|
message: string;
|
|
payload: unknown;
|
|
isRead: boolean;
|
|
createdAt: Date;
|
|
}) {
|
|
return {
|
|
id: notification.id,
|
|
userId: notification.userId,
|
|
type: notification.type,
|
|
title: notification.title,
|
|
message: notification.message,
|
|
payloadJson: notification.payload ? JSON.stringify(notification.payload) : '',
|
|
isRead: notification.isRead,
|
|
createdAt: notification.createdAt.toISOString()
|
|
};
|
|
}
|
|
}
|