import admin from 'firebase-admin'; import { loadNotificationConfig } from '../config/notification.config'; import type { PushNotification, NotificationResult } from '../types/notification.types'; const config = loadNotificationConfig(); let fcmApp: admin.app.App | null = null; function getFCMApp(): admin.app.App { if (!fcmApp) { fcmApp = admin.initializeApp({ credential: admin.credential.cert({ projectId: config.fcm.projectId, clientEmail: config.fcm.clientEmail, privateKey: config.fcm.privateKey.replace(/\\n/g, '\n'), }), }); } return fcmApp; } export class PushService { private static instance: PushService; private sentCount = new Map(); private cleanupInterval: NodeJS.Timeout; private constructor() { this.cleanupInterval = setInterval(() => { const now = Date.now(); for (const [key, timestamp] of this.sentCount.entries()) { if (now - timestamp > 60000) { this.sentCount.delete(key); } } }, 60000); } static getInstance(): PushService { if (!PushService.instance) { PushService.instance = new PushService(); } return PushService.instance; } async send(notification: PushNotification): Promise { const rateLimitKey = `push:${notification.userId}`; const currentCount = this.sentCount.get(rateLimitKey) || 0; if (currentCount >= config.rateLimits.pushPerMinute) { throw new Error(`Push rate limit exceeded for user ${notification.userId}`); } try { const fcmApp = getFCMApp(); const messaging = admin.messaging(fcmApp); const message: admin.messaging.Message = { notification: { title: notification.title, body: notification.body, }, data: notification.data ? Object.fromEntries( Object.entries(notification.data).map(([k, v]) => [k, String(v)]) ) : undefined, token: notification.userId, apns: { payload: { aps: { badge: notification.badge, sound: notification.sound || 'default', category: notification.category, }, }, }, }; const response = await messaging.send(message); this.sentCount.set(rateLimitKey, currentCount + 1); return { notificationId: `push-${response}`, channel: 'push', status: 'sent', externalId: response, deliveredAt: new Date(), }; } catch (error) { return { notificationId: `push-${Date.now()}`, channel: 'push', status: 'failed', error: error instanceof Error ? error.message : 'Unknown error', }; } } async sendBatch(notifications: PushNotification[]): Promise { const results = await Promise.all( notifications.map(n => this.send(n)) ); return results; } getRateLimitStatus(): { remaining: number; limit: number } { return { remaining: config.rateLimits.pushPerMinute - this.sentCount.size, limit: config.rateLimits.pushPerMinute, }; } }