npm package: @laqus/notifications
Description: Laqus Notifications Libs - Emails and push
Latest version: 1.1.0-rc

This library is accessible as an [NPM package](https://www.npmjs.com/package/@laqus/notifications) and is recommended - as a must have - as the cornerstone for any NestJS-based project to ensure standardization. Its code is designed to be verbose, aiding in debugging and troubleshooting, while also being extensible, simplifying the addition of new features.

This package enables the sending of emails and notifications through the Laqus notifications service.

## Requirements

### Env vars

- LAQUS_NOTIFICATIONS_API_URL

```dotenv
LAQUS_APP_NAME
LOG_LEVEL [DEBUG|VERBOSE|LOG|INFO|WARNING|CRITICAL|ERROR|FATAL]
CLUSTERS (optional)
```

## Installing

```shell
$ npm i @laqus/notifications
```

## TO DOs

--

# Sobre esta lib

Esta lib permite o envio de notificações e emails, bem como, consulta de notificações e subscrições. Ela provem essas funcionalidades abstraindo toda a API do serviço de Notifications da Laqus, provendo a correta tipagem dos dados e métodos.

### Dependências

- @laqus/base
- @nestjs/common,
- @nestjs/config,
- @nestjs/core

### Novos deploys/releases de melhorias e novas features

Deve-se apenas incrementar a versão da release no ./libs/notifications/package.json e fazer push na branch main e o processo de build e publicação no NPM será automatico.

### Testes e debuggings

Este projeto é composto por uma api padrão REST com dependência na lib. O que torna o processo muito mais simples e rápido.

## Estrutura do projeto

```shell
> .vscode   # Definições padrão do workspace
> libs      # Projeto da lib
> src       # Projeto host da lib, para testes, etc.
```

### Estrutura da lib ( ./libs/notifications )

```shell
> src
    > application                   # implementação
        > emails
            > builders
            > models
        > notifications
            > builders
            > models
    > infrastructure                # implementação do client da API
        > laqus
            > notifications-api
                > models
                    > emails
                    > notifications
> LICENSE.md
> package.json
> README.md
> tsconfig.lib.json
```

# Utilização

Para adicionar ao projeto, basta configurar o modulo e adicionar as env vars:

```typescript
import { MiddlewareConsumer, Module, NestModule, OnApplicationShutdown } from '@nestjs/common';
import { CorrelationIdExpressMiddleware, LaqusCorrelationModule } from '@laqus/base/infrastructure/correlation-ids';
import { LaqusLoggingModule } from '@laqus/base/infrastructure/logging';
import { ConfigService } from '@nestjs/config';
import { ILaqusNotificationsServiceConfig, MessageBrokerType } from '@laqus/notifications/config';

@Module({
  imports: [
    LaqusCorrelationModule,
    LaqusLoggingModule,
    LaqusNotificationsModule.forRootAsync({
      useFactory: (configService: ConfigService): ILaqusNotificationsServiceConfig => ({
        notificationServiceApiUrl: configService.get<string>('LAQUS_NOTIFICATIONS_API_URL'),
        logLevel: configService.get<string>('LOG_LEVEL'),
        laqusAppName: configService.get<string>('LAQUS_APP_NAME'),
        queuesConfigs: {
          receiverIntegrationEventsQueue: configService.get<string>('RECEIVER_INTEGRATION_EVENTS_QUEUE'),
          receiverNotificationsQueue: configService.get<string>('RECEIVER_NOTIFICATIONS_QUEUE'),
          brokerType: configService.get<MessageBrokerType>('MESSAGE_BROKER_TYPE'),
          sqsConfig:
            configService.get<MessageBrokerType>('MESSAGE_BROKER_TYPE') === MessageBrokerType.SQS
              ? {
                  region: configService.get<string>('MESSAGE_BROKER_AWS_REGION'),
                  accessKey: configService.get<string>('MESSAGE_BROKER_AWS_ACCESS_KEY'),
                  secretKey: configService.get<string>('MESSAGE_BROKER_AWS_SECRET_KEY')
                }
              : undefined,
          rabbitMqConfig:
            configService.get<MessageBrokerType>('MESSAGE_BROKER_TYPE') === MessageBrokerType.RabbitMQ
              ? {
                  uri: configService.get<string>('MESSAGE_BROKER_RABBITMQ_URI')
                }
              : undefined
        }
      }),
      inject: [ConfigService]
    })
  ],
  controllers: [],
  providers: [],
  exports: []
})
export class AppModule {}
```

Para usar dentro da sua aplicação, basta importar o **LaqusNotificationServicesProvider**

Com ele injetado, é só escolher o service desejado do Notification-API e também o tipo de canal (Queue ou Http)

```typescript
  //Serviço de email
  public getEmailService(httpScopedRequestContext: IHttpScopedRequestContext): EmailService;

  //Serviço de Integration Events via Http
  public getIntegrationEventsServiceHttpChannel(httpScopedRequestContext: IHttpScopedRequestContext): ILaqusIntegrationEventsService;

  //Serviço de Integration Events via Broker
  public getIntegrationEventsServiceBrokerChannel(optionalLogger?: LoggerService): ILaqusIntegrationEventsService;

  //Serviço de notifications via Http
  public getNotificationServiceHttpChannel(httpScopedRequestContext: IHttpScopedRequestContext): ILaqusNotificationsService;

  //Serviço de notifications via Broker
  public getNotificationServiceBrokerChannel(httpScopedRequestContext: IHttpScopedRequestContext): ILaqusNotificationsService;
```

## Exemplo

```typescript
export enum ChannelType {
  HTTP = 'HTTP',
  QUEUE = 'QUEUE'
}

@Controller()
export class AppController {
  public constructor(
    private readonly appService: AppService,
    private readonly notificationServicesProvider: LaqusNotificationServicesProvider
  ) {}

  @Get()
  public async getHello(): Promise<string> {
    return await this.appService.getHello();
  }

  @Post('integration-events')
  @ApiOperation({
    summary:
      'Envia um integration event para a exchange laqus e frontier webhooks (e outros channels configurados no notification-api para integration event)'
  })
  @ApiBadRequestResponse()
  @ApiForbiddenResponse()
  @ApiQuery({ name: 'channel', enum: ChannelType, required: true, example: ChannelType.QUEUE, description: 'channel de envio' })
  public async sendIntegrationEvent(
    @Query('channel', new ParseEnumPipe(ChannelType)) channel: ChannelType,
    @Body() dto: SendIntegrationEventDto
  ): Promise<string> {
    let integrationEventService: ILaqusIntegrationEventsService;
    if (channel === ChannelType.HTTP) {
      integrationEventService = this.notificationServicesProvider.getIntegrationEventsServiceHttpChannel({
        authToken: 'AQUI VAI O TOKEN DO USUARIO QUE PRECISA IR PARA O NOTIFICATION-API'
      });
    } else {
      integrationEventService = this.notificationServicesProvider.getIntegrationEventsServiceBrokerChannel();
    }

    integrationEventService.sendIntegrationEvent(
      new IntegrationEventBuilder()
        .withCorrelationId(dto.correlationId || randomUUID())
        .withCreatedAt(dto.createdAt)
        .withEventType(dto.eventType || 'teste')
        .withMetadata(dto.metadata)
        .withId(dto.id)
        .withReceiverId(dto.receiverTenantId || 'teste')
        .withSrcTenantId(dto.srcTenantId || 'teste')
        .withSubjectId(dto.subjectId || 'teste')
        .withSubjectType(dto.subjectType || 'teste')
    );

    return 'Veja os resultados nos logs para ver se teve algum erro';
  }

  @Post('notifications')
  @ApiOperation({ summary: 'Envia uma notificação para receivers configurados nas Subscriptions ou explicitos nas audiences' })
  @ApiBadRequestResponse()
  @ApiForbiddenResponse()
  @ApiQuery({ name: 'channel', enum: ChannelType, required: true, example: ChannelType.QUEUE, description: 'channel de envio' })
  public async sendNotification(
    @Query('channel', new ParseEnumPipe(ChannelType)) channel: ChannelType,
    @Body() notif: SendNotificationDto
  ): Promise<string> {
    const builder: NotificationBuilder = new NotificationBuilder(
      notif.topic || 'teste',
      notif.title || 'teste enviado via HTTP',
      notif.content || 'teste enviado via HTTP',
      notif.correlationId,
      notif.srcCompanyId,
      notif.srcEvtResponsibleId,
      'laqus-notifications lib playground',
      NotificationPriority.NORMAL,
      NotificationType.INFORMATION
    );

    if (notif.audiences) {
      for (const a of notif.audiences) {
        builder.addAudience(a.receiverId);
      }
    }

    let notificationSvc: ILaqusNotificationsService;
    if (channel === ChannelType.HTTP) {
      notificationSvc = this.notificationServicesProvider.getNotificationServiceHttpChannel({
        authToken: 'AQUI VAI O TOKEN DO USUARIO QUE PRECISA IR PARA O NOTIFICATION-API'
      });
    } else {
      notificationSvc = this.notificationServicesProvider.getNotificationServiceBrokerChannel({
        authToken:
          'AQUI VAI O TOKEN DO USUARIO QUE PRECISA IR PARA O NOTIFICATION-API - A notificacao é enviada por Queue, mas as operações extras feitas no envio podem precisar do http'
      });
    }

    await notificationSvc.sendNotification(builder);
    return 'Veja os resultados nos logs para ver se teve algum erro';
  }
}
```

### Emails

A interface _IEmailService_ fornece as seguintes funcionalidades:

```typescript
export interface IEmailService {
  //Criar um template
  createTemplate(name: string, description: string, contents: string): Promise<ITemplate | null>;

  //Listagem dos templates
  getTemplates(): Promise<ITemplate[] | null>;

  //Busca do template pelo nome
  getTemplateByName(name: string): Promise<ITemplate | null>;

  //Busca do template pelo id
  getTemplateById(id: string): Promise<ITemplate | null>;

  //Envio do email
  sendEmail(builder: EmailBuilder): Promise<IEmail | null>;
}
```

#### Enviando um email

```typescript
const content: string = '<h1>Olá!</h1>';

//Através da factory, escolhemos o tipo de email. Neste exemplo, email de conteúdo cru, de forma explícita e não por template
const rawContentEmailBuilder = EmailBuilderFactory.withContentRaw(content);

//definimos as informações
rawContentEmail
    .addAttachment("arquivo.pdf", buffer*) /*que saudade de C++, caras!*/
    .addAttachment("conciliacao.xsl", buffer*)
    .to('leovolpatto@gmail.com')
    .from('depositaria@laqus.com.br')
    .withSubject("Arquivos");

//por fim, a promessa do envio
await emailServiceInstance.sendEmail(rawContentEmail);
```

### Notifications

Para a obtenção da instância do service, procedemos da mesma forma que para o serviço de emails:

```typescript
const service: ILaqusNotificationsService = this.notificationsService.getNotificationServiceInstance(this);
```

A interface _ILaqusNotificationsService_ expõe as seguintes funcionalidades:

```typescript
export interface ILaqusNotificationsService {
  //confirmação de recebimento da notificação
  ackNotification(notificationId: string, receiverId: string): Promise<boolean>;

  //Envio de notificação
  sendNotification(builder: AbstractNotificationBuilder): Promise<INotification | null>;

  //Listar notificações pendentes
  getNotAckedNotifications(args: IGetNotificationsArgs): Promise<INotification[]>;

  //Listar notificações
  getNotifications(args: IGetNotificationsArgs): Promise<INotification[]>;

  //Realizar a subscrição em um tópico
  subscribeToTopic(builder: SubscriptionsBuilder): Promise<ISubscription[]>;

  //Remover notificação
  removeNotification(notificationId: string, args?: IRemoveNotificationsArgs): Promise<boolean>;

  //Cancelar uma subscrição
  removeSubscription(args: IRemoveSubscriptionsArgs): Promise<boolean>;
}
```

#### Enviando uma notificação com audiência específica

No exemplo é mostrado um exemplo com vários atributos, o que tornará o código extenso. É importante notar que a **maioria dos atributos são opcionais**

```typescript
const notification = await service.sendNotification(
    //através da factory, pedimos uma notificação com audiencia que queremos enviar de forma exclusiva, ignorando outros usuários subscritos
    NotificationFactory.buildNotificationWithSpecificAudience(
        //esse é o topico/evento/causa da notificação
        'ASSINATURA_CONCLUIDA',
        //titulo
        'O documento foi assinado',
        //Conteudo (pode ser qualquer coisa)
        'A minuta acabou de ser assinada por todos',
        //Id da correlação para rastrearmos qual operação originou esse evento
        this.correlationIdService.correlationId,
        //poderiamos informar o id da empresa (caso queiramos um dia, enviar pra todos da empresa X ou Y)
        null,
        //Esse é o id do usuário que causou esse evento
        'dc13fd39-4ef6-4f41-aaf0-b7a87beffbe0',
        //O nome do serviço que está disparando a notificação
        'DocSigner Api',
        //A prioridade de entrega
        NotificationPriority.LOW,
        //Como se espera que seja tratada no front, nesse caso, como um simples push de informativo
        NotificationType.INFORMATION
    )
    //adicionamos a audiencia especifica
    .addSpecificReceiverToTheAudience('leo')
    .addSpecificReceiverToTheAudience('grupo_operacoes')
    .addSpecificReceiverToTheAudience('dc13fd39-4ef6-4f41-aaf0-b7a87beffbe0')
    //podemos enviar informações adicionais, qualquer coisa que possa ser usada na leitura/ação/renderização da notificação
    .setMetadata({ ...})
    //podemos definir também o prazo de retenção da notificação
    .setRetentionWhenNotAck(10)
);
```

#### Enviando uma notificação para a audiência subscrita ao tópico

No exemplo é mostrado um exemplo que vai enviar a notificação para todos os usuários que estão subscritos para o topico 'INSTRUMENTO_EMITIDO' da empresa de id '00000000-1010-1111-0000-111111111111'.

Também podemos ver que esta notificação é uma solicitação de ação (ACTION_REQUEST), ou seja, baseado nisso, o frontend pode reagir de alguma maneira diferente, atualizando alguma coisa, perguntando confirmação do usuário, etc.

```typescript
const notification = await service.sendNotification(
  NotificationFactory.buildNotification(
    'INSTRUMENTO_EMITIDO',
    'A NC LNC20240078 foi emitida',
    'Clique <a href="#">aqui</a> para ver',
    this.correlationIdService.correlationId,
    '00000000-1010-1111-0000-111111111111',
    'dc13fd39-4ef6-4f41-aaf0-b7a87beffbe0',
    'Depositaria',
    NotificationPriority.LOW,
    NotificationType.ACTION_REQUEST
  )
);
```

#### Realizando a subscrição de alguem a um tópico

No exemplo a seguir, estaremos subscrevendo o usuario "Wild Bill Wickup" para ser notificado sempre que houver uma EMISSAO_CANCELADA no seu escopo de empresa/cliente laqus

```typescript
await service.subscribeToTopic(
  new SubscriptionsBuilder('EMISSAO_CANCELADA', 'Id da empresa').addSubscriber('01010101010', 'Wild Bill Wickup')
);
```

### Outros

Atualmente, a partir da versão 1.0.2-rc, alem de notificar através de push, podemos configurar que tais notificações também sejam entregues por email e/ou chat ou channel do Teams (Mas exige conf extra no Teams).

## Build da lib para uso local

```shell
$ npm run build:lib
```