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,9 @@
import type { NotificationChannelValue } from './value-objects/notification-channel.vo.js';
/** DI-токен для набора отправщиков, по одному на каждый канал доставки */
export const NOTIFICATION_CHANNEL_SENDERS = Symbol('NOTIFICATION_CHANNEL_SENDERS');
export interface NotificationChannelSender {
readonly channel: NotificationChannelValue;
send(params: { userId: string; subject?: string; content: string }): Promise<void>;
}
@@ -0,0 +1,16 @@
import type { NotificationChannelValue } from '../value-objects/notification-channel.vo.js';
/**
* Событие, которым любой модуль просит отправить уведомление, не зная,
* как именно notification-модуль его доставит (email/push, сейчас/потом).
*/
export const NOTIFY_EVENT = 'notification.notify';
export interface NotifyEventPayload {
userId: string;
/** Если не задано — уведомление уходит по всем каналам, включённым у пользователя */
channels?: NotificationChannelValue[];
/** Используется email-каналом как тема письма */
subject?: string;
content: string;
}
@@ -0,0 +1,11 @@
import type { NotificationDeviceToken } from '../../../../generated/prisma/client.js';
/** DI-токен, т.к. интерфейсы стираются при компиляции TS */
export const DEVICE_TOKEN_REPOSITORY = Symbol('DEVICE_TOKEN_REPOSITORY');
export interface IDeviceTokenRepository {
/** Регистрирует токен устройства, обновляет запись при повторной регистрации того же токена */
upsert(userId: string, token: string, platform: string): Promise<void>;
remove(userId: string, token: string): Promise<void>;
findByUserId(userId: string): Promise<NotificationDeviceToken[]>;
}
@@ -0,0 +1,16 @@
import type { NotificationMessage } from '../../../../generated/prisma/client.js';
import type { NotificationChannelValue } from '../value-objects/notification-channel.vo.js';
import type { NotificationStatusValue } from '../value-objects/notification-status.vo.js';
/** DI-токен, т.к. интерфейсы стираются при компиляции TS */
export const NOTIFICATION_MESSAGE_REPOSITORY = Symbol('NOTIFICATION_MESSAGE_REPOSITORY');
export interface INotificationMessageRepository {
create(params: {
userId: string;
channel: NotificationChannelValue;
subject?: string;
content: string;
}): Promise<NotificationMessage>;
markStatus(id: string, status: NotificationStatusValue, error?: string): Promise<void>;
}
@@ -0,0 +1,10 @@
import type { NotificationPreference } from '../../../../generated/prisma/client.js';
import type { NotificationChannelValue } from '../value-objects/notification-channel.vo.js';
/** DI-токен, т.к. интерфейсы стираются при компиляции TS */
export const NOTIFICATION_PREFERENCE_REPOSITORY = Symbol('NOTIFICATION_PREFERENCE_REPOSITORY');
export interface INotificationPreferenceRepository {
findByUserId(userId: string): Promise<NotificationPreference[]>;
set(userId: string, channel: NotificationChannelValue, enabled: boolean): Promise<void>;
}
@@ -0,0 +1,23 @@
export const NOTIFICATION_CHANNELS = ['EMAIL', 'PUSH'] as const;
export type NotificationChannelValue = (typeof NOTIFICATION_CHANNELS)[number];
export class NotificationChannel {
private constructor(private readonly _value: NotificationChannelValue) {}
static create(value: string): NotificationChannel {
if (!NOTIFICATION_CHANNELS.includes(value as NotificationChannelValue)) {
throw new Error(`Недопустимый канал уведомлений: "${value}". Разрешены: ${NOTIFICATION_CHANNELS.join(', ')}`);
}
return new NotificationChannel(value as NotificationChannelValue);
}
get value(): NotificationChannelValue {
return this._value;
}
equals(other: NotificationChannel): boolean {
return this._value === other.value;
}
}
@@ -0,0 +1,27 @@
export const NOTIFICATION_STATUSES = ['PENDING', 'SENT', 'FAILED'] as const;
export type NotificationStatusValue = (typeof NOTIFICATION_STATUSES)[number];
export class NotificationStatus {
private constructor(private readonly _value: NotificationStatusValue) {}
static create(value: string): NotificationStatus {
if (!NOTIFICATION_STATUSES.includes(value as NotificationStatusValue)) {
throw new Error(`Недопустимый статус уведомления: "${value}". Разрешены: ${NOTIFICATION_STATUSES.join(', ')}`);
}
return new NotificationStatus(value as NotificationStatusValue);
}
static pending(): NotificationStatus {
return new NotificationStatus('PENDING');
}
get value(): NotificationStatusValue {
return this._value;
}
equals(other: NotificationStatus): boolean {
return this._value === other.value;
}
}