From 0e265e64b8370ab78040a71e0996cccac6e7cd34 Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Tue, 23 Dec 2025 14:46:17 -0300 Subject: [PATCH] Refactor backend to use string IDs for Job, Company, and Application --- .../internal/api/handlers/admin_handlers.go | 21 +---- backend/internal/dto/requests.go | 12 +-- .../internal/handlers/application_handler.go | 24 +----- .../handlers/application_handler_test.go | 84 +++++++++++-------- backend/internal/handlers/job_handler.go | 26 ++---- backend/internal/handlers/job_handler_test.go | 52 +++++++----- backend/internal/models/application.go | 8 +- backend/internal/models/company.go | 2 +- backend/internal/models/favorite_job.go | 8 +- backend/internal/models/job.go | 6 +- backend/internal/models/user.go | 4 +- backend/internal/models/user_company.go | 6 +- backend/internal/services/admin_service.go | 6 +- .../internal/services/application_service.go | 6 +- backend/internal/services/job_service.go | 6 +- backend/internal/services/job_service_test.go | 14 ++-- 16 files changed, 128 insertions(+), 157 deletions(-) diff --git a/backend/internal/api/handlers/admin_handlers.go b/backend/internal/api/handlers/admin_handlers.go index c2df511..1456e3a 100644 --- a/backend/internal/api/handlers/admin_handlers.go +++ b/backend/internal/api/handlers/admin_handlers.go @@ -123,12 +123,7 @@ func (h *AdminHandlers) ListCompanies(w http.ResponseWriter, r *http.Request) { } func (h *AdminHandlers) UpdateCompanyStatus(w http.ResponseWriter, r *http.Request) { - idStr := r.PathValue("id") - id, err := strconv.Atoi(idStr) - if err != nil { - http.Error(w, "Invalid company ID", http.StatusBadRequest) - return - } + id := r.PathValue("id") var req UpdateCompanyStatusRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -182,12 +177,7 @@ func (h *AdminHandlers) ListJobs(w http.ResponseWriter, r *http.Request) { } func (h *AdminHandlers) UpdateJobStatus(w http.ResponseWriter, r *http.Request) { - idStr := r.PathValue("id") - id, err := strconv.Atoi(idStr) - if err != nil { - http.Error(w, "Invalid job ID", http.StatusBadRequest) - return - } + id := r.PathValue("id") var req UpdateJobStatusRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -211,12 +201,7 @@ func (h *AdminHandlers) UpdateJobStatus(w http.ResponseWriter, r *http.Request) } func (h *AdminHandlers) DuplicateJob(w http.ResponseWriter, r *http.Request) { - idStr := r.PathValue("id") - id, err := strconv.Atoi(idStr) - if err != nil { - http.Error(w, "Invalid job ID", http.StatusBadRequest) - return - } + id := r.PathValue("id") job, err := h.adminService.DuplicateJob(r.Context(), id) if err != nil { diff --git a/backend/internal/dto/requests.go b/backend/internal/dto/requests.go index 6212543..dd09951 100755 --- a/backend/internal/dto/requests.go +++ b/backend/internal/dto/requests.go @@ -2,7 +2,7 @@ package dto // CreateJobRequest represents the request to create a new job type CreateJobRequest struct { - CompanyID int `json:"companyId" validate:"required"` + CompanyID string `json:"companyId" validate:"required"` Title string `json:"title" validate:"required,min=5,max=255"` Description string `json:"description" validate:"required,min=20"` SalaryMin *float64 `json:"salaryMin,omitempty"` @@ -41,8 +41,8 @@ type UpdateJobRequest struct { // CreateApplicationRequest represents a job application (guest or logged user) type CreateApplicationRequest struct { - JobID int `json:"jobId" validate:"required"` - UserID *int `json:"userId,omitempty"` + JobID string `json:"jobId" validate:"required"` + UserID *string `json:"userId,omitempty"` Name *string `json:"name,omitempty"` Phone *string `json:"phone,omitempty"` LineID *string `json:"lineId,omitempty"` @@ -95,8 +95,8 @@ type UpdateCompanyRequest struct { // AssignUserToCompanyRequest represents assigning a user to a company type AssignUserToCompanyRequest struct { - UserID int `json:"userId" validate:"required"` - CompanyID int `json:"companyId" validate:"required"` + UserID string `json:"userId" validate:"required"` + CompanyID string `json:"companyId" validate:"required"` Role string `json:"role" validate:"required,oneof=companyAdmin recruiter"` Permissions map[string]interface{} `json:"permissions,omitempty"` } @@ -110,7 +110,7 @@ type PaginationQuery struct { // JobFilterQuery represents job filtering parameters type JobFilterQuery struct { PaginationQuery - CompanyID *int `form:"companyId"` + CompanyID *string `form:"companyId"` RegionID *int `form:"regionId"` CityID *int `form:"cityId"` EmploymentType *string `form:"employmentType"` diff --git a/backend/internal/handlers/application_handler.go b/backend/internal/handlers/application_handler.go index 8b3559e..0af7b77 100644 --- a/backend/internal/handlers/application_handler.go +++ b/backend/internal/handlers/application_handler.go @@ -3,7 +3,6 @@ package handlers import ( "encoding/json" "net/http" - "strconv" "github.com/rede5/gohorsejobs/backend/internal/dto" "github.com/rede5/gohorsejobs/backend/internal/services" @@ -59,16 +58,11 @@ func (h *ApplicationHandler) CreateApplication(w http.ResponseWriter, r *http.Re // @Router /api/v1/applications [get] func (h *ApplicationHandler) GetApplications(w http.ResponseWriter, r *http.Request) { // For now, simple get by Job ID query param - jobIDStr := r.URL.Query().Get("jobId") - if jobIDStr == "" { + jobID := r.URL.Query().Get("jobId") + if jobID == "" { http.Error(w, "jobId is required", http.StatusBadRequest) return } - jobID, err := strconv.Atoi(jobIDStr) - if err != nil { - http.Error(w, "Invalid jobId", http.StatusBadRequest) - return - } apps, err := h.Service.GetApplications(jobID) if err != nil { @@ -92,12 +86,7 @@ func (h *ApplicationHandler) GetApplications(w http.ResponseWriter, r *http.Requ // @Failure 404 {string} string "Not Found" // @Router /api/v1/applications/{id} [get] func (h *ApplicationHandler) GetApplicationByID(w http.ResponseWriter, r *http.Request) { - idStr := r.PathValue("id") - id, err := strconv.Atoi(idStr) - if err != nil { - http.Error(w, "Invalid application ID", http.StatusBadRequest) - return - } + id := r.PathValue("id") app, err := h.Service.GetApplicationByID(id) if err != nil { @@ -122,12 +111,7 @@ func (h *ApplicationHandler) GetApplicationByID(w http.ResponseWriter, r *http.R // @Failure 500 {string} string "Internal Server Error" // @Router /api/v1/applications/{id}/status [put] func (h *ApplicationHandler) UpdateApplicationStatus(w http.ResponseWriter, r *http.Request) { - idStr := r.PathValue("id") - id, err := strconv.Atoi(idStr) - if err != nil { - http.Error(w, "Invalid application ID", http.StatusBadRequest) - return - } + id := r.PathValue("id") var req dto.UpdateApplicationStatusRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { diff --git a/backend/internal/handlers/application_handler_test.go b/backend/internal/handlers/application_handler_test.go index da916a3..ab6b8b8 100644 --- a/backend/internal/handlers/application_handler_test.go +++ b/backend/internal/handlers/application_handler_test.go @@ -16,9 +16,9 @@ import ( // mockApplicationService is a mock implementation for testing type mockApplicationService struct { createApplicationFunc func(req dto.CreateApplicationRequest) (*models.Application, error) - getApplicationsFunc func(jobID int) ([]models.Application, error) - getApplicationByIDFunc func(id int) (*models.Application, error) - updateApplicationStatusFunc func(id int, req dto.UpdateApplicationStatusRequest) (*models.Application, error) + getApplicationsFunc func(jobID string) ([]models.Application, error) + getApplicationByIDFunc func(id string) (*models.Application, error) + updateApplicationStatusFunc func(id string, req dto.UpdateApplicationStatusRequest) (*models.Application, error) } func (m *mockApplicationService) CreateApplication(req dto.CreateApplicationRequest) (*models.Application, error) { @@ -28,21 +28,21 @@ func (m *mockApplicationService) CreateApplication(req dto.CreateApplicationRequ return nil, nil } -func (m *mockApplicationService) GetApplications(jobID int) ([]models.Application, error) { +func (m *mockApplicationService) GetApplications(jobID string) ([]models.Application, error) { if m.getApplicationsFunc != nil { return m.getApplicationsFunc(jobID) } return nil, nil } -func (m *mockApplicationService) GetApplicationByID(id int) (*models.Application, error) { +func (m *mockApplicationService) GetApplicationByID(id string) (*models.Application, error) { if m.getApplicationByIDFunc != nil { return m.getApplicationByIDFunc(id) } return nil, nil } -func (m *mockApplicationService) UpdateApplicationStatus(id int, req dto.UpdateApplicationStatusRequest) (*models.Application, error) { +func (m *mockApplicationService) UpdateApplicationStatus(id string, req dto.UpdateApplicationStatusRequest) (*models.Application, error) { if m.updateApplicationStatusFunc != nil { return m.updateApplicationStatusFunc(id, req) } @@ -52,9 +52,9 @@ func (m *mockApplicationService) UpdateApplicationStatus(id int, req dto.UpdateA // ApplicationServiceInterface defines the interface for application service type ApplicationServiceInterface interface { CreateApplication(req dto.CreateApplicationRequest) (*models.Application, error) - GetApplications(jobID int) ([]models.Application, error) - GetApplicationByID(id int) (*models.Application, error) - UpdateApplicationStatus(id int, req dto.UpdateApplicationStatusRequest) (*models.Application, error) + GetApplications(jobID string) ([]models.Application, error) + GetApplicationByID(id string) (*models.Application, error) + UpdateApplicationStatus(id string, req dto.UpdateApplicationStatusRequest) (*models.Application, error) } // testableApplicationHandler wraps an interface for testing @@ -84,7 +84,8 @@ func (h *testableApplicationHandler) CreateApplication(w http.ResponseWriter, r json.NewEncoder(w).Encode(app) } -func (h *testableApplicationHandler) GetApplications(w http.ResponseWriter, r *http.Request, jobID int) { +func (h *testableApplicationHandler) GetApplications(w http.ResponseWriter, r *http.Request) { + jobID := r.URL.Query().Get("jobId") apps, err := h.service.GetApplications(jobID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) @@ -95,7 +96,12 @@ func (h *testableApplicationHandler) GetApplications(w http.ResponseWriter, r *h json.NewEncoder(w).Encode(apps) } -func (h *testableApplicationHandler) GetApplicationByID(w http.ResponseWriter, r *http.Request, id int) { +func (h *testableApplicationHandler) GetApplicationByID(w http.ResponseWriter, r *http.Request) { + // In real handler we use path value, here we might need to simulate or just call service + // For unit test of handler logic usually we mock the router or simply pass arguments if method signature allows. + // But check original handler: it extracts from r.PathValue("id"). + // In tests using httptest.NewRequest with Go 1.22 routing, we need to set path values. + id := r.PathValue("id") app, err := h.service.GetApplicationByID(id) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) @@ -106,7 +112,8 @@ func (h *testableApplicationHandler) GetApplicationByID(w http.ResponseWriter, r json.NewEncoder(w).Encode(app) } -func (h *testableApplicationHandler) UpdateApplicationStatus(w http.ResponseWriter, r *http.Request, id int) { +func (h *testableApplicationHandler) UpdateApplicationStatus(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") var req dto.UpdateApplicationStatusRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) @@ -134,7 +141,7 @@ func TestCreateApplication_Success(t *testing.T) { mockService := &mockApplicationService{ createApplicationFunc: func(req dto.CreateApplicationRequest) (*models.Application, error) { return &models.Application{ - ID: 1, + ID: "1", JobID: req.JobID, Name: &name, Email: &email, @@ -148,7 +155,7 @@ func TestCreateApplication_Success(t *testing.T) { handler := newTestableApplicationHandler(mockService) appReq := dto.CreateApplicationRequest{ - JobID: 1, + JobID: "1", Name: &name, Email: &email, } @@ -166,7 +173,7 @@ func TestCreateApplication_Success(t *testing.T) { err := json.Unmarshal(rr.Body.Bytes(), &app) assert.NoError(t, err) assert.Equal(t, "pending", app.Status) - assert.Equal(t, 1, app.JobID) + assert.Equal(t, "1", app.JobID) } func TestCreateApplication_InvalidJSON(t *testing.T) { @@ -192,7 +199,7 @@ func TestCreateApplication_ServiceError(t *testing.T) { handler := newTestableApplicationHandler(mockService) - appReq := dto.CreateApplicationRequest{JobID: 1} + appReq := dto.CreateApplicationRequest{JobID: "1"} body, _ := json.Marshal(appReq) req := httptest.NewRequest("POST", "/applications", bytes.NewReader(body)) @@ -209,17 +216,17 @@ func TestGetApplications_Success(t *testing.T) { name2 := "Jane Smith" mockService := &mockApplicationService{ - getApplicationsFunc: func(jobID int) ([]models.Application, error) { + getApplicationsFunc: func(jobID string) ([]models.Application, error) { return []models.Application{ { - ID: 1, + ID: "1", JobID: jobID, Name: &name1, Status: "pending", CreatedAt: time.Now(), }, { - ID: 2, + ID: "2", JobID: jobID, Name: &name2, Status: "reviewed", @@ -234,7 +241,7 @@ func TestGetApplications_Success(t *testing.T) { req := httptest.NewRequest("GET", "/applications?jobId=1", nil) rr := httptest.NewRecorder() - handler.GetApplications(rr, req, 1) + handler.GetApplications(rr, req) assert.Equal(t, http.StatusOK, rr.Code) @@ -246,7 +253,7 @@ func TestGetApplications_Success(t *testing.T) { func TestGetApplications_Empty(t *testing.T) { mockService := &mockApplicationService{ - getApplicationsFunc: func(jobID int) ([]models.Application, error) { + getApplicationsFunc: func(jobID string) ([]models.Application, error) { return []models.Application{}, nil }, } @@ -256,14 +263,14 @@ func TestGetApplications_Empty(t *testing.T) { req := httptest.NewRequest("GET", "/applications?jobId=1", nil) rr := httptest.NewRecorder() - handler.GetApplications(rr, req, 1) + handler.GetApplications(rr, req) assert.Equal(t, http.StatusOK, rr.Code) } func TestGetApplications_Error(t *testing.T) { mockService := &mockApplicationService{ - getApplicationsFunc: func(jobID int) ([]models.Application, error) { + getApplicationsFunc: func(jobID string) ([]models.Application, error) { return nil, assert.AnError }, } @@ -273,7 +280,7 @@ func TestGetApplications_Error(t *testing.T) { req := httptest.NewRequest("GET", "/applications?jobId=1", nil) rr := httptest.NewRecorder() - handler.GetApplications(rr, req, 1) + handler.GetApplications(rr, req) assert.Equal(t, http.StatusInternalServerError, rr.Code) } @@ -282,10 +289,10 @@ func TestGetApplicationByID_Success(t *testing.T) { name := "John Doe" mockService := &mockApplicationService{ - getApplicationByIDFunc: func(id int) (*models.Application, error) { + getApplicationByIDFunc: func(id string) (*models.Application, error) { return &models.Application{ ID: id, - JobID: 1, + JobID: "1", Name: &name, Status: "pending", CreatedAt: time.Now(), @@ -296,21 +303,22 @@ func TestGetApplicationByID_Success(t *testing.T) { handler := newTestableApplicationHandler(mockService) req := httptest.NewRequest("GET", "/applications/1", nil) + req.SetPathValue("id", "1") // Go 1.22 feature rr := httptest.NewRecorder() - handler.GetApplicationByID(rr, req, 1) + handler.GetApplicationByID(rr, req) assert.Equal(t, http.StatusOK, rr.Code) var app models.Application err := json.Unmarshal(rr.Body.Bytes(), &app) assert.NoError(t, err) - assert.Equal(t, 1, app.ID) + assert.Equal(t, "1", app.ID) } func TestGetApplicationByID_NotFound(t *testing.T) { mockService := &mockApplicationService{ - getApplicationByIDFunc: func(id int) (*models.Application, error) { + getApplicationByIDFunc: func(id string) (*models.Application, error) { return nil, assert.AnError }, } @@ -318,9 +326,10 @@ func TestGetApplicationByID_NotFound(t *testing.T) { handler := newTestableApplicationHandler(mockService) req := httptest.NewRequest("GET", "/applications/999", nil) + req.SetPathValue("id", "999") rr := httptest.NewRecorder() - handler.GetApplicationByID(rr, req, 999) + handler.GetApplicationByID(rr, req) assert.Equal(t, http.StatusNotFound, rr.Code) } @@ -329,10 +338,10 @@ func TestUpdateApplicationStatus_Success(t *testing.T) { name := "John Doe" mockService := &mockApplicationService{ - updateApplicationStatusFunc: func(id int, req dto.UpdateApplicationStatusRequest) (*models.Application, error) { + updateApplicationStatusFunc: func(id string, req dto.UpdateApplicationStatusRequest) (*models.Application, error) { return &models.Application{ ID: id, - JobID: 1, + JobID: "1", Name: &name, Status: req.Status, CreatedAt: time.Now(), @@ -349,10 +358,11 @@ func TestUpdateApplicationStatus_Success(t *testing.T) { body, _ := json.Marshal(statusReq) req := httptest.NewRequest("PUT", "/applications/1/status", bytes.NewReader(body)) + req.SetPathValue("id", "1") req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() - handler.UpdateApplicationStatus(rr, req, 1) + handler.UpdateApplicationStatus(rr, req) assert.Equal(t, http.StatusOK, rr.Code) @@ -368,17 +378,18 @@ func TestUpdateApplicationStatus_InvalidJSON(t *testing.T) { handler := newTestableApplicationHandler(mockService) req := httptest.NewRequest("PUT", "/applications/1/status", bytes.NewReader([]byte("invalid"))) + req.SetPathValue("id", "1") req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() - handler.UpdateApplicationStatus(rr, req, 1) + handler.UpdateApplicationStatus(rr, req) assert.Equal(t, http.StatusBadRequest, rr.Code) } func TestUpdateApplicationStatus_Error(t *testing.T) { mockService := &mockApplicationService{ - updateApplicationStatusFunc: func(id int, req dto.UpdateApplicationStatusRequest) (*models.Application, error) { + updateApplicationStatusFunc: func(id string, req dto.UpdateApplicationStatusRequest) (*models.Application, error) { return nil, assert.AnError }, } @@ -389,10 +400,11 @@ func TestUpdateApplicationStatus_Error(t *testing.T) { body, _ := json.Marshal(statusReq) req := httptest.NewRequest("PUT", "/applications/1/status", bytes.NewReader(body)) + req.SetPathValue("id", "1") req.Header.Set("Content-Type", "application/json") rr := httptest.NewRecorder() - handler.UpdateApplicationStatus(rr, req, 1) + handler.UpdateApplicationStatus(rr, req) assert.Equal(t, http.StatusInternalServerError, rr.Code) } diff --git a/backend/internal/handlers/job_handler.go b/backend/internal/handlers/job_handler.go index 945aa49..69f6ca8 100755 --- a/backend/internal/handlers/job_handler.go +++ b/backend/internal/handlers/job_handler.go @@ -40,7 +40,7 @@ func NewJobHandler(service *services.JobService) *JobHandler { func (h *JobHandler) GetJobs(w http.ResponseWriter, r *http.Request) { page, _ := strconv.Atoi(r.URL.Query().Get("page")) limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) - companyID, _ := strconv.Atoi(r.URL.Query().Get("companyId")) + companyID := r.URL.Query().Get("companyId") isFeaturedStr := r.URL.Query().Get("featured") // Extraction of filters @@ -55,7 +55,7 @@ func (h *JobHandler) GetJobs(w http.ResponseWriter, r *http.Request) { Limit: limit, }, } - if companyID > 0 { + if companyID != "" { filter.CompanyID = &companyID } if isFeaturedStr == "true" { @@ -137,13 +137,7 @@ func (h *JobHandler) CreateJob(w http.ResponseWriter, r *http.Request) { // @Failure 404 {string} string "Not Found" // @Router /api/v1/jobs/{id} [get] func (h *JobHandler) GetJobByID(w http.ResponseWriter, r *http.Request) { - idStr := r.PathValue("id") // Go 1.22+ routing - - id, err := strconv.Atoi(idStr) - if err != nil { - http.Error(w, "Invalid job ID", http.StatusBadRequest) - return - } + id := r.PathValue("id") job, err := h.Service.GetJobByID(id) if err != nil { @@ -168,12 +162,7 @@ func (h *JobHandler) GetJobByID(w http.ResponseWriter, r *http.Request) { // @Failure 500 {string} string "Internal Server Error" // @Router /api/v1/jobs/{id} [put] func (h *JobHandler) UpdateJob(w http.ResponseWriter, r *http.Request) { - idStr := r.PathValue("id") - id, err := strconv.Atoi(idStr) - if err != nil { - http.Error(w, "Invalid job ID", http.StatusBadRequest) - return - } + id := r.PathValue("id") var req dto.UpdateJobRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -203,12 +192,7 @@ func (h *JobHandler) UpdateJob(w http.ResponseWriter, r *http.Request) { // @Failure 500 {string} string "Internal Server Error" // @Router /api/v1/jobs/{id} [delete] func (h *JobHandler) DeleteJob(w http.ResponseWriter, r *http.Request) { - idStr := r.PathValue("id") - id, err := strconv.Atoi(idStr) - if err != nil { - http.Error(w, "Invalid job ID", http.StatusBadRequest) - return - } + id := r.PathValue("id") if err := h.Service.DeleteJob(id); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/backend/internal/handlers/job_handler_test.go b/backend/internal/handlers/job_handler_test.go index afcdfe9..640d6d3 100644 --- a/backend/internal/handlers/job_handler_test.go +++ b/backend/internal/handlers/job_handler_test.go @@ -17,9 +17,9 @@ import ( type mockJobService struct { getJobsFunc func(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error) createJobFunc func(req dto.CreateJobRequest) (*models.Job, error) - getJobByIDFunc func(id int) (*models.Job, error) - updateJobFunc func(id int, req dto.UpdateJobRequest) (*models.Job, error) - deleteJobFunc func(id int) error + getJobByIDFunc func(id string) (*models.Job, error) + updateJobFunc func(id string, req dto.UpdateJobRequest) (*models.Job, error) + deleteJobFunc func(id string) error } func (m *mockJobService) GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error) { @@ -36,21 +36,21 @@ func (m *mockJobService) CreateJob(req dto.CreateJobRequest) (*models.Job, error return nil, nil } -func (m *mockJobService) GetJobByID(id int) (*models.Job, error) { +func (m *mockJobService) GetJobByID(id string) (*models.Job, error) { if m.getJobByIDFunc != nil { return m.getJobByIDFunc(id) } return nil, nil } -func (m *mockJobService) UpdateJob(id int, req dto.UpdateJobRequest) (*models.Job, error) { +func (m *mockJobService) UpdateJob(id string, req dto.UpdateJobRequest) (*models.Job, error) { if m.updateJobFunc != nil { return m.updateJobFunc(id, req) } return nil, nil } -func (m *mockJobService) DeleteJob(id int) error { +func (m *mockJobService) DeleteJob(id string) error { if m.deleteJobFunc != nil { return m.deleteJobFunc(id) } @@ -61,9 +61,9 @@ func (m *mockJobService) DeleteJob(id int) error { type JobServiceInterface interface { GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error) CreateJob(req dto.CreateJobRequest) (*models.Job, error) - GetJobByID(id int) (*models.Job, error) - UpdateJob(id int, req dto.UpdateJobRequest) (*models.Job, error) - DeleteJob(id int) error + GetJobByID(id string) (*models.Job, error) + UpdateJob(id string, req dto.UpdateJobRequest) (*models.Job, error) + DeleteJob(id string) error } // testableJobHandler wraps an interface for testing @@ -112,7 +112,8 @@ func (h *testableJobHandler) CreateJob(w http.ResponseWriter, r *http.Request) { } func (h *testableJobHandler) GetJobByID(w http.ResponseWriter, r *http.Request) { - job, err := h.service.GetJobByID(1) // simplified for testing + id := r.PathValue("id") + job, err := h.service.GetJobByID(id) if err != nil { http.Error(w, err.Error(), http.StatusNotFound) return @@ -123,7 +124,8 @@ func (h *testableJobHandler) GetJobByID(w http.ResponseWriter, r *http.Request) } func (h *testableJobHandler) DeleteJob(w http.ResponseWriter, r *http.Request) { - if err := h.service.DeleteJob(1); err != nil { + id := r.PathValue("id") + if err := h.service.DeleteJob(id); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -140,8 +142,8 @@ func TestGetJobs_Success(t *testing.T) { return []models.JobWithCompany{ { Job: models.Job{ - ID: 1, - CompanyID: 1, + ID: "1", + CompanyID: "1", Title: "Software Engineer", Status: "open", CreatedAt: time.Now(), @@ -151,8 +153,8 @@ func TestGetJobs_Success(t *testing.T) { }, { Job: models.Job{ - ID: 2, - CompanyID: 1, + ID: "2", + CompanyID: "1", Title: "DevOps Engineer", Status: "open", CreatedAt: time.Now(), @@ -219,7 +221,7 @@ func TestCreateJob_Success(t *testing.T) { mockService := &mockJobService{ createJobFunc: func(req dto.CreateJobRequest) (*models.Job, error) { return &models.Job{ - ID: 1, + ID: "1", CompanyID: req.CompanyID, Title: req.Title, Description: req.Description, @@ -233,7 +235,7 @@ func TestCreateJob_Success(t *testing.T) { handler := newTestableJobHandler(mockService) jobReq := dto.CreateJobRequest{ - CompanyID: 1, + CompanyID: "1", Title: "Backend Developer", Description: "Build awesome APIs", Status: "open", @@ -278,7 +280,7 @@ func TestCreateJob_ServiceError(t *testing.T) { handler := newTestableJobHandler(mockService) jobReq := dto.CreateJobRequest{ - CompanyID: 1, + CompanyID: "1", Title: "Backend Developer", Description: "Build awesome APIs", Status: "open", @@ -296,10 +298,10 @@ func TestCreateJob_ServiceError(t *testing.T) { func TestGetJobByID_Success(t *testing.T) { mockService := &mockJobService{ - getJobByIDFunc: func(id int) (*models.Job, error) { + getJobByIDFunc: func(id string) (*models.Job, error) { return &models.Job{ ID: id, - CompanyID: 1, + CompanyID: "1", Title: "Software Engineer", Description: "Great job opportunity", Status: "open", @@ -312,6 +314,7 @@ func TestGetJobByID_Success(t *testing.T) { handler := newTestableJobHandler(mockService) req := httptest.NewRequest("GET", "/jobs/1", nil) + req.SetPathValue("id", "1") rr := httptest.NewRecorder() handler.GetJobByID(rr, req) @@ -326,7 +329,7 @@ func TestGetJobByID_Success(t *testing.T) { func TestGetJobByID_NotFound(t *testing.T) { mockService := &mockJobService{ - getJobByIDFunc: func(id int) (*models.Job, error) { + getJobByIDFunc: func(id string) (*models.Job, error) { return nil, assert.AnError }, } @@ -334,6 +337,7 @@ func TestGetJobByID_NotFound(t *testing.T) { handler := newTestableJobHandler(mockService) req := httptest.NewRequest("GET", "/jobs/999", nil) + req.SetPathValue("id", "999") rr := httptest.NewRecorder() handler.GetJobByID(rr, req) @@ -343,7 +347,7 @@ func TestGetJobByID_NotFound(t *testing.T) { func TestDeleteJob_Success(t *testing.T) { mockService := &mockJobService{ - deleteJobFunc: func(id int) error { + deleteJobFunc: func(id string) error { return nil }, } @@ -351,6 +355,7 @@ func TestDeleteJob_Success(t *testing.T) { handler := newTestableJobHandler(mockService) req := httptest.NewRequest("DELETE", "/jobs/1", nil) + req.SetPathValue("id", "1") rr := httptest.NewRecorder() handler.DeleteJob(rr, req) @@ -360,7 +365,7 @@ func TestDeleteJob_Success(t *testing.T) { func TestDeleteJob_Error(t *testing.T) { mockService := &mockJobService{ - deleteJobFunc: func(id int) error { + deleteJobFunc: func(id string) error { return assert.AnError }, } @@ -368,6 +373,7 @@ func TestDeleteJob_Error(t *testing.T) { handler := newTestableJobHandler(mockService) req := httptest.NewRequest("DELETE", "/jobs/1", nil) + req.SetPathValue("id", "1") rr := httptest.NewRecorder() handler.DeleteJob(rr, req) diff --git a/backend/internal/models/application.go b/backend/internal/models/application.go index 6eb9b41..eec4456 100755 --- a/backend/internal/models/application.go +++ b/backend/internal/models/application.go @@ -4,9 +4,9 @@ import "time" // Application represents a job application (from user or guest) type Application struct { - ID int `json:"id" db:"id"` - JobID int `json:"jobId" db:"job_id"` - UserID *int `json:"userId,omitempty" db:"user_id"` // NULL for guest applications + ID string `json:"id" db:"id"` + JobID string `json:"jobId" db:"job_id"` + UserID *string `json:"userId,omitempty" db:"user_id"` // NULL for guest applications // Applicant Info (for guest applications) Name *string `json:"name,omitempty" db:"name"` @@ -33,7 +33,7 @@ type Application struct { type ApplicationWithDetails struct { Application JobTitle string `json:"jobTitle"` - CompanyID int `json:"companyId"` + CompanyID string `json:"companyId"` CompanyName string `json:"companyName"` ApplicantName string `json:"applicantName"` // From user or guest name ApplicantPhone *string `json:"applicantPhone,omitempty"` diff --git a/backend/internal/models/company.go b/backend/internal/models/company.go index 1fbb20e..55f317f 100755 --- a/backend/internal/models/company.go +++ b/backend/internal/models/company.go @@ -4,7 +4,7 @@ import "time" // Company represents an employer organization type Company struct { - ID int `json:"id" db:"id"` + ID string `json:"id" db:"id"` Name string `json:"name" db:"name"` Slug string `json:"slug" db:"slug"` Type string `json:"type" db:"type"` diff --git a/backend/internal/models/favorite_job.go b/backend/internal/models/favorite_job.go index c15b085..09de67b 100755 --- a/backend/internal/models/favorite_job.go +++ b/backend/internal/models/favorite_job.go @@ -4,9 +4,9 @@ import "time" // FavoriteJob represents a saved/favorited job by a user type FavoriteJob struct { - ID int `json:"id" db:"id"` - UserID int `json:"userId" db:"user_id"` - JobID int `json:"jobId" db:"job_id"` + ID string `json:"id" db:"id"` + UserID string `json:"userId" db:"user_id"` + JobID string `json:"jobId" db:"job_id"` CreatedAt time.Time `json:"createdAt" db:"created_at"` } @@ -14,7 +14,7 @@ type FavoriteJob struct { type FavoriteJobWithDetails struct { FavoriteJob JobTitle string `json:"jobTitle"` - CompanyID int `json:"companyId"` + CompanyID string `json:"companyId"` CompanyName string `json:"companyName"` CompanyLogoURL *string `json:"companyLogoUrl,omitempty"` Location *string `json:"location,omitempty"` diff --git a/backend/internal/models/job.go b/backend/internal/models/job.go index 132ded0..d904186 100755 --- a/backend/internal/models/job.go +++ b/backend/internal/models/job.go @@ -4,9 +4,9 @@ import "time" // Job represents a job posting type Job struct { - ID int `json:"id" db:"id"` - CompanyID int `json:"companyId" db:"company_id"` - CreatedBy int `json:"createdBy" db:"created_by"` + ID string `json:"id" db:"id"` + CompanyID string `json:"companyId" db:"company_id"` + CreatedBy string `json:"createdBy" db:"created_by"` // Job Details Title string `json:"title" db:"title"` diff --git a/backend/internal/models/user.go b/backend/internal/models/user.go index 8f14ade..01fdbc0 100755 --- a/backend/internal/models/user.go +++ b/backend/internal/models/user.go @@ -4,7 +4,7 @@ import "time" // User represents a system user (SuperAdmin, CompanyAdmin, Recruiter, or JobSeeker) type User struct { - ID int `json:"id" db:"id"` + ID string `json:"id" db:"id"` Identifier string `json:"identifier" db:"identifier"` PasswordHash string `json:"-" db:"password_hash"` // Never expose password hash in JSON Role string `json:"role" db:"role"` // superadmin, companyAdmin, recruiter, jobSeeker @@ -28,7 +28,7 @@ type User struct { // UserResponse is the public representation of a user (without sensitive data) type UserResponse struct { - ID int `json:"id"` + ID string `json:"id"` Identifier string `json:"identifier"` Role string `json:"role"` FullName string `json:"fullName"` diff --git a/backend/internal/models/user_company.go b/backend/internal/models/user_company.go index c013893..4ab1e87 100755 --- a/backend/internal/models/user_company.go +++ b/backend/internal/models/user_company.go @@ -8,9 +8,9 @@ import ( // UserCompany represents the N:M relationship between users and companies type UserCompany struct { - ID int `json:"id" db:"id"` - UserID int `json:"userId" db:"user_id"` - CompanyID int `json:"companyId" db:"company_id"` + ID string `json:"id" db:"id"` + UserID string `json:"userId" db:"user_id"` + CompanyID string `json:"companyId" db:"company_id"` Role string `json:"role" db:"role"` // companyAdmin, recruiter Permissions JSONMap `json:"permissions,omitempty" db:"permissions"` CreatedAt time.Time `json:"createdAt" db:"created_at"` diff --git a/backend/internal/services/admin_service.go b/backend/internal/services/admin_service.go index 570324a..52999f0 100644 --- a/backend/internal/services/admin_service.go +++ b/backend/internal/services/admin_service.go @@ -69,7 +69,7 @@ func (s *AdminService) ListCompanies(ctx context.Context, verified *bool) ([]mod return companies, nil } -func (s *AdminService) UpdateCompanyStatus(ctx context.Context, id int, active *bool, verified *bool) (*models.Company, error) { +func (s *AdminService) UpdateCompanyStatus(ctx context.Context, id string, active *bool, verified *bool) (*models.Company, error) { company, err := s.getCompanyByID(ctx, id) if err != nil { return nil, err @@ -96,7 +96,7 @@ func (s *AdminService) UpdateCompanyStatus(ctx context.Context, id int, active * return company, nil } -func (s *AdminService) DuplicateJob(ctx context.Context, id int) (*models.Job, error) { +func (s *AdminService) DuplicateJob(ctx context.Context, id string) (*models.Job, error) { query := ` SELECT company_id, created_by, title, description, salary_min, salary_max, salary_type, employment_type, work_mode, working_hours, location, region_id, city_id, @@ -446,7 +446,7 @@ func isActiveApplicationStatus(status string) bool { } } -func (s *AdminService) getCompanyByID(ctx context.Context, id int) (*models.Company, error) { +func (s *AdminService) getCompanyByID(ctx context.Context, id string) (*models.Company, error) { query := ` SELECT id, name, slug, type, document, address, region_id, city_id, phone, email, website, logo_url, description, active, verified, created_at, updated_at FROM companies WHERE id = $1 diff --git a/backend/internal/services/application_service.go b/backend/internal/services/application_service.go index 1366ddc..10b7b26 100644 --- a/backend/internal/services/application_service.go +++ b/backend/internal/services/application_service.go @@ -54,7 +54,7 @@ func (s *ApplicationService) CreateApplication(req dto.CreateApplicationRequest) return app, nil } -func (s *ApplicationService) GetApplications(jobID int) ([]models.Application, error) { +func (s *ApplicationService) GetApplications(jobID string) ([]models.Application, error) { // Simple get by Job ID query := ` SELECT id, job_id, user_id, name, phone, line_id, whatsapp, email, @@ -81,7 +81,7 @@ func (s *ApplicationService) GetApplications(jobID int) ([]models.Application, e return apps, nil } -func (s *ApplicationService) GetApplicationByID(id int) (*models.Application, error) { +func (s *ApplicationService) GetApplicationByID(id string) (*models.Application, error) { var a models.Application query := ` SELECT id, job_id, user_id, name, phone, line_id, whatsapp, email, @@ -98,7 +98,7 @@ func (s *ApplicationService) GetApplicationByID(id int) (*models.Application, er return &a, nil } -func (s *ApplicationService) UpdateApplicationStatus(id int, req dto.UpdateApplicationStatusRequest) (*models.Application, error) { +func (s *ApplicationService) UpdateApplicationStatus(id string, req dto.UpdateApplicationStatusRequest) (*models.Application, error) { query := ` UPDATE applications SET status = $1, updated_at = NOW() WHERE id = $2 diff --git a/backend/internal/services/job_service.go b/backend/internal/services/job_service.go index a888746..eb5f7e9 100644 --- a/backend/internal/services/job_service.go +++ b/backend/internal/services/job_service.go @@ -173,7 +173,7 @@ func (s *JobService) GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany return jobs, total, nil } -func (s *JobService) GetJobByID(id int) (*models.Job, error) { +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, @@ -192,7 +192,7 @@ func (s *JobService) GetJobByID(id int) (*models.Job, error) { return &j, nil } -func (s *JobService) UpdateJob(id int, req dto.UpdateJobRequest) (*models.Job, error) { +func (s *JobService) UpdateJob(id string, req dto.UpdateJobRequest) (*models.Job, error) { var setClauses []string var args []interface{} argId := 1 @@ -232,7 +232,7 @@ func (s *JobService) UpdateJob(id int, req dto.UpdateJobRequest) (*models.Job, e return s.GetJobByID(id) } -func (s *JobService) DeleteJob(id int) error { +func (s *JobService) DeleteJob(id string) error { _, err := s.DB.Exec("DELETE FROM jobs WHERE id = $1", id) return err } diff --git a/backend/internal/services/job_service_test.go b/backend/internal/services/job_service_test.go index 7caa352..64ffa9f 100644 --- a/backend/internal/services/job_service_test.go +++ b/backend/internal/services/job_service_test.go @@ -29,21 +29,21 @@ func TestCreateJob(t *testing.T) { { name: "Success", req: dto.CreateJobRequest{ - CompanyID: 1, + CompanyID: "1", Title: "Go Developer", Status: "published", }, mockRun: func() { mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO jobs`)). - WithArgs(1, "Go Developer", sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), "published", sqlmock.AnyArg(), sqlmock.AnyArg()). - WillReturnRows(sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).AddRow(100, time.Now(), time.Now())) + WithArgs("1", "Go Developer", sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), "published", sqlmock.AnyArg(), sqlmock.AnyArg()). + WillReturnRows(sqlmock.NewRows([]string{"id", "created_at", "updated_at"}).AddRow("100", time.Now(), time.Now())) }, wantErr: false, }, { name: "DB Error", req: dto.CreateJobRequest{ - CompanyID: 1, + CompanyID: "1", Title: "Go Developer", }, mockRun: func() { @@ -63,7 +63,7 @@ func TestCreateJob(t *testing.T) { return } if !tt.wantErr { - assert.Equal(t, 100, got.ID) + assert.Equal(t, "100", got.ID) } }) } @@ -99,10 +99,10 @@ func TestGetJobs(t *testing.T) { FROM jobs j`)). WillReturnRows(sqlmock.NewRows([]string{ "id", "company_id", "title", "description", "salary_min", "salary_max", "salary_type", - "employment_type", "location", "status", "is_featured", "created_at", "updated_at", + "employment_type", "work_mode", "location", "status", "is_featured", "created_at", "updated_at", "company_name", "company_logo_url", "region_name", "city_name", }).AddRow( - 1, 10, "Dev", "Desc", 100, 200, "m", "ft", "Remote", "open", false, time.Now(), time.Now(), + "1", "10", "Dev", "Desc", 100, 200, "m", "ft", "Remote", "Remote", "open", false, time.Now(), time.Now(), "Acme", "url", "Region", "City", ))