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 }