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 }