notification

This commit is contained in:
vadim committed 2026-09-29 18:42:29 +03:00
1 parent 382cafa152
commit 60391ee123
26 files changed
+1077 -14

No files matched your search

@@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common';
import { PrismaAdapter } from '../../../../../../common/prisma-adapter/prisma.adapter.js';
import type { NotificationDeviceToken } from '../../../../../../generated/prisma/client.js';
import type { IDeviceTokenRepository } from '../../../../domain/repositories/device-token.repository.interface.js';
@Injectable()
export class DeviceTokenPrismaRepository implements IDeviceTokenRepository {
constructor(private readonly prisma: PrismaAdapter) {}
async upsert(userId: string, token: string, platform: string): Promise<void> {
await this.prisma.notificationDeviceToken.upsert({
where: { userId_token: { userId, token } },
create: { userId, token, platform },
update: { platform },
});
}
async remove(userId: string, token: string): Promise<void> {
await this.prisma.notificationDeviceToken.deleteMany({ where: { userId, token } });
}
findByUserId(userId: string): Promise<NotificationDeviceToken[]> {
return this.prisma.notificationDeviceToken.findMany({ where: { userId } });
}
}
@@ -0,0 +1,27 @@
import { Injectable } from '@nestjs/common';
import { PrismaAdapter } from '../../../../../../common/prisma-adapter/prisma.adapter.js';
import type { NotificationMessage } from '../../../../../../generated/prisma/client.js';
import type { INotificationMessageRepository } from '../../../../domain/repositories/notification-message.repository.interface.js';
import type { NotificationChannelValue } from '../../../../domain/value-objects/notification-channel.vo.js';
import type { NotificationStatusValue } from '../../../../domain/value-objects/notification-status.vo.js';
@Injectable()
export class NotificationMessagePrismaRepository implements INotificationMessageRepository {
constructor(private readonly prisma: PrismaAdapter) {}
create(params: {
userId: string;
channel: NotificationChannelValue;
subject?: string;
content: string;
}): Promise<NotificationMessage> {
return this.prisma.notificationMessage.create({ data: params });
}
async markStatus(id: string, status: NotificationStatusValue, error?: string): Promise<void> {
await this.prisma.notificationMessage.update({
where: { id },
data: { status, error, sentAt: status === 'SENT' ? new Date() : undefined },
});
}
}
@@ -0,0 +1,22 @@
import { Injectable } from '@nestjs/common';
import { PrismaAdapter } from '../../../../../../common/prisma-adapter/prisma.adapter.js';
import type { NotificationPreference } from '../../../../../../generated/prisma/client.js';
import type { INotificationPreferenceRepository } from '../../../../domain/repositories/notification-preference.repository.interface.js';
import type { NotificationChannelValue } from '../../../../domain/value-objects/notification-channel.vo.js';
@Injectable()
export class NotificationPreferencePrismaRepository implements INotificationPreferenceRepository {
constructor(private readonly prisma: PrismaAdapter) {}
findByUserId(userId: string): Promise<NotificationPreference[]> {
return this.prisma.notificationPreference.findMany({ where: { userId } });
}
async set(userId: string, channel: NotificationChannelValue, enabled: boolean): Promise<void> {
await this.prisma.notificationPreference.upsert({
where: { userId_channel: { userId, channel } },
create: { userId, channel, enabled },
update: { enabled },
});
}
}
@@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common';
import EmailSender from '../../../../lib/email-sender.js';
import { PrismaAdapter } from '../../../../common/prisma-adapter/prisma.adapter.js';
import type { NotificationChannelSender } from '../../domain/channel-sender.interface.js';
import type { NotificationChannelValue } from '../../domain/value-objects/notification-channel.vo.js';
@Injectable()
export class EmailChannelSender implements NotificationChannelSender {
readonly channel: NotificationChannelValue = 'EMAIL';
constructor(private readonly prisma: PrismaAdapter) {}
async send(params: { userId: string; subject?: string; content: string }): Promise<void> {
const user = await this.prisma.user.findUniqueOrThrow({
where: { id: params.userId },
select: { email: true },
});
await EmailSender.getTransporter().sendMail({
to: user.email,
subject: params.subject ?? 'Уведомление',
text: params.content,
});
}
}
@@ -0,0 +1,31 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import type { NotificationChannelSender } from '../../domain/channel-sender.interface.js';
import type { NotificationChannelValue } from '../../domain/value-objects/notification-channel.vo.js';
import {
DEVICE_TOKEN_REPOSITORY,
type IDeviceTokenRepository,
} from '../../domain/repositories/device-token.repository.interface.js';
/**
* Провайдер push-доставки (FCM/APNs) пока не подключён — отправка логируется.
* Токены уже читаются из БД, так что при подключении провайдера
* меняется только тело send(), остальной пайплайн трогать не нужно.
*/
@Injectable()
export class PushChannelSender implements NotificationChannelSender {
private readonly logger = new Logger(PushChannelSender.name);
readonly channel: NotificationChannelValue = 'PUSH';
constructor(@Inject(DEVICE_TOKEN_REPOSITORY) private readonly deviceTokens: IDeviceTokenRepository) {}
async send(params: { userId: string; content: string }): Promise<void> {
const tokens = await this.deviceTokens.findByUserId(params.userId);
if (tokens.length === 0) {
throw new Error(`No device tokens registered for user ${params.userId}`);
}
this.logger.warn(
`Push provider is not configured yet, skipping actual delivery to ${tokens.length} device(s): ${params.content}`,
);
}
}