gohorsejobs/backend/internal/services/job_service.go

488 lines
16 KiB
Go

package services
import (
"database/sql"
"fmt"
"strings"
"time"
"github.com/rede5/gohorsejobs/backend/internal/dto"
"github.com/rede5/gohorsejobs/backend/internal/models"
)
type JobService struct {
DB *sql.DB
}
func NewJobService(db *sql.DB) *JobService {
return &JobService{DB: db}
}
func (s *JobService) CreateJob(req dto.CreateJobRequest, createdBy string) (*models.Job, error) {
fmt.Println("[JOB_SERVICE DEBUG] === CreateJob Started ===")
fmt.Printf("[JOB_SERVICE DEBUG] CompanyID=%s, CreatedBy=%s, Title=%s, Status=%s\n", req.CompanyID, createdBy, req.Title, req.Status)
query := `
INSERT INTO jobs (
company_id, created_by, title, description, salary_min, salary_max, salary_type, currency,
employment_type, work_mode, working_hours, location, region_id, city_id,
requirements, benefits, questions, visa_support, language_level, status, date_posted, created_at, updated_at, salary_negotiable
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24)
RETURNING id, date_posted, created_at, updated_at
`
job := &models.Job{
CompanyID: req.CompanyID,
CreatedBy: createdBy,
Title: req.Title,
Description: req.Description,
SalaryMin: req.SalaryMin,
SalaryMax: req.SalaryMax,
SalaryType: req.SalaryType,
Currency: req.Currency,
SalaryNegotiable: req.SalaryNegotiable,
EmploymentType: req.EmploymentType,
WorkMode: req.WorkMode,
WorkingHours: req.WorkingHours,
Location: req.Location,
RegionID: req.RegionID,
CityID: req.CityID,
Requirements: models.JSONMap(req.Requirements),
Benefits: models.JSONMap(req.Benefits),
Questions: models.JSONMap(req.Questions),
VisaSupport: req.VisaSupport,
LanguageLevel: req.LanguageLevel,
Status: req.Status,
DatePosted: ptrTime(time.Now()),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
fmt.Println("[JOB_SERVICE DEBUG] Executing INSERT query...")
fmt.Printf("[JOB_SERVICE DEBUG] Job struct: %+v\n", job)
err := s.DB.QueryRow(
query,
job.CompanyID, job.CreatedBy, job.Title, job.Description, job.SalaryMin, job.SalaryMax, job.SalaryType, job.Currency,
job.EmploymentType, job.WorkMode, job.WorkingHours, job.Location, job.RegionID, job.CityID,
job.Requirements, job.Benefits, job.Questions, job.VisaSupport, job.LanguageLevel, job.Status, job.DatePosted, job.CreatedAt, job.UpdatedAt, job.SalaryNegotiable,
).Scan(&job.ID, &job.DatePosted, &job.CreatedAt, &job.UpdatedAt)
if err != nil {
fmt.Printf("[JOB_SERVICE ERROR] INSERT query failed: %v\n", err)
return nil, err
}
fmt.Printf("[JOB_SERVICE DEBUG] Job created successfully! ID=%s\n", job.ID)
return job, nil
}
func (s *JobService) GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error) {
// Merged Query: Includes both HEAD and dev fields
baseQuery := `
SELECT
j.id, j.company_id, j.title, j.description, j.salary_min, j.salary_max, j.salary_type,
j.employment_type, j.work_mode, j.working_hours, j.location, j.status, j.salary_negotiable, j.is_featured, COALESCE(j.date_posted, j.created_at) AS date_posted, j.created_at, j.updated_at,
CASE
WHEN c.type = 'CANDIDATE_WORKSPACE' OR c.name LIKE 'Candidate - %' THEN ''
ELSE COALESCE(c.name, '')
END as company_name, c.logo_url as company_logo_url,
r.name as region_name, ci.name as city_name,
j.view_count, j.featured_until,
(SELECT COUNT(*) FROM applications a WHERE a.job_id = j.id) as applications_count
FROM jobs j
LEFT JOIN companies c ON j.company_id = c.id
LEFT JOIN states r ON j.region_id = r.id
LEFT JOIN cities ci ON j.city_id = ci.id
WHERE 1=1`
countQuery := `SELECT COUNT(*) FROM jobs j WHERE 1=1`
var args []interface{}
argId := 1
// Search (merged logic)
if filter.Search != nil && *filter.Search != "" {
searchTerm := fmt.Sprintf("%%%s%%", *filter.Search)
clause := fmt.Sprintf(" AND (j.title ILIKE $%d OR j.description ILIKE $%d OR c.name ILIKE $%d)", argId, argId, argId)
baseQuery += clause
// Se tem busca textual que checa c.name, a query de count *precisa* do JOIN
countQueryBase := `SELECT COUNT(*) FROM jobs j LEFT JOIN companies c ON j.company_id = c.id WHERE 1=1` // Usando count interno modificado apenas se tiver c.name
countQuery = countQueryBase + clause
args = append(args, searchTerm)
argId++
}
// Company filter
if filter.CompanyID != nil {
baseQuery += fmt.Sprintf(" AND j.company_id = $%d", argId)
countQuery += fmt.Sprintf(" AND j.company_id = $%d", argId)
args = append(args, *filter.CompanyID)
argId++
}
// Region filter
if filter.RegionID != nil {
baseQuery += fmt.Sprintf(" AND j.region_id = $%d", argId)
countQuery += fmt.Sprintf(" AND j.region_id = $%d", argId)
args = append(args, *filter.RegionID)
argId++
}
// City filter
if filter.CityID != nil {
baseQuery += fmt.Sprintf(" AND j.city_id = $%d", argId)
countQuery += fmt.Sprintf(" AND j.city_id = $%d", argId)
args = append(args, *filter.CityID)
argId++
}
// Employment type filter
if filter.EmploymentType != nil && *filter.EmploymentType != "" && *filter.EmploymentType != "all" {
baseQuery += fmt.Sprintf(" AND j.employment_type = $%d", argId)
countQuery += fmt.Sprintf(" AND j.employment_type = $%d", argId)
args = append(args, *filter.EmploymentType)
argId++
}
// Work mode filter
if filter.WorkMode != nil && *filter.WorkMode != "" && *filter.WorkMode != "all" {
baseQuery += fmt.Sprintf(" AND j.work_mode = $%d", argId)
countQuery += fmt.Sprintf(" AND j.work_mode = $%d", argId)
args = append(args, *filter.WorkMode)
argId++
}
// Location filter (Partial Match)
if filter.Location != nil && *filter.Location != "" && *filter.Location != "all" {
locTerm := fmt.Sprintf("%%%s%%", *filter.Location)
baseQuery += fmt.Sprintf(" AND j.location ILIKE $%d", argId)
countQuery += fmt.Sprintf(" AND j.location ILIKE $%d", argId)
args = append(args, locTerm)
argId++
}
// Support HEAD's LocationSearch explicitly if different
if filter.LocationSearch != nil && *filter.LocationSearch != "" && (filter.Location == nil || *filter.Location != *filter.LocationSearch) {
locTerm := fmt.Sprintf("%%%s%%", *filter.LocationSearch)
baseQuery += fmt.Sprintf(" AND j.location ILIKE $%d", argId)
countQuery += fmt.Sprintf(" AND j.location ILIKE $%d", argId)
args = append(args, locTerm)
argId++
}
// Status filter
if filter.Status != nil && *filter.Status != "" {
baseQuery += fmt.Sprintf(" AND j.status = $%d", argId)
countQuery += fmt.Sprintf(" AND j.status = $%d", argId)
args = append(args, *filter.Status)
argId++
}
// Featured filter
if filter.IsFeatured != nil {
baseQuery += fmt.Sprintf(" AND j.is_featured = $%d", argId)
countQuery += fmt.Sprintf(" AND j.is_featured = $%d", argId)
args = append(args, *filter.IsFeatured)
argId++
}
// Visa support filter
if filter.VisaSupport != nil {
baseQuery += fmt.Sprintf(" AND j.visa_support = $%d", argId)
countQuery += fmt.Sprintf(" AND j.visa_support = $%d", argId)
args = append(args, *filter.VisaSupport)
argId++
}
// Language Level
if filter.LanguageLevel != nil && *filter.LanguageLevel != "" && *filter.LanguageLevel != "all" {
baseQuery += fmt.Sprintf(" AND j.language_level = $%d", argId)
countQuery += fmt.Sprintf(" AND j.language_level = $%d", argId)
args = append(args, *filter.LanguageLevel)
argId++
}
// Currency
if filter.Currency != nil && *filter.Currency != "" && *filter.Currency != "all" {
baseQuery += fmt.Sprintf(" AND j.currency = $%d", argId)
countQuery += fmt.Sprintf(" AND j.currency = $%d", argId)
args = append(args, *filter.Currency)
argId++
}
// Salary range filters
if filter.SalaryMin != nil {
baseQuery += fmt.Sprintf(" AND j.salary_min >= $%d", argId)
countQuery += fmt.Sprintf(" AND j.salary_min >= $%d", argId)
args = append(args, *filter.SalaryMin)
argId++
}
if filter.SalaryMax != nil {
baseQuery += fmt.Sprintf(" AND j.salary_max <= $%d", argId)
countQuery += fmt.Sprintf(" AND j.salary_max <= $%d", argId)
args = append(args, *filter.SalaryMax)
argId++
}
if filter.SalaryType != nil && *filter.SalaryType != "" {
baseQuery += fmt.Sprintf(" AND j.salary_type = $%d", argId)
countQuery += fmt.Sprintf(" AND j.salary_type = $%d", argId)
args = append(args, *filter.SalaryType)
argId++
}
// Date Posted filter (24h, 7d, 30d)
if filter.DatePosted != nil && *filter.DatePosted != "" {
var hours int
switch *filter.DatePosted {
case "24h":
hours = 24
case "7d":
hours = 24 * 7
case "30d":
hours = 24 * 30
default:
hours = 0
}
if hours > 0 {
cutoffTime := time.Now().Add(-time.Duration(hours) * time.Hour)
baseQuery += fmt.Sprintf(" AND COALESCE(j.date_posted, j.created_at) >= $%d", argId)
countQuery += fmt.Sprintf(" AND COALESCE(j.date_posted, j.created_at) >= $%d", argId)
args = append(args, cutoffTime)
argId++
}
}
// Sorting
sortClause := " ORDER BY j.is_featured DESC, COALESCE(j.date_posted, j.created_at) DESC" // default
if filter.SortBy != nil {
switch *filter.SortBy {
case "recent", "date":
sortClause = " ORDER BY j.is_featured DESC, COALESCE(j.date_posted, j.created_at) DESC"
case "salary", "salary_asc":
sortClause = " ORDER BY j.salary_min ASC NULLS LAST"
case "salary_desc":
sortClause = " ORDER BY j.salary_max DESC NULLS LAST"
case "relevance":
sortClause = " ORDER BY j.is_featured DESC, j.created_at DESC"
}
}
// Override sort order if explicit
if filter.SortOrder != nil {
if *filter.SortOrder == "asc" {
// Rely on SortBy providing correct default or direction.
}
}
baseQuery += sortClause
// Pagination
limit := filter.Limit
if limit == 0 {
limit = 10
}
if limit > 100 {
limit = 100
}
offset := (filter.Page - 1) * limit
if offset < 0 {
offset = 0
}
paginationQuery := baseQuery + fmt.Sprintf(" LIMIT $%d OFFSET $%d", argId, argId+1)
paginationArgs := append(args, limit, offset)
rows, err := s.DB.Query(paginationQuery, paginationArgs...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
jobs := []models.JobWithCompany{}
for rows.Next() {
var j models.JobWithCompany
if err := rows.Scan(
&j.ID, &j.CompanyID, &j.Title, &j.Description, &j.SalaryMin, &j.SalaryMax, &j.SalaryType,
&j.EmploymentType, &j.WorkMode, &j.WorkingHours, &j.Location, &j.Status, &j.SalaryNegotiable, &j.IsFeatured, &j.DatePosted, &j.CreatedAt, &j.UpdatedAt,
&j.CompanyName, &j.CompanyLogoURL, &j.RegionName, &j.CityName,
&j.ViewCount, &j.FeaturedUntil, &j.ApplicationsCount,
); err != nil {
return nil, 0, err
}
jobs = append(jobs, j)
}
var total int
err = s.DB.QueryRow(countQuery, args...).Scan(&total)
if err != nil {
return nil, 0, err
}
return jobs, total, nil
}
func (s *JobService) GetJobByID(id string) (*models.Job, error) {
var j models.Job
query := `
SELECT id, company_id, title, description, salary_min, salary_max, salary_type,
employment_type, working_hours, location, region_id, city_id,
requirements, benefits, visa_support, language_level, status, is_featured, featured_until, view_count, date_posted, created_at, updated_at,
salary_negotiable, currency, work_mode
FROM jobs WHERE id = $1
`
err := s.DB.QueryRow(query, id).Scan(
&j.ID, &j.CompanyID, &j.Title, &j.Description, &j.SalaryMin, &j.SalaryMax, &j.SalaryType,
&j.EmploymentType, &j.WorkingHours, &j.Location, &j.RegionID, &j.CityID,
&j.Requirements, &j.Benefits, &j.VisaSupport, &j.LanguageLevel, &j.Status, &j.IsFeatured, &j.FeaturedUntil, &j.ViewCount, &j.DatePosted, &j.CreatedAt, &j.UpdatedAt,
&j.SalaryNegotiable, &j.Currency, &j.WorkMode,
)
if err != nil {
return nil, err
}
return &j, nil
}
func (s *JobService) UpdateJob(id string, req dto.UpdateJobRequest) (*models.Job, error) {
var setClauses []string
var args []interface{}
argId := 1
if req.Title != nil {
setClauses = append(setClauses, fmt.Sprintf("title = $%d", argId))
args = append(args, *req.Title)
argId++
}
if req.Description != nil {
setClauses = append(setClauses, fmt.Sprintf("description = $%d", argId))
args = append(args, *req.Description)
argId++
}
if req.SalaryMin != nil {
setClauses = append(setClauses, fmt.Sprintf("salary_min = $%d", argId))
args = append(args, *req.SalaryMin)
argId++
}
if req.SalaryMax != nil {
setClauses = append(setClauses, fmt.Sprintf("salary_max = $%d", argId))
args = append(args, *req.SalaryMax)
argId++
}
if req.SalaryType != nil {
setClauses = append(setClauses, fmt.Sprintf("salary_type = $%d", argId))
args = append(args, *req.SalaryType)
argId++
}
if req.Currency != nil {
setClauses = append(setClauses, fmt.Sprintf("currency = $%d", argId))
args = append(args, *req.Currency)
argId++
}
if req.EmploymentType != nil {
setClauses = append(setClauses, fmt.Sprintf("employment_type = $%d", argId))
args = append(args, *req.EmploymentType)
argId++
}
if req.WorkMode != nil {
setClauses = append(setClauses, fmt.Sprintf("work_mode = $%d", argId))
args = append(args, *req.WorkMode)
argId++
}
if req.WorkingHours != nil {
setClauses = append(setClauses, fmt.Sprintf("working_hours = $%d", argId))
args = append(args, *req.WorkingHours)
argId++
}
if req.Location != nil {
setClauses = append(setClauses, fmt.Sprintf("location = $%d", argId))
args = append(args, *req.Location)
argId++
}
if req.RegionID != nil {
setClauses = append(setClauses, fmt.Sprintf("region_id = $%d", argId))
args = append(args, *req.RegionID)
argId++
}
if req.CityID != nil {
setClauses = append(setClauses, fmt.Sprintf("city_id = $%d", argId))
args = append(args, *req.CityID)
argId++
}
if req.Requirements != nil {
setClauses = append(setClauses, fmt.Sprintf("requirements = $%d", argId))
args = append(args, req.Requirements)
argId++
}
if req.Benefits != nil {
setClauses = append(setClauses, fmt.Sprintf("benefits = $%d", argId))
args = append(args, req.Benefits)
argId++
}
if req.Questions != nil {
setClauses = append(setClauses, fmt.Sprintf("questions = $%d", argId))
args = append(args, req.Questions)
argId++
}
if req.VisaSupport != nil {
setClauses = append(setClauses, fmt.Sprintf("visa_support = $%d", argId))
args = append(args, *req.VisaSupport)
argId++
}
if req.LanguageLevel != nil {
setClauses = append(setClauses, fmt.Sprintf("language_level = $%d", argId))
args = append(args, *req.LanguageLevel)
argId++
}
if req.Status != nil {
setClauses = append(setClauses, fmt.Sprintf("status = $%d", argId))
args = append(args, *req.Status)
argId++
}
if req.IsFeatured != nil {
setClauses = append(setClauses, fmt.Sprintf("is_featured = $%d", argId))
args = append(args, *req.IsFeatured)
argId++
}
if req.FeaturedUntil != nil {
setClauses = append(setClauses, fmt.Sprintf("featured_until = $%d", argId))
parsedTime, err := time.Parse(time.RFC3339, *req.FeaturedUntil)
if err == nil {
args = append(args, parsedTime)
} else {
args = append(args, nil)
}
argId++
}
if req.SalaryNegotiable != nil {
setClauses = append(setClauses, fmt.Sprintf("salary_negotiable = $%d", argId))
args = append(args, *req.SalaryNegotiable)
argId++
}
if len(setClauses) == 0 {
return s.GetJobByID(id)
}
setClauses = append(setClauses, "updated_at = NOW()")
query := fmt.Sprintf("UPDATE jobs SET %s WHERE id = $%d RETURNING id, updated_at", strings.Join(setClauses, ", "), argId)
args = append(args, id)
var j models.Job
err := s.DB.QueryRow(query, args...).Scan(&j.ID, &j.UpdatedAt)
if err != nil {
return nil, err
}
return s.GetJobByID(id)
}
func (s *JobService) DeleteJob(id string) error {
_, err := s.DB.Exec("DELETE FROM jobs WHERE id = $1", id)
return err
}
func ptrTime(t time.Time) *time.Time {
return &t
}