Files
hMarket/backend/src/services/notificationService.js
2026-03-01 20:26:44 +03:00

534 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
};