miinventario-v2/apps/backend/src/modules/notifications/notifications.service.ts
rckrdmrd 1a53b5c4d3 [MIINVENTARIO] feat: Initial commit - Sistema de inventario con análisis de video IA
- Backend NestJS con módulos de autenticación, inventario, créditos
- Frontend React con dashboard y componentes UI
- Base de datos PostgreSQL con migraciones
- Tests E2E configurados
- Configuración de Docker y deployment

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 02:25:48 -06:00

229 lines
6.4 KiB
TypeScript

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<Notification>,
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<string, unknown>,
): Promise<Notification> {
// 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<void> {
await this.notificationsRepository.update(
{ id: notificationId, userId },
{ isRead: true },
);
}
async markAllAsRead(userId: string): Promise<void> {
await this.notificationsRepository.update(
{ userId, isRead: false },
{ isRead: true },
);
}
async getUnreadCount(userId: string): Promise<number> {
return this.notificationsRepository.count({
where: { userId, isRead: false },
});
}
private stringifyData(data: Record<string, unknown>): Record<string, string> {
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(data)) {
result[key] = typeof value === 'string' ? value : JSON.stringify(value);
}
return result;
}
}