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): apenascreate()(INSERT no banco, sem WebSocket). E a unica em uso: o unico produtor real de notificacao hoje eraffle/draw-raffle.use-case.ts, que injeta a Infra e chamacreate()comNotificationType.SYSTEM.O restante deste documento descreve a App
NotificationService(o desenho completo com WebSocket) e sinaliza onde o comportamento atual difere.
Arquivos do Codigo
| Arquivo | Responsabilidade |
|---|---|
src/application/services/notification.service.ts | App service - createAndSend + helpers notify* (INSERT + WebSocket); sem callers hoje |
src/infrastructure/notification/notification.service.ts | Infra service - apenas create() (INSERT, sem WebSocket); usado pelo sorteio (draw-raffle) |
src/presentation/controllers/notification.controller.ts | Controller com 4 endpoints |
src/application/use-cases/notification/get-notifications.use-case.ts | Listar notificacoes do usuario |
src/application/use-cases/notification/get-unread-count.use-case.ts | Contar nao lidas |
src/application/use-cases/notification/mark-as-read.use-case.ts | Marcar como lida |
src/application/use-cases/notification/mark-all-as-read.use-case.ts | Marcar todas como lidas |
src/infrastructure/database/repositories/notification.repository.ts | Repository Prisma |
src/domain/repositories/notification.repository.interface.ts | Interface do repository |
src/application/dto/notification.dto.ts | DTOs de validacao |
src/infrastructure/websocket/websocket.gateway.ts | Emissao WebSocket |
Schema do Banco de Dados
Notification Model
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
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
enum NotificationStatus {
UNREAD // Nao lida (padrao)
READ // Lida pelo usuario
ARCHIVED // Arquivada
}Indices
| Indice | Uso |
|---|---|
(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.
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:
- Cria registro no banco com Snowflake ID, status
UNREAD - Emite evento
notificationpara salauser:${userId}via WebSocket - 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
NotificationServicee 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)
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)
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)
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)
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)
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)
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.
// 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:
{
"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).
// 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):
| Parametro | Tipo | Obrigatorio | Validacao | Default |
|---|---|---|---|---|
status | string | Nao | @IsEnum(['UNREAD', 'READ', 'ARCHIVED']) | Sem filtro (todas) |
limit | number | Nao | @Type(() => Number) | 50 |
offset | number | Nao | @Type(() => Number) | 0 |
Response (NotificationResponseDto[]):
[
{
"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:
SELECT * FROM notifications
WHERE user_id = $1
AND status = $2 -- se filtro fornecido
ORDER BY created_at DESC
LIMIT $3 OFFSET $4GET /notifications/unread-count
Retorna a quantidade de notificacoes nao lidas.
Response (UnreadCountResponseDto):
{
"count": 5
}Query SQL gerada:
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:
- Notificacao existe (
NotFoundExceptionse nao) - Notificacao pertence ao usuario (
BadRequestExceptionse nao)
Operacao no banco:
await this.prisma.notification.update({
where: { id },
data: {
status: 'READ',
readAt: new Date(),
},
});Response:
{
"success": true
}Erros:
404 Not Found- Notificacao nao existe400 Bad Request- Notificacao nao pertence ao usuario
POST /notifications/read-all
Marca todas as notificacoes nao lidas do usuario como lidas.
Operacao no banco:
const result = await this.prisma.notification.updateMany({
where: {
userId,
status: 'UNREAD',
},
data: {
status: 'READ',
readAt: new Date(),
},
});
return result.count;Response:
{
"count": 5
}count indica quantas notificacoes foram marcadas como lidas.
Repository: NotificationRepository
Metodos Implementados
| Metodo | Descricao |
|---|---|
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 posteriorEstado 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 InfraNotificationService.create()(equivalente ao passo 3a apenas: INSERT no banco, tipoSYSTEM, sem emissao WebSocket). As notificacoes ficam disponiveis viaGET /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 + readAtMetadata por Tipo
O campo metadata (JSON) armazena dados adicionais para cada tipo de notificacao:
| Tipo | Metadata |
|---|---|
DEPOSIT_CONFIRMED | { amountCents: "150000" } |
WITHDRAWAL_COMPLETED | { amountCents: "50000" } |
WITHDRAWAL_FAILED | Sem metadata |
CASE_OPENING | { itemName: "AK-47 | Redline", isFlip: false } |
UPGRADE_SUCCESS | { targetItemName: "AWP | Dragon Lore" } |
UPGRADE_FAIL | { targetItemName: "AWP | Dragon Lore" } |
SYSTEM | Variavel (definido pelo admin) |
PROMOTION | Variavel (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
// 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);