File

src/notification/service/notification.service.ts

Index

Properties
Methods

Constructor

constructor(config: ConfigService, mailJet: Email.Client, twilio: twilioLibrary.Twilio, messageService: MessageService, exportService: ExportService, fileRepository: FileRepository, fileService: FileService, positiveInfoRepository: PositiveInfoRepository, userService: UserService)
Parameters :
Name Type Optional
config ConfigService No
mailJet Email.Client No
twilio twilioLibrary.Twilio No
messageService MessageService No
exportService ExportService No
fileRepository FileRepository No
fileService FileService No
positiveInfoRepository PositiveInfoRepository No
userService UserService No

Methods

Async handleSmsException
handleSmsException(error: Record, params: literal type)
Parameters :
Name Type Optional
error Record<string | > No
params literal type No
Returns : any
Async prepareAttachmentWithExportedData
prepareAttachmentWithExportedData(userId: string, isFromQueue: boolean)
Parameters :
Name Type Optional
userId string No
isFromQueue boolean No
Returns : Promise<Email.Attachment>
prepareEmailData
prepareEmailData(templateId: number, variablesToEscapeAndSend: object, variablesToSend: object, to: literal type[], wantToGetNotifications?: number | null)
Parameters :
Name Type Optional
templateId number No
variablesToEscapeAndSend object No
variablesToSend object No
to literal type[] No
wantToGetNotifications number | null Yes
Returns : Email.SendParams
Async prepareEmergencyMessageAttachments
prepareEmergencyMessageAttachments(userId: string, isFromQueue: boolean)
Parameters :
Name Type Optional
userId string No
isFromQueue boolean No
Returns : Promise<Email.Attachment[]>
prepareSmsData
prepareSmsData(to: string, message: string)
Parameters :
Name Type Optional
to string No
message string No
Returns : MessageListInstanceCreateOptions
Async sendEmail
sendEmail(params?: literal type)
Parameters :
Name Type Optional
params literal type Yes
Returns : Promise<Email.Response>
Async sendEmergencyMessage
sendEmergencyMessage(contact: literal type, user: UserEntity, data)
Parameters :
Name Type Optional
contact literal type No
user UserEntity No
data No
Returns : Promise<void>
Async sendSms
sendSms(params: literal type)
Parameters :
Name Type Optional
params literal type No
Returns : Promise<MessageInstance | void>

Properties

Private Readonly logger
Default value : new Logger(NotificationService.name)
import {
  Inject,
  Injectable,
  BadRequestException,
  Logger,
} from "@nestjs/common";
import { Email } from "node-mailjet";
import { ConfigService } from "@nestjs/config";
import { DICTIONARY } from "../../common/constant/dictionary.constant";
import {
  SEND_MAIL_FAILED,
  SEND_SMS_FAILED,
  EMAIL_AND_SMS_NOT_ALLOWED,
  EXPORT_DATA_FAILED,
  GET_FILE_CONTENT_FAILED,
} from "../../common/error/keys";
import { CustomError } from "../../common/error/custom-error";
import { DICTIONARY as NOTIFICATION_DI } from "../constant/dictionary.constant";
import * as twilioLibrary from "twilio";
import { UserEntity } from "../../user/entity/user.entity";
import { escapeHTML } from "../helper/escape-html";
import { getMailTemplateId } from "../helper/get-template-id";
import { getNameOrEmail } from "../../common/helper/get-name-or-email";
import { SendEmergencyMessageDTO } from "../../message/request/dto/send-emergency-message.dto";
import { MessageListInstanceCreateOptions } from "twilio/lib/rest/api/v2010/account/message";
import { MessageInstance } from "twilio/lib/rest/api/v2010/account/message";
import { MessageService } from "../../queue/service/message.service";
import { PROCESS } from "../../queue/constant/process.constant";
import { ExportService } from "../../user/service/export.service";
import { FileRepository } from "../../file/repository/file.repository";
import { FileService } from "../../file/service/file.service";
import { PositiveInfoRepository } from "../../user/repository/positive-info.repository";
import { UserService } from "../../user/service/user.service";

@Injectable()
export class NotificationService {
  private readonly logger = new Logger(NotificationService.name);

