jobs-monorepo/services/api/pkg/http/job.go
Elshimy Ziad Magdy Taha 2ab810be72
All checks were successful
Deploy scraper-google / build (push) Successful in 21m58s
Deploy scraper-google / deploy (push) Has been skipped
Move shared Go packages from libs/ to internal/
internal/ and libs/ were both shared-code roots with no rule for which
one a package belonged in, so the split carried no information. Adopt a
rule the language answers by itself:

  shared Go          -> internal/
  shared TypeScript  -> libs/   (npm workspace packages)

libs/{ports,repo,server} move to internal/, leaving libs/ holding only
the two TypeScript packages (@jobs-scraper/rabbitmq-ts and
@jobs-scraper/browser-automation), which matches npm workspace
convention. internal/ is also Go's marker for code not importable from
outside the repo, which is accurate here since none of it is published.

Import paths are rewritten mechanically (jobs-scraper/libs/ ->
jobs-scraper/internal/) across 15 lines in 10 files. The 6 moved files
are pure renames with no content change. Doing this after the module
collapse in the previous commit meant no go.mod or replace-directive
edits were needed.

gofmt is applied to services/api/pkg/http/job.go, whose import group the
rewrite left out of order. Three files were already unformatted before
this refactor (internal/openai/interface.go, internal/ports/job-queries.go,
internal/repo/job-analysis-result.go) and are deliberately left alone to
keep this diff limited to the move.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 17:30:04 +05:00

248 lines
8.6 KiB
Go

package http
import (
"encoding/json"
"fmt"
"log/slog"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/gorilla/schema"
"github.com/jobs-scraper/internal/ports"
"github.com/jobs-scraper/internal/repo"
"github.com/jobs-scraper/internal/utils"
)
// JobHandler handles HTTP requests related to jobs
type JobHandler struct {
jobCommands *ports.JobCommands
jobAnalysisResultRepo *repo.JobAnalysisResultRepository
jobQueries *ports.JobQueries
}
// NewJobHandler creates a new instance of JobHandler
func NewJobHandler(jobCommands *ports.JobCommands, jobAnalysisResultRepo *repo.JobAnalysisResultRepository, jobQueries *ports.JobQueries) *JobHandler {
return &JobHandler{
jobCommands: jobCommands,
jobAnalysisResultRepo: jobAnalysisResultRepo,
jobQueries: jobQueries,
}
}
var decoder = schema.NewDecoder()
// RegisterRoutes registers all routes to the router
func (h *JobHandler) RegisterRoutes(router *mux.Router) {
jobs := router.PathPrefix("/jobs").Subrouter()
//jobs.Use(AuthMiddleware)
jobs.HandleFunc("", utils.Make(h.CreateJob)).Methods("POST")
jobs.HandleFunc("/analysis", utils.Make(h.GetAllAnalysisResults)).Methods("GET")
jobs.HandleFunc("/{id}/analysis", utils.Make(h.GetJobAnalysisResult)).Methods("GET")
jobs.HandleFunc("/analysis/top-matches", utils.Make(h.GetTopMatches)).Methods("GET")
jobs.HandleFunc("", utils.Make(h.GetJobs)).Methods("GET")
jobs.HandleFunc("/apply/{id}", utils.Make(h.ApplyForJob)).Methods("POST")
}
// CreateJob handles the creation of a new job
// @Summary Create a new job
// @Description Creates a new job with the provided specifications
// @Tags jobs
// @Accept json
// @Produce json
// @Param job body ports.CreateJobCommand true "Job creation data"
// @Success 201 {object} map[string]string "Successfully created job"
// @Failure 400 {object} map[string]string "Invalid request data"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /jobs [post]
func (h *JobHandler) CreateJob(w http.ResponseWriter, r *http.Request) error {
var cmd ports.CreateJobCommand
if err := json.NewDecoder(r.Body).Decode(&cmd); err != nil {
slog.Error(err.Error())
return utils.NewAPIError(http.StatusBadRequest, utils.InvalidJSON())
}
err := h.jobCommands.CreateJob.Handle(cmd)
if err != nil {
slog.Error(err.Error())
return utils.NewAPIError(http.StatusInternalServerError, err)
}
utils.WriteJSON(w, http.StatusCreated, map[string]string{"message": "Success"})
return nil
}
// Apply applies for a job by id
// @Summary applies for a job
// @Description Applies for a job with the provided id
// @Tags jobs
// @Param id query string false "Job ID"
// @Accept json
// @Produce json
// @Success 201 {object} map[string]string "Successfully applied for job"
// @Failure 400 {object} map[string]string "Invalid request data"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /jobs/apply/{id} [post]
func (h *JobHandler) ApplyForJob(w http.ResponseWriter, r *http.Request) error {
jobId := r.URL.Query().Get("id")
if jobId == "" {
slog.Error("Job ID is required")
return utils.NewAPIError(http.StatusBadRequest, fmt.Errorf("job ID is required"))
}
jobIdInt, err := strconv.Atoi(jobId)
if err != nil {
slog.Error("Invalid Job ID", "error", err)
return utils.NewAPIError(http.StatusBadRequest, fmt.Errorf("invalid job ID"))
}
err = h.jobCommands.ApplyForJob.Handle(jobIdInt)
if err != nil {
slog.Error(err.Error())
return utils.NewAPIError(http.StatusInternalServerError, fmt.Errorf("Internal server error"))
}
utils.WriteJSON(w, http.StatusCreated, map[string]string{"message": "Success"})
return nil
}
// GetJobs handles the retrieval of all jobs
// @Summary Gets all jobs
// @Description Gets all jobs with the provided specifications
// @Tags jobs
// @Param location query string false "Location filter"
// @Param keywords query string false "Keywords filter (searches title and company)"
// @Param fwt query string false "Full/Part time filter"
// @Param provider query int false "Job provider (LinkedIn=0, Indeed=1, Glassdoor=2, Bayt=3, TokyoDev=4, JapanDev=5, Google=6, HiringCafe=7)" Enums(0,1,2,3,4,5,6,7)
// @Param pageNumber query int false "Page number"
// @Param pageSize query int false "Number of elements"
// @Accept json
// @Produce json
// @Success 200 {object} map[string]any "Successfully retrieved jobs"
// @Failure 400 {object} map[string]any "Invalid request data"
// @Failure 500 {object} map[string]any "Internal server error"
// @Router /jobs [get]
func (h *JobHandler) GetJobs(w http.ResponseWriter, r *http.Request) error {
query := ports.GetJobQuery{
PageNumber: 1,
PageSize: 10,
}
if pageNumberStr := r.URL.Query().Get("pageNumber"); pageNumberStr != "" {
if page, err := strconv.Atoi(pageNumberStr); err == nil && page > 0 {
query.PageNumber = page
}
}
if pageSizeStr := r.URL.Query().Get("pageSize"); pageSizeStr != "" {
if pageSize, err := strconv.Atoi(pageSizeStr); err == nil && pageSize > 0 {
query.PageSize = pageSize
}
}
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
slog.Error("Failed to decode query params", "error", err)
return utils.NewAPIError(http.StatusBadRequest, fmt.Errorf("invalid query parameters: %w", err))
}
result, totalCount, err := h.jobQueries.GetJobs.Handle(query)
if err != nil {
slog.Error(err.Error())
return utils.NewAPIError(http.StatusInternalServerError, err)
}
pagination := utils.NewPageViewModel(totalCount, query.PageNumber, query.PageSize)
utils.WriteJSON(w, http.StatusOK, map[string]any{
"jobs": result,
"pagination": pagination,
})
return nil
}
// GetAllAnalysisResults handles retrieving all job analysis results
// @Summary Get all job analysis results
// @Description Retrieves all job analysis results from the database
// @Tags jobs
// @Produce json
// @Success 200 {array} domain.JobAnalysisResult "List of job analysis results"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /jobs/analysis [get]
func (h *JobHandler) GetAllAnalysisResults(w http.ResponseWriter, r *http.Request) error {
results, err := h.jobAnalysisResultRepo.GetAllAnalysisResults()
if err != nil {
slog.Error("Failed to get analysis results", "error", err)
return utils.NewAPIError(http.StatusInternalServerError, err)
}
utils.WriteJSON(w, http.StatusOK, results)
return nil
}
// GetJobAnalysisResult handles retrieving analysis result for a specific job
// @Summary Get job analysis result by job ID
// @Description Retrieves the analysis result for a specific job
// @Tags jobs
// @Produce json
// @Param id path int true "Job ID"
// @Success 200 {object} domain.JobAnalysisResult "Job analysis result"
// @Failure 400 {object} map[string]string "Invalid job ID"
// @Failure 404 {object} map[string]string "Analysis result not found"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /jobs/{id}/analysis [get]
func (h *JobHandler) GetJobAnalysisResult(w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
jobID, err := strconv.ParseInt(vars["id"], 10, 64)
if err != nil {
return utils.NewAPIError(http.StatusBadRequest, fmt.Errorf("invalid job ID"))
}
result, err := h.jobAnalysisResultRepo.GetAnalysisResultByJobID(jobID)
if err != nil {
slog.Error("Failed to get analysis result", "jobID", jobID, "error", err)
return utils.NewAPIError(http.StatusNotFound, fmt.Errorf("analysis result not found"))
}
utils.WriteJSON(w, http.StatusOK, result)
return nil
}
// GetTopMatches handles retrieving top matching jobs based on analysis score
// @Summary Get top matching jobs
// @Description Retrieves jobs with high analysis match scores
// @Tags jobs
// @Produce json
// @Param min_score query int false "Minimum match score (default: 70)"
// @Success 200 {array} domain.JobAnalysisResult "List of top matching job analysis results"
// @Failure 400 {object} map[string]string "Invalid minimum score"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /jobs/analysis/top-matches [get]
func (h *JobHandler) GetTopMatches(w http.ResponseWriter, r *http.Request) error {
minScoreStr := r.URL.Query().Get("min_score")
minScore := 70 // default minimum score
if minScoreStr != "" {
var err error
minScore, err = strconv.Atoi(minScoreStr)
if err != nil {
return utils.NewAPIError(http.StatusBadRequest, fmt.Errorf("invalid minimum score"))
}
}
results, err := h.jobAnalysisResultRepo.GetAnalysisResultsByMatchScore(minScore)
if err != nil {
slog.Error("Failed to get top matches", "minScore", minScore, "error", err)
return utils.NewAPIError(http.StatusInternalServerError, err)
}
utils.WriteJSON(w, http.StatusOK, results)
return nil
}