src/notifications/push.service.ts
Sends FCM push notifications to drivers using Firebase Admin SDK (FCM v1 API).
Setup: Set FIREBASE_SERVICE_ACCOUNT env var with the JSON string of your service account key, OR place the key file and set GOOGLE_APPLICATION_CREDENTIALS.
Methods |
|
constructor(prisma: PrismaService)
|
||||||
|
Defined in src/notifications/push.service.ts:43
|
||||||
|
Parameters :
|
| onModuleInit |
onModuleInit()
|
|
Defined in src/notifications/push.service.ts:47
|
|
Returns :
void
|
| Async sendToDriver | ||||||||||||||||||
sendToDriver(driverId: string, organizationId: string, title: string, body: string, data?: Record
|
||||||||||||||||||
|
Defined in src/notifications/push.service.ts:95
|
||||||||||||||||||
|
Send a push notification to a driver by their driver record ID. Matches driver → user by email or name.
Parameters :
Returns :
unknown
|
| Async sendToUser | |||||||||||||||
sendToUser(userId: string, title: string, body: string, data?: Record
|
|||||||||||||||
|
Defined in src/notifications/push.service.ts:77
|
|||||||||||||||
|
Send a push notification to a specific user by their user ID.
Parameters :
Returns :
unknown
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import * as admin from 'firebase-admin';
import { PrismaService } from '../prisma/prisma.service';
/**
* Sends FCM push notifications to drivers using Firebase Admin SDK (FCM v1 API).
*
* Setup: Set FIREBASE_SERVICE_ACCOUNT env var with the JSON string of your
* service account key, OR place the key file and set GOOGLE_APPLICATION_CREDENTIALS.
*/
@Injectable()
export class PushService implements OnModuleInit {
private readonly logger = new Logger(PushService.name);
private initialized = false;
constructor(private readonly prisma: PrismaService) {}
onModuleInit() {
try {
const serviceAccount = process.env.FIREBASE_SERVICE_ACCOUNT;
if (serviceAccount) {
// Parse JSON string from environment variable
const credentials = JSON.parse(serviceAccount);
admin.initializeApp({
credential: admin.credential.cert(credentials),
});
this.initialized = true;
this.logger.log('Firebase Admin SDK initialized from FIREBASE_SERVICE_ACCOUNT');
} else if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
// Use default credentials from file path
admin.initializeApp({
credential: admin.credential.applicationDefault(),
});
this.initialized = true;
this.logger.log('Firebase Admin SDK initialized from GOOGLE_APPLICATION_CREDENTIALS');
} else {
this.logger.warn('Firebase not configured — push notifications disabled. Set FIREBASE_SERVICE_ACCOUNT env var.');
}
} catch (err: any) {
this.logger.error(`Firebase init failed: ${err.message}`);
}
}
/**
* Send a push notification to a specific user by their user ID.
*/
async sendToUser(userId: string, title: string, body: string, data?: Record<string, string>) {
const user = await this.prisma.user.findUnique({
where: { id: userId },
select: { fcmToken: true, firstName: true },
});
if (!user?.fcmToken) {
this.logger.warn(`No FCM token for user ${userId} — push skipped`);
return;
}
return this.send(user.fcmToken, title, body, data);
}
/**
* Send a push notification to a driver by their driver record ID.
* Matches driver → user by email or name.
*/
async sendToDriver(driverId: string, organizationId: string, title: string, body: string, data?: Record<string, string>) {
const driver = await this.prisma.driver.findUnique({
where: { id: driverId },
select: { email: true, firstName: true, lastName: true },
});
if (!driver) return;
// Find the user account matching this driver
const user = await this.prisma.user.findFirst({
where: {
organizationId,
role: 'DRIVER',
OR: [
...(driver.email ? [{ email: driver.email }] : []),
{ firstName: driver.firstName, lastName: driver.lastName },
],
},
select: { fcmToken: true },
});
if (!user?.fcmToken) {
this.logger.warn(`No FCM token for driver ${driver.firstName} ${driver.lastName} — push skipped`);
return;
}
return this.send(user.fcmToken, title, body, data);
}
/**
* Low-level FCM send. Constructs the message with Android high-priority
* config and the `fleet_trips` notification channel, then delivers via
* Firebase Admin SDK.
*
* If the token is no longer registered (device uninstalled the app),
* the token is automatically cleared from the user record to prevent
* repeated failures.
*
* @param token - FCM device registration token.
* @param title - Notification title.
* @param body - Notification body text.
* @param data - Optional key-value data payload for the client app.
*/
private async send(token: string, title: string, body: string, data?: Record<string, string>) {
if (!this.initialized) {
this.logger.warn('Firebase not initialized — push skipped');
return;
}
try {
const message: admin.messaging.Message = {
token,
notification: { title, body },
data: data || {},
android: {
priority: 'high',
notification: {
sound: 'default',
channelId: 'fleet_trips',
},
},
};
const response = await admin.messaging().send(message);
this.logger.log(`Push sent: "${title}" → ${response}`);
} catch (err: any) {
if (err.code === 'messaging/registration-token-not-registered') {
// Token is invalid — remove it
this.logger.warn(`Invalid FCM token, removing: ${token.substring(0, 20)}...`);
await this.prisma.user.updateMany({
where: { fcmToken: token },
data: { fcmToken: null },
});
} else {
this.logger.error(`Push failed: ${err.message}`);
}
}
}
}