  constructor(
    @Inject(DICTIONARY.CONFIG) private readonly config: ConfigService,
    @Inject(NOTIFICATION_DI.MAIL_JET) private readonly mailJet: Email.Client,
    @Inject(twilioLibrary.Twilio) private readonly twilio: twilioLibrary.Twilio,
    private readonly messageService: MessageService,
    private readonly exportService: ExportService,
    @Inject(FileRepository)
    private readonly fileRepository: FileRepository,
    private readonly fileService: FileService,
    @Inject(PositiveInfoRepository)
    private readonly positiveInfoRepository: PositiveInfoRepository,
    private readonly userService: UserService
  ) { }

  prepareSmsData(
    to: string,
    message: string
  ): MessageListInstanceCreateOptions {
    return {
      from: this.config.get("twilio.phoneNumber"),
      to,
      body: message,
    };
  }

  async handleSmsException(
    error: Record<string, unknown>,
    params: {
      data: MessageListInstanceCreateOptions;
      isPositiveInfoQuestion?: boolean;
      userId?: string;
      isFromQueue?: boolean;
      stopPropagation?: boolean;
    }
  ) {
    this.logger.error(`Sms has not been sent (uid: ${params.userId})`);
    this.logger.error(JSON.stringify(error));

    if (params?.stopPropagation) {
      return;
    }

    if (!params?.isFromQueue) {
      await this.messageService.addJobToQueue(
        PROCESS.SMS,
        params,
        this.config.get("queue.sendAfterTime.repeatTryingToSendMessage")
      );

      throw new CustomError(SEND_SMS_FAILED, error);
    }

    await this.sendSms({ ...params, stopPropagation: true });
  }

  async sendSms(params: {
    data: MessageListInstanceCreateOptions;
    isPositiveInfoQuestion?: boolean;
    userId?: string;
    isFromQueue?: boolean;
    stopPropagation?: boolean;
  }): Promise<MessageInstance | void> {
    try {
      return this.twilio.messages
        .create(params.data)
        .then(async (result) => {
          this.logger.log(`(uid: ${params?.userId}) SMS has been ${result?.status || 'sent to twilio'}. SMS status available at: ${result?.uri || 'not available'}. SMS body: `)
          this.logger.log(result?.body || 'not delivered')

          if (result.errorMessage) {
            this.logger.error(
              `[sendSms 1] Twilio result contains error message`,
              JSON.stringify(result)
            );
            throw result;
          }

          if (params.isPositiveInfoQuestion && params.userId) {
            await this.positiveInfoRepository.setSmsTime(
              params.userId,
              this.config.get(
                "queue.sendAfterTime.triggerIfNoPositiveInfoAfterSms"
              ),
              this.config.get(
                "queue.sendAfterTime.alertIfNoPositiveInfoAfterSms"
              )
            );
          }

          return result;
        })
        .catch(async (error) => {
          await this.handleSmsException(error, params);
        });
    } catch (error) {
      await this.handleSmsException(error, params);
    }
  }

  prepareEmailData(
    templateId: number,
    variablesToEscapeAndSend: object,
    variablesToSend: object,
    to: { Email: string; Name?: string }[],
    wantToGetNotifications?: number | null
  ): Email.SendParams {
    if (wantToGetNotifications === 0) {
      return;
    }

    const escapedVariables = {};
    for (const [key, value] of Object.entries(variablesToEscapeAndSend)) {
      escapedVariables[key] =
        typeof value === "string" ? escapeHTML(value) : value;
    }

    return {
      Messages: [
        {
          From: {
            Email: this.config.get("mailJet.email"),
            Name: this.config.get("mailJet.username"),
          },
          To: to,
          TemplateID: templateId,
          TemplateLanguage: true,
          Variables:
            Object.keys(variablesToSend).length > 0
              ? { ...escapedVariables, ...variablesToSend }
              : escapedVariables,
        },
      ],
    };
  }

  async prepareAttachmentWithExportedData(
    userId: string,
    isFromQueue: boolean
  ): Promise<Email.Attachment> {
    const content = await this.exportService.exportDataAsBase64(userId);

    if (!content) {
      this.logger.error("Exported content is empty");

      if (!isFromQueue) {
        throw new BadRequestException(EXPORT_DATA_FAILED);
      }
    }

    return {
      ContentType: "application/vnd.ms-excel",
      Filename: "exported-data.xls",
      Base64Content: content,
    };
  }

  async prepareEmergencyMessageAttachments(
    userId: string,
    isFromQueue: boolean
  ): Promise<Email.Attachment[]> {
    const files = await this.fileRepository.findManyByParams({ userId });
    const attachments: Email.Attachment[] = [];
    let content: string;

    for (const file of files) {
      /**
       * Possible attachment's size problem
       */

      content = await this.fileService.getFileAsBase64(file.key);

      if (!content && !isFromQueue) {
        throw new BadRequestException(GET_FILE_CONTENT_FAILED);
      }

      attachments.push({
        ContentType: file.mimeType,
        Filename: file.name,
        Base64Content: content,
      });
    }

    return attachments;
  }

