const admin = require('firebase-admin'); class NotificationService { constructor(prisma, io) { this.prisma = prisma; this.io = io; this.initializeFirebase(); } initializeFirebase() { try { // Firebase credentials kontrolü const hasCredentials = process.env.FIREBASE_PROJECT_ID && process.env.FIREBASE_PRIVATE_KEY && process.env.FIREBASE_CLIENT_EMAIL; if (!hasCredentials) { console.log('⚠️ Firebase credentials not configured. Push notifications disabled.'); return; } if (!admin.apps.length) { const serviceAccount = { type: process.env.FIREBASE_TYPE, project_id: process.env.FIREBASE_PROJECT_ID, private_key_id: process.env.FIREBASE_PRIVATE_KEY_ID, private_key: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'), client_email: process.env.FIREBASE_CLIENT_EMAIL, client_id: process.env.FIREBASE_CLIENT_ID, auth_uri: process.env.FIREBASE_AUTH_URI, token_uri: process.env.FIREBASE_TOKEN_URI, auth_provider_x509_cert_url: process.env.FIREBASE_AUTH_PROVIDER_X509_CERT_URL, client_x509_cert_url: process.env.FIREBASE_CLIENT_X509_CERT_URL }; admin.initializeApp({ credential: admin.credential.cert(serviceAccount) }); console.log('✅ Firebase Admin SDK initialized successfully'); } } catch (error) { console.error('Firebase initialization error:', error.message); console.warn('Push notifications will be disabled.'); } } /** * Veritabanına bildirim kaydet */ async createNotification(data) { try { const notification = await this.prisma.notification.create({ data: { userId: data.userId, type: data.type, title: data.title, message: data.message, data: data.data ? JSON.stringify(data.data) : null, relatedListId: data.relatedListId || null, relatedUserId: data.relatedUserId || null }, include: { relatedList: { select: { id: true, name: true } }, relatedUser: { select: { id: true, username: true, firstName: true, lastName: true, avatar: true } } } }); return notification; } catch (error) { console.error('Error creating notification:', error); throw error; } } /** * Socket.IO ile gerçek zamanlı bildirim gönder */ async sendRealtimeNotification(userId, notification) { try { if (this.io) { this.io.to(`user_${userId}`).emit('notification', notification); } } catch (error) { console.error('Error sending realtime notification:', error); } } /** * Push notification gönder */ async sendPushNotification(userId, notification) { try { // Kullanıcının push notification ayarını kontrol et const setting = await this.prisma.setting.findUnique({ where: { userId_key: { userId, key: 'notification_push_enabled' } } }); if (setting && setting.value === 'false') { return; // Push notification kapalı } // Kullanıcının device token'larını al const deviceTokens = await this.prisma.deviceToken.findMany({ where: { userId, isActive: true } }); if (deviceTokens.length === 0) { return; // Device token yok } const tokens = deviceTokens.map(dt => dt.token); const message = { notification: { title: notification.title, body: notification.message }, data: { type: notification.type, notificationId: notification.id, ...(notification.data ? JSON.parse(notification.data) : {}) }, tokens }; const response = await admin.messaging().sendMulticast(message); // Başarısız token'ları temizle if (response.failureCount > 0) { const failedTokens = []; response.responses.forEach((resp, idx) => { if (!resp.success) { failedTokens.push(tokens[idx]); } }); if (failedTokens.length > 0) { await this.prisma.deviceToken.updateMany({ where: { token: { in: failedTokens } }, data: { isActive: false } }); } } console.log(`Push notification sent to ${response.successCount} devices`); } catch (error) { console.error('Error sending push notification:', error); } } /** * Tam bildirim gönderme (DB + Realtime + Push) */ async sendNotification(data) { try { // Kullanıcının bildirim ayarını kontrol et const notificationKey = `notification_${data.type}`; const setting = await this.prisma.setting.findUnique({ where: { userId_key: { userId: data.userId, key: notificationKey } } }); if (setting && setting.value === 'false') { return; // Bu tür bildirim kapalı } // Veritabanına kaydet const notification = await this.createNotification(data); // Gerçek zamanlı gönder await this.sendRealtimeNotification(data.userId, notification); // Push notification gönder await this.sendPushNotification(data.userId, notification); return notification; } catch (error) { console.error('Error in sendNotification:', error); throw error; } } /** * Birden fazla kullanıcıya bildirim gönder */ async sendNotificationToMultipleUsers(userIds, data) { try { const promises = userIds.map(userId => this.sendNotification({ ...data, userId }) ); const results = await Promise.allSettled(promises); const successful = results.filter(r => r.status === 'fulfilled').length; const failed = results.filter(r => r.status === 'rejected').length; console.log(`Notifications sent: ${successful} successful, ${failed} failed`); return { successful, failed }; } catch (error) { console.error('Error sending notifications to multiple users:', error); throw error; } } /** * Liste üyelerine bildirim gönder */ async sendNotificationToListMembers(listId, data, excludeUserId = null) { try { const members = await this.prisma.listMember.findMany({ where: { listId, userId: excludeUserId ? { not: excludeUserId } : undefined }, select: { userId: true } }); const userIds = members.map(m => m.userId); if (userIds.length === 0) { return { successful: 0, failed: 0 }; } return await this.sendNotificationToMultipleUsers(userIds, { ...data, relatedListId: listId }); } catch (error) { console.error('Error sending notifications to list members:', error); throw error; } } /** * Liste daveti bildirimi */ async sendListInviteNotification(invitedUserId, inviterUser, list) { return await this.sendNotification({ userId: invitedUserId, type: 'list_invite', title: 'Liste Daveti', message: `${inviterUser.firstName} ${inviterUser.lastName} sizi "${list.name}" listesine davet etti`, data: { inviterId: inviterUser.id, listId: list.id }, relatedListId: list.id, relatedUserId: inviterUser.id }); } /** * Ürün eklendi bildirimi */ async sendItemAddedNotification(listId, adderUser, itemName, excludeUserId = null) { return await this.sendNotificationToListMembers(listId, { type: 'item_added', title: 'Ürün Eklendi', message: `${adderUser.firstName} "${itemName}" ürününü listeye ekledi`, data: { adderId: adderUser.id, itemName }, relatedUserId: adderUser.id }, excludeUserId); } /** * Ürün silindi bildirimi */ async sendItemRemovedNotification(listId, removerUser, itemName, excludeUserId = null) { return await this.sendNotificationToListMembers(listId, { type: 'item_removed', title: 'Ürün Silindi', message: `${removerUser.firstName} "${itemName}" ürününü listeden sildi`, data: { removerId: removerUser.id, itemName }, relatedUserId: removerUser.id }, excludeUserId); } /** * Ürün satın alındı bildirimi */ async sendItemPurchasedNotification(listId, purchaserUser, itemName, excludeUserId = null) { return await this.sendNotificationToListMembers(listId, { type: 'item_purchased', title: 'Ürün Satın Alındı', message: `${purchaserUser.firstName} "${itemName}" ürününü satın aldı`, data: { purchaserId: purchaserUser.id, itemName }, relatedUserId: purchaserUser.id }, excludeUserId); } /** * Liste paylaşıldı bildirimi */ async sendListSharedNotification(listId, sharerUser, sharedWithUser, listName) { return await this.sendNotification({ userId: sharedWithUser.id, type: 'list_shared', title: 'Liste Paylaşıldı', message: `${sharerUser.firstName} "${listName}" listesini sizinle paylaştı`, data: { sharerId: sharerUser.id, listName }, relatedListId: listId, relatedUserId: sharerUser.id }); } /** * Device token kaydet */ async registerDeviceToken(userId, token, deviceInfo = {}) { try { // Mevcut token'ı kontrol et const existingToken = await this.prisma.deviceToken.findFirst({ where: { token, userId } }); if (existingToken) { // Token'ı güncelle return await this.prisma.deviceToken.update({ where: { id: existingToken.id }, data: { isActive: true, deviceInfo: deviceInfo ? JSON.stringify(deviceInfo) : null, updatedAt: new Date() } }); } else { // Yeni token oluştur return await this.prisma.deviceToken.create({ data: { userId, token, deviceInfo: deviceInfo ? JSON.stringify(deviceInfo) : null } }); } } catch (error) { console.error('Error registering device token:', error); throw error; } } /** * Device token'ı deaktive et */ async unregisterDeviceToken(userId, token) { try { return await this.prisma.deviceToken.updateMany({ where: { userId, token }, data: { isActive: false } }); } catch (error) { console.error('Error unregistering device token:', error); throw error; } } /** * Liste üyelerine bildirim gönder */ async notifyListMembers(listId, excludeUserId, notificationData) { try { // Liste üyelerini getir const listMembers = await this.prisma.listMember.findMany({ where: { listId, userId: { not: excludeUserId // Aksiyonu yapan kullanıcıyı hariç tut } }, include: { user: { select: { id: true, firstName: true, lastName: true } } } }); // Liste sahibini de dahil et (eğer üye değilse) const list = await this.prisma.shoppingList.findUnique({ where: { id: listId }, include: { owner: { select: { id: true, firstName: true, lastName: true } } } }); const memberUserIds = listMembers.map(member => member.userId); if (list.owner.id !== excludeUserId && !memberUserIds.includes(list.owner.id)) { listMembers.push({ user: list.owner, userId: list.owner.id }); } // Her üyeye bildirim gönder const notifications = []; for (const member of listMembers) { // Socket.IO ile gerçek zamanlı bildirim if (this.io) { this.io.to(`user_${member.userId}`).emit('notification', { type: notificationData.type, title: notificationData.title, message: notificationData.message, data: notificationData.data, timestamp: new Date() }); } // Veritabanına bildirim kaydet const notification = await this.createNotification({ userId: member.userId, type: notificationData.type, title: notificationData.title, message: notificationData.message, data: notificationData.data, relatedListId: listId, relatedUserId: excludeUserId }); notifications.push(notification); // Push notification gönder await this.sendPushNotification(member.userId, { title: notificationData.title, body: notificationData.message, data: notificationData.data }); } return notifications; } catch (error) { console.error('Error notifying list members:', error); throw error; } } } // Singleton instance let notificationServiceInstance = null; // Factory function const createNotificationService = (prisma, io) => { if (!notificationServiceInstance) { notificationServiceInstance = new NotificationService(prisma, io); } return notificationServiceInstance; }; // Export both class and factory module.exports = { NotificationService, createNotificationService, // Default export for backward compatibility notifyListMembers: async (...args) => { if (!notificationServiceInstance) { throw new Error('NotificationService not initialized. Call createNotificationService first.'); } return notificationServiceInstance.notifyListMembers(...args); }, createNotification: async (...args) => { if (!notificationServiceInstance) { throw new Error('NotificationService not initialized. Call createNotificationService first.'); } return notificationServiceInstance.createNotification(...args); }, sendPushNotification: async (...args) => { if (!notificationServiceInstance) { throw new Error('NotificationService not initialized. Call createNotificationService first.'); } return notificationServiceInstance.sendPushNotification(...args); } };