File

src/notifications/push.service.ts

Description

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.

Index

Methods

Constructor

constructor(prisma: PrismaService)
Parameters :
Name Type Optional
prisma PrismaService No

Methods

onModuleInit
onModuleInit()
Returns : void
Async sendToDriver
sendToDriver(driverId: string, organizationId: string, title: string, body: string, data?: Record)

Send a push notification to a driver by their driver record ID. Matches driver → user by email or name.

Parameters :
Name Type Optional
driverId string No
organizationId string No
title string No
body string No
data Record<string | string> Yes
Returns : unknown
Async sendToUser
sendToUser(userId: string, title: string, body: string, data?: Record)

Send a push notification to a specific user by their user ID.

Parameters :
Name Type Optional
userId string No
title string No
body string No
data Record<string | string> Yes
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}`);
      }
    }
  }
}

results matching ""

    No results matching ""