  async sendEmail(params?: {
    data: Email.SendParams;
    isFromQueue?: boolean;
    exportedData?: boolean;
    emergencyMessage?: boolean;
    userId?: string;
  }): Promise<Email.Response> {
    const user = await this.userService.findById(params.userId);

    if (!params.userId || !user || user?.profile?.uploadedDocumentsAccess) {
      let attachments: Email.Attachment[] = [];

      if (params?.exportedData) {
        const attachment = await this.prepareAttachmentWithExportedData(
          params?.userId,
          params?.isFromQueue
        );

        attachments.push(attachment);
      }

      if (params?.emergencyMessage) {
        attachments = await this.prepareEmergencyMessageAttachments(
          params.userId,
          params.isFromQueue
        );
      }

      params.data.Messages[0].Attachments = attachments;
    }

    return this.mailJet
      .post("send", { version: "v3.1" })
      .request(params.data)
      .then((result: any) => {
        if (result.body.Messages[0].Status !== "success") {
          throw result.body;
        }

        return result;
      })
      .catch(async (error) => {
        Logger.error(`${error?.ErrorMessage || 'Mail has not been sent.'}`)
        if (!params.isFromQueue) {
          await this.messageService.addJobToQueue(
            PROCESS.EMAIL,
            params,
            this.config.get("queue.sendAfterTime.repeatTryingToSendMessage")
          );
        } else {
          throw new CustomError(SEND_MAIL_FAILED, error);
        }
      });
  }

  async sendEmergencyMessage(
    contact: {
      name: string;
      email: string;
      phone: string;
    },
    user: UserEntity,
    data: SendEmergencyMessageDTO & {
      isFromQueue?: boolean;
      locationUrl?: string;
    }
  ): Promise<void> {
    if (user.profile?.emergencyEmailAndSms === false) {
      throw new BadRequestException(EMAIL_AND_SMS_NOT_ALLOWED);
    }

    let message =
      user.profile?.emergencyMessage ??
      this.config
        .get("emergencyTrigger.defaultMessage")
        .toString()
        .replace(
          "{name}",
          getNameOrEmail(user.profile?.name, user.profile?.surname, user.email)
        );

    let smsData: MessageListInstanceCreateOptions;

    if (contact.phone) {
      smsData = this.prepareSmsData(
        contact.phone,
        `${message === user.profile?.emergencyMessage
          ? `${this.config.get(
            "emergencyTrigger.customMessagePrefix"
          )} ${message}`
          : message
          } ${user.profile?.locationAccess === true && data.locationUrl
            ? data.locationUrl
            : ""
          }`.trim()
      );

      if (!data.delayed) {
        await this.sendSms({ data: smsData, isFromQueue: data.isFromQueue });
      }
    }

    const files = await this.fileRepository.findManyByParams({
      userId: user.id,
    });

    let params: Record<string, unknown> = {
      contactName: contact.name,
      username: getNameOrEmail(
        user.profile?.name,
        user.profile?.surname,
        user.email
      ),
      message:
        files.length > 0
          ? `${message} ${this.config.get(
            "emergencyTrigger.ifThereAreAttachments"
          )}`
          : message,
    };

    if (user.profile?.locationAccess === true) {
      params.locationUrl = data.locationUrl ?? "";
    }

    const emailData = this.prepareEmailData(
      getMailTemplateId(
        `EMERGENCY_MESSAGE_WITH${user.profile?.locationAccess !== true ? "OUT" : ""
        }_LOCATION`
      ),
      params,
      {},
      [
        {
          Email: contact.email,
        },
      ]
    );

    if (!data.delayed) {
      await this.sendEmail({
        data: emailData,
        emergencyMessage: true,
        userId: user.id,
        isFromQueue: data.isFromQueue,
      });
    }

    if (data.delayed) {
      await this.messageService.addJobToQueue(
        PROCESS.EMERGENCY,
        {
          sms: smsData,
          email: emailData,
        },
        this.config.get(`queue.sendAfterTime.${data.messageType}`),
        `${PROCESS.EMERGENCY}_${user.id}`
      );
    }
  }
}

results matching ""

    No results matching ""