wip
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Infrastructure Adapter: DiscordNotificationAdapter (Stub)
|
||||
*
|
||||
* Handles Discord webhook notifications.
|
||||
* Currently a stub - to be implemented when Discord integration is needed.
|
||||
*/
|
||||
|
||||
import type { Notification } from '../../domain/entities/Notification';
|
||||
import type {
|
||||
INotificationGateway,
|
||||
NotificationDeliveryResult
|
||||
} from '../../application/ports/INotificationGateway';
|
||||
import type { NotificationChannel } from '../../domain/value-objects/NotificationChannel';
|
||||
|
||||
export interface DiscordAdapterConfig {
|
||||
webhookUrl?: string;
|
||||
}
|
||||
|
||||
export class DiscordNotificationAdapter implements INotificationGateway {
|
||||
private readonly channel: NotificationChannel = 'discord';
|
||||
private webhookUrl?: string;
|
||||
|
||||
constructor(config: DiscordAdapterConfig = {}) {
|
||||
this.webhookUrl = config.webhookUrl;
|
||||
}
|
||||
|
||||
async send(notification: Notification): Promise<NotificationDeliveryResult> {
|
||||
if (!this.isConfigured()) {
|
||||
return {
|
||||
success: false,
|
||||
channel: this.channel,
|
||||
error: 'Discord webhook URL not configured',
|
||||
attemptedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Implement actual Discord webhook call
|
||||
// For now, this is a stub that logs and returns success
|
||||
console.log(`[Discord Stub] Would send notification to ${this.webhookUrl}:`, {
|
||||
title: notification.title,
|
||||
body: notification.body,
|
||||
type: notification.type,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
channel: this.channel,
|
||||
externalId: `discord-stub-${notification.id}`,
|
||||
attemptedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
supportsChannel(channel: NotificationChannel): boolean {
|
||||
return channel === this.channel;
|
||||
}
|
||||
|
||||
isConfigured(): boolean {
|
||||
return !!this.webhookUrl;
|
||||
}
|
||||
|
||||
getChannel(): NotificationChannel {
|
||||
return this.channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the webhook URL
|
||||
*/
|
||||
setWebhookUrl(url: string): void {
|
||||
this.webhookUrl = url;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Infrastructure Adapter: EmailNotificationAdapter (Stub)
|
||||
*
|
||||
* Handles email notifications.
|
||||
* Currently a stub - to be implemented when email integration is needed.
|
||||
*/
|
||||
|
||||
import type { Notification } from '../../domain/entities/Notification';
|
||||
import type {
|
||||
INotificationGateway,
|
||||
NotificationDeliveryResult
|
||||
} from '../../application/ports/INotificationGateway';
|
||||
import type { NotificationChannel } from '../../domain/value-objects/NotificationChannel';
|
||||
|
||||
export interface EmailAdapterConfig {
|
||||
smtpHost?: string;
|
||||
smtpPort?: number;
|
||||
smtpUser?: string;
|
||||
smtpPassword?: string;
|
||||
fromAddress?: string;
|
||||
}
|
||||
|
||||
export class EmailNotificationAdapter implements INotificationGateway {
|
||||
private readonly channel: NotificationChannel = 'email';
|
||||
private config: EmailAdapterConfig;
|
||||
|
||||
constructor(config: EmailAdapterConfig = {}) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
async send(notification: Notification): Promise<NotificationDeliveryResult> {
|
||||
if (!this.isConfigured()) {
|
||||
return {
|
||||
success: false,
|
||||
channel: this.channel,
|
||||
error: 'Email SMTP not configured',
|
||||
attemptedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Implement actual email sending
|
||||
// For now, this is a stub that logs and returns success
|
||||
console.log(`[Email Stub] Would send email:`, {
|
||||
to: notification.recipientId, // Would need to resolve to actual email
|
||||
subject: notification.title,
|
||||
body: notification.body,
|
||||
type: notification.type,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
channel: this.channel,
|
||||
externalId: `email-stub-${notification.id}`,
|
||||
attemptedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
supportsChannel(channel: NotificationChannel): boolean {
|
||||
return channel === this.channel;
|
||||
}
|
||||
|
||||
isConfigured(): boolean {
|
||||
return !!(this.config.smtpHost && this.config.fromAddress);
|
||||
}
|
||||
|
||||
getChannel(): NotificationChannel {
|
||||
return this.channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update SMTP configuration
|
||||
*/
|
||||
configure(config: EmailAdapterConfig): void {
|
||||
this.config = { ...this.config, ...config };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Infrastructure Adapter: InAppNotificationAdapter
|
||||
*
|
||||
* Handles in-app notifications (stored in database, shown in UI).
|
||||
* This is the primary/default notification channel.
|
||||
*/
|
||||
|
||||
import type { Notification } from '../../domain/entities/Notification';
|
||||
import type {
|
||||
INotificationGateway,
|
||||
NotificationDeliveryResult
|
||||
} from '../../application/ports/INotificationGateway';
|
||||
import type { NotificationChannel } from '../../domain/value-objects/NotificationChannel';
|
||||
|
||||
export class InAppNotificationAdapter implements INotificationGateway {
|
||||
private readonly channel: NotificationChannel = 'in_app';
|
||||
|
||||
/**
|
||||
* For in_app, sending is essentially a no-op since the notification
|
||||
* is already persisted by the use case. This just confirms delivery.
|
||||
*/
|
||||
async send(notification: Notification): Promise<NotificationDeliveryResult> {
|
||||
// In-app notifications are stored directly in the repository
|
||||
// This adapter just confirms the "delivery" was successful
|
||||
return {
|
||||
success: true,
|
||||
channel: this.channel,
|
||||
externalId: notification.id,
|
||||
attemptedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
supportsChannel(channel: NotificationChannel): boolean {
|
||||
return channel === this.channel;
|
||||
}
|
||||
|
||||
isConfigured(): boolean {
|
||||
return true; // Always configured
|
||||
}
|
||||
|
||||
getChannel(): NotificationChannel {
|
||||
return this.channel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Infrastructure: NotificationGatewayRegistry
|
||||
*
|
||||
* Manages notification gateways and routes notifications to appropriate channels.
|
||||
*/
|
||||
|
||||
import type { Notification } from '../../domain/entities/Notification';
|
||||
import type { NotificationChannel } from '../../domain/value-objects/NotificationChannel';
|
||||
import type {
|
||||
INotificationGateway,
|
||||
INotificationGatewayRegistry,
|
||||
NotificationDeliveryResult
|
||||
} from '../../application/ports/INotificationGateway';
|
||||
|
||||
export class NotificationGatewayRegistry implements INotificationGatewayRegistry {
|
||||
private gateways: Map<NotificationChannel, INotificationGateway> = new Map();
|
||||
|
||||
constructor(initialGateways: INotificationGateway[] = []) {
|
||||
initialGateways.forEach(gateway => this.register(gateway));
|
||||
}
|
||||
|
||||
register(gateway: INotificationGateway): void {
|
||||
const channel = gateway.getChannel();
|
||||
this.gateways.set(channel, gateway);
|
||||
}
|
||||
|
||||
getGateway(channel: NotificationChannel): INotificationGateway | null {
|
||||
return this.gateways.get(channel) || null;
|
||||
}
|
||||
|
||||
getAllGateways(): INotificationGateway[] {
|
||||
return Array.from(this.gateways.values());
|
||||
}
|
||||
|
||||
async send(notification: Notification): Promise<NotificationDeliveryResult> {
|
||||
const gateway = this.gateways.get(notification.channel);
|
||||
|
||||
if (!gateway) {
|
||||
return {
|
||||
success: false,
|
||||
channel: notification.channel,
|
||||
error: `No gateway registered for channel: ${notification.channel}`,
|
||||
attemptedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
if (!gateway.isConfigured()) {
|
||||
return {
|
||||
success: false,
|
||||
channel: notification.channel,
|
||||
error: `Gateway for channel ${notification.channel} is not configured`,
|
||||
attemptedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return await gateway.send(notification);
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
channel: notification.channel,
|
||||
error: error instanceof Error ? error.message : 'Unknown error during delivery',
|
||||
attemptedAt: new Date(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
11
packages/notifications/infrastructure/index.ts
Normal file
11
packages/notifications/infrastructure/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Infrastructure layer exports for notifications package
|
||||
*/
|
||||
|
||||
// Repositories
|
||||
export { InMemoryNotificationRepository } from './repositories/InMemoryNotificationRepository';
|
||||
export { InMemoryNotificationPreferenceRepository } from './repositories/InMemoryNotificationPreferenceRepository';
|
||||
|
||||
// Adapters
|
||||
export { InAppNotificationAdapter } from './adapters/InAppNotificationAdapter';
|
||||
export { NotificationGatewayRegistry } from './adapters/NotificationGatewayRegistry';
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* In-Memory Implementation: InMemoryNotificationPreferenceRepository
|
||||
*
|
||||
* Provides an in-memory storage implementation for notification preferences.
|
||||
*/
|
||||
|
||||
import { NotificationPreference } from '../../domain/entities/NotificationPreference';
|
||||
import type { INotificationPreferenceRepository } from '../../domain/repositories/INotificationPreferenceRepository';
|
||||
|
||||
export class InMemoryNotificationPreferenceRepository implements INotificationPreferenceRepository {
|
||||
private preferences: Map<string, NotificationPreference> = new Map();
|
||||
|
||||
constructor(initialPreferences: NotificationPreference[] = []) {
|
||||
initialPreferences.forEach(pref => {
|
||||
this.preferences.set(pref.driverId, pref);
|
||||
});
|
||||
}
|
||||
|
||||
async findByDriverId(driverId: string): Promise<NotificationPreference | null> {
|
||||
return this.preferences.get(driverId) || null;
|
||||
}
|
||||
|
||||
async save(preference: NotificationPreference): Promise<void> {
|
||||
this.preferences.set(preference.driverId, preference);
|
||||
}
|
||||
|
||||
async delete(driverId: string): Promise<void> {
|
||||
this.preferences.delete(driverId);
|
||||
}
|
||||
|
||||
async getOrCreateDefault(driverId: string): Promise<NotificationPreference> {
|
||||
const existing = this.preferences.get(driverId);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const defaultPreference = NotificationPreference.createDefault(driverId);
|
||||
this.preferences.set(driverId, defaultPreference);
|
||||
return defaultPreference;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* In-Memory Implementation: InMemoryNotificationRepository
|
||||
*
|
||||
* Provides an in-memory storage implementation for notifications.
|
||||
*/
|
||||
|
||||
import { Notification } from '../../domain/entities/Notification';
|
||||
import type { INotificationRepository } from '../../domain/repositories/INotificationRepository';
|
||||
import type { NotificationType } from '../../domain/value-objects/NotificationType';
|
||||
|
||||
export class InMemoryNotificationRepository implements INotificationRepository {
|
||||
private notifications: Map<string, Notification> = new Map();
|
||||
|
||||
constructor(initialNotifications: Notification[] = []) {
|
||||
initialNotifications.forEach(notification => {
|
||||
this.notifications.set(notification.id, notification);
|
||||
});
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Notification | null> {
|
||||
return this.notifications.get(id) || null;
|
||||
}
|
||||
|
||||
async findByRecipientId(recipientId: string): Promise<Notification[]> {
|
||||
return Array.from(this.notifications.values())
|
||||
.filter(n => n.recipientId === recipientId)
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
}
|
||||
|
||||
async findUnreadByRecipientId(recipientId: string): Promise<Notification[]> {
|
||||
return Array.from(this.notifications.values())
|
||||
.filter(n => n.recipientId === recipientId && n.isUnread())
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
}
|
||||
|
||||
async findByRecipientIdAndType(recipientId: string, type: NotificationType): Promise<Notification[]> {
|
||||
return Array.from(this.notifications.values())
|
||||
.filter(n => n.recipientId === recipientId && n.type === type)
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
}
|
||||
|
||||
async countUnreadByRecipientId(recipientId: string): Promise<number> {
|
||||
return Array.from(this.notifications.values())
|
||||
.filter(n => n.recipientId === recipientId && n.isUnread())
|
||||
.length;
|
||||
}
|
||||
|
||||
async create(notification: Notification): Promise<void> {
|
||||
if (this.notifications.has(notification.id)) {
|
||||
throw new Error(`Notification with ID ${notification.id} already exists`);
|
||||
}
|
||||
this.notifications.set(notification.id, notification);
|
||||
}
|
||||
|
||||
async update(notification: Notification): Promise<void> {
|
||||
if (!this.notifications.has(notification.id)) {
|
||||
throw new Error(`Notification with ID ${notification.id} not found`);
|
||||
}
|
||||
this.notifications.set(notification.id, notification);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
this.notifications.delete(id);
|
||||
}
|
||||
|
||||
async deleteAllByRecipientId(recipientId: string): Promise<void> {
|
||||
const toDelete = Array.from(this.notifications.values())
|
||||
.filter(n => n.recipientId === recipientId)
|
||||
.map(n => n.id);
|
||||
|
||||
toDelete.forEach(id => this.notifications.delete(id));
|
||||
}
|
||||
|
||||
async markAllAsReadByRecipientId(recipientId: string): Promise<void> {
|
||||
const toUpdate = Array.from(this.notifications.values())
|
||||
.filter(n => n.recipientId === recipientId && n.isUnread());
|
||||
|
||||
toUpdate.forEach(n => {
|
||||
const updated = n.markAsRead();
|
||||
this.notifications.set(updated.id, updated);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user