import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ConfigService } from '@nestjs/config'; import * as admin from 'firebase-admin'; import { Notification, NotificationType, } from './entities/notification.entity'; import { UsersService } from '../users/users.service'; @Injectable() export class NotificationsService { private readonly logger = new Logger(NotificationsService.name); private firebaseApp: admin.app.App | null = null; constructor( @InjectRepository(Notification) private readonly notificationsRepository: Repository, private readonly usersService: UsersService, private readonly configService: ConfigService, ) { this.initializeFirebase(); } private initializeFirebase() { const projectId = this.configService.get('FIREBASE_PROJECT_ID'); const clientEmail = this.configService.get('FIREBASE_CLIENT_EMAIL'); const privateKey = this.configService.get('FIREBASE_PRIVATE_KEY'); if (projectId && clientEmail && privateKey && !privateKey.includes('YOUR_KEY')) { try { this.firebaseApp = admin.initializeApp({ credential: admin.credential.cert({ projectId, clientEmail, privateKey: privateKey.replace(/\\n/g, '\n'), }), }); this.logger.log('Firebase initialized successfully'); } catch (error) { this.logger.error('Failed to initialize Firebase:', error.message); } } else { this.logger.warn('Firebase not configured - push notifications disabled'); } } async sendPush( userId: string, type: NotificationType, title: string, body: string, data?: Record, ): Promise { // Create notification record const notification = this.notificationsRepository.create({ userId, type, title, body, data, }); await this.notificationsRepository.save(notification); // Try to send push notification const user = await this.usersService.findById(userId); if (user?.fcmToken && this.firebaseApp) { try { await admin.messaging().send({ token: user.fcmToken, notification: { title, body }, data: data ? this.stringifyData(data) : undefined, android: { priority: 'high', notification: { channelId: 'miinventario_default', }, }, apns: { payload: { aps: { sound: 'default', badge: 1, }, }, }, }); notification.isPushSent = true; await this.notificationsRepository.save(notification); this.logger.log(`Push sent to user ${userId}: ${title}`); } catch (error) { this.logger.error(`Failed to send push to ${userId}:`, error.message); // If token is invalid, clear it if (error.code === 'messaging/invalid-registration-token' || error.code === 'messaging/registration-token-not-registered') { await this.usersService.updateFcmToken(userId, null); } } } else if (!this.firebaseApp) { this.logger.warn(`Push simulation for ${userId}: ${title}`); } return notification; } async notifyVideoProcessingComplete( userId: string, videoId: string, itemsDetected: number, ) { return this.sendPush( userId, NotificationType.VIDEO_PROCESSING_COMPLETE, 'Video procesado', `Se detectaron ${itemsDetected} productos en tu anaquel`, { videoId, itemsDetected }, ); } async notifyVideoProcessingFailed( userId: string, videoId: string, errorMessage: string, ) { return this.sendPush( userId, NotificationType.VIDEO_PROCESSING_FAILED, 'Error al procesar video', 'Hubo un problema al procesar tu video. Intenta de nuevo.', { videoId, error: errorMessage }, ); } async notifyLowCredits(userId: string, currentBalance: number) { return this.sendPush( userId, NotificationType.LOW_CREDITS, 'Creditos bajos', `Te quedan ${currentBalance} creditos. Recarga para seguir escaneando.`, { balance: currentBalance }, ); } async notifyPaymentComplete( userId: string, paymentId: string, creditsGranted: number, ) { return this.sendPush( userId, NotificationType.PAYMENT_COMPLETE, 'Pago completado', `Se agregaron ${creditsGranted} creditos a tu cuenta`, { paymentId, creditsGranted }, ); } async notifyPaymentFailed(userId: string, paymentId: string) { return this.sendPush( userId, NotificationType.PAYMENT_FAILED, 'Pago fallido', 'Tu pago no pudo ser procesado. Intenta con otro metodo.', { paymentId }, ); } async notifyReferralBonus( userId: string, bonusCredits: number, referredName: string, ) { return this.sendPush( userId, NotificationType.REFERRAL_BONUS, 'Bonus de referido', `${referredName} uso tu codigo. Ganaste ${bonusCredits} creditos.`, { bonusCredits, referredName }, ); } async getUserNotifications( userId: string, page = 1, limit = 20, ): Promise<{ notifications: Notification[]; total: number }> { const [notifications, total] = await this.notificationsRepository.findAndCount({ where: { userId }, order: { createdAt: 'DESC' }, skip: (page - 1) * limit, take: limit, }); return { notifications, total }; } async markAsRead(userId: string, notificationId: string): Promise { await this.notificationsRepository.update( { id: notificationId, userId }, { isRead: true }, ); } async markAllAsRead(userId: string): Promise { await this.notificationsRepository.update( { userId, isRead: false }, { isRead: true }, ); } async getUnreadCount(userId: string): Promise { return this.notificationsRepository.count({ where: { userId, isRead: false }, }); } private stringifyData(data: Record): Record { const result: Record = {}; for (const [key, value] of Object.entries(data)) { result[key] = typeof value === 'string' ? value : JSON.stringify(value); } return result; } }