gohorsejobs/backend/internal/services/notification_service.go
Tiago Yamamoto 63023b922f feat(notifications): implementar sistema de notificações e FCM
- Migration 017: tabelas notifications e fcm_tokens
- Models: Notification, FCMToken
- NotificationService: CRUD, push notifications helper
- FCMService: Firebase Cloud Messaging integration
- NotificationHandler: endpoints REST
- Rotas autenticadas: /api/v1/notifications/*

Endpoints:
- GET /api/v1/notifications
- GET /api/v1/notifications/unread-count
- PUT /api/v1/notifications/read-all
- PUT /api/v1/notifications/{id}/read
- DELETE /api/v1/notifications/{id}
- POST /api/v1/notifications/fcm-token
- DELETE /api/v1/notifications/fcm-token
2025-12-27 11:24:27 -03:00

228 lines
6.8 KiB
Go

package services
import (
"database/sql"
"encoding/json"
"github.com/rede5/gohorsejobs/backend/internal/models"
)
type NotificationService struct {
db *sql.DB
fcmService *FCMService
}
func NewNotificationService(db *sql.DB, fcmService *FCMService) *NotificationService {
return &NotificationService{db: db, fcmService: fcmService}
}
// Create creates a new notification
func (s *NotificationService) Create(req models.CreateNotificationRequest) (*models.Notification, error) {
var metadataJSON []byte
if req.Metadata != nil {
metadataJSON, _ = json.Marshal(req.Metadata)
}
query := `
INSERT INTO notifications (user_id, title, message, type, action_url, action_label, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, created_at
`
notif := &models.Notification{
UserID: req.UserID,
Title: req.Title,
Message: req.Message,
Type: req.Type,
ActionURL: req.ActionURL,
ActionLabel: req.ActionLabel,
Metadata: metadataJSON,
Read: false,
}
err := s.db.QueryRow(query, req.UserID, req.Title, req.Message, req.Type, req.ActionURL, req.ActionLabel, metadataJSON).
Scan(&notif.ID, &notif.CreatedAt)
if err != nil {
return nil, err
}
// Send push notification if requested
if req.SendPush && s.fcmService != nil {
go s.sendPushNotification(notif)
}
return notif, nil
}
// sendPushNotification sends push via FCM
func (s *NotificationService) sendPushNotification(notif *models.Notification) {
tokens, err := s.GetUserFCMTokens(notif.UserID)
if err != nil || len(tokens) == 0 {
return
}
for _, token := range tokens {
err := s.fcmService.Send(token.Token, notif.Title, notif.Message, map[string]string{
"notificationId": string(rune(notif.ID)),
"type": notif.Type,
})
if err == nil {
// Mark as sent
s.db.Exec("UPDATE notifications SET push_sent = true, push_sent_at = NOW() WHERE id = $1", notif.ID)
}
}
}
// GetByUserID lists notifications for a user
func (s *NotificationService) GetByUserID(userID int, limit, offset int, unreadOnly bool) ([]models.Notification, error) {
query := `
SELECT id, user_id, tenant_id, title, message, type, action_url, action_label,
read, read_at, push_sent, push_sent_at, metadata, created_at
FROM notifications
WHERE user_id = $1
`
if unreadOnly {
query += " AND read = false"
}
query += " ORDER BY created_at DESC LIMIT $2 OFFSET $3"
rows, err := s.db.Query(query, userID, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
var notifications []models.Notification
for rows.Next() {
var n models.Notification
err := rows.Scan(
&n.ID, &n.UserID, &n.TenantID, &n.Title, &n.Message, &n.Type,
&n.ActionURL, &n.ActionLabel, &n.Read, &n.ReadAt,
&n.PushSent, &n.PushSentAt, &n.Metadata, &n.CreatedAt,
)
if err != nil {
return nil, err
}
notifications = append(notifications, n)
}
return notifications, nil
}
// MarkAsRead marks a notification as read
func (s *NotificationService) MarkAsRead(id, userID int) error {
_, err := s.db.Exec(
"UPDATE notifications SET read = true, read_at = NOW() WHERE id = $1 AND user_id = $2",
id, userID,
)
return err
}
// MarkAllAsRead marks all notifications as read for a user
func (s *NotificationService) MarkAllAsRead(userID int) error {
_, err := s.db.Exec(
"UPDATE notifications SET read = true, read_at = NOW() WHERE user_id = $1 AND read = false",
userID,
)
return err
}
// GetUnreadCount returns unread count for a user
func (s *NotificationService) GetUnreadCount(userID int) (int, error) {
var count int
err := s.db.QueryRow("SELECT COUNT(*) FROM notifications WHERE user_id = $1 AND read = false", userID).Scan(&count)
return count, err
}
// Delete deletes a notification
func (s *NotificationService) Delete(id, userID int) error {
_, err := s.db.Exec("DELETE FROM notifications WHERE id = $1 AND user_id = $2", id, userID)
return err
}
// RegisterFCMToken registers or updates a device token
func (s *NotificationService) RegisterFCMToken(userID int, req models.RegisterFCMTokenRequest) error {
query := `
INSERT INTO fcm_tokens (user_id, token, device_type, device_name, active, updated_at)
VALUES ($1, $2, $3, $4, true, NOW())
ON CONFLICT (user_id, token) DO UPDATE SET active = true, updated_at = NOW()
`
_, err := s.db.Exec(query, userID, req.Token, req.DeviceType, req.DeviceName)
return err
}
// UnregisterFCMToken deactivates a device token
func (s *NotificationService) UnregisterFCMToken(userID int, token string) error {
_, err := s.db.Exec("UPDATE fcm_tokens SET active = false, updated_at = NOW() WHERE user_id = $1 AND token = $2", userID, token)
return err
}
// GetUserFCMTokens gets active FCM tokens for a user
func (s *NotificationService) GetUserFCMTokens(userID int) ([]models.FCMToken, error) {
query := `SELECT id, user_id, token, device_type, device_name, active, created_at, updated_at
FROM fcm_tokens WHERE user_id = $1 AND active = true`
rows, err := s.db.Query(query, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var tokens []models.FCMToken
for rows.Next() {
var t models.FCMToken
if err := rows.Scan(&t.ID, &t.UserID, &t.Token, &t.DeviceType, &t.DeviceName, &t.Active, &t.CreatedAt, &t.UpdatedAt); err != nil {
return nil, err
}
tokens = append(tokens, t)
}
return tokens, nil
}
// NotifyNewApplication sends notification when someone applies to a job
func (s *NotificationService) NotifyNewApplication(companyUserID int, applicantName, jobTitle string, applicationID int) error {
actionURL := "/dashboard/applications/" + string(rune(applicationID))
req := models.CreateNotificationRequest{
UserID: companyUserID,
Title: "Nova Candidatura",
Message: applicantName + " se candidatou para " + jobTitle,
Type: "application",
ActionURL: &actionURL,
ActionLabel: strPtr("Ver Candidatura"),
SendPush: true,
}
_, err := s.Create(req)
return err
}
// NotifyApplicationStatus sends notification when application status changes
func (s *NotificationService) NotifyApplicationStatus(userID int, jobTitle, status string) error {
var title, message string
switch status {
case "shortlisted":
title = "Você foi selecionado!"
message = "Sua candidatura para " + jobTitle + " avançou para a próxima fase"
case "rejected":
title = "Atualização de Candidatura"
message = "Sua candidatura para " + jobTitle + " não foi aprovada desta vez"
case "hired":
title = "Parabéns! 🎉"
message = "Você foi contratado para " + jobTitle
default:
title = "Atualização de Candidatura"
message = "Sua candidatura para " + jobTitle + " foi atualizada"
}
req := models.CreateNotificationRequest{
UserID: userID,
Title: title,
Message: message,
Type: "application",
SendPush: true,
}
_, err := s.Create(req)
return err
}
func strPtr(s string) *string {
return &s
}