- 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
228 lines
6.8 KiB
Go
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(¬if.ID, ¬if.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
|
|
}
|