gohorsejobs/backend/internal/services/job_alert_service.go
GoHorse Deploy ae475e41a9 feat: implement careerjet gap analysis improvements
- Video Interview system (backend + frontend)
- Date Posted filter (24h, 7d, 30d)
- Company filter in jobs listing
- Recent searches persistence (LocalStorage)
- Job Alerts with email confirmation
- Favorite jobs with API
- Company followers system
- Careerjet URL compatibility (s/l aliases)
2026-02-14 19:37:25 +00:00

208 lines
5.6 KiB
Go

package services
import (
"crypto/rand"
"database/sql"
"encoding/hex"
"fmt"
"time"
"github.com/rede5/gohorsejobs/backend/internal/dto"
"github.com/rede5/gohorsejobs/backend/internal/models"
)
type JobAlertService struct {
DB *sql.DB
EmailService EmailService
}
func NewJobAlertService(db *sql.DB, emailService EmailService) *JobAlertService {
return &JobAlertService{DB: db, EmailService: emailService}
}
func generateToken() string {
bytes := make([]byte, 32)
rand.Read(bytes)
return hex.EncodeToString(bytes)
}
func (s *JobAlertService) CreateAlert(req dto.CreateJobAlertRequest, userID *string) (*models.JobAlert, error) {
frequency := "daily"
if req.Frequency != nil && *req.Frequency == "weekly" {
frequency = *req.Frequency
}
currency := "BRL"
if req.Currency != nil {
currency = *req.Currency
}
token := generateToken()
nextSend := time.Now().Add(24 * time.Hour)
if frequency == "weekly" {
nextSend = time.Now().Add(7 * 24 * time.Hour)
}
query := `
INSERT INTO job_alerts (
user_id, search_query, location, employment_type, work_mode,
salary_min, salary_max, currency, frequency, is_active,
confirmation_token, next_send_at, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
RETURNING id, created_at, updated_at
`
alert := &models.JobAlert{
UserID: userID,
SearchQuery: req.SearchQuery,
Location: req.Location,
EmploymentType: req.EmploymentType,
WorkMode: req.WorkMode,
SalaryMin: req.SalaryMin,
SalaryMax: req.SalaryMax,
Currency: currency,
Frequency: frequency,
IsActive: false, // Requires email confirmation
ConfirmationToken: &token,
NextSendAt: &nextSend,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err := s.DB.QueryRow(
query,
alert.UserID, alert.SearchQuery, alert.Location, alert.EmploymentType, alert.WorkMode,
alert.SalaryMin, alert.SalaryMax, alert.Currency, alert.Frequency, alert.IsActive,
alert.ConfirmationToken, alert.NextSendAt, alert.CreatedAt, alert.UpdatedAt,
).Scan(&alert.ID, &alert.CreatedAt, &alert.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("failed to create alert: %w", err)
}
// Send confirmation email
email := ""
if userID == nil && req.Email != nil {
email = *req.Email
} else if userID != nil {
// Get user email
var userEmail string
err := s.DB.QueryRow("SELECT email FROM users WHERE id = $1", userID).Scan(&userEmail)
if err == nil {
email = userEmail
}
}
if email != "" {
confirmLink := fmt.Sprintf("https://gohorsejobs.com/alerts/confirm?token=%s", token)
subject := "Confirme seu alerta de vagas no GoHorse Jobs"
body := fmt.Sprintf(`
Olá,
Confirme seu alerta de vagas para receber as melhores oportunidades.
Link de confirmação: %s
Se você não criou este alerta, ignore este email.
`, confirmLink)
go s.EmailService.SendEmail(email, subject, body)
}
return alert, nil
}
func (s *JobAlertService) ConfirmAlert(token string) (*models.JobAlert, error) {
var alert models.JobAlert
query := `
SELECT id, user_id, search_query, location, employment_type, work_mode,
salary_min, salary_max, currency, frequency, is_active,
confirmation_token, confirmed_at, created_at, updated_at
FROM job_alerts WHERE confirmation_token = $1
`
err := s.DB.QueryRow(query, token).Scan(
&alert.ID, &alert.UserID, &alert.SearchQuery, &alert.Location, &alert.EmploymentType, &alert.WorkMode,
&alert.SalaryMin, &alert.SalaryMax, &alert.Currency, &alert.Frequency, &alert.IsActive,
&alert.ConfirmationToken, &alert.ConfirmedAt, &alert.CreatedAt, &alert.UpdatedAt,
)
if err != nil {
return nil, err
}
if alert.ConfirmedAt != nil {
return &alert, nil // Already confirmed
}
now := time.Now()
nextSend := now.Add(24 * time.Hour)
if alert.Frequency == "weekly" {
nextSend = now.Add(7 * 24 * time.Hour)
}
_, err = s.DB.Exec(
"UPDATE job_alerts SET is_active = true, confirmed_at = $1, next_send_at = $2 WHERE id = $3",
now, nextSend, alert.ID,
)
if err != nil {
return nil, err
}
alert.IsActive = true
alert.ConfirmedAt = &now
alert.NextSendAt = &nextSend
return &alert, nil
}
func (s *JobAlertService) GetAlertsByUser(userID string) ([]models.JobAlert, error) {
query := `
SELECT id, user_id, search_query, location, employment_type, work_mode,
salary_min, salary_max, currency, frequency, is_active,
last_sent_at, next_send_at, confirmed_at, created_at, updated_at
FROM job_alerts WHERE user_id = $1 ORDER BY created_at DESC
`
rows, err := s.DB.Query(query, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var alerts []models.JobAlert
for rows.Next() {
var alert models.JobAlert
err := rows.Scan(
&alert.ID, &alert.UserID, &alert.SearchQuery, &alert.Location, &alert.EmploymentType, &alert.WorkMode,
&alert.SalaryMin, &alert.SalaryMax, &alert.Currency, &alert.Frequency, &alert.IsActive,
&alert.LastSentAt, &alert.NextSendAt, &alert.ConfirmedAt, &alert.CreatedAt, &alert.UpdatedAt,
)
if err != nil {
return nil, err
}
alerts = append(alerts, alert)
}
return alerts, nil
}
func (s *JobAlertService) DeleteAlert(id string, userID string) error {
result, err := s.DB.Exec("DELETE FROM job_alerts WHERE id = $1 AND user_id = $2", id, userID)
if err != nil {
return err
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
return sql.ErrNoRows
}
return nil
}
func (s *JobAlertService) ToggleAlert(id string, userID string, active bool) error {
_, err := s.DB.Exec("UPDATE job_alerts SET is_active = $1, updated_at = $2 WHERE id = $3 AND user_id = $4",
active, time.Now(), id, userID)
return err
}