Skip to content

Sistema de Notificacoes

O sistema de notificacoes persiste mensagens no banco e pode entrega-las em tempo real via WebSocket. O modelo Notification, o repository e os endpoints estao totalmente implementados.

Estado atual da implementacao (importante): existem duas classes distintas chamadas NotificationService:

  • App (src/application/services/notification.service.ts): createAndSend() grava no banco e emite via WebSocket, alem dos helpers tipados (notifyDepositConfirmed, notifyCaseOpening, etc.). Hoje esta classe nao possui nenhum caller no backend (dead code) — os helpers e a emissao WebSocket existem, mas nao sao acionados por nenhum fluxo de jogo/pagamento.
  • Infra (src/infrastructure/notification/notification.service.ts): apenas create() (INSERT no banco, sem WebSocket). E a unica em uso: o unico produtor real de notificacao hoje e raffle/draw-raffle.use-case.ts, que injeta a Infra e chama create() com NotificationType.SYSTEM.

O restante deste documento descreve a App NotificationService (o desenho completo com WebSocket) e sinaliza onde o comportamento atual difere.

Arquivos do Codigo

ArquivoResponsabilidade
src/application/services/notification.service.tsApp service - createAndSend + helpers notify* (INSERT + WebSocket); sem callers hoje
src/infrastructure/notification/notification.service.tsInfra service - apenas create() (INSERT, sem WebSocket); usado pelo sorteio (draw-raffle)
src/presentation/controllers/notification.controller.tsController com 4 endpoints
src/application/use-cases/notification/get-notifications.use-case.tsListar notificacoes do usuario
src/application/use-cases/notification/get-unread-count.use-case.tsContar nao lidas
src/application/use-cases/notification/mark-as-read.use-case.tsMarcar como lida
src/application/use-cases/notification/mark-all-as-read.use-case.tsMarcar todas como lidas
src/infrastructure/database/repositories/notification.repository.tsRepository Prisma
src/domain/repositories/notification.repository.interface.tsInterface do repository
src/application/dto/notification.dto.tsDTOs de validacao
src/infrastructure/websocket/websocket.gateway.tsEmissao WebSocket

Schema do Banco de Dados

Notification Model

prisma
model Notification {
  id          BigInt              @id
  userId      BigInt              @map("user_id")
  type        NotificationType
  title       String
  message     String
  metadata    Json?
  status      NotificationStatus  @default(UNREAD)
  createdAt   DateTime            @default(now()) @map("created_at")
  readAt      DateTime?           @map("read_at")

  user        User                @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@map("notifications")
  @@index([userId, status])
  @@index([userId, createdAt])
  @@index([createdAt])
}

NotificationType Enum

prisma
enum NotificationType {
  DEPOSIT_CONFIRMED     // Deposito PIX confirmado
  WITHDRAWAL_COMPLETED  // Saque de skin concluido
  WITHDRAWAL_FAILED     // Saque de skin falhou
  CASE_OPENING          // Abertura de caixa (item recebido)
  UPGRADE_SUCCESS       // Upgrade bem-sucedido
  UPGRADE_FAIL          // Upgrade falhou
  SYSTEM                // Notificacao do sistema (manutencao, etc)
  PROMOTION             // Promocoes e ofertas
}

NotificationStatus Enum

prisma
enum NotificationStatus {
  UNREAD    // Nao lida (padrao)
  READ      // Lida pelo usuario
  ARCHIVED  // Arquivada
}

Indices

IndiceUso
(userId, status)Buscar notificacoes nao lidas de um usuario
(userId, createdAt)Listar notificacoes ordenadas por data
(createdAt)Queries administrativas e limpeza

Servico: NotificationService

O servico principal que cria notificacoes e as envia via WebSocket.

createAndSend(data)

Metodo central que cria a notificacao no banco e emite via WebSocket simultaneamente.

typescript
async createAndSend(data: {
  userId: bigint;
  type: NotificationType;
  title: string;
  message: string;
  metadata?: any;
}) {
  // 1. Persiste no banco de dados
  const notification = await this.notificationRepository.create(data);

  // 2. Emite via WebSocket para o usuario (sala privada)
  this.websocketGateway.emitNotificationToUser(data.userId, {
    id: notification.id.toString(),
    type: notification.type,
    title: notification.title,
    message: notification.message,
    metadata: notification.metadata,
    createdAt: notification.createdAt,
  });

  return notification;
}

Fluxo:

  1. Cria registro no banco com Snowflake ID, status UNREAD
  2. Emite evento notification para sala user:${userId} via WebSocket
  3. Retorna o registro criado

Metodos de Conveniencia

O servico oferece metodos especificos para cada tipo de notificacao.

Nota de estado: estes helpers pertencem a App NotificationService e nao possuem callers no backend atualmente (dead code). Os campos "Uso previsto" abaixo indicam o evento a que cada metodo se destina pelo desenho, nao um fluxo ativo.

notifyDepositConfirmed(userId, amountCents)

typescript
async notifyDepositConfirmed(userId: bigint, amountCents: bigint) {
  return this.createAndSend({
    userId,
    type: 'DEPOSIT_CONFIRMED',
    title: 'Deposito Confirmado',
    message: 'Seu deposito foi confirmado e creditado em sua conta!',
    metadata: { amountCents: amountCents.toString() },
  });
}

Uso previsto: confirmacao de pagamento PIX (DepositService, webhook HorsePay)

notifyWithdrawalCompleted(userId, amountCents)

typescript
async notifyWithdrawalCompleted(userId: bigint, amountCents: bigint) {
  return this.createAndSend({
    userId,
    type: 'WITHDRAWAL_COMPLETED',
    title: 'Saque Concluido',
    message: 'Seu saque foi processado com sucesso!',
    metadata: { amountCents: amountCents.toString() },
  });
}

Uso previsto: saque de skin concluido (WaxpeerWebSocketService / WaxpeerWithdrawalTrackerService, status COMPLETED)

notifyWithdrawalFailed(userId, reason)

typescript
async notifyWithdrawalFailed(userId: bigint, reason: string) {
  return this.createAndSend({
    userId,
    type: 'WITHDRAWAL_FAILED',
    title: 'Saque Falhou',
    message: `Seu saque nao pode ser processado. Motivo: ${reason}`,
  });
}

Uso previsto: saque de skin com falha (status FAILED)

notifyCaseOpening(userId, itemName, isFlip)

typescript
async notifyCaseOpening(userId: bigint, itemName: string, isFlip: boolean) {
  return this.createAndSend({
    userId,
    type: 'CASE_OPENING',
    title: isFlip ? 'FLIP! Item Raro!' : 'Item Recebido',
    message: `Voce recebeu: ${itemName}`,
    metadata: { itemName, isFlip },
  });
}

Uso previsto: abertura de caixa (OpenCaseUseCase)

notifyUpgradeSuccess(userId, targetItemName)

typescript
async notifyUpgradeSuccess(userId: bigint, targetItemName: string) {
  return this.createAndSend({
    userId,
    type: 'UPGRADE_SUCCESS',
    title: 'Upgrade Bem-Sucedido!',
    message: `Parabens! Voce ganhou: ${targetItemName}`,
    metadata: { targetItemName },
  });
}

Uso previsto: upgrade bem-sucedido (PerformUpgradeUseCase)

notifyUpgradeFail(userId, targetItemName)

typescript
async notifyUpgradeFail(userId: bigint, targetItemName: string) {
  return this.createAndSend({
    userId,
    type: 'UPGRADE_FAIL',
    title: 'Upgrade Falhou',
    message: `Infelizmente voce nao conseguiu: ${targetItemName}`,
    metadata: { targetItemName },
  });
}

Uso previsto: upgrade com falha (PerformUpgradeUseCase)

Eventos WebSocket

Evento Privado: notification

Emitido para a sala privada user:${userId} quando uma notificacao e criada.

typescript
// websocket.gateway.ts
emitNotificationToUser(userId: bigint, notification: any) {
  this.server.to(`user:${userId}`).emit('notification', notification);
}

Evento: notificationRoom: user:${userId} (apenas o usuario destinatario recebe) Payload:

json
{
  "id": "123456789",
  "type": "DEPOSIT_CONFIRMED",
  "title": "Deposito Confirmado",
  "message": "Seu deposito foi confirmado e creditado em sua conta!",
  "metadata": { "amountCents": "150000" },
  "createdAt": "2024-02-01T10:00:00.000Z"
}

Evento Global: notification:global

Emitido para TODOS os clientes conectados (broadcast).

typescript
// websocket.gateway.ts
emitGlobalNotification(notification: any) {
  this.server.emit('notification:global', notification);
}

Evento: notification:globalRoom: broadcast (todos recebem) Uso: Notificacoes de sistema (manutencao, promocoes)

Endpoints da API

Todos os endpoints requerem autenticacao (@UseGuards(AuthGuard)).

GET /notifications

Lista notificacoes do usuario com filtros e paginacao.

Query Parameters (GetNotificationsQueryDto):

ParametroTipoObrigatorioValidacaoDefault
statusstringNao@IsEnum(['UNREAD', 'READ', 'ARCHIVED'])Sem filtro (todas)
limitnumberNao@Type(() => Number)50
offsetnumberNao@Type(() => Number)0

Response (NotificationResponseDto[]):

json
[
  {
    "id": "123456789",
    "type": "DEPOSIT_CONFIRMED",
    "title": "Deposito Confirmado",
    "message": "Seu deposito foi confirmado e creditado em sua conta!",
    "metadata": { "amountCents": "150000" },
    "status": "UNREAD",
    "createdAt": "2024-02-01T10:00:00.000Z"
  },
  {
    "id": "123456788",
    "type": "CASE_OPENING",
    "title": "Item Recebido",
    "message": "Voce recebeu: AK-47 | Redline",
    "metadata": { "itemName": "AK-47 | Redline", "isFlip": false },
    "status": "READ",
    "createdAt": "2024-02-01T09:30:00.000Z",
    "readAt": "2024-02-01T09:35:00.000Z"
  }
]

Ordenacao: createdAt DESC (mais recentes primeiro)

Query SQL gerada:

sql
SELECT * FROM notifications
WHERE user_id = $1
  AND status = $2  -- se filtro fornecido
ORDER BY created_at DESC
LIMIT $3 OFFSET $4

GET /notifications/unread-count

Retorna a quantidade de notificacoes nao lidas.

Response (UnreadCountResponseDto):

json
{
  "count": 5
}

Query SQL gerada:

sql
SELECT COUNT(*) FROM notifications
WHERE user_id = $1 AND status = 'UNREAD'

POST /notifications/:id/read

Marca uma notificacao especifica como lida.

URL Params:

  • id: Snowflake ID da notificacao (string)

Validacoes:

  1. Notificacao existe (NotFoundException se nao)
  2. Notificacao pertence ao usuario (BadRequestException se nao)

Operacao no banco:

typescript
await this.prisma.notification.update({
  where: { id },
  data: {
    status: 'READ',
    readAt: new Date(),
  },
});

Response:

json
{
  "success": true
}

Erros:

  • 404 Not Found - Notificacao nao existe
  • 400 Bad Request - Notificacao nao pertence ao usuario

POST /notifications/read-all

Marca todas as notificacoes nao lidas do usuario como lidas.

Operacao no banco:

typescript
const result = await this.prisma.notification.updateMany({
  where: {
    userId,
    status: 'UNREAD',
  },
  data: {
    status: 'READ',
    readAt: new Date(),
  },
});
return result.count;

Response:

json
{
  "count": 5
}

count indica quantas notificacoes foram marcadas como lidas.

Repository: NotificationRepository

Metodos Implementados

MetodoDescricao
create(data)Cria notificacao com Snowflake ID
findById(id)Busca por ID
findByUserId(userId, filters?)Lista com filtros e paginacao
markAsRead(id)Atualiza status para READ + readAt
markAllAsRead(userId)Atualiza todas UNREAD para READ
countUnread(userId)Conta notificacoes UNREAD
delete(id)Remove notificacao (hard delete)

Fluxo Completo

Criacao de Notificacao

1. Evento ocorre (deposito confirmado, saque completo, etc)
2. Servico responsavel chama NotificationService.notifyXxx()
3. NotificationService.createAndSend() executa:
   a. notificationRepository.create() -> INSERT no banco
   b. websocketGateway.emitNotificationToUser() -> emit('notification') para sala user:${userId}
4. Frontend recebe evento WebSocket 'notification'
5. Frontend exibe toast/badge com a notificacao
6. Notificacao persiste no banco para acesso posterior

Estado atual: o fluxo acima descreve a App NotificationService (desenho com WebSocket), que nao esta conectada hoje. O fluxo realmente ativo e mais simples — o sorteio (draw-raffle.use-case.ts) chama a Infra NotificationService.create() (equivalente ao passo 3a apenas: INSERT no banco, tipo SYSTEM, sem emissao WebSocket). As notificacoes ficam disponiveis via GET /notifications.

Leitura de Notificacao

1. Frontend chama GET /notifications (ao abrir painel de notificacoes)
2. Controller delega para GetNotificationsUseCase
3. Use case chama notificationRepository.findByUserId()
4. Retorna lista ordenada por createdAt DESC
5. Frontend exibe lista de notificacoes
6. Usuario clica em notificacao -> POST /notifications/:id/read
7. Backend atualiza status para READ + readAt

Metadata por Tipo

O campo metadata (JSON) armazena dados adicionais para cada tipo de notificacao:

TipoMetadata
DEPOSIT_CONFIRMED{ amountCents: "150000" }
WITHDRAWAL_COMPLETED{ amountCents: "50000" }
WITHDRAWAL_FAILEDSem metadata
CASE_OPENING{ itemName: "AK-47 | Redline", isFlip: false }
UPGRADE_SUCCESS{ targetItemName: "AWP | Dragon Lore" }
UPGRADE_FAIL{ targetItemName: "AWP | Dragon Lore" }
SYSTEMVariavel (definido pelo admin)
PROMOTIONVariavel (definido pelo admin)

O metadata pode ser usado pelo frontend para deep linking (ex: clicar na notificacao de case opening leva ao inventario).

Transicoes de Status

UNREAD (padrao ao criar)
  |
  +--> READ (usuario leu a notificacao)
  |      |
  |      +--> ARCHIVED (usuario arquivou - nao implementado no controller atualmente)
  |
  +--> ARCHIVED (usuario arquivou sem ler - nao implementado)

Nota: O enum ARCHIVED existe no schema mas nao ha endpoint para arquivar notificacoes no controller atual. O status so pode transitar de UNREAD para READ via os endpoints existentes.

Cascade Delete

A relacao com User tem onDelete: Cascade, o que significa que se um usuario for deletado, todas as suas notificacoes sao automaticamente removidas.

Debugging

typescript
// Verificar se notificacao foi criada
console.log('[Notification] Created:', notification.id, notification.type);

// Verificar emissao WebSocket
console.log('[Notification] Emitting to user:', userId, notification.title);

// Verificar contagem de nao lidas
console.log('[Notification] Unread count for user:', userId, count);

Documentação Técnica - ProCases