gohorsejobs/backend/internal/services/video_interview_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

302 lines
8.9 KiB
Go

package services
import (
"database/sql"
"fmt"
"time"
"github.com/rede5/gohorsejobs/backend/internal/dto"
"github.com/rede5/gohorsejobs/backend/internal/models"
)
type VideoInterviewService struct {
DB *sql.DB
}
func NewVideoInterviewService(db *sql.DB) *VideoInterviewService {
return &VideoInterviewService{DB: db}
}
func (s *VideoInterviewService) CreateInterview(req dto.CreateVideoInterviewRequest, createdBy string) (*models.VideoInterview, error) {
scheduledAt, err := time.Parse(time.RFC3339, req.ScheduledAt)
if err != nil {
return nil, fmt.Errorf("invalid scheduled_at format: %w", err)
}
if req.Timezone == "" {
req.Timezone = "UTC"
}
if req.MeetingProvider == nil {
provider := "custom"
req.MeetingProvider = &provider
}
query := `
INSERT INTO video_interviews (
application_id, scheduled_at, duration_minutes, timezone,
meeting_provider, notes, status, created_by, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING id, created_at, updated_at
`
interview := &models.VideoInterview{
ApplicationID: req.ApplicationID,
ScheduledAt: scheduledAt,
DurationMinutes: req.DurationMinutes,
Timezone: req.Timezone,
MeetingProvider: req.MeetingProvider,
Notes: req.Notes,
Status: "scheduled",
CreatedBy: &createdBy,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err = s.DB.QueryRow(
query,
interview.ApplicationID, interview.ScheduledAt, interview.DurationMinutes, interview.Timezone,
interview.MeetingProvider, interview.Notes, interview.Status, interview.CreatedBy,
interview.CreatedAt, interview.UpdatedAt,
).Scan(&interview.ID, &interview.CreatedAt, &interview.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("failed to create interview: %w", err)
}
return interview, nil
}
func (s *VideoInterviewService) GetInterviewByID(id string) (*models.VideoInterview, error) {
query := `
SELECT id, application_id, scheduled_at, duration_minutes, timezone,
meeting_link, meeting_provider, meeting_id, meeting_password,
status, started_at, ended_at, notes, interviewer_feedback,
candidate_feedback, rating, created_by, created_at, updated_at
FROM video_interviews WHERE id = $1
`
interview := &models.VideoInterview{}
err := s.DB.QueryRow(query, id).Scan(
&interview.ID, &interview.ApplicationID, &interview.ScheduledAt, &interview.DurationMinutes, &interview.Timezone,
&interview.MeetingLink, &interview.MeetingProvider, &interview.MeetingID, &interview.MeetingPassword,
&interview.Status, &interview.StartedAt, &interview.EndedAt, &interview.Notes, &interview.InterviewerFeedback,
&interview.CandidateFeedback, &interview.Rating, &interview.CreatedBy, &interview.CreatedAt, &interview.UpdatedAt,
)
if err != nil {
return nil, err
}
return interview, nil
}
func (s *VideoInterviewService) GetInterviewsByApplication(applicationID string) ([]models.VideoInterview, error) {
query := `
SELECT id, application_id, scheduled_at, duration_minutes, timezone,
meeting_link, meeting_provider, meeting_id, meeting_password,
status, started_at, ended_at, notes, interviewer_feedback,
candidate_feedback, rating, created_by, created_at, updated_at
FROM video_interviews
WHERE application_id = $1
ORDER BY scheduled_at DESC
`
rows, err := s.DB.Query(query, applicationID)
if err != nil {
return nil, err
}
defer rows.Close()
var interviews []models.VideoInterview
for rows.Next() {
var interview models.VideoInterview
err := rows.Scan(
&interview.ID, &interview.ApplicationID, &interview.ScheduledAt, &interview.DurationMinutes, &interview.Timezone,
&interview.MeetingLink, &interview.MeetingProvider, &interview.MeetingID, &interview.MeetingPassword,
&interview.Status, &interview.StartedAt, &interview.EndedAt, &interview.Notes, &interview.InterviewerFeedback,
&interview.CandidateFeedback, &interview.Rating, &interview.CreatedBy, &interview.CreatedAt, &interview.UpdatedAt,
)
if err != nil {
return nil, err
}
interviews = append(interviews, interview)
}
return interviews, nil
}
func (s *VideoInterviewService) GetInterviewsByCompany(companyID string) ([]models.VideoInterviewWithDetails, error) {
query := `
SELECT
vi.id, vi.application_id, vi.scheduled_at, vi.duration_minutes, vi.timezone,
vi.meeting_link, vi.meeting_provider, vi.meeting_id, vi.meeting_password,
vi.status, vi.started_at, vi.ended_at, vi.notes, vi.interviewer_feedback,
vi.candidate_feedback, vi.rating, vi.created_by, vi.created_at, vi.updated_at,
j.title as job_title, c.id as company_id, c.name as company_name,
COALESCE(u.name, a.name) as candidate_name,
COALESCE(u.email, a.email) as candidate_email
FROM video_interviews vi
JOIN applications a ON vi.application_id = a.id
JOIN jobs j ON a.job_id = j.id
JOIN companies c ON j.company_id = c.id
LEFT JOIN users u ON a.user_id = u.id
WHERE c.id = $1
ORDER BY vi.scheduled_at DESC
`
rows, err := s.DB.Query(query, companyID)
if err != nil {
return nil, err
}
defer rows.Close()
var interviews []models.VideoInterviewWithDetails
for rows.Next() {
var interview models.VideoInterviewWithDetails
err := rows.Scan(
&interview.ID, &interview.ApplicationID, &interview.ScheduledAt, &interview.DurationMinutes, &interview.Timezone,
&interview.MeetingLink, &interview.MeetingProvider, &interview.MeetingID, &interview.MeetingPassword,
&interview.Status, &interview.StartedAt, &interview.EndedAt, &interview.Notes, &interview.InterviewerFeedback,
&interview.CandidateFeedback, &interview.Rating, &interview.CreatedBy, &interview.CreatedAt, &interview.UpdatedAt,
&interview.JobTitle, &interview.CompanyID, &interview.CompanyName,
&interview.CandidateName, &interview.CandidateEmail,
)
if err != nil {
return nil, err
}
interviews = append(interviews, interview)
}
return interviews, nil
}
func (s *VideoInterviewService) UpdateInterview(id string, req dto.UpdateVideoInterviewRequest) (*models.VideoInterview, error) {
interview, err := s.GetInterviewByID(id)
if err != nil {
return nil, err
}
if req.ScheduledAt != nil {
scheduledAt, err := time.Parse(time.RFC3339, *req.ScheduledAt)
if err != nil {
return nil, fmt.Errorf("invalid scheduled_at format: %w", err)
}
interview.ScheduledAt = scheduledAt
}
if req.DurationMinutes != nil {
interview.DurationMinutes = *req.DurationMinutes
}
if req.Timezone != nil {
interview.Timezone = *req.Timezone
}
if req.MeetingLink != nil {
interview.MeetingLink = req.MeetingLink
}
if req.MeetingProvider != nil {
interview.MeetingProvider = req.MeetingProvider
}
if req.MeetingID != nil {
interview.MeetingID = req.MeetingID
}
if req.MeetingPassword != nil {
interview.MeetingPassword = req.MeetingPassword
}
if req.Status != nil {
interview.Status = *req.Status
if *req.Status == "in_progress" {
now := time.Now()
interview.StartedAt = &now
} else if *req.Status == "completed" {
now := time.Now()
interview.EndedAt = &now
}
}
if req.Notes != nil {
interview.Notes = req.Notes
}
interview.UpdatedAt = time.Now()
query := `
UPDATE video_interviews SET
scheduled_at = $1, duration_minutes = $2, timezone = $3,
meeting_link = $4, meeting_provider = $5, meeting_id = $6, meeting_password = $7,
status = $8, started_at = $9, ended_at = $10, notes = $11, updated_at = $12
WHERE id = $13
`
_, err = s.DB.Exec(query,
interview.ScheduledAt, interview.DurationMinutes, interview.Timezone,
interview.MeetingLink, interview.MeetingProvider, interview.MeetingID, interview.MeetingPassword,
interview.Status, interview.StartedAt, interview.EndedAt, interview.Notes, interview.UpdatedAt,
id,
)
if err != nil {
return nil, fmt.Errorf("failed to update interview: %w", err)
}
return interview, nil
}
func (s *VideoInterviewService) SubmitFeedback(id string, req dto.VideoInterviewFeedbackRequest) (*models.VideoInterview, error) {
interview, err := s.GetInterviewByID(id)
if err != nil {
return nil, err
}
if req.InterviewerFeedback != nil {
interview.InterviewerFeedback = req.InterviewerFeedback
}
if req.CandidateFeedback != nil {
interview.CandidateFeedback = req.CandidateFeedback
}
if req.Rating != nil {
interview.Rating = req.Rating
}
interview.UpdatedAt = time.Now()
query := `
UPDATE video_interviews SET
interviewer_feedback = $1, candidate_feedback = $2, rating = $3, updated_at = $4
WHERE id = $5
`
_, err = s.DB.Exec(query,
interview.InterviewerFeedback, interview.CandidateFeedback, interview.Rating,
interview.UpdatedAt, id,
)
if err != nil {
return nil, fmt.Errorf("failed to submit feedback: %w", err)
}
return interview, nil
}
func (s *VideoInterviewService) DeleteInterview(id string) error {
result, err := s.DB.Exec("DELETE FROM video_interviews WHERE id = $1", id)
if err != nil {
return err
}
rowsAffected, _ := result.RowsAffected()
if rowsAffected == 0 {
return sql.ErrNoRows
}
return nil
}