fix: resolve remaining merge conflicts

This commit is contained in:
Rede5 2026-02-14 17:21:10 +00:00
parent eb1276eac4
commit 948858eca0
8 changed files with 3292 additions and 3528 deletions

File diff suppressed because it is too large Load diff

View file

@ -1,364 +1,354 @@
package handlers_test package handlers_test
import ( import (
"bytes" "bytes"
"context" "context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"regexp" "regexp"
"testing" "testing"
"time" "time"
"github.com/DATA-DOG/go-sqlmock" "github.com/DATA-DOG/go-sqlmock"
"github.com/rede5/gohorsejobs/backend/internal/api/handlers" "github.com/rede5/gohorsejobs/backend/internal/api/handlers"
"github.com/rede5/gohorsejobs/backend/internal/api/middleware" "github.com/rede5/gohorsejobs/backend/internal/api/middleware"
"github.com/rede5/gohorsejobs/backend/internal/core/domain/entity" "github.com/rede5/gohorsejobs/backend/internal/core/domain/entity"
"github.com/rede5/gohorsejobs/backend/internal/core/dto" "github.com/rede5/gohorsejobs/backend/internal/core/dto"
auth "github.com/rede5/gohorsejobs/backend/internal/core/usecases/auth" auth "github.com/rede5/gohorsejobs/backend/internal/core/usecases/auth"
tenant "github.com/rede5/gohorsejobs/backend/internal/core/usecases/tenant" tenant "github.com/rede5/gohorsejobs/backend/internal/core/usecases/tenant"
user "github.com/rede5/gohorsejobs/backend/internal/core/usecases/user" user "github.com/rede5/gohorsejobs/backend/internal/core/usecases/user"
"github.com/rede5/gohorsejobs/backend/internal/services" "github.com/rede5/gohorsejobs/backend/internal/services"
) )
// --- Mock Implementations --- // --- Mock Implementations ---
type mockUserRepo struct { type mockUserRepo struct {
saveFunc func(user *entity.User) (*entity.User, error) saveFunc func(user *entity.User) (*entity.User, error)
findByEmailFunc func(email string) (*entity.User, error) findByEmailFunc func(email string) (*entity.User, error)
} }
func (m *mockUserRepo) Save(ctx context.Context, user *entity.User) (*entity.User, error) { func (m *mockUserRepo) Save(ctx context.Context, user *entity.User) (*entity.User, error) {
if m.saveFunc != nil { if m.saveFunc != nil {
return m.saveFunc(user) return m.saveFunc(user)
} }
return user, nil return user, nil
} }
func (m *mockUserRepo) FindByEmail(ctx context.Context, email string) (*entity.User, error) { func (m *mockUserRepo) FindByEmail(ctx context.Context, email string) (*entity.User, error) {
if m.findByEmailFunc != nil { if m.findByEmailFunc != nil {
return m.findByEmailFunc(email) return m.findByEmailFunc(email)
} }
return nil, nil return nil, nil
} }
func (m *mockUserRepo) FindByID(ctx context.Context, id string) (*entity.User, error) { func (m *mockUserRepo) FindByID(ctx context.Context, id string) (*entity.User, error) {
return nil, nil return nil, nil
} }
func (m *mockUserRepo) FindAllByTenant(ctx context.Context, tenantID string, l, o int) ([]*entity.User, int, error) { func (m *mockUserRepo) FindAllByTenant(ctx context.Context, tenantID string, l, o int) ([]*entity.User, int, error) {
return nil, 0, nil return nil, 0, nil
} }
func (m *mockUserRepo) Update(ctx context.Context, user *entity.User) (*entity.User, error) { func (m *mockUserRepo) Update(ctx context.Context, user *entity.User) (*entity.User, error) {
return nil, nil return nil, nil
} }
func (m *mockUserRepo) Delete(ctx context.Context, id string) error { return nil } func (m *mockUserRepo) Delete(ctx context.Context, id string) error { return nil }
func (m *mockUserRepo) LinkGuestApplications(ctx context.Context, email string, userID string) error { func (m *mockUserRepo) LinkGuestApplications(ctx context.Context, email string, userID string) error {
return nil return nil
} }
type mockAuthService struct{} type mockAuthService struct{}
func (m *mockAuthService) HashPassword(password string) (string, error) { func (m *mockAuthService) HashPassword(password string) (string, error) {
return "hashed_" + password, nil return "hashed_" + password, nil
} }
func (m *mockAuthService) GenerateToken(userID, tenantID string, roles []string) (string, error) { func (m *mockAuthService) GenerateToken(userID, tenantID string, roles []string) (string, error) {
return "mock_token", nil return "mock_token", nil
} }
func (m *mockAuthService) VerifyPassword(hash, password string) bool { return true } func (m *mockAuthService) VerifyPassword(hash, password string) bool { return true }
func (m *mockAuthService) ValidateToken(token string) (map[string]interface{}, error) { func (m *mockAuthService) ValidateToken(token string) (map[string]interface{}, error) {
return nil, nil return nil, nil
} }
// --- Test Cases --- // --- Test Cases ---
func TestRegisterCandidateHandler_Success(t *testing.T) { func TestRegisterCandidateHandler_Success(t *testing.T) {
t.Skip("Integration test requires full DI setup") t.Skip("Integration test requires full DI setup")
} }
func TestRegisterCandidateHandler_InvalidPayload(t *testing.T) { func TestRegisterCandidateHandler_InvalidPayload(t *testing.T) {
coreHandlers := createTestCoreHandlers(t, nil, nil) coreHandlers := createTestCoreHandlers(t, nil, nil)
body := bytes.NewBufferString("{invalid json}") body := bytes.NewBufferString("{invalid json}")
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", body) req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", body)
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
coreHandlers.RegisterCandidate(rec, req) coreHandlers.RegisterCandidate(rec, req)
if rec.Code != http.StatusBadRequest { if rec.Code != http.StatusBadRequest {
t.Errorf("Expected status %d, got %d", http.StatusBadRequest, rec.Code) t.Errorf("Expected status %d, got %d", http.StatusBadRequest, rec.Code)
} }
} }
func TestRegisterCandidateHandler_MissingFields(t *testing.T) { func TestRegisterCandidateHandler_MissingFields(t *testing.T) {
coreHandlers := createTestCoreHandlers(t, nil, nil) coreHandlers := createTestCoreHandlers(t, nil, nil)
testCases := []struct { testCases := []struct {
name string name string
payload dto.RegisterCandidateRequest payload dto.RegisterCandidateRequest
wantCode int wantCode int
}{ }{
{ {
name: "Missing Email", name: "Missing Email",
payload: dto.RegisterCandidateRequest{Name: "John", Password: "123456"}, payload: dto.RegisterCandidateRequest{Name: "John", Password: "123456"},
wantCode: http.StatusBadRequest, wantCode: http.StatusBadRequest,
}, },
{ {
name: "Missing Password", name: "Missing Password",
payload: dto.RegisterCandidateRequest{Name: "John", Email: "john@example.com"}, payload: dto.RegisterCandidateRequest{Name: "John", Email: "john@example.com"},
wantCode: http.StatusBadRequest, wantCode: http.StatusBadRequest,
}, },
{ {
name: "Missing Name", name: "Missing Name",
payload: dto.RegisterCandidateRequest{Email: "john@example.com", Password: "123456"}, payload: dto.RegisterCandidateRequest{Email: "john@example.com", Password: "123456"},
wantCode: http.StatusBadRequest, wantCode: http.StatusBadRequest,
}, },
{ {
name: "All Empty", name: "All Empty",
payload: dto.RegisterCandidateRequest{}, payload: dto.RegisterCandidateRequest{},
wantCode: http.StatusBadRequest, wantCode: http.StatusBadRequest,
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
body, _ := json.Marshal(tc.payload) body, _ := json.Marshal(tc.payload)
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", bytes.NewBuffer(body)) req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/register", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
coreHandlers.RegisterCandidate(rec, req) coreHandlers.RegisterCandidate(rec, req)
if rec.Code != tc.wantCode { if rec.Code != tc.wantCode {
t.Errorf("Expected status %d, got %d", tc.wantCode, rec.Code) t.Errorf("Expected status %d, got %d", tc.wantCode, rec.Code)
} }
}) })
} }
} }
func TestLoginHandler_InvalidPayload(t *testing.T) { func TestLoginHandler_InvalidPayload(t *testing.T) {
coreHandlers := createTestCoreHandlers(t, nil, nil) coreHandlers := createTestCoreHandlers(t, nil, nil)
body := bytes.NewBufferString("{invalid}") body := bytes.NewBufferString("{invalid}")
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", body) req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", body)
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
coreHandlers.Login(rec, req) coreHandlers.Login(rec, req)
if rec.Code != http.StatusBadRequest { if rec.Code != http.StatusBadRequest {
t.Errorf("Expected status %d, got %d", http.StatusBadRequest, rec.Code) t.Errorf("Expected status %d, got %d", http.StatusBadRequest, rec.Code)
} }
} }
func TestLoginHandler_Success(t *testing.T) { func TestLoginHandler_Success(t *testing.T) {
// Mocks // Mocks
mockRepo := &mockUserRepo{ mockRepo := &mockUserRepo{
findByEmailFunc: func(email string) (*entity.User, error) { findByEmailFunc: func(email string) (*entity.User, error) {
if email == "john@example.com" { if email == "john@example.com" {
// Return entity.User // Return entity.User
u := entity.NewUser("u1", "t1", "John", "john@example.com") u := entity.NewUser("u1", "t1", "John", "john@example.com")
u.PasswordHash = "hashed_123456" u.PasswordHash = "hashed_123456"
// Add Role if needed (mocked) return u, nil
// u.Roles = ... }
return u, nil return nil, nil // Not found
} },
return nil, nil // Not found }
}, mockAuth := &mockAuthService{}
}
mockAuth := &mockAuthService{} // Real UseCase with Mocks
loginUC := auth.NewLoginUseCase(mockRepo, mockAuth)
// Real UseCase with Mocks
loginUC := auth.NewLoginUseCase(mockRepo, mockAuth) coreHandlers := createTestCoreHandlers(t, nil, loginUC)
coreHandlers := createTestCoreHandlers(t, nil, loginUC) // Request
payload := dto.LoginRequest{Email: "john@example.com", Password: "123456"}
// Request body, _ := json.Marshal(payload)
payload := dto.LoginRequest{Email: "john@example.com", Password: "123456"} req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewBuffer(body))
body, _ := json.Marshal(payload) req.Header.Set("Content-Type", "application/json")
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/login", bytes.NewBuffer(body)) rec := httptest.NewRecorder()
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder() coreHandlers.Login(rec, req)
coreHandlers.Login(rec, req) // Assert Response Code
if rec.Code != http.StatusOK {
// Assert Response Code t.Errorf("Expected status %d, got %d. Body: %s", http.StatusOK, rec.Code, rec.Body.String())
if rec.Code != http.StatusOK { }
t.Errorf("Expected status %d, got %d. Body: %s", http.StatusOK, rec.Code, rec.Body.String())
} // Assert Cookie
cookies := rec.Result().Cookies()
// Assert Cookie var jwtCookie *http.Cookie
cookies := rec.Result().Cookies() for _, c := range cookies {
var jwtCookie *http.Cookie if c.Name == "jwt" {
for _, c := range cookies { jwtCookie = c
if c.Name == "jwt" { break
jwtCookie = c }
break }
}
} if jwtCookie == nil {
t.Fatal("Expected jwt cookie not found")
if jwtCookie == nil { }
t.Fatal("Expected jwt cookie not found") if !jwtCookie.HttpOnly {
} t.Error("Cookie should be HttpOnly")
if !jwtCookie.HttpOnly { }
t.Error("Cookie should be HttpOnly") if jwtCookie.Value != "mock_token" {
} t.Errorf("Expected cookie value 'mock_token', got '%s'", jwtCookie.Value)
if jwtCookie.Value != "mock_token" { }
t.Errorf("Expected cookie value 'mock_token', got '%s'", jwtCookie.Value) }
}
} // createTestCoreHandlers creates handlers with mocks and optional DB
func createTestCoreHandlers(t *testing.T, db *sql.DB, loginUC *auth.LoginUseCase) *handlers.CoreHandlers {
// createTestCoreHandlers creates handlers with mocks and optional DB t.Helper()
func createTestCoreHandlers(t *testing.T, db *sql.DB, loginUC *auth.LoginUseCase) *handlers.CoreHandlers {
t.Helper() // Init services if DB provided
var auditSvc *services.AuditService
// Init services if DB provided var notifSvc *services.NotificationService
var auditSvc *services.AuditService var ticketSvc *services.TicketService
var notifSvc *services.NotificationService var adminSvc *services.AdminService
var ticketSvc *services.TicketService var credSvc *services.CredentialsService
var adminSvc *services.AdminService
var credSvc *services.CredentialsService if db != nil {
auditSvc = services.NewAuditService(db)
if db != nil { notifSvc = services.NewNotificationService(db, nil)
auditSvc = services.NewAuditService(db) ticketSvc = services.NewTicketService(db)
notifSvc = services.NewNotificationService(db, nil) adminSvc = services.NewAdminService(db)
ticketSvc = services.NewTicketService(db) credSvc = services.NewCredentialsService(db)
adminSvc = services.NewAdminService(db) }
credSvc = services.NewCredentialsService(db)
} return handlers.NewCoreHandlers(
loginUC,
return handlers.NewCoreHandlers( (*auth.RegisterCandidateUseCase)(nil),
loginUC, (*tenant.CreateCompanyUseCase)(nil),
(*auth.RegisterCandidateUseCase)(nil), (*user.CreateUserUseCase)(nil),
(*tenant.CreateCompanyUseCase)(nil), (*user.ListUsersUseCase)(nil),
(*user.CreateUserUseCase)(nil), (*user.DeleteUserUseCase)(nil),
(*user.ListUsersUseCase)(nil), (*user.UpdateUserUseCase)(nil),
(*user.DeleteUserUseCase)(nil), (*user.UpdatePasswordUseCase)(nil),
(*user.UpdateUserUseCase)(nil), (*tenant.ListCompaniesUseCase)(nil),
(*user.UpdatePasswordUseCase)(nil), nil, // forgotPasswordUC
(*tenant.ListCompaniesUseCase)(nil), nil, // resetPasswordUC
<<<<<<< HEAD auditSvc,
nil, notifSvc,
nil, ticketSvc,
nil, adminSvc,
nil, credSvc,
nil, )
nil, }
nil,
======= func TestCoreHandlers_ListNotifications(t *testing.T) {
auditSvc, // Setup DB Mock
notifSvc, db, mock, err := sqlmock.New()
ticketSvc, if err != nil {
adminSvc, t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
credSvc, }
>>>>>>> dev defer db.Close()
)
} // Setup Handlers with DB
handlers := createTestCoreHandlers(t, db, nil)
func TestCoreHandlers_ListNotifications(t *testing.T) {
// Setup DB Mock // User ID
db, mock, err := sqlmock.New() userID := "user-123"
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) // Mock DB Query for ListNotifications
} mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, type, title, message, link, read_at, created_at, updated_at FROM notifications`)).
defer db.Close() WithArgs(userID).
WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "type", "title", "message", "link", "read_at", "created_at", "updated_at"}).
// Setup Handlers with DB AddRow("1", userID, "info", "Welcome", "Hello", nil, nil, time.Now(), time.Now()))
handlers := createTestCoreHandlers(t, db, nil)
// Request
// User ID req := httptest.NewRequest(http.MethodGet, "/api/v1/notifications", nil)
userID := "user-123"
// Inject Context
// Mock DB Query for ListNotifications ctx := context.WithValue(req.Context(), middleware.ContextUserID, userID)
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, type, title, message, link, read_at, created_at, updated_at FROM notifications`)). req = req.WithContext(ctx)
WithArgs(userID).
WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "type", "title", "message", "link", "read_at", "created_at", "updated_at"}). rec := httptest.NewRecorder()
AddRow("1", userID, "info", "Welcome", "Hello", nil, nil, time.Now(), time.Now()))
// Execute
// Request handlers.ListNotifications(rec, req)
req := httptest.NewRequest(http.MethodGet, "/api/v1/notifications", nil)
// Assert
// Inject Context if rec.Code != http.StatusOK {
ctx := context.WithValue(req.Context(), middleware.ContextUserID, userID) t.Errorf("Expected status %d, got %d", http.StatusOK, rec.Code)
req = req.WithContext(ctx) }
rec := httptest.NewRecorder() // Check Body (simple check)
if !bytes.Contains(rec.Body.Bytes(), []byte("Welcome")) {
// Execute t.Errorf("Expected body to contain 'Welcome'")
handlers.ListNotifications(rec, req) }
}
// Assert
if rec.Code != http.StatusOK { func TestCoreHandlers_Tickets(t *testing.T) {
t.Errorf("Expected status %d, got %d", http.StatusOK, rec.Code) userID := "user-123"
}
t.Run("CreateTicket", func(t *testing.T) {
// Check Body (simple check) db, mock, err := sqlmock.New()
if !bytes.Contains(rec.Body.Bytes(), []byte("Welcome")) { if err != nil {
t.Errorf("Expected body to contain 'Welcome'") t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
} }
} defer db.Close()
func TestCoreHandlers_Tickets(t *testing.T) { handlers := createTestCoreHandlers(t, db, nil)
userID := "user-123"
// Mock Insert: user_id, subject, priority
t.Run("CreateTicket", func(t *testing.T) { mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO tickets`)).
db, mock, err := sqlmock.New() WithArgs(userID, "Issue", "low").
if err != nil { WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subject", "status", "priority", "created_at", "updated_at"}).
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) AddRow("ticket-1", userID, "Issue", "open", "low", time.Now(), time.Now()))
}
defer db.Close() payload := map[string]string{
"subject": "Issue",
handlers := createTestCoreHandlers(t, db, nil) "message": "Help",
"priority": "low",
// Mock Insert: user_id, subject, priority }
mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO tickets`)). body, _ := json.Marshal(payload)
WithArgs(userID, "Issue", "low"). req := httptest.NewRequest(http.MethodPost, "/api/v1/support/tickets", bytes.NewBuffer(body))
WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subject", "status", "priority", "created_at", "updated_at"}). req.Header.Set("Content-Type", "application/json")
AddRow("ticket-1", userID, "Issue", "open", "low", time.Now(), time.Now())) ctx := context.WithValue(req.Context(), middleware.ContextUserID, userID)
rec := httptest.NewRecorder()
payload := map[string]string{
"subject": "Issue", handlers.CreateTicket(rec, req.WithContext(ctx))
"message": "Help",
"priority": "low", if rec.Code != http.StatusCreated {
} t.Errorf("CreateTicket status = %d, want %d. Body: %s", rec.Code, http.StatusCreated, rec.Body.String())
body, _ := json.Marshal(payload) }
req := httptest.NewRequest(http.MethodPost, "/api/v1/support/tickets", bytes.NewBuffer(body)) })
req.Header.Set("Content-Type", "application/json")
ctx := context.WithValue(req.Context(), middleware.ContextUserID, userID) t.Run("ListTickets", func(t *testing.T) {
rec := httptest.NewRecorder() db, mock, err := sqlmock.New()
if err != nil {
handlers.CreateTicket(rec, req.WithContext(ctx)) t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
if rec.Code != http.StatusCreated { defer db.Close()
t.Errorf("CreateTicket status = %d, want %d. Body: %s", rec.Code, http.StatusCreated, rec.Body.String())
} handlers := createTestCoreHandlers(t, db, nil)
})
// Mock Select
t.Run("ListTickets", func(t *testing.T) { mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, subject, status, priority, created_at, updated_at FROM tickets`)).
db, mock, err := sqlmock.New() WithArgs(userID).
if err != nil { WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subject", "status", "priority", "created_at", "updated_at"}).
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) AddRow("ticket-1", userID, "Issue", "open", "low", time.Now(), time.Now()))
}
defer db.Close() req := httptest.NewRequest(http.MethodGet, "/api/v1/support/tickets", nil)
ctx := context.WithValue(req.Context(), middleware.ContextUserID, userID)
handlers := createTestCoreHandlers(t, db, nil) rec := httptest.NewRecorder()
// Mock Select handlers.ListTickets(rec, req.WithContext(ctx))
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, subject, status, priority, created_at, updated_at FROM tickets`)).
WithArgs(userID). if rec.Code != http.StatusOK {
WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subject", "status", "priority", "created_at", "updated_at"}). t.Errorf("ListTickets status = %d, want %d. Body: %s", rec.Code, http.StatusOK, rec.Body.String())
AddRow("ticket-1", userID, "Issue", "open", "low", time.Now(), time.Now())) }
})
req := httptest.NewRequest(http.MethodGet, "/api/v1/support/tickets", nil) }
ctx := context.WithValue(req.Context(), middleware.ContextUserID, userID)
rec := httptest.NewRecorder()
handlers.ListTickets(rec, req.WithContext(ctx))
if rec.Code != http.StatusOK {
t.Errorf("ListTickets status = %d, want %d. Body: %s", rec.Code, http.StatusOK, rec.Body.String())
}
})
}

View file

@ -1,102 +1,82 @@
package entity package entity
import "time" import "time"
// Role type alias // Role type alias
type RoleString string type RoleString string
const ( const (
// RoleSuperAdmin is the platform administrator // RoleSuperAdmin is the platform administrator
RoleSuperAdmin = "superadmin" RoleSuperAdmin = "superadmin"
// RoleAdmin is the company administrator (formerly admin) // RoleAdmin is the company administrator (formerly admin)
RoleAdmin = "admin" RoleAdmin = "admin"
// RoleRecruiter is a recruiter within a company // RoleRecruiter is a recruiter within a company
RoleRecruiter = "recruiter" RoleRecruiter = "recruiter"
// RoleCandidate is a job seeker (formerly candidate) // RoleCandidate is a job seeker (formerly candidate)
RoleCandidate = "candidate" RoleCandidate = "candidate"
// User Status // User Status
UserStatusActive = "active" UserStatusActive = "active"
UserStatusInactive = "inactive" UserStatusInactive = "inactive"
UserStatusForceChangePassword = "force_change_password" UserStatusForceChangePassword = "force_change_password"
) )
// User represents a user within a specific Tenant (Company). // User represents a user within a specific Tenant (Company).
type User struct { type User struct {
<<<<<<< HEAD ID string `json:"id"`
ID string `json:"id"` TenantID string `json:"tenant_id"` // Link to Company
TenantID string `json:"tenant_id"` // Link to Company Name string `json:"name"`
Name string `json:"name"` Email string `json:"email"`
Email string `json:"email"` Phone *string `json:"phone,omitempty"`
PasswordHash string `json:"-"` Bio *string `json:"bio,omitempty"`
Roles []Role `json:"roles"` Address *string `json:"address,omitempty"`
Status string `json:"status"` // "ACTIVE", "INACTIVE" City *string `json:"city,omitempty"`
CreatedAt time.Time `json:"created_at"` State *string `json:"state,omitempty"`
UpdatedAt time.Time `json:"updated_at"` ZipCode *string `json:"zip_code,omitempty"`
BirthDate *time.Time `json:"birth_date,omitempty"`
// HML Fields Education *string `json:"education,omitempty"`
AvatarUrl string `json:"avatar_url"` Experience *string `json:"experience,omitempty"`
Metadata map[string]interface{} `json:"metadata"` Skills []string `json:"skills,omitempty"`
Objective *string `json:"objective,omitempty"`
// HEAD Fields (Profile Profile) Title *string `json:"title,omitempty"`
Bio string `json:"bio"` PasswordHash string `json:"-"`
ProfilePictureURL string `json:"profile_picture_url"` AvatarUrl string `json:"avatar_url"`
Skills []string `json:"skills"` // Stored as JSONB, mapped to slice Roles []Role `json:"roles"`
Experience []any `json:"experience,omitempty"` // Flexible JSON structure Status string `json:"status"` // "ACTIVE", "INACTIVE"
Education []any `json:"education,omitempty"` // Flexible JSON structure Metadata map[string]interface{} `json:"metadata"`
======= CreatedAt time.Time `json:"created_at"`
ID string `json:"id"` UpdatedAt time.Time `json:"updated_at"`
TenantID string `json:"tenant_id"` // Link to Company
Name string `json:"name"` // HEAD Fields (Profile)
Email string `json:"email"` ProfilePictureURL string `json:"profile_picture_url,omitempty"`
Phone *string `json:"phone,omitempty"` }
Bio *string `json:"bio,omitempty"`
Address *string `json:"address,omitempty"` // NewUser creates a new User instance.
City *string `json:"city,omitempty"` func NewUser(id, tenantID, name, email string) *User {
State *string `json:"state,omitempty"` return &User{
ZipCode *string `json:"zip_code,omitempty"` ID: id,
BirthDate *time.Time `json:"birth_date,omitempty"` TenantID: tenantID,
Education *string `json:"education,omitempty"` Name: name,
Experience *string `json:"experience,omitempty"` Email: email,
Skills []string `json:"skills,omitempty"` Status: UserStatusActive,
Objective *string `json:"objective,omitempty"` Roles: []Role{},
Title *string `json:"title,omitempty"` CreatedAt: time.Now(),
PasswordHash string `json:"-"` UpdatedAt: time.Now(),
AvatarUrl string `json:"avatar_url"` }
Roles []Role `json:"roles"` }
Status string `json:"status"` // "ACTIVE", "INACTIVE"
Metadata map[string]interface{} `json:"metadata"` func (u *User) AssignRole(role Role) {
CreatedAt time.Time `json:"created_at"` u.Roles = append(u.Roles, role)
UpdatedAt time.Time `json:"updated_at"` u.UpdatedAt = time.Now()
>>>>>>> dev }
}
func (u *User) HasPermission(permissionCode string) bool {
// NewUser creates a new User instance. for _, role := range u.Roles {
func NewUser(id, tenantID, name, email string) *User { for _, perm := range role.Permissions {
return &User{ if perm.Code == permissionCode {
ID: id, return true
TenantID: tenantID, }
Name: name, }
Email: email, }
Status: UserStatusActive, return false
Roles: []Role{}, }
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
}
func (u *User) AssignRole(role Role) {
u.Roles = append(u.Roles, role)
u.UpdatedAt = time.Now()
}
func (u *User) HasPermission(permissionCode string) bool {
for _, role := range u.Roles {
for _, perm := range role.Permissions {
if perm.Code == permissionCode {
return true
}
}
}
return false
}

View file

@ -1,91 +1,85 @@
package dto package dto
import "time" import "time"
type LoginRequest struct { type LoginRequest struct {
Email string `json:"email"` Email string `json:"email"`
Password string `json:"password"` Password string `json:"password"`
} }
type AuthResponse struct { type AuthResponse struct {
Token string `json:"token"` Token string `json:"token"`
User UserResponse `json:"user"` User UserResponse `json:"user"`
MustChangePassword bool `json:"mustChangePassword"` MustChangePassword bool `json:"mustChangePassword"`
} }
type CreateUserRequest struct { type CreateUserRequest struct {
Name string `json:"name"` Name string `json:"name"`
Email string `json:"email"` Email string `json:"email"`
Password string `json:"password"` Password string `json:"password"`
Roles []string `json:"roles"` Roles []string `json:"roles"`
Status *string `json:"status,omitempty"` Status *string `json:"status,omitempty"`
TenantID *string `json:"companyId,omitempty"` // Optional, mainly for superads to assign user to company TenantID *string `json:"companyId,omitempty"` // Optional, mainly for superads to assign user to company
} }
type UpdateUserRequest struct { type UpdateUserRequest struct {
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Email *string `json:"email,omitempty"` Email *string `json:"email,omitempty"`
Phone *string `json:"phone,omitempty"` Phone *string `json:"phone,omitempty"`
Bio *string `json:"bio,omitempty"` Active *bool `json:"active,omitempty"`
Active *bool `json:"active,omitempty"` Status *string `json:"status,omitempty"`
Status *string `json:"status,omitempty"` Roles *[]string `json:"roles,omitempty"`
Roles *[]string `json:"roles,omitempty"` AvatarUrl *string `json:"avatarUrl,omitempty"`
AvatarUrl *string `json:"avatarUrl,omitempty"`
// Profile Fields
// HEAD Fields Bio *string `json:"bio,omitempty"`
Bio *string `json:"bio,omitempty"` ProfilePictureURL *string `json:"profilePictureUrl,omitempty"`
ProfilePictureURL *string `json:"profilePictureUrl,omitempty"` Skills []string `json:"skills,omitempty"`
Skills []string `json:"skills,omitempty"` Experience []any `json:"experience,omitempty"`
Experience []any `json:"experience,omitempty"` Education []any `json:"education,omitempty"`
Education []any `json:"education,omitempty"` }
}
type UserResponse struct {
type UserResponse struct { ID string `json:"id"`
ID string `json:"id"` Name string `json:"name"`
Name string `json:"name"` Email string `json:"email"`
Email string `json:"email"` Roles []string `json:"roles"`
Roles []string `json:"roles"` Status string `json:"status"`
Status string `json:"status"` AvatarUrl string `json:"avatar_url,omitempty"`
<<<<<<< HEAD Phone *string `json:"phone,omitempty"`
======= Bio *string `json:"bio,omitempty"`
AvatarUrl string `json:"avatar_url"` CreatedAt time.Time `json:"created_at"`
Phone *string `json:"phone,omitempty"`
Bio *string `json:"bio,omitempty"` // Profile Fields
>>>>>>> dev ProfilePictureURL string `json:"profile_picture_url,omitempty"`
CreatedAt time.Time `json:"created_at"` Skills []string `json:"skills,omitempty"`
Experience []any `json:"experience,omitempty"`
// Merged Fields Education []any `json:"education,omitempty"`
AvatarUrl string `json:"avatar_url,omitempty"` // hml }
Bio string `json:"bio,omitempty"` // HEAD
ProfilePictureURL string `json:"profile_picture_url,omitempty"` // HEAD type UpdatePasswordRequest struct {
Skills []string `json:"skills,omitempty"` // HEAD CurrentPassword string `json:"currentPassword"`
Experience []any `json:"experience,omitempty"` // HEAD NewPassword string `json:"newPassword"`
Education []any `json:"education,omitempty"` // HEAD }
}
type RegisterCandidateRequest struct {
type UpdatePasswordRequest struct { Name string `json:"name"`
CurrentPassword string `json:"currentPassword"` Email string `json:"email"`
NewPassword string `json:"newPassword"` Password string `json:"password"`
} Username string `json:"username"`
Phone string `json:"phone"`
type RegisterCandidateRequest struct { Address string `json:"address,omitempty"`
Name string `json:"name"` City string `json:"city,omitempty"`
Email string `json:"email"` State string `json:"state,omitempty"`
Password string `json:"password"` ZipCode string `json:"zipCode,omitempty"`
Username string `json:"username"` BirthDate string `json:"birthDate,omitempty"`
Phone string `json:"phone"` Education string `json:"education,omitempty"`
Address string `json:"address,omitempty"` Experience string `json:"experience,omitempty"`
City string `json:"city,omitempty"` Skills string `json:"skills,omitempty"`
State string `json:"state,omitempty"` Objective string `json:"objective,omitempty"`
ZipCode string `json:"zipCode,omitempty"` }
BirthDate string `json:"birthDate,omitempty"`
Education string `json:"education,omitempty"` type SaveFCMTokenRequest struct {
Experience string `json:"experience,omitempty"` Token string `json:"token"`
Skills string `json:"skills,omitempty"` Platform string `json:"platform"`
Objective string `json:"objective,omitempty"` }
}
type SaveFCMTokenRequest struct {
Token string `json:"token"`
Platform string `json:"platform"`
}

View file

@ -1,277 +1,270 @@
package handlers package handlers
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
"github.com/rede5/gohorsejobs/backend/internal/api/middleware" "github.com/rede5/gohorsejobs/backend/internal/api/middleware"
"github.com/rede5/gohorsejobs/backend/internal/dto" "github.com/rede5/gohorsejobs/backend/internal/dto"
"github.com/rede5/gohorsejobs/backend/internal/models" "github.com/rede5/gohorsejobs/backend/internal/models"
) )
// JobServiceInterface describes the service needed by JobHandler // JobServiceInterface describes the service needed by JobHandler
type JobServiceInterface interface { type JobServiceInterface interface {
GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error) GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error)
CreateJob(req dto.CreateJobRequest, createdBy string) (*models.Job, error) CreateJob(req dto.CreateJobRequest, createdBy string) (*models.Job, error)
GetJobByID(id string) (*models.Job, error) GetJobByID(id string) (*models.Job, error)
UpdateJob(id string, req dto.UpdateJobRequest) (*models.Job, error) UpdateJob(id string, req dto.UpdateJobRequest) (*models.Job, error)
DeleteJob(id string) error DeleteJob(id string) error
} }
type JobHandler struct { type JobHandler struct {
Service JobServiceInterface Service JobServiceInterface
} }
func NewJobHandler(service JobServiceInterface) *JobHandler { func NewJobHandler(service JobServiceInterface) *JobHandler {
return &JobHandler{Service: service} return &JobHandler{Service: service}
} }
// GetJobs godoc // GetJobs godoc
// @Summary List all jobs // @Summary List all jobs
// @Description Get a paginated list of job postings with optional filters // @Description Get a paginated list of job postings with optional filters
// @Tags Jobs // @Tags Jobs
// @Accept json // @Accept json
// @Produce json // @Produce json
<<<<<<< HEAD // @Param page query int false "Page number (default: 1)"
// @Param page query int false "Page number (default: 1)" // @Param limit query int false "Items per page (default: 10, max: 100)"
// @Param limit query int false "Items per page (default: 10, max: 100)" // @Param companyId query string false "Filter by company ID"
// @Param companyId query int false "Filter by company ID" // @Param featured query bool false "Filter by featured status"
// @Param featured query bool false "Filter by featured status" // @Param search query string false "Full-text search query"
// @Param search query string false "Full-text search query" // @Param employmentType query string false "Filter by employment type"
// @Param employmentType query string false "Filter by employment type" // @Param workMode query string false "Filter by work mode (onsite, hybrid, remote)"
// @Param workMode query string false "Filter by work mode (onsite, hybrid, remote)" // @Param location query string false "Filter by location text"
// @Param location query string false "Filter by location text" // @Param salaryMin query number false "Minimum salary filter"
// @Param salaryMin query number false "Minimum salary filter" // @Param salaryMax query number false "Maximum salary filter"
// @Param salaryMax query number false "Maximum salary filter" // @Param sortBy query string false "Sort by: date, salary, relevance"
// @Param sortBy query string false "Sort by: date, salary, relevance" // @Param sortOrder query string false "Sort order: asc, desc"
// @Param sortOrder query string false "Sort order: asc, desc" // @Success 200 {object} dto.PaginatedResponse
======= // @Failure 500 {string} string "Internal Server Error"
// @Param page query int false "Page number (default: 1)" // @Router /api/v1/jobs [get]
// @Param limit query int false "Items per page (default: 10, max: 100)" func (h *JobHandler) GetJobs(w http.ResponseWriter, r *http.Request) {
// @Param companyId query string false "Filter by company ID" page, _ := strconv.Atoi(r.URL.Query().Get("page"))
// @Param featured query bool false "Filter by featured status" limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
>>>>>>> dev companyID := r.URL.Query().Get("companyId")
// @Success 200 {object} dto.PaginatedResponse isFeaturedStr := r.URL.Query().Get("featured")
// @Failure 500 {string} string "Internal Server Error"
// @Router /api/v1/jobs [get] // Legacy and New Filter Handling
func (h *JobHandler) GetJobs(w http.ResponseWriter, r *http.Request) { search := r.URL.Query().Get("search")
page, _ := strconv.Atoi(r.URL.Query().Get("page")) if search == "" {
limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) search = r.URL.Query().Get("q")
companyID := r.URL.Query().Get("companyId") }
isFeaturedStr := r.URL.Query().Get("featured")
employmentType := r.URL.Query().Get("employmentType")
// Legacy and New Filter Handling if employmentType == "" {
search := r.URL.Query().Get("search") employmentType = r.URL.Query().Get("type")
if search == "" { }
search = r.URL.Query().Get("q")
} workMode := r.URL.Query().Get("workMode")
location := r.URL.Query().Get("location")
employmentType := r.URL.Query().Get("employmentType") salaryMinStr := r.URL.Query().Get("salaryMin")
if employmentType == "" { salaryMaxStr := r.URL.Query().Get("salaryMax")
employmentType = r.URL.Query().Get("type") sortBy := r.URL.Query().Get("sortBy")
} sortOrder := r.URL.Query().Get("sortOrder")
workMode := r.URL.Query().Get("workMode") filter := dto.JobFilterQuery{
location := r.URL.Query().Get("location") PaginationQuery: dto.PaginationQuery{
salaryMinStr := r.URL.Query().Get("salaryMin") Page: page,
salaryMaxStr := r.URL.Query().Get("salaryMax") Limit: limit,
sortBy := r.URL.Query().Get("sortBy") },
sortOrder := r.URL.Query().Get("sortOrder") SortBy: &sortBy,
SortOrder: &sortOrder,
filter := dto.JobFilterQuery{ }
PaginationQuery: dto.PaginationQuery{
Page: page, if companyID != "" {
Limit: limit, filter.CompanyID = &companyID
}, }
SortBy: &sortBy, if isFeaturedStr == "true" {
SortOrder: &sortOrder, val := true
} filter.IsFeatured = &val
}
if companyID != "" { if search != "" {
filter.CompanyID = &companyID filter.Search = &search
} }
if isFeaturedStr == "true" { if employmentType != "" {
val := true filter.EmploymentType = &employmentType
filter.IsFeatured = &val }
} if workMode != "" {
if search != "" { filter.WorkMode = &workMode
filter.Search = &search }
} if location != "" {
if employmentType != "" { filter.Location = &location
filter.EmploymentType = &employmentType filter.LocationSearch = &location // Map to both for compatibility
} }
if workMode != "" { if salaryMinStr != "" {
filter.WorkMode = &workMode if val, err := strconv.ParseFloat(salaryMinStr, 64); err == nil {
} filter.SalaryMin = &val
if location != "" { }
filter.Location = &location }
filter.LocationSearch = &location // Map to both for compatibility if salaryMaxStr != "" {
} if val, err := strconv.ParseFloat(salaryMaxStr, 64); err == nil {
if salaryMinStr != "" { filter.SalaryMax = &val
if val, err := strconv.ParseFloat(salaryMinStr, 64); err == nil { }
filter.SalaryMin = &val }
}
} jobs, total, err := h.Service.GetJobs(filter)
if salaryMaxStr != "" { if err != nil {
if val, err := strconv.ParseFloat(salaryMaxStr, 64); err == nil { http.Error(w, err.Error(), http.StatusInternalServerError)
filter.SalaryMax = &val return
} }
}
if page == 0 {
jobs, total, err := h.Service.GetJobs(filter) page = 1
if err != nil { }
http.Error(w, err.Error(), http.StatusInternalServerError) if limit == 0 {
return limit = 10
} }
if page == 0 { response := dto.PaginatedResponse{
page = 1 Data: jobs,
} Pagination: dto.Pagination{
if limit == 0 { Page: page,
limit = 10 Limit: limit,
} Total: total,
},
response := dto.PaginatedResponse{ }
Data: jobs,
Pagination: dto.Pagination{ w.Header().Set("Content-Type", "application/json")
Page: page, json.NewEncoder(w).Encode(response)
Limit: limit, }
Total: total,
}, // CreateJob godoc
} // @Summary Create a new job
// @Description Create a new job posting
w.Header().Set("Content-Type", "application/json") // @Tags Jobs
json.NewEncoder(w).Encode(response) // @Accept json
} // @Produce json
// @Param job body dto.CreateJobRequest true "Job data"
// CreateJob godoc // @Success 201 {object} models.Job
// @Summary Create a new job // @Failure 400 {string} string "Bad Request"
// @Description Create a new job posting // @Failure 401 {string} string "Unauthorized"
// @Tags Jobs // @Failure 500 {string} string "Internal Server Error"
// @Accept json // @Router /api/v1/jobs [post]
// @Produce json func (h *JobHandler) CreateJob(w http.ResponseWriter, r *http.Request) {
// @Param job body dto.CreateJobRequest true "Job data" fmt.Println("[CREATE_JOB DEBUG] === CreateJob Handler Started ===")
// @Success 201 {object} models.Job
// @Failure 400 {string} string "Bad Request" var req dto.CreateJobRequest
// @Failure 401 {string} string "Unauthorized" if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
// @Failure 500 {string} string "Internal Server Error" fmt.Printf("[CREATE_JOB ERROR] Failed to decode request body: %v\n", err)
// @Router /api/v1/jobs [post] http.Error(w, err.Error(), http.StatusBadRequest)
func (h *JobHandler) CreateJob(w http.ResponseWriter, r *http.Request) { return
fmt.Println("[CREATE_JOB DEBUG] === CreateJob Handler Started ===") }
var req dto.CreateJobRequest fmt.Printf("[CREATE_JOB DEBUG] Request received: title=%s, companyId=%s, status=%s\n", req.Title, req.CompanyID, req.Status)
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { fmt.Printf("[CREATE_JOB DEBUG] Full request: %+v\n", req)
fmt.Printf("[CREATE_JOB ERROR] Failed to decode request body: %v\n", err)
http.Error(w, err.Error(), http.StatusBadRequest) // Extract UserID from context
return val := r.Context().Value(middleware.ContextUserID)
} fmt.Printf("[CREATE_JOB DEBUG] Context UserID value: %v (type: %T)\n", val, val)
fmt.Printf("[CREATE_JOB DEBUG] Request received: title=%s, companyId=%s, status=%s\n", req.Title, req.CompanyID, req.Status) userID, ok := val.(string)
fmt.Printf("[CREATE_JOB DEBUG] Full request: %+v\n", req) if !ok || userID == "" {
fmt.Printf("[CREATE_JOB ERROR] UserID extraction failed. ok=%v, userID='%s'\n", ok, userID)
// Extract UserID from context http.Error(w, "Unauthorized: User ID missing", http.StatusUnauthorized)
val := r.Context().Value(middleware.ContextUserID) return
fmt.Printf("[CREATE_JOB DEBUG] Context UserID value: %v (type: %T)\n", val, val) }
userID, ok := val.(string) fmt.Printf("[CREATE_JOB DEBUG] UserID extracted: %s\n", userID)
if !ok || userID == "" { fmt.Println("[CREATE_JOB DEBUG] Calling service.CreateJob...")
fmt.Printf("[CREATE_JOB ERROR] UserID extraction failed. ok=%v, userID='%s'\n", ok, userID)
http.Error(w, "Unauthorized: User ID missing", http.StatusUnauthorized) job, err := h.Service.CreateJob(req, userID)
return if err != nil {
} fmt.Printf("[CREATE_JOB ERROR] Service.CreateJob failed: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
fmt.Printf("[CREATE_JOB DEBUG] UserID extracted: %s\n", userID) return
fmt.Println("[CREATE_JOB DEBUG] Calling service.CreateJob...") }
job, err := h.Service.CreateJob(req, userID) fmt.Printf("[CREATE_JOB DEBUG] Job created successfully! ID=%s\n", job.ID)
if err != nil {
fmt.Printf("[CREATE_JOB ERROR] Service.CreateJob failed: %v\n", err) w.Header().Set("Content-Type", "application/json")
http.Error(w, err.Error(), http.StatusInternalServerError) w.WriteHeader(http.StatusCreated)
return json.NewEncoder(w).Encode(job)
} }
fmt.Printf("[CREATE_JOB DEBUG] Job created successfully! ID=%s\n", job.ID) // GetJobByID godoc
// @Summary Get job by ID
w.Header().Set("Content-Type", "application/json") // @Description Get a single job posting by its ID
w.WriteHeader(http.StatusCreated) // @Tags Jobs
json.NewEncoder(w).Encode(job) // @Accept json
} // @Produce json
// @Param id path string true "Job ID"
// GetJobByID godoc // @Success 200 {object} models.Job
// @Summary Get job by ID // @Failure 400 {string} string "Bad Request"
// @Description Get a single job posting by its ID // @Failure 404 {string} string "Not Found"
// @Tags Jobs // @Router /api/v1/jobs/{id} [get]
// @Accept json func (h *JobHandler) GetJobByID(w http.ResponseWriter, r *http.Request) {
// @Produce json id := r.PathValue("id")
// @Param id path string true "Job ID"
// @Success 200 {object} models.Job job, err := h.Service.GetJobByID(id)
// @Failure 400 {string} string "Bad Request" if err != nil {
// @Failure 404 {string} string "Not Found" http.Error(w, err.Error(), http.StatusNotFound)
// @Router /api/v1/jobs/{id} [get] return
func (h *JobHandler) GetJobByID(w http.ResponseWriter, r *http.Request) { }
id := r.PathValue("id")
w.Header().Set("Content-Type", "application/json")
job, err := h.Service.GetJobByID(id) json.NewEncoder(w).Encode(job)
if err != nil { }
http.Error(w, err.Error(), http.StatusNotFound)
return // UpdateJob godoc
} // @Summary Update a job
// @Description Update an existing job posting
w.Header().Set("Content-Type", "application/json") // @Tags Jobs
json.NewEncoder(w).Encode(job) // @Accept json
} // @Produce json
// @Param id path string true "Job ID"
// UpdateJob godoc // @Param job body dto.UpdateJobRequest true "Updated job data"
// @Summary Update a job // @Success 200 {object} models.Job
// @Description Update an existing job posting // @Failure 400 {string} string "Bad Request"
// @Tags Jobs // @Failure 500 {string} string "Internal Server Error"
// @Accept json // @Router /api/v1/jobs/{id} [put]
// @Produce json func (h *JobHandler) UpdateJob(w http.ResponseWriter, r *http.Request) {
// @Param id path string true "Job ID" id := r.PathValue("id")
// @Param job body dto.UpdateJobRequest true "Updated job data"
// @Success 200 {object} models.Job var req dto.UpdateJobRequest
// @Failure 400 {string} string "Bad Request" if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
// @Failure 500 {string} string "Internal Server Error" http.Error(w, err.Error(), http.StatusBadRequest)
// @Router /api/v1/jobs/{id} [put] return
func (h *JobHandler) UpdateJob(w http.ResponseWriter, r *http.Request) { }
id := r.PathValue("id")
job, err := h.Service.UpdateJob(id, req)
var req dto.UpdateJobRequest if err != nil {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusBadRequest) return
return }
}
w.Header().Set("Content-Type", "application/json")
job, err := h.Service.UpdateJob(id, req) json.NewEncoder(w).Encode(job)
if err != nil { }
http.Error(w, err.Error(), http.StatusInternalServerError)
return // DeleteJob godoc
} // @Summary Delete a job
// @Description Delete a job posting
w.Header().Set("Content-Type", "application/json") // @Tags Jobs
json.NewEncoder(w).Encode(job) // @Accept json
} // @Produce json
// @Param id path string true "Job ID"
// DeleteJob godoc // @Success 204 "No Content"
// @Summary Delete a job // @Failure 400 {string} string "Bad Request"
// @Description Delete a job posting // @Failure 500 {string} string "Internal Server Error"
// @Tags Jobs // @Router /api/v1/jobs/{id} [delete]
// @Accept json func (h *JobHandler) DeleteJob(w http.ResponseWriter, r *http.Request) {
// @Produce json id := r.PathValue("id")
// @Param id path string true "Job ID"
// @Success 204 "No Content" if err := h.Service.DeleteJob(id); err != nil {
// @Failure 400 {string} string "Bad Request" http.Error(w, err.Error(), http.StatusInternalServerError)
// @Failure 500 {string} string "Internal Server Error" return
// @Router /api/v1/jobs/{id} [delete] }
func (h *JobHandler) DeleteJob(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") w.WriteHeader(http.StatusNoContent)
}
if err := h.Service.DeleteJob(id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}

View file

@ -1,461 +1,372 @@
package postgres package postgres
import ( import (
"context" "context"
"database/sql" "database/sql"
"encoding/json" "time"
"time"
"github.com/lib/pq"
"github.com/lib/pq" "github.com/rede5/gohorsejobs/backend/internal/core/domain/entity"
"github.com/rede5/gohorsejobs/backend/internal/core/domain/entity" )
)
type UserRepository struct {
type UserRepository struct { db *sql.DB
db *sql.DB }
}
func NewUserRepository(db *sql.DB) *UserRepository {
func NewUserRepository(db *sql.DB) *UserRepository { return &UserRepository{db: db}
return &UserRepository{db: db} }
}
func (r *UserRepository) LinkGuestApplications(ctx context.Context, email string, userID string) error {
func (r *UserRepository) LinkGuestApplications(ctx context.Context, email string, userID string) error { query := `
query := ` UPDATE applications
UPDATE applications SET user_id = $1
SET user_id = $1 WHERE email = $2 AND (user_id IS NULL OR user_id LIKE 'guest_%')
WHERE email = $2 AND (user_id IS NULL OR user_id LIKE 'guest_%') `
` _, err := r.db.ExecContext(ctx, query, userID, email)
_, err := r.db.ExecContext(ctx, query, userID, email) return err
return err }
}
func (r *UserRepository) Save(ctx context.Context, user *entity.User) (*entity.User, error) {
func (r *UserRepository) Save(ctx context.Context, user *entity.User) (*entity.User, error) { tx, err := r.db.BeginTx(ctx, nil)
tx, err := r.db.BeginTx(ctx, nil) if err != nil {
if err != nil { return nil, err
return nil, err }
} defer tx.Rollback()
defer tx.Rollback()
// TenantID is string (UUID) or empty
// TenantID is string (UUID) or empty var tenantID *string
var tenantID *string if user.TenantID != "" {
if user.TenantID != "" { tenantID = &user.TenantID
tenantID = &user.TenantID }
}
// 1. Insert User
// 1. Insert User query := `
query := ` INSERT INTO users (
INSERT INTO users ( identifier, password_hash, role, full_name, email, name, tenant_id, status, created_at, updated_at, avatar_url,
identifier, password_hash, role, full_name, email, name, tenant_id, status, created_at, updated_at, avatar_url, phone, bio, address, city, state, zip_code, birth_date, education, experience, skills, objective, title
phone, bio, address, city, state, zip_code, birth_date, education, experience, skills, objective, title )
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23) RETURNING id
RETURNING id `
`
var id string
var id string // Map the first role to the role column, default to 'candidate'
// Map the first role to the role column, default to 'candidate' role := "candidate"
role := "candidate" if len(user.Roles) > 0 {
if len(user.Roles) > 0 { role = user.Roles[0].Name
role = user.Roles[0].Name }
}
err = tx.QueryRowContext(ctx, query,
// Prepare pq Array for skills user.Email, // identifier = email
// IMPORTANT: import "github.com/lib/pq" needed at top user.PasswordHash,
role,
err = tx.QueryRowContext(ctx, query, user.Name,
user.Email, // identifier = email user.Email,
user.PasswordHash, user.Name,
role, tenantID,
user.Name, user.Status,
user.Email, user.CreatedAt,
user.Name, user.UpdatedAt,
tenantID, user.AvatarUrl,
user.Status, user.Phone,
user.CreatedAt, user.Bio,
user.UpdatedAt, user.Address,
user.AvatarUrl, user.City,
user.Phone, user.State,
user.Bio, user.ZipCode,
user.Address, user.BirthDate,
user.City, user.Education,
user.State, user.Experience,
user.ZipCode, pq.Array(user.Skills),
user.BirthDate, user.Objective,
user.Education, user.Title,
user.Experience, ).Scan(&id)
pq.Array(user.Skills),
user.Objective, if err != nil {
user.Title, return nil, err
).Scan(&id) }
user.ID = id
if err != nil {
return nil, err // 2. Insert Roles into user_roles table
} if len(user.Roles) > 0 {
user.ID = id roleQuery := `INSERT INTO user_roles (user_id, role) VALUES ($1, $2) ON CONFLICT DO NOTHING`
for _, role := range user.Roles {
// 2. Insert Roles into user_roles table _, err := tx.ExecContext(ctx, roleQuery, id, role.Name)
if len(user.Roles) > 0 { if err != nil {
roleQuery := `INSERT INTO user_roles (user_id, role) VALUES ($1, $2) ON CONFLICT DO NOTHING` return nil, err
for _, role := range user.Roles { }
_, err := tx.ExecContext(ctx, roleQuery, id, role.Name) }
if err != nil { }
return nil, err
} if err := tx.Commit(); err != nil {
} return nil, err
} }
if err := tx.Commit(); err != nil { return user, nil
return nil, err }
}
func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*entity.User, error) {
return user, nil query := `SELECT id, COALESCE(tenant_id::text, ''), COALESCE(name, full_name, ''),
} COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at, COALESCE(avatar_url, ''),
phone, bio, address, city, state, zip_code, birth_date, education, experience, skills, objective, title
func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*entity.User, error) { FROM users WHERE email = $1 OR identifier = $1`
<<<<<<< HEAD row := r.db.QueryRowContext(ctx, query, email)
query := `
SELECT u := &entity.User{}
id, COALESCE(tenant_id::text, ''), COALESCE(name, full_name, ''), var dbID string
COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at, COALESCE(avatar_url, ''), var phone sql.NullString
bio, profile_picture_url, skills, experience, education var bio sql.NullString
FROM users WHERE email = $1 OR identifier = $1 err := row.Scan(
` &dbID,
======= &u.TenantID,
query := `SELECT id, COALESCE(tenant_id::text, ''), COALESCE(name, full_name, ''), &u.Name,
COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at, COALESCE(avatar_url, ''), &u.Email,
phone, bio, address, city, state, zip_code, birth_date, education, experience, skills, objective, title &u.PasswordHash,
FROM users WHERE email = $1 OR identifier = $1` &u.Status,
>>>>>>> dev &u.CreatedAt,
row := r.db.QueryRowContext(ctx, query, email) &u.UpdatedAt,
&u.AvatarUrl,
u := &entity.User{} &phone,
var dbID string &bio,
<<<<<<< HEAD &u.Address,
var skills, experience, education []byte // temp for Scanning &u.City,
&u.State,
err := row.Scan( &u.ZipCode,
&dbID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt, &u.AvatarUrl, &u.BirthDate,
&u.Bio, &u.ProfilePictureURL, &skills, &experience, &education, &u.Education,
======= &u.Experience,
var phone sql.NullString pq.Array(&u.Skills),
var bio sql.NullString &u.Objective,
err := row.Scan( &u.Title,
&dbID, )
&u.TenantID, if err != nil {
&u.Name, if err == sql.ErrNoRows {
&u.Email, return nil, nil // Return nil if not found
&u.PasswordHash, }
&u.Status, return nil, err
&u.CreatedAt, }
&u.UpdatedAt, u.ID = dbID
&u.AvatarUrl, u.Phone = nullStringPtr(phone)
&phone, u.Bio = nullStringPtr(bio)
&bio, u.Roles, _ = r.getRoles(ctx, dbID)
&u.Address, return u, nil
&u.City, }
&u.State,
&u.ZipCode, func (r *UserRepository) FindByID(ctx context.Context, id string) (*entity.User, error) {
&u.BirthDate, query := `SELECT id, COALESCE(tenant_id::text, ''), COALESCE(name, full_name, ''),
&u.Education, COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at, COALESCE(avatar_url, ''),
&u.Experience, phone, bio, address, city, state, zip_code, birth_date, education, experience, skills, objective, title
pq.Array(&u.Skills), FROM users WHERE id = $1`
&u.Objective, row := r.db.QueryRowContext(ctx, query, id)
&u.Title,
>>>>>>> dev u := &entity.User{}
) var dbID string
if err != nil { var phone sql.NullString
if err == sql.ErrNoRows { var bio sql.NullString
return nil, nil // Return nil if not found err := row.Scan(
} &dbID,
return nil, err &u.TenantID,
} &u.Name,
u.ID = dbID &u.Email,
<<<<<<< HEAD &u.PasswordHash,
&u.Status,
// Unmarshal JSONB fields &u.CreatedAt,
if len(skills) > 0 { &u.UpdatedAt,
_ = json.Unmarshal(skills, &u.Skills) &u.AvatarUrl,
} &phone,
if len(experience) > 0 { &bio,
_ = json.Unmarshal(experience, &u.Experience) &u.Address,
} &u.City,
if len(education) > 0 { &u.State,
_ = json.Unmarshal(education, &u.Education) &u.ZipCode,
} &u.BirthDate,
&u.Education,
======= &u.Experience,
u.Phone = nullStringPtr(phone) pq.Array(&u.Skills),
u.Bio = nullStringPtr(bio) &u.Objective,
>>>>>>> dev &u.Title,
u.Roles, _ = r.getRoles(ctx, dbID) )
return u, nil if err != nil {
} return nil, err
}
func (r *UserRepository) FindByID(ctx context.Context, id string) (*entity.User, error) { u.ID = dbID
<<<<<<< HEAD u.Phone = nullStringPtr(phone)
query := ` u.Bio = nullStringPtr(bio)
SELECT u.Roles, _ = r.getRoles(ctx, dbID)
id, COALESCE(tenant_id::text, ''), COALESCE(name, full_name, ''), return u, nil
COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at, COALESCE(avatar_url, ''), }
bio, profile_picture_url, skills, experience, education
FROM users WHERE id = $1 func (r *UserRepository) FindAllByTenant(ctx context.Context, tenantID string, limit, offset int) ([]*entity.User, int, error) {
` var total int
======= countQuery := `SELECT COUNT(*) FROM users WHERE tenant_id = $1`
query := `SELECT id, COALESCE(tenant_id::text, ''), COALESCE(name, full_name, ''), if err := r.db.QueryRowContext(ctx, countQuery, tenantID).Scan(&total); err != nil {
COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at, COALESCE(avatar_url, ''), return nil, 0, err
phone, bio, address, city, state, zip_code, birth_date, education, experience, skills, objective, title }
FROM users WHERE id = $1`
>>>>>>> dev query := `SELECT id, COALESCE(tenant_id::text, ''), COALESCE(name, full_name, ''),
row := r.db.QueryRowContext(ctx, query, id) COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at, COALESCE(avatar_url, ''),
phone, bio, address, city, state, zip_code, birth_date, education, experience, skills, objective, title
u := &entity.User{} FROM users
var dbID string WHERE tenant_id = $1
<<<<<<< HEAD ORDER BY created_at DESC
var skills, experience, education []byte // temp for Scanning LIMIT $2 OFFSET $3`
rows, err := r.db.QueryContext(ctx, query, tenantID, limit, offset)
err := row.Scan( if err != nil {
&dbID, &u.TenantID, &u.Name, &u.Email, &u.PasswordHash, &u.Status, &u.CreatedAt, &u.UpdatedAt, &u.AvatarUrl, return nil, 0, err
&u.Bio, &u.ProfilePictureURL, &skills, &experience, &education, }
======= defer rows.Close()
var phone sql.NullString
var bio sql.NullString var users []*entity.User
err := row.Scan( for rows.Next() {
&dbID, u := &entity.User{}
&u.TenantID, var dbID string
&u.Name, var phone sql.NullString
&u.Email, var bio sql.NullString
&u.PasswordHash, if err := rows.Scan(
&u.Status, &dbID,
&u.CreatedAt, &u.TenantID,
&u.UpdatedAt, &u.Name,
&u.AvatarUrl, &u.Email,
&phone, &u.PasswordHash,
&bio, &u.Status,
&u.Address, &u.CreatedAt,
&u.City, &u.UpdatedAt,
&u.State, &u.AvatarUrl,
&u.ZipCode, &phone,
&u.BirthDate, &bio,
&u.Education, &u.Address,
&u.Experience, &u.City,
pq.Array(&u.Skills), &u.State,
&u.Objective, &u.ZipCode,
&u.Title, &u.BirthDate,
>>>>>>> dev &u.Education,
) &u.Experience,
if err != nil { pq.Array(&u.Skills),
return nil, err &u.Objective,
} &u.Title,
u.ID = dbID ); err != nil {
<<<<<<< HEAD return nil, 0, err
}
// Unmarshal JSONB fields u.ID = dbID
if len(skills) > 0 { u.Phone = nullStringPtr(phone)
_ = json.Unmarshal(skills, &u.Skills) u.Bio = nullStringPtr(bio)
} u.Roles, _ = r.getRoles(ctx, dbID)
if len(experience) > 0 { users = append(users, u)
_ = json.Unmarshal(experience, &u.Experience) }
} return users, total, nil
if len(education) > 0 { }
_ = json.Unmarshal(education, &u.Education)
} func (r *UserRepository) Update(ctx context.Context, user *entity.User) (*entity.User, error) {
user.UpdatedAt = time.Now()
=======
u.Phone = nullStringPtr(phone) tx, err := r.db.BeginTx(ctx, nil)
u.Bio = nullStringPtr(bio) if err != nil {
>>>>>>> dev return nil, err
u.Roles, _ = r.getRoles(ctx, dbID) }
return u, nil defer tx.Rollback()
}
// 1. Update basic fields + legacy role column
func (r *UserRepository) FindAllByTenant(ctx context.Context, tenantID string, limit, offset int) ([]*entity.User, int, error) { // We use the first role as the "legacy" role for compatibility
var total int primaryRole := ""
countQuery := `SELECT COUNT(*) FROM users WHERE tenant_id = $1` if len(user.Roles) > 0 {
if err := r.db.QueryRowContext(ctx, countQuery, tenantID).Scan(&total); err != nil { primaryRole = user.Roles[0].Name
return nil, 0, err }
}
query := `
query := `SELECT id, COALESCE(tenant_id::text, ''), COALESCE(name, full_name, ''), UPDATE users
COALESCE(email, identifier), password_hash, COALESCE(status, 'active'), created_at, updated_at, COALESCE(avatar_url, ''), SET name=$1, full_name=$2, email=$3, status=$4, role=$5, updated_at=$6, avatar_url=$7,
phone, bio, address, city, state, zip_code, birth_date, education, experience, skills, objective, title phone=$8, bio=$9, password_hash=$10,
FROM users address=$11, city=$12, state=$13, zip_code=$14, birth_date=$15,
WHERE tenant_id = $1 education=$16, experience=$17, skills=$18, objective=$19, title=$20
ORDER BY created_at DESC WHERE id=$21
LIMIT $2 OFFSET $3` `
rows, err := r.db.QueryContext(ctx, query, tenantID, limit, offset) _, err = tx.ExecContext(
if err != nil { ctx,
return nil, 0, err query,
} user.Name,
defer rows.Close() user.Name,
user.Email,
var users []*entity.User user.Status,
for rows.Next() { primaryRole,
u := &entity.User{} user.UpdatedAt,
var dbID string user.AvatarUrl,
var phone sql.NullString user.Phone,
var bio sql.NullString user.Bio,
if err := rows.Scan( user.PasswordHash,
&dbID, user.Address,
&u.TenantID, user.City,
&u.Name, user.State,
&u.Email, user.ZipCode,
&u.PasswordHash, user.BirthDate,
&u.Status, user.Education,
&u.CreatedAt, user.Experience,
&u.UpdatedAt, pq.Array(user.Skills),
&u.AvatarUrl, user.Objective,
&phone, user.Title,
&bio, user.ID,
&u.Address, )
&u.City, if err != nil {
&u.State, return nil, err
&u.ZipCode, }
&u.BirthDate,
&u.Education, // 2. Update user_roles (Delete all and re-insert)
&u.Experience, _, err = tx.ExecContext(ctx, `DELETE FROM user_roles WHERE user_id=$1`, user.ID)
pq.Array(&u.Skills), if err != nil {
&u.Objective, return nil, err
&u.Title, }
); err != nil {
return nil, 0, err if len(user.Roles) > 0 {
} stmt, err := tx.PrepareContext(ctx, `INSERT INTO user_roles (user_id, role) VALUES ($1, $2)`)
u.ID = dbID if err != nil {
u.Phone = nullStringPtr(phone) return nil, err
u.Bio = nullStringPtr(bio) }
u.Roles, _ = r.getRoles(ctx, dbID) defer stmt.Close()
users = append(users, u)
} for _, role := range user.Roles {
return users, total, nil if _, err := stmt.ExecContext(ctx, user.ID, role.Name); err != nil {
} return nil, err
}
func (r *UserRepository) Update(ctx context.Context, user *entity.User) (*entity.User, error) { }
user.UpdatedAt = time.Now() }
tx, err := r.db.BeginTx(ctx, nil) if err := tx.Commit(); err != nil {
if err != nil { return nil, err
return nil, err }
}
defer tx.Rollback() return user, nil
}
// 1. Update basic fields + legacy role column + Profile fields
// We use the first role as the "legacy" role for compatibility func (r *UserRepository) Delete(ctx context.Context, id string) error {
// Prepare pq Array for skills _, err := r.db.ExecContext(ctx, `DELETE FROM users WHERE id=$1`, id)
// 1. Update basic fields + legacy role column return err
// We use the first role as the "legacy" role for compatibility }
primaryRole := ""
if len(user.Roles) > 0 { func (r *UserRepository) getRoles(ctx context.Context, userID string) ([]entity.Role, error) {
primaryRole = user.Roles[0].Name // Query both user_roles table AND legacy role column from users table
} // This ensures backward compatibility with users who have role set in users.role
query := `
<<<<<<< HEAD SELECT role FROM user_roles WHERE user_id = $1
skillsJSON, _ := json.Marshal(user.Skills) UNION
experienceJSON, _ := json.Marshal(user.Experience) SELECT role FROM users WHERE id = $1 AND role IS NOT NULL AND role != ''
educationJSON, _ := json.Marshal(user.Education) `
rows, err := r.db.QueryContext(ctx, query, userID)
query := ` if err != nil {
UPDATE users return nil, err
SET name=$1, email=$2, status=$3, role=$4, updated_at=$5, avatar_url=$6, }
bio=$7, profile_picture_url=$8, skills=$9, experience=$10, education=$11 defer rows.Close()
WHERE id=$12 var roles []entity.Role
` for rows.Next() {
_, err = tx.ExecContext(ctx, query, var roleName string
user.Name, user.Email, user.Status, primaryRole, user.UpdatedAt, user.AvatarUrl, rows.Scan(&roleName)
user.Bio, user.ProfilePictureURL, skillsJSON, experienceJSON, educationJSON, roles = append(roles, entity.Role{Name: roleName})
======= }
query := ` return roles, nil
UPDATE users }
SET name=$1, full_name=$2, email=$3, status=$4, role=$5, updated_at=$6, avatar_url=$7,
phone=$8, bio=$9, password_hash=$10, func nullStringPtr(value sql.NullString) *string {
address=$11, city=$12, state=$13, zip_code=$14, birth_date=$15, if value.Valid {
education=$16, experience=$17, skills=$18, objective=$19, title=$20 return &value.String
WHERE id=$21 }
` return nil
_, err = tx.ExecContext( }
ctx,
query,
user.Name,
user.Name,
user.Email,
user.Status,
primaryRole,
user.UpdatedAt,
user.AvatarUrl,
user.Phone,
user.Bio,
user.PasswordHash,
user.Address,
user.City,
user.State,
user.ZipCode,
user.BirthDate,
user.Education,
user.Experience,
pq.Array(user.Skills),
user.Objective,
user.Title,
>>>>>>> dev
user.ID,
)
if err != nil {
return nil, err
}
// 2. Update user_roles (Delete all and re-insert)
_, err = tx.ExecContext(ctx, `DELETE FROM user_roles WHERE user_id=$1`, user.ID)
if err != nil {
return nil, err
}
if len(user.Roles) > 0 {
stmt, err := tx.PrepareContext(ctx, `INSERT INTO user_roles (user_id, role) VALUES ($1, $2)`)
if err != nil {
return nil, err
}
defer stmt.Close()
for _, role := range user.Roles {
if _, err := stmt.ExecContext(ctx, user.ID, role.Name); err != nil {
return nil, err
}
}
}
if err := tx.Commit(); err != nil {
return nil, err
}
return user, nil
}
func (r *UserRepository) Delete(ctx context.Context, id string) error {
_, err := r.db.ExecContext(ctx, `DELETE FROM users WHERE id=$1`, id)
return err
}
func (r *UserRepository) getRoles(ctx context.Context, userID string) ([]entity.Role, error) {
// Query both user_roles table AND legacy role column from users table
// This ensures backward compatibility with users who have role set in users.role
query := `
SELECT role FROM user_roles WHERE user_id = $1
UNION
SELECT role FROM users WHERE id = $1 AND role IS NOT NULL AND role != ''
`
rows, err := r.db.QueryContext(ctx, query, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var roles []entity.Role
for rows.Next() {
var roleName string
rows.Scan(&roleName)
roles = append(roles, entity.Role{Name: roleName})
}
return roles, nil
}
func nullStringPtr(value sql.NullString) *string {
if value.Valid {
return &value.String
}
return nil
}

View file

@ -1,370 +1,334 @@
package router package router
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"os" "os"
"time" "time"
// Added this import // Added this import
"github.com/rede5/gohorsejobs/backend/internal/api/middleware" "github.com/rede5/gohorsejobs/backend/internal/api/middleware"
"github.com/rede5/gohorsejobs/backend/internal/database" "github.com/rede5/gohorsejobs/backend/internal/database"
"github.com/rede5/gohorsejobs/backend/internal/handlers" "github.com/rede5/gohorsejobs/backend/internal/handlers"
"github.com/rede5/gohorsejobs/backend/internal/infrastructure/persistence/postgres" "github.com/rede5/gohorsejobs/backend/internal/infrastructure/persistence/postgres"
"github.com/rede5/gohorsejobs/backend/internal/services" "github.com/rede5/gohorsejobs/backend/internal/services"
// Core Imports // Core Imports
apiHandlers "github.com/rede5/gohorsejobs/backend/internal/api/handlers" apiHandlers "github.com/rede5/gohorsejobs/backend/internal/api/handlers"
authUC "github.com/rede5/gohorsejobs/backend/internal/core/usecases/auth" authUC "github.com/rede5/gohorsejobs/backend/internal/core/usecases/auth"
tenantUC "github.com/rede5/gohorsejobs/backend/internal/core/usecases/tenant" tenantUC "github.com/rede5/gohorsejobs/backend/internal/core/usecases/tenant"
userUC "github.com/rede5/gohorsejobs/backend/internal/core/usecases/user" userUC "github.com/rede5/gohorsejobs/backend/internal/core/usecases/user"
authInfra "github.com/rede5/gohorsejobs/backend/internal/infrastructure/auth" authInfra "github.com/rede5/gohorsejobs/backend/internal/infrastructure/auth"
legacyMiddleware "github.com/rede5/gohorsejobs/backend/internal/middleware" legacyMiddleware "github.com/rede5/gohorsejobs/backend/internal/middleware"
// Admin Imports // Admin Imports
_ "github.com/rede5/gohorsejobs/backend/docs" // Import generated docs _ "github.com/rede5/gohorsejobs/backend/docs" // Import generated docs
httpSwagger "github.com/swaggo/http-swagger/v2" httpSwagger "github.com/swaggo/http-swagger/v2"
) )
func NewRouter() http.Handler { func NewRouter() http.Handler {
mux := http.NewServeMux() mux := http.NewServeMux()
// Initialize Services // Initialize Services
// --- CORE ARCHITECTURE INITIALIZATION --- // --- CORE ARCHITECTURE INITIALIZATION ---
// Infrastructure // Infrastructure
// Infrastructure userRepo := postgres.NewUserRepository(database.DB)
userRepo := postgres.NewUserRepository(database.DB) companyRepo := postgres.NewCompanyRepository(database.DB)
companyRepo := postgres.NewCompanyRepository(database.DB) locationRepo := postgres.NewLocationRepository(database.DB)
locationRepo := postgres.NewLocationRepository(database.DB)
// Utils Services (Moved up for dependency injection)
// Utils Services (Moved up for dependency injection) credentialsService := services.NewCredentialsService(database.DB)
credentialsService := services.NewCredentialsService(database.DB) settingsService := services.NewSettingsService(database.DB)
settingsService := services.NewSettingsService(database.DB) storageService := services.NewStorageService(credentialsService)
storageService := services.NewStorageService(credentialsService) fcmService := services.NewFCMService(credentialsService)
fcmService := services.NewFCMService(credentialsService) cloudflareService := services.NewCloudflareService(credentialsService)
cloudflareService := services.NewCloudflareService(credentialsService) emailService := services.NewEmailService(database.DB, credentialsService)
emailService := services.NewEmailService(database.DB, credentialsService) locationService := services.NewLocationService(locationRepo)
locationService := services.NewLocationService(locationRepo)
adminService := services.NewAdminService(database.DB)
adminService := services.NewAdminService(database.DB) jobService := services.NewJobService(database.DB)
jobService := services.NewJobService(database.DB) applicationService := services.NewApplicationService(database.DB, emailService)
applicationService := services.NewApplicationService(database.DB, emailService)
jwtSecret := os.Getenv("JWT_SECRET")
jwtSecret := os.Getenv("JWT_SECRET") if jwtSecret == "" {
if jwtSecret == "" { // Fallback for dev, but really should be in env
// Fallback for dev, but really should be in env jwtSecret = "default-dev-secret-do-not-use-in-prod"
jwtSecret = "default-dev-secret-do-not-use-in-prod" }
}
authService := authInfra.NewJWTService(jwtSecret, "todai-jobs")
authService := authInfra.NewJWTService(jwtSecret, "todai-jobs")
// Token Repository for Password Reset
// Token Repository for Password Reset tokenRepo := postgres.NewPasswordResetTokenRepository(database.DB)
tokenRepo := postgres.NewPasswordResetTokenRepository(database.DB)
// Frontend URL for reset link
// Frontend URL for reset link frontendURL := os.Getenv("FRONTEND_URL")
frontendURL := os.Getenv("FRONTEND_URL") if frontendURL == "" {
if frontendURL == "" { frontendURL = "http://localhost:3000"
frontendURL = "http://localhost:3000" }
}
// UseCases
// UseCases loginUC := authUC.NewLoginUseCase(userRepo, authService)
loginUC := authUC.NewLoginUseCase(userRepo, authService) registerCandidateUC := authUC.NewRegisterCandidateUseCase(userRepo, companyRepo, authService, emailService)
registerCandidateUC := authUC.NewRegisterCandidateUseCase(userRepo, companyRepo, authService, emailService) createCompanyUC := tenantUC.NewCreateCompanyUseCase(companyRepo, userRepo, authService)
createCompanyUC := tenantUC.NewCreateCompanyUseCase(companyRepo, userRepo, authService) listCompaniesUC := tenantUC.NewListCompaniesUseCase(companyRepo)
listCompaniesUC := tenantUC.NewListCompaniesUseCase(companyRepo) createUserUC := userUC.NewCreateUserUseCase(userRepo, authService)
createUserUC := userUC.NewCreateUserUseCase(userRepo, authService) listUsersUC := userUC.NewListUsersUseCase(userRepo)
listUsersUC := userUC.NewListUsersUseCase(userRepo) deleteUserUC := userUC.NewDeleteUserUseCase(userRepo)
deleteUserUC := userUC.NewDeleteUserUseCase(userRepo) updateUserUC := userUC.NewUpdateUserUseCase(userRepo)
updateUserUC := userUC.NewUpdateUserUseCase(userRepo) updatePasswordUC := userUC.NewUpdatePasswordUseCase(userRepo, authService)
<<<<<<< HEAD forgotPasswordUC := authUC.NewForgotPasswordUseCase(userRepo, tokenRepo, emailService, frontendURL)
forgotPasswordUC := authUC.NewForgotPasswordUseCase(userRepo, tokenRepo, emailService, frontendURL) resetPasswordUC := authUC.NewResetPasswordUseCase(userRepo, tokenRepo, authService)
resetPasswordUC := authUC.NewResetPasswordUseCase(userRepo, tokenRepo, authService)
// Admin Logic Services
// Admin Logic Services auditService := services.NewAuditService(database.DB)
======= notificationService := services.NewNotificationService(database.DB, fcmService)
updatePasswordUC := userUC.NewUpdatePasswordUseCase(userRepo, authService) ticketService := services.NewTicketService(database.DB)
>>>>>>> dev
auditService := services.NewAuditService(database.DB) // Handlers & Middleware
notificationService := services.NewNotificationService(database.DB, fcmService) coreHandlers := apiHandlers.NewCoreHandlers(
ticketService := services.NewTicketService(database.DB) loginUC,
registerCandidateUC,
// Handlers & Middleware createCompanyUC,
coreHandlers := apiHandlers.NewCoreHandlers( createUserUC,
loginUC, listUsersUC,
registerCandidateUC, deleteUserUC,
createCompanyUC, updateUserUC,
createUserUC, updatePasswordUC,
listUsersUC, listCompaniesUC,
deleteUserUC, forgotPasswordUC,
updateUserUC, resetPasswordUC,
updatePasswordUC, auditService,
listCompaniesUC, notificationService,
forgotPasswordUC, ticketService,
resetPasswordUC, adminService,
auditService, credentialsService,
notificationService, )
ticketService, authMiddleware := middleware.NewMiddleware(authService)
adminService,
credentialsService, // Chat Services
) appwriteService := services.NewAppwriteService(credentialsService)
authMiddleware := middleware.NewMiddleware(authService) chatService := services.NewChatService(database.DB, appwriteService)
chatHandlers := apiHandlers.NewChatHandlers(chatService)
// Chat Services
appwriteService := services.NewAppwriteService(credentialsService) settingsHandler := apiHandlers.NewSettingsHandler(settingsService)
chatService := services.NewChatService(database.DB, appwriteService) credentialsHandler := apiHandlers.NewCredentialsHandler(credentialsService) // Added
chatHandlers := apiHandlers.NewChatHandlers(chatService) storageHandler := apiHandlers.NewStorageHandler(storageService)
adminHandlers := apiHandlers.NewAdminHandlers(adminService, auditService, jobService, cloudflareService)
settingsHandler := apiHandlers.NewSettingsHandler(settingsService) locationHandlers := apiHandlers.NewLocationHandlers(locationService)
credentialsHandler := apiHandlers.NewCredentialsHandler(credentialsService) // Added
storageHandler := apiHandlers.NewStorageHandler(storageService) seederService := services.NewSeederService(database.DB)
adminHandlers := apiHandlers.NewAdminHandlers(adminService, auditService, jobService, cloudflareService) seederHandlers := apiHandlers.NewSeederHandlers(seederService)
locationHandlers := apiHandlers.NewLocationHandlers(locationService)
// Initialize Legacy Handlers
seederService := services.NewSeederService(database.DB) jobHandler := handlers.NewJobHandler(jobService)
seederHandlers := apiHandlers.NewSeederHandlers(seederService) applicationHandler := handlers.NewApplicationHandler(applicationService)
paymentHandler := handlers.NewPaymentHandler(credentialsService)
// Initialize Legacy Handlers
jobHandler := handlers.NewJobHandler(jobService) // --- HEALTH CHECK ---
applicationHandler := handlers.NewApplicationHandler(applicationService) mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
paymentHandler := handlers.NewPaymentHandler(credentialsService) w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "text/plain")
// --- HEALTH CHECK --- w.Write([]byte("OK"))
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) { })
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "text/plain") // --- ROOT ROUTE ---
w.Write([]byte("OK")) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
}) if r.URL.Path != "/" {
http.NotFound(w, r)
// --- ROOT ROUTE --- return
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { }
if r.URL.Path != "/" {
http.NotFound(w, r) response := map[string]interface{}{
return "message": "GoHorseJobs API is running!",
} "docs": "/docs",
"health": "/health",
response := map[string]interface{}{ "version": "1.0.0",
"message": "🐴 GoHorseJobs API is running!", }
"docs": "/docs",
"health": "/health", w.Header().Set("Content-Type", "application/json")
"version": "1.0.0", if err := json.NewEncoder(w).Encode(response); err != nil {
} http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
w.Header().Set("Content-Type", "application/json") })
if err := json.NewEncoder(w).Encode(response); err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError) // --- CORE ROUTES ---
} // Public
}) mux.HandleFunc("POST /api/v1/auth/login", coreHandlers.Login)
mux.HandleFunc("POST /api/v1/auth/logout", coreHandlers.Logout)
// --- CORE ROUTES --- mux.HandleFunc("POST /api/v1/auth/forgot-password", coreHandlers.ForgotPassword)
// Public mux.HandleFunc("POST /api/v1/auth/reset-password", coreHandlers.ResetPassword)
mux.HandleFunc("POST /api/v1/auth/login", coreHandlers.Login) mux.HandleFunc("POST /api/v1/auth/register", coreHandlers.RegisterCandidate)
<<<<<<< HEAD mux.HandleFunc("POST /api/v1/auth/register/candidate", coreHandlers.RegisterCandidate)
mux.HandleFunc("POST /api/v1/auth/forgot-password", coreHandlers.ForgotPassword) mux.HandleFunc("POST /api/v1/auth/register/company", coreHandlers.CreateCompany)
mux.HandleFunc("POST /api/v1/auth/reset-password", coreHandlers.ResetPassword) mux.HandleFunc("POST /api/v1/companies", coreHandlers.CreateCompany)
======= // Public/Protected with RBAC (Smart Handler)
mux.HandleFunc("POST /api/v1/auth/logout", coreHandlers.Logout) mux.Handle("GET /api/v1/companies", authMiddleware.OptionalHeaderAuthGuard(http.HandlerFunc(coreHandlers.ListCompanies)))
>>>>>>> dev
mux.HandleFunc("POST /api/v1/auth/register", coreHandlers.RegisterCandidate) adminOnly := authMiddleware.RequireRoles("ADMIN", "SUPERADMIN", "admin", "superadmin")
mux.HandleFunc("POST /api/v1/auth/register/candidate", coreHandlers.RegisterCandidate)
mux.HandleFunc("POST /api/v1/auth/register/company", coreHandlers.CreateCompany) // Protected Core
mux.HandleFunc("POST /api/v1/companies", coreHandlers.CreateCompany) mux.Handle("POST /api/v1/users", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.CreateUser)))
// Public/Protected with RBAC (Smart Handler) mux.Handle("GET /api/v1/users", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(coreHandlers.ListUsers))))
mux.Handle("GET /api/v1/companies", authMiddleware.OptionalHeaderAuthGuard(http.HandlerFunc(coreHandlers.ListCompanies))) mux.Handle("PATCH /api/v1/users/{id}", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.UpdateUser)))
mux.Handle("DELETE /api/v1/users/{id}", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.DeleteUser)))
adminOnly := authMiddleware.RequireRoles("ADMIN", "SUPERADMIN", "admin", "superadmin") mux.Handle("PUT /api/v1/users/me", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.UpdateMe))) // New Profile Update
// Protected Core // Job Routes
mux.Handle("POST /api/v1/users", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.CreateUser))) mux.HandleFunc("GET /api/v1/jobs", jobHandler.GetJobs)
mux.Handle("GET /api/v1/users", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(coreHandlers.ListUsers)))) mux.Handle("POST /api/v1/jobs", authMiddleware.HeaderAuthGuard(http.HandlerFunc(jobHandler.CreateJob)))
mux.Handle("PATCH /api/v1/users/{id}", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.UpdateUser))) mux.HandleFunc("GET /api/v1/jobs/{id}", jobHandler.GetJobByID)
mux.Handle("DELETE /api/v1/users/{id}", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.DeleteUser))) mux.Handle("PUT /api/v1/jobs/{id}", authMiddleware.HeaderAuthGuard(http.HandlerFunc(jobHandler.UpdateJob)))
mux.Handle("PUT /api/v1/users/me", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.UpdateMe))) // New Profile Update mux.Handle("DELETE /api/v1/jobs/{id}", authMiddleware.HeaderAuthGuard(http.HandlerFunc(jobHandler.DeleteJob)))
// Job Routes // --- ADMIN ROUTES (Consolidated to Standard Paths with RBAC) ---
mux.HandleFunc("GET /api/v1/jobs", jobHandler.GetJobs) // /api/v1/admin/access/roles -> /api/v1/users/roles
mux.Handle("POST /api/v1/jobs", authMiddleware.HeaderAuthGuard(http.HandlerFunc(jobHandler.CreateJob))) mux.Handle("GET /api/v1/users/roles", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.ListAccessRoles))))
mux.HandleFunc("GET /api/v1/jobs/{id}", jobHandler.GetJobByID)
mux.Handle("PUT /api/v1/jobs/{id}", authMiddleware.HeaderAuthGuard(http.HandlerFunc(jobHandler.UpdateJob))) // /api/v1/admin/audit/logins -> /api/v1/audit/logins
mux.Handle("DELETE /api/v1/jobs/{id}", authMiddleware.HeaderAuthGuard(http.HandlerFunc(jobHandler.DeleteJob))) mux.Handle("GET /api/v1/audit/logins", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.ListLoginAudits))))
// --- ADMIN ROUTES (Consolidated to Standard Paths with RBAC) --- // Public /api/v1/users/me (Authenticated)
// /api/v1/admin/access/roles -> /api/v1/users/roles mux.Handle("GET /api/v1/users/me", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.Me)))
mux.Handle("GET /api/v1/users/roles", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.ListAccessRoles)))) mux.Handle("PATCH /api/v1/users/me/profile", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.UpdateMyProfile)))
mux.Handle("PATCH /api/v1/users/me/password", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.UpdateMyPassword)))
// /api/v1/admin/audit/logins -> /api/v1/audit/logins
mux.Handle("GET /api/v1/audit/logins", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.ListLoginAudits)))) // Company Management
mux.Handle("PATCH /api/v1/companies/{id}/status", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.UpdateCompanyStatus))))
// Public /api/v1/users/me (Authenticated) mux.Handle("PATCH /api/v1/companies/{id}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.UpdateCompany))))
mux.Handle("GET /api/v1/users/me", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.Me))) mux.Handle("DELETE /api/v1/companies/{id}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.DeleteCompany))))
mux.Handle("PATCH /api/v1/users/me/profile", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.UpdateMyProfile)))
mux.Handle("PATCH /api/v1/users/me/password", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.UpdateMyPassword))) mux.Handle("GET /api/v1/jobs/moderation", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.ListJobs))))
// /api/v1/admin/companies -> Handled by coreHandlers.ListCompanies (Smart Branching) // /api/v1/admin/jobs/{id}/status
// Needs to be wired with Optional Auth to support both Public and Admin. mux.Handle("PATCH /api/v1/jobs/{id}/status", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.UpdateJobStatus))))
// I will create OptionalHeaderAuthGuard in middleware next.
// /api/v1/admin/jobs/{id}/duplicate -> /api/v1/jobs/{id}/duplicate
// Company Management mux.Handle("POST /api/v1/jobs/{id}/duplicate", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.DuplicateJob))))
mux.Handle("PATCH /api/v1/companies/{id}/status", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.UpdateCompanyStatus))))
mux.Handle("PATCH /api/v1/companies/{id}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.UpdateCompany)))) // /api/v1/tags (GET public/auth, POST/PATCH admin)
mux.Handle("DELETE /api/v1/companies/{id}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.DeleteCompany)))) mux.Handle("GET /api/v1/tags", authMiddleware.HeaderAuthGuard(http.HandlerFunc(adminHandlers.ListTags)))
mux.Handle("POST /api/v1/tags", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.CreateTag))))
// /api/v1/admin/jobs -> /api/v1/jobs?mode=admin (Need Smart Handler) or just separate path /api/v1/jobs/management? mux.Handle("PATCH /api/v1/tags/{id}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.UpdateTag))))
// User said "remove admin from ALL routes".
// Maybe /api/v1/management/jobs? // /api/v1/admin/candidates -> /api/v1/candidates
// Or just /api/v1/jobs (guarded)? mux.Handle("GET /api/v1/candidates", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.ListCandidates))))
// JobHandler.GetJobs is Public.
// I will leave /api/v1/admin/jobs mapped to `GET /api/v1/jobs` for now (Collision). // Get Company by ID (Public)
// OK, I will map it to `GET /api/v1/jobs/moderation` for clearer distinction without "admin" prefix? mux.HandleFunc("GET /api/v1/companies/{id}", coreHandlers.GetCompanyByID)
// Or simply `GET /api/v1/jobs` handle it?
// Given safe constraints, `GET /api/v1/jobs/moderation` is safer than breaking public `GET /api/v1/jobs`. // Location Routes (Public)
mux.Handle("GET /api/v1/jobs/moderation", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.ListJobs)))) mux.HandleFunc("GET /api/v1/locations/countries", locationHandlers.ListCountries)
mux.HandleFunc("GET /api/v1/locations/countries/{id}/states", locationHandlers.ListStatesByCountry)
// /api/v1/admin/jobs/{id}/status mux.HandleFunc("GET /api/v1/locations/states/{id}/cities", locationHandlers.ListCitiesByState)
mux.Handle("PATCH /api/v1/jobs/{id}/status", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.UpdateJobStatus)))) mux.HandleFunc("GET /api/v1/locations/search", locationHandlers.SearchLocations)
// /api/v1/admin/jobs/{id}/duplicate -> /api/v1/jobs/{id}/duplicate // Notifications Route
mux.Handle("POST /api/v1/jobs/{id}/duplicate", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.DuplicateJob)))) mux.Handle("GET /api/v1/notifications", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.ListNotifications)))
mux.Handle("POST /api/v1/tokens", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.SaveFCMToken)))
// /api/v1/tags (GET public/auth, POST/PATCH admin)
mux.Handle("GET /api/v1/tags", authMiddleware.HeaderAuthGuard(http.HandlerFunc(adminHandlers.ListTags))) // Support Ticket Routes
mux.Handle("POST /api/v1/tags", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.CreateTag)))) mux.Handle("GET /api/v1/support/tickets", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.ListTickets)))
mux.Handle("PATCH /api/v1/tags/{id}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.UpdateTag)))) mux.Handle("POST /api/v1/support/tickets", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.CreateTicket)))
mux.Handle("GET /api/v1/support/tickets/all", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.ListAllTickets)))
// /api/v1/admin/candidates -> /api/v1/candidates mux.Handle("GET /api/v1/support/tickets/{id}", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.GetTicket)))
mux.Handle("GET /api/v1/candidates", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.ListCandidates)))) mux.Handle("POST /api/v1/support/tickets/{id}/messages", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.AddMessage)))
mux.Handle("PATCH /api/v1/support/tickets/{id}", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.UpdateTicket)))
// Get Company by ID (Public) mux.Handle("PATCH /api/v1/support/tickets/{id}/close", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.CloseTicket)))
mux.HandleFunc("GET /api/v1/companies/{id}", coreHandlers.GetCompanyByID) mux.Handle("DELETE /api/v1/support/tickets/{id}", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.DeleteTicket)))
// Location Routes (Public) // System Settings
mux.HandleFunc("GET /api/v1/locations/countries", locationHandlers.ListCountries) mux.Handle("GET /api/v1/system/settings/{key}", authMiddleware.OptionalHeaderAuthGuard(http.HandlerFunc(settingsHandler.GetSettings)))
mux.HandleFunc("GET /api/v1/locations/countries/{id}/states", locationHandlers.ListStatesByCountry) mux.Handle("POST /api/v1/system/settings/{key}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(settingsHandler.SaveSettings))))
mux.HandleFunc("GET /api/v1/locations/states/{id}/cities", locationHandlers.ListCitiesByState)
mux.HandleFunc("GET /api/v1/locations/search", locationHandlers.SearchLocations) // System Credentials
mux.Handle("GET /api/v1/system/credentials", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(credentialsHandler.ListCredentials))))
// Notifications Route mux.Handle("POST /api/v1/system/credentials", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(credentialsHandler.SaveCredential))))
mux.Handle("GET /api/v1/notifications", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.ListNotifications))) mux.Handle("DELETE /api/v1/system/credentials/{service}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(credentialsHandler.DeleteCredential))))
mux.Handle("POST /api/v1/tokens", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.SaveFCMToken)))
// Storage (Presigned URL)
// Support Ticket Routes mux.Handle("GET /api/v1/storage/upload-url", authMiddleware.OptionalHeaderAuthGuard(http.HandlerFunc(storageHandler.GetUploadURL)))
mux.Handle("GET /api/v1/support/tickets", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.ListTickets))) // Storage (Direct Proxy)
mux.Handle("POST /api/v1/support/tickets", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.CreateTicket))) mux.Handle("POST /api/v1/storage/upload", authMiddleware.OptionalHeaderAuthGuard(http.HandlerFunc(storageHandler.UploadFile)))
mux.Handle("GET /api/v1/support/tickets/all", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.ListAllTickets)))
mux.Handle("GET /api/v1/support/tickets/{id}", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.GetTicket))) mux.Handle("POST /api/v1/system/cloudflare/purge", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.PurgeCache))))
mux.Handle("POST /api/v1/support/tickets/{id}/messages", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.AddMessage)))
mux.Handle("PATCH /api/v1/support/tickets/{id}", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.UpdateTicket))) // Seeder Routes (Dev Only)
mux.Handle("PATCH /api/v1/support/tickets/{id}/close", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.CloseTicket))) mux.HandleFunc("GET /api/v1/seeder/seed/stream", seederHandlers.HandleSeedStream)
mux.Handle("DELETE /api/v1/support/tickets/{id}", authMiddleware.HeaderAuthGuard(http.HandlerFunc(coreHandlers.DeleteTicket))) mux.HandleFunc("POST /api/v1/seeder/reset", seederHandlers.HandleReset)
// System Settings // Email Templates & Settings (Admin Only)
mux.Handle("GET /api/v1/system/settings/{key}", authMiddleware.OptionalHeaderAuthGuard(http.HandlerFunc(settingsHandler.GetSettings))) mux.Handle("GET /api/v1/admin/email-templates", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.ListEmailTemplates))))
mux.Handle("POST /api/v1/system/settings/{key}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(settingsHandler.SaveSettings)))) mux.Handle("POST /api/v1/admin/email-templates", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.CreateEmailTemplate))))
mux.Handle("GET /api/v1/admin/email-templates/{slug}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.GetEmailTemplate))))
// System Credentials mux.Handle("PUT /api/v1/admin/email-templates/{slug}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.UpdateEmailTemplate))))
mux.Handle("GET /api/v1/system/credentials", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(credentialsHandler.ListCredentials)))) mux.Handle("DELETE /api/v1/admin/email-templates/{slug}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.DeleteEmailTemplate))))
mux.Handle("POST /api/v1/system/credentials", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(credentialsHandler.SaveCredential)))) mux.Handle("GET /api/v1/admin/email-settings", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.GetEmailSettings))))
mux.Handle("DELETE /api/v1/system/credentials/{service}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(credentialsHandler.DeleteCredential)))) mux.Handle("PUT /api/v1/admin/email-settings", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.UpdateEmailSettings))))
// Storage (Presigned URL) // Chat Routes
mux.Handle("GET /api/v1/storage/upload-url", authMiddleware.OptionalHeaderAuthGuard(http.HandlerFunc(storageHandler.GetUploadURL))) mux.Handle("GET /api/v1/conversations", authMiddleware.HeaderAuthGuard(http.HandlerFunc(chatHandlers.ListConversations)))
// Storage (Direct Proxy) mux.Handle("GET /api/v1/conversations/{id}/messages", authMiddleware.HeaderAuthGuard(http.HandlerFunc(chatHandlers.ListMessages)))
mux.Handle("POST /api/v1/storage/upload", authMiddleware.OptionalHeaderAuthGuard(http.HandlerFunc(storageHandler.UploadFile))) mux.Handle("POST /api/v1/conversations/{id}/messages", authMiddleware.HeaderAuthGuard(http.HandlerFunc(chatHandlers.SendMessage)))
mux.Handle("POST /api/v1/system/cloudflare/purge", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.PurgeCache)))) // Metrics Routes
metricsService := services.NewMetricsService(database.DB)
// Seeder Routes (Dev Only) metricsHandler := handlers.NewMetricsHandler(metricsService)
// Guarded by Admin Roles, or you could make it Dev only via env check mux.HandleFunc("GET /api/v1/jobs/{id}/metrics", metricsHandler.GetJobMetrics)
mux.HandleFunc("GET /api/v1/seeder/seed/stream", seederHandlers.HandleSeedStream) // Has its own auth or unrestricted for dev? Better unrestricted for simplicity in dev if safe. mux.HandleFunc("POST /api/v1/jobs/{id}/view", metricsHandler.RecordJobView)
// Actually, let's keep it open for now or simple admin guard if user is logged in.
// The frontend uses EventSource which sends cookies but not custom headers easily without polyfill. // Subscription Routes
// We'll leave it public for the requested "Dev" purpose, or rely on internal network. subService := services.NewSubscriptionService(database.DB)
// If needed, we can add query param token. subHandler := handlers.NewSubscriptionHandler(subService)
mux.HandleFunc("POST /api/v1/seeder/reset", seederHandlers.HandleReset) mux.HandleFunc("POST /api/v1/subscription/checkout", subHandler.CreateCheckoutSession)
mux.HandleFunc("POST /api/v1/subscription/webhook", subHandler.HandleWebhook)
// Email Templates & Settings (Admin Only)
mux.Handle("GET /api/v1/admin/email-templates", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.ListEmailTemplates)))) // Application Routes (merged: both OptionalAuth for create + both /me endpoints)
mux.Handle("POST /api/v1/admin/email-templates", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.CreateEmailTemplate)))) mux.Handle("POST /api/v1/applications", authMiddleware.OptionalHeaderAuthGuard(http.HandlerFunc(applicationHandler.CreateApplication)))
mux.Handle("GET /api/v1/admin/email-templates/{slug}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.GetEmailTemplate)))) mux.Handle("GET /api/v1/applications/me", authMiddleware.HeaderAuthGuard(http.HandlerFunc(applicationHandler.GetMyApplications)))
mux.Handle("PUT /api/v1/admin/email-templates/{slug}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.UpdateEmailTemplate)))) mux.HandleFunc("GET /api/v1/applications", applicationHandler.GetApplications)
mux.Handle("DELETE /api/v1/admin/email-templates/{slug}", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.DeleteEmailTemplate)))) mux.HandleFunc("GET /api/v1/applications/{id}", applicationHandler.GetApplicationByID)
mux.Handle("GET /api/v1/admin/email-settings", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.GetEmailSettings)))) mux.HandleFunc("PUT /api/v1/applications/{id}/status", applicationHandler.UpdateApplicationStatus)
mux.Handle("PUT /api/v1/admin/email-settings", authMiddleware.HeaderAuthGuard(adminOnly(http.HandlerFunc(adminHandlers.UpdateEmailSettings)))) mux.HandleFunc("DELETE /api/v1/applications/{id}", applicationHandler.DeleteApplication)
// Chat Routes // Payment Routes
mux.Handle("GET /api/v1/conversations", authMiddleware.HeaderAuthGuard(http.HandlerFunc(chatHandlers.ListConversations))) mux.Handle("POST /api/v1/payments/create-checkout", authMiddleware.HeaderAuthGuard(http.HandlerFunc(paymentHandler.CreateCheckout)))
mux.Handle("GET /api/v1/conversations/{id}/messages", authMiddleware.HeaderAuthGuard(http.HandlerFunc(chatHandlers.ListMessages))) mux.HandleFunc("POST /api/v1/payments/webhook", paymentHandler.HandleWebhook)
mux.Handle("POST /api/v1/conversations/{id}/messages", authMiddleware.HeaderAuthGuard(http.HandlerFunc(chatHandlers.SendMessage))) mux.HandleFunc("GET /api/v1/payments/status/{id}", paymentHandler.GetPaymentStatus)
// Metrics Routes // --- STORAGE ROUTES (Legacy Removed) ---
metricsService := services.NewMetricsService(database.DB)
metricsHandler := handlers.NewMetricsHandler(metricsService) // --- TICKET ROUTES ---
mux.HandleFunc("GET /api/v1/jobs/{id}/metrics", metricsHandler.GetJobMetrics) ticketHandler := handlers.NewTicketHandler(ticketService)
mux.HandleFunc("POST /api/v1/jobs/{id}/view", metricsHandler.RecordJobView) mux.HandleFunc("GET /api/v1/tickets", ticketHandler.GetTickets)
mux.HandleFunc("POST /api/v1/tickets", ticketHandler.CreateTicket)
// Subscription Routes mux.HandleFunc("GET /api/v1/tickets/{id}", ticketHandler.GetTicketByID)
subService := services.NewSubscriptionService(database.DB) mux.HandleFunc("PUT /api/v1/tickets/{id}", ticketHandler.UpdateTicket)
subHandler := handlers.NewSubscriptionHandler(subService) mux.HandleFunc("POST /api/v1/tickets/{id}/messages", ticketHandler.AddTicketMessage)
mux.HandleFunc("POST /api/v1/subscription/checkout", subHandler.CreateCheckoutSession)
mux.HandleFunc("POST /api/v1/subscription/webhook", subHandler.HandleWebhook) // --- ACTIVITY LOG ROUTES ---
activityLogService := services.NewActivityLogService(database.DB)
// Application Routes activityLogHandler := handlers.NewActivityLogHandler(activityLogService)
<<<<<<< HEAD mux.HandleFunc("GET /api/v1/activity-logs/stats", activityLogHandler.GetActivityLogStats)
mux.HandleFunc("POST /api/v1/applications", applicationHandler.CreateApplication) mux.HandleFunc("GET /api/v1/activity-logs", activityLogHandler.GetActivityLogs)
mux.Handle("GET /api/v1/applications/me", authMiddleware.HeaderAuthGuard(http.HandlerFunc(applicationHandler.ListUserApplications))) // New endpoint
======= // --- NOTIFICATION ROUTES ---
mux.Handle("POST /api/v1/applications", authMiddleware.OptionalHeaderAuthGuard(http.HandlerFunc(applicationHandler.CreateApplication))) notificationHandler := handlers.NewNotificationHandler(notificationService)
mux.Handle("GET /api/v1/applications/me", authMiddleware.HeaderAuthGuard(http.HandlerFunc(applicationHandler.GetMyApplications))) mux.Handle("PUT /api/v1/notifications/read-all", authMiddleware.HeaderAuthGuard(http.HandlerFunc(notificationHandler.MarkAllAsRead)))
>>>>>>> dev mux.Handle("PUT /api/v1/notifications/{id}/read", authMiddleware.HeaderAuthGuard(http.HandlerFunc(notificationHandler.MarkAsRead)))
mux.HandleFunc("GET /api/v1/applications", applicationHandler.GetApplications) mux.Handle("POST /api/v1/notifications/fcm-token", authMiddleware.HeaderAuthGuard(http.HandlerFunc(notificationHandler.RegisterFCMToken)))
mux.HandleFunc("GET /api/v1/applications/{id}", applicationHandler.GetApplicationByID)
mux.HandleFunc("PUT /api/v1/applications/{id}/status", applicationHandler.UpdateApplicationStatus) // Swagger Route - available at /docs
mux.HandleFunc("DELETE /api/v1/applications/{id}", applicationHandler.DeleteApplication) mux.HandleFunc("/docs/", httpSwagger.WrapHandler)
// Payment Routes // Apply middleware chain: Security Headers -> Rate Limiting -> CORS -> Router
mux.Handle("POST /api/v1/payments/create-checkout", authMiddleware.HeaderAuthGuard(http.HandlerFunc(paymentHandler.CreateCheckout))) // Order matters: outer middleware
mux.HandleFunc("POST /api/v1/payments/webhook", paymentHandler.HandleWebhook) var handler http.Handler = mux
mux.HandleFunc("GET /api/v1/payments/status/{id}", paymentHandler.GetPaymentStatus) handler = middleware.CORSMiddleware(handler)
handler = legacyMiddleware.SanitizeMiddleware(handler) // Sanitize XSS from JSON bodies
// --- STORAGE ROUTES (Legacy Removed) --- handler = legacyMiddleware.RateLimitMiddleware(100, time.Minute)(handler) // 100 req/min per IP
handler = legacyMiddleware.SecurityHeadersMiddleware(handler)
// --- TICKET ROUTES ---
ticketHandler := handlers.NewTicketHandler(ticketService) return handler
// mux.HandleFunc("GET /api/v1/tickets/stats", ticketHandler.GetTicketStats) // Removed in hml }
mux.HandleFunc("GET /api/v1/tickets", ticketHandler.GetTickets)
mux.HandleFunc("POST /api/v1/tickets", ticketHandler.CreateTicket)
mux.HandleFunc("GET /api/v1/tickets/{id}", ticketHandler.GetTicketByID)
mux.HandleFunc("PUT /api/v1/tickets/{id}", ticketHandler.UpdateTicket)
// mux.HandleFunc("GET /api/v1/tickets/{id}/messages", ticketHandler.GetTicketMessages) // Merged into GetByID
mux.HandleFunc("POST /api/v1/tickets/{id}/messages", ticketHandler.AddTicketMessage)
// --- ACTIVITY LOG ROUTES ---
activityLogService := services.NewActivityLogService(database.DB)
activityLogHandler := handlers.NewActivityLogHandler(activityLogService)
mux.HandleFunc("GET /api/v1/activity-logs/stats", activityLogHandler.GetActivityLogStats)
mux.HandleFunc("GET /api/v1/activity-logs", activityLogHandler.GetActivityLogs)
// --- NOTIFICATION ROUTES ---
notificationHandler := handlers.NewNotificationHandler(notificationService)
mux.Handle("GET /api/v1/notifications", authMiddleware.HeaderAuthGuard(http.HandlerFunc(notificationHandler.GetNotifications)))
// mux.Handle("GET /api/v1/notifications/unread-count", authMiddleware.HeaderAuthGuard(http.HandlerFunc(notificationHandler.GetUnreadCount))) // Removed in hml
mux.Handle("PUT /api/v1/notifications/read-all", authMiddleware.HeaderAuthGuard(http.HandlerFunc(notificationHandler.MarkAllAsRead)))
mux.Handle("PUT /api/v1/notifications/{id}/read", authMiddleware.HeaderAuthGuard(http.HandlerFunc(notificationHandler.MarkAsRead)))
// mux.Handle("DELETE /api/v1/notifications/{id}", authMiddleware.HeaderAuthGuard(http.HandlerFunc(notificationHandler.DeleteNotification))) // Removed in hml
mux.Handle("POST /api/v1/notifications/fcm-token", authMiddleware.HeaderAuthGuard(http.HandlerFunc(notificationHandler.RegisterFCMToken)))
// mux.Handle("DELETE /api/v1/notifications/fcm-token", authMiddleware.HeaderAuthGuard(http.HandlerFunc(notificationHandler.UnregisterFCMToken))) // Removed in hml
// Swagger Route - available at /docs
mux.HandleFunc("/docs/", httpSwagger.WrapHandler)
// Apply middleware chain: Security Headers -> Rate Limiting -> CORS -> Router
// Order matters: outer middleware
var handler http.Handler = mux
handler = middleware.CORSMiddleware(handler)
handler = legacyMiddleware.SanitizeMiddleware(handler) // Sanitize XSS from JSON bodies
handler = legacyMiddleware.RateLimitMiddleware(100, time.Minute)(handler) // 100 req/min per IP
handler = legacyMiddleware.SecurityHeadersMiddleware(handler)
return handler
}

View file

@ -1,493 +1,450 @@
package services package services
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"strings" "strings"
"time" "time"
"github.com/rede5/gohorsejobs/backend/internal/dto" "github.com/rede5/gohorsejobs/backend/internal/dto"
"github.com/rede5/gohorsejobs/backend/internal/models" "github.com/rede5/gohorsejobs/backend/internal/models"
) )
type JobService struct { type JobService struct {
DB *sql.DB DB *sql.DB
} }
func NewJobService(db *sql.DB) *JobService { func NewJobService(db *sql.DB) *JobService {
return &JobService{DB: db} return &JobService{DB: db}
} }
func (s *JobService) CreateJob(req dto.CreateJobRequest, createdBy string) (*models.Job, error) { func (s *JobService) CreateJob(req dto.CreateJobRequest, createdBy string) (*models.Job, error) {
fmt.Println("[JOB_SERVICE DEBUG] === CreateJob Started ===") 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) fmt.Printf("[JOB_SERVICE DEBUG] CompanyID=%s, CreatedBy=%s, Title=%s, Status=%s\n", req.CompanyID, createdBy, req.Title, req.Status)
query := ` query := `
INSERT INTO jobs ( INSERT INTO jobs (
company_id, created_by, title, description, salary_min, salary_max, salary_type, currency, company_id, created_by, title, description, salary_min, salary_max, salary_type, currency,
employment_type, working_hours, location, region_id, city_id, employment_type, working_hours, location, region_id, city_id,
requirements, benefits, questions, visa_support, language_level, status, created_at, updated_at, salary_negotiable requirements, benefits, questions, visa_support, language_level, status, 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) ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)
RETURNING id, created_at, updated_at RETURNING id, created_at, updated_at
` `
job := &models.Job{ job := &models.Job{
CompanyID: req.CompanyID, CompanyID: req.CompanyID,
CreatedBy: createdBy, CreatedBy: createdBy,
Title: req.Title, Title: req.Title,
Description: req.Description, Description: req.Description,
SalaryMin: req.SalaryMin, SalaryMin: req.SalaryMin,
SalaryMax: req.SalaryMax, SalaryMax: req.SalaryMax,
SalaryType: req.SalaryType, SalaryType: req.SalaryType,
Currency: req.Currency, Currency: req.Currency,
SalaryNegotiable: req.SalaryNegotiable, SalaryNegotiable: req.SalaryNegotiable,
EmploymentType: req.EmploymentType, EmploymentType: req.EmploymentType,
WorkingHours: req.WorkingHours, WorkingHours: req.WorkingHours,
Location: req.Location, Location: req.Location,
RegionID: req.RegionID, RegionID: req.RegionID,
CityID: req.CityID, CityID: req.CityID,
Requirements: models.JSONMap(req.Requirements), Requirements: models.JSONMap(req.Requirements),
Benefits: models.JSONMap(req.Benefits), Benefits: models.JSONMap(req.Benefits),
Questions: models.JSONMap(req.Questions), Questions: models.JSONMap(req.Questions),
VisaSupport: req.VisaSupport, VisaSupport: req.VisaSupport,
LanguageLevel: req.LanguageLevel, LanguageLevel: req.LanguageLevel,
Status: req.Status, Status: req.Status,
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
fmt.Println("[JOB_SERVICE DEBUG] Executing INSERT query...") fmt.Println("[JOB_SERVICE DEBUG] Executing INSERT query...")
fmt.Printf("[JOB_SERVICE DEBUG] Job struct: %+v\n", job) fmt.Printf("[JOB_SERVICE DEBUG] Job struct: %+v\n", job)
err := s.DB.QueryRow( err := s.DB.QueryRow(
query, query,
job.CompanyID, job.CreatedBy, job.Title, job.Description, job.SalaryMin, job.SalaryMax, job.SalaryType, job.Currency, job.CompanyID, job.CreatedBy, job.Title, job.Description, job.SalaryMin, job.SalaryMax, job.SalaryType, job.Currency,
job.EmploymentType, job.WorkingHours, job.Location, job.RegionID, job.CityID, job.EmploymentType, job.WorkingHours, job.Location, job.RegionID, job.CityID,
job.Requirements, job.Benefits, job.Questions, job.VisaSupport, job.LanguageLevel, job.Status, job.CreatedAt, job.UpdatedAt, job.SalaryNegotiable, job.Requirements, job.Benefits, job.Questions, job.VisaSupport, job.LanguageLevel, job.Status, job.CreatedAt, job.UpdatedAt, job.SalaryNegotiable,
).Scan(&job.ID, &job.CreatedAt, &job.UpdatedAt) ).Scan(&job.ID, &job.CreatedAt, &job.UpdatedAt)
if err != nil { if err != nil {
fmt.Printf("[JOB_SERVICE ERROR] INSERT query failed: %v\n", err) fmt.Printf("[JOB_SERVICE ERROR] INSERT query failed: %v\n", err)
return nil, err return nil, err
} }
fmt.Printf("[JOB_SERVICE DEBUG] Job created successfully! ID=%s\n", job.ID) fmt.Printf("[JOB_SERVICE DEBUG] Job created successfully! ID=%s\n", job.ID)
return job, nil return job, nil
} }
func (s *JobService) GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error) { func (s *JobService) GetJobs(filter dto.JobFilterQuery) ([]models.JobWithCompany, int, error) {
// Merged Query: Includes hml fields + key HEAD logic // Merged Query: Includes both HEAD and dev fields
baseQuery := ` baseQuery := `
<<<<<<< HEAD SELECT
SELECT j.id, j.company_id, j.title, j.description, j.salary_min, j.salary_max, j.salary_type,
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, j.created_at, j.updated_at,
j.employment_type, j.work_mode, j.working_hours, j.location, j.status, j.salary_negotiable, j.is_featured, j.created_at, j.updated_at, CASE
COALESCE(c.name, '') as company_name, c.logo_url as company_logo_url, WHEN c.type = 'CANDIDATE_WORKSPACE' OR c.name LIKE 'Candidate - %' THEN ''
r.name as region_name, ci.name as city_name, ELSE COALESCE(c.name, '')
j.view_count, j.featured_until END as company_name, c.logo_url as company_logo_url,
FROM jobs j r.name as region_name, ci.name as city_name,
LEFT JOIN companies c ON j.company_id::text = c.id::text j.view_count, j.featured_until,
LEFT JOIN states r ON j.region_id::text = r.id::text (SELECT COUNT(*) FROM applications a WHERE a.job_id = j.id) as applications_count
LEFT JOIN cities ci ON j.city_id::text = ci.id::text FROM jobs j
WHERE 1=1` LEFT JOIN companies c ON j.company_id::text = c.id::text
LEFT JOIN states r ON j.region_id::text = r.id::text
======= LEFT JOIN cities ci ON j.city_id::text = ci.id::text
SELECT WHERE 1=1`
j.id, j.company_id, j.title, j.description, j.salary_min, j.salary_max, j.salary_type, countQuery := `SELECT COUNT(*) FROM jobs j WHERE 1=1`
j.employment_type, j.work_mode, j.working_hours, j.location, j.status, j.salary_negotiable, j.is_featured, j.created_at, j.updated_at,
CASE var args []interface{}
WHEN c.type = 'CANDIDATE_WORKSPACE' OR c.name LIKE 'Candidate - %' THEN '' argId := 1
ELSE COALESCE(c.name, '')
END as company_name, c.logo_url as company_logo_url, // Search (merged logic)
r.name as region_name, ci.name as city_name, if filter.Search != nil && *filter.Search != "" {
(SELECT COUNT(*) FROM applications a WHERE a.job_id = j.id) as applications_count searchTerm := fmt.Sprintf("%%%s%%", *filter.Search)
FROM jobs j clause := fmt.Sprintf(" AND (j.title ILIKE $%d OR j.description ILIKE $%d OR c.name ILIKE $%d)", argId, argId, argId)
LEFT JOIN companies c ON j.company_id::text = c.id::text baseQuery += clause
LEFT JOIN states r ON j.region_id::text = r.id::text countQuery += clause
LEFT JOIN cities ci ON j.city_id::text = ci.id::text args = append(args, searchTerm)
WHERE 1=1` argId++
>>>>>>> dev }
countQuery := `SELECT COUNT(*) FROM jobs j WHERE 1=1`
// Company filter
var args []interface{} if filter.CompanyID != nil {
argId := 1 baseQuery += fmt.Sprintf(" AND j.company_id = $%d", argId)
countQuery += fmt.Sprintf(" AND j.company_id = $%d", argId)
// Search (merged logic) args = append(args, *filter.CompanyID)
// Supports full text search if available, or ILIKE fallback. argId++
// Using generic ILIKE for broad compatibility as hml did, but incorporating HEAD's concept. }
if filter.Search != nil && *filter.Search != "" {
searchTerm := fmt.Sprintf("%%%s%%", *filter.Search) // Region filter
// HEAD had tsvector. If DB supports it great. But to avoid "function not found" if extension missing, safe bet is ILIKE. if filter.RegionID != nil {
// hml used ILIKE. baseQuery += fmt.Sprintf(" AND j.region_id = $%d", argId)
clause := fmt.Sprintf(" AND (j.title ILIKE $%d OR j.description ILIKE $%d OR c.name ILIKE $%d)", argId, argId, argId) countQuery += fmt.Sprintf(" AND j.region_id = $%d", argId)
baseQuery += clause args = append(args, *filter.RegionID)
countQuery += clause argId++
args = append(args, searchTerm) }
argId++
} // City filter
if filter.CityID != nil {
// Company filter baseQuery += fmt.Sprintf(" AND j.city_id = $%d", argId)
if filter.CompanyID != nil { countQuery += fmt.Sprintf(" AND j.city_id = $%d", argId)
baseQuery += fmt.Sprintf(" AND j.company_id = $%d", argId) args = append(args, *filter.CityID)
countQuery += fmt.Sprintf(" AND j.company_id = $%d", argId) argId++
args = append(args, *filter.CompanyID) }
argId++
} // Employment type filter
if filter.EmploymentType != nil && *filter.EmploymentType != "" && *filter.EmploymentType != "all" {
// Region filter baseQuery += fmt.Sprintf(" AND j.employment_type = $%d", argId)
if filter.RegionID != nil { countQuery += fmt.Sprintf(" AND j.employment_type = $%d", argId)
baseQuery += fmt.Sprintf(" AND j.region_id = $%d", argId) args = append(args, *filter.EmploymentType)
countQuery += fmt.Sprintf(" AND j.region_id = $%d", argId) argId++
args = append(args, *filter.RegionID) }
argId++
} // Work mode filter
if filter.WorkMode != nil && *filter.WorkMode != "" && *filter.WorkMode != "all" {
// City filter baseQuery += fmt.Sprintf(" AND j.work_mode = $%d", argId)
if filter.CityID != nil { countQuery += fmt.Sprintf(" AND j.work_mode = $%d", argId)
baseQuery += fmt.Sprintf(" AND j.city_id = $%d", argId) args = append(args, *filter.WorkMode)
countQuery += fmt.Sprintf(" AND j.city_id = $%d", argId) argId++
args = append(args, *filter.CityID) }
argId++
} // Location filter (Partial Match)
if filter.Location != nil && *filter.Location != "" && *filter.Location != "all" {
// Employment type filter locTerm := fmt.Sprintf("%%%s%%", *filter.Location)
if filter.EmploymentType != nil && *filter.EmploymentType != "" && *filter.EmploymentType != "all" { baseQuery += fmt.Sprintf(" AND j.location ILIKE $%d", argId)
baseQuery += fmt.Sprintf(" AND j.employment_type = $%d", argId) countQuery += fmt.Sprintf(" AND j.location ILIKE $%d", argId)
countQuery += fmt.Sprintf(" AND j.employment_type = $%d", argId) args = append(args, locTerm)
args = append(args, *filter.EmploymentType) argId++
argId++ }
} // Support HEAD's LocationSearch explicitly if different
if filter.LocationSearch != nil && *filter.LocationSearch != "" && (filter.Location == nil || *filter.Location != *filter.LocationSearch) {
// Work mode filter locTerm := fmt.Sprintf("%%%s%%", *filter.LocationSearch)
if filter.WorkMode != nil && *filter.WorkMode != "" && *filter.WorkMode != "all" { baseQuery += fmt.Sprintf(" AND j.location ILIKE $%d", argId)
baseQuery += fmt.Sprintf(" AND j.work_mode = $%d", argId) countQuery += fmt.Sprintf(" AND j.location ILIKE $%d", argId)
countQuery += fmt.Sprintf(" AND j.work_mode = $%d", argId) args = append(args, locTerm)
args = append(args, *filter.WorkMode) argId++
argId++ }
}
// Status filter
// Location filter (Partial Match) if filter.Status != nil && *filter.Status != "" {
if filter.Location != nil && *filter.Location != "" && *filter.Location != "all" { baseQuery += fmt.Sprintf(" AND j.status = $%d", argId)
locTerm := fmt.Sprintf("%%%s%%", *filter.Location) countQuery += fmt.Sprintf(" AND j.status = $%d", argId)
baseQuery += fmt.Sprintf(" AND j.location ILIKE $%d", argId) args = append(args, *filter.Status)
countQuery += fmt.Sprintf(" AND j.location ILIKE $%d", argId) argId++
args = append(args, locTerm) }
argId++
} // Featured filter
// Support HEAD's LocationSearch explicitly if different (mapped to same in requests.go but just in case) if filter.IsFeatured != nil {
if filter.LocationSearch != nil && *filter.LocationSearch != "" && (filter.Location == nil || *filter.Location != *filter.LocationSearch) { baseQuery += fmt.Sprintf(" AND j.is_featured = $%d", argId)
locTerm := fmt.Sprintf("%%%s%%", *filter.LocationSearch) countQuery += fmt.Sprintf(" AND j.is_featured = $%d", argId)
baseQuery += fmt.Sprintf(" AND j.location ILIKE $%d", argId) args = append(args, *filter.IsFeatured)
countQuery += fmt.Sprintf(" AND j.location ILIKE $%d", argId) argId++
args = append(args, locTerm) }
argId++
} // Visa support filter
if filter.VisaSupport != nil {
// Status filter baseQuery += fmt.Sprintf(" AND j.visa_support = $%d", argId)
if filter.Status != nil && *filter.Status != "" { countQuery += fmt.Sprintf(" AND j.visa_support = $%d", argId)
baseQuery += fmt.Sprintf(" AND j.status = $%d", argId) args = append(args, *filter.VisaSupport)
countQuery += fmt.Sprintf(" AND j.status = $%d", argId) argId++
args = append(args, *filter.Status) }
argId++
} // Language Level
if filter.LanguageLevel != nil && *filter.LanguageLevel != "" && *filter.LanguageLevel != "all" {
// Featured filter baseQuery += fmt.Sprintf(" AND j.language_level = $%d", argId)
if filter.IsFeatured != nil { countQuery += fmt.Sprintf(" AND j.language_level = $%d", argId)
baseQuery += fmt.Sprintf(" AND j.is_featured = $%d", argId) args = append(args, *filter.LanguageLevel)
countQuery += fmt.Sprintf(" AND j.is_featured = $%d", argId) argId++
args = append(args, *filter.IsFeatured) }
argId++
} // Currency
if filter.Currency != nil && *filter.Currency != "" && *filter.Currency != "all" {
// Visa support filter baseQuery += fmt.Sprintf(" AND j.currency = $%d", argId)
if filter.VisaSupport != nil { countQuery += fmt.Sprintf(" AND j.currency = $%d", argId)
baseQuery += fmt.Sprintf(" AND j.visa_support = $%d", argId) args = append(args, *filter.Currency)
countQuery += fmt.Sprintf(" AND j.visa_support = $%d", argId) argId++
args = append(args, *filter.VisaSupport) }
argId++
} // Salary range filters
if filter.SalaryMin != nil {
// Language Level baseQuery += fmt.Sprintf(" AND j.salary_min >= $%d", argId)
if filter.LanguageLevel != nil && *filter.LanguageLevel != "" && *filter.LanguageLevel != "all" { countQuery += fmt.Sprintf(" AND j.salary_min >= $%d", argId)
baseQuery += fmt.Sprintf(" AND j.language_level = $%d", argId) args = append(args, *filter.SalaryMin)
countQuery += fmt.Sprintf(" AND j.language_level = $%d", argId) argId++
args = append(args, *filter.LanguageLevel) }
argId++ if filter.SalaryMax != nil {
} baseQuery += fmt.Sprintf(" AND j.salary_max <= $%d", argId)
countQuery += fmt.Sprintf(" AND j.salary_max <= $%d", argId)
// Currency args = append(args, *filter.SalaryMax)
if filter.Currency != nil && *filter.Currency != "" && *filter.Currency != "all" { argId++
baseQuery += fmt.Sprintf(" AND j.currency = $%d", argId) }
countQuery += fmt.Sprintf(" AND j.currency = $%d", argId) if filter.SalaryType != nil && *filter.SalaryType != "" {
args = append(args, *filter.Currency) baseQuery += fmt.Sprintf(" AND j.salary_type = $%d", argId)
argId++ countQuery += fmt.Sprintf(" AND j.salary_type = $%d", argId)
} args = append(args, *filter.SalaryType)
argId++
// Salary range filters }
if filter.SalaryMin != nil {
baseQuery += fmt.Sprintf(" AND j.salary_min >= $%d", argId) // Sorting
countQuery += fmt.Sprintf(" AND j.salary_min >= $%d", argId) sortClause := " ORDER BY j.is_featured DESC, j.created_at DESC" // default
args = append(args, *filter.SalaryMin) if filter.SortBy != nil {
argId++ switch *filter.SortBy {
} case "recent", "date":
if filter.SalaryMax != nil { sortClause = " ORDER BY j.is_featured DESC, j.created_at DESC"
baseQuery += fmt.Sprintf(" AND j.salary_max <= $%d", argId) case "salary", "salary_asc":
countQuery += fmt.Sprintf(" AND j.salary_max <= $%d", argId) sortClause = " ORDER BY j.salary_min ASC NULLS LAST"
args = append(args, *filter.SalaryMax) case "salary_desc":
argId++ sortClause = " ORDER BY j.salary_max DESC NULLS LAST"
} case "relevance":
if filter.SalaryType != nil && *filter.SalaryType != "" { sortClause = " ORDER BY j.is_featured DESC, j.created_at DESC"
baseQuery += fmt.Sprintf(" AND j.salary_type = $%d", argId) }
countQuery += fmt.Sprintf(" AND j.salary_type = $%d", argId) }
args = append(args, *filter.SalaryType)
argId++ // Override sort order if explicit
} if filter.SortOrder != nil {
if *filter.SortOrder == "asc" {
// Sorting // Rely on SortBy providing correct default or direction.
sortClause := " ORDER BY j.is_featured DESC, j.created_at DESC" // default }
if filter.SortBy != nil { }
switch *filter.SortBy {
case "recent", "date": baseQuery += sortClause
sortClause = " ORDER BY j.is_featured DESC, j.created_at DESC"
case "salary", "salary_asc": // Pagination
sortClause = " ORDER BY j.salary_min ASC NULLS LAST" limit := filter.Limit
case "salary_desc": if limit == 0 {
sortClause = " ORDER BY j.salary_max DESC NULLS LAST" limit = 10
case "relevance": }
// Simple relevance if no fulltext rank if limit > 100 {
sortClause = " ORDER BY j.is_featured DESC, j.created_at DESC" limit = 100
} }
} offset := (filter.Page - 1) * limit
if offset < 0 {
// Override sort order if explicit offset = 0
if filter.SortOrder != nil { }
if *filter.SortOrder == "asc" {
// Naive replace/append. hml logic didn't support generic SortOrder param well (it embedded in SortBy). paginationQuery := baseQuery + fmt.Sprintf(" LIMIT $%d OFFSET $%d", argId, argId+1)
// If SortBy was one of the above, we might just append ASC? paginationArgs := append(args, limit, offset)
// But for now, rely on SortBy providing correct default or direction.
// HEAD relied on SortOrder. rows, err := s.DB.Query(paginationQuery, paginationArgs...)
} if err != nil {
} return nil, 0, err
}
baseQuery += sortClause defer rows.Close()
// Pagination jobs := []models.JobWithCompany{}
limit := filter.Limit for rows.Next() {
if limit == 0 { var j models.JobWithCompany
limit = 10 if err := rows.Scan(
} &j.ID, &j.CompanyID, &j.Title, &j.Description, &j.SalaryMin, &j.SalaryMax, &j.SalaryType,
if limit > 100 { &j.EmploymentType, &j.WorkMode, &j.WorkingHours, &j.Location, &j.Status, &j.SalaryNegotiable, &j.IsFeatured, &j.CreatedAt, &j.UpdatedAt,
limit = 100 &j.CompanyName, &j.CompanyLogoURL, &j.RegionName, &j.CityName,
} &j.ViewCount, &j.FeaturedUntil, &j.ApplicationsCount,
offset := (filter.Page - 1) * limit ); err != nil {
if offset < 0 { return nil, 0, err
offset = 0 }
} jobs = append(jobs, j)
}
paginationQuery := baseQuery + fmt.Sprintf(" LIMIT $%d OFFSET $%d", argId, argId+1)
paginationArgs := append(args, limit, offset) var total int
err = s.DB.QueryRow(countQuery, args...).Scan(&total)
rows, err := s.DB.Query(paginationQuery, paginationArgs...) if err != nil {
if err != nil { return nil, 0, err
return nil, 0, err }
}
defer rows.Close() return jobs, total, nil
}
jobs := []models.JobWithCompany{}
for rows.Next() { func (s *JobService) GetJobByID(id string) (*models.Job, error) {
var j models.JobWithCompany var j models.Job
if err := rows.Scan( query := `
&j.ID, &j.CompanyID, &j.Title, &j.Description, &j.SalaryMin, &j.SalaryMax, &j.SalaryType, SELECT id, company_id, title, description, salary_min, salary_max, salary_type,
&j.EmploymentType, &j.WorkMode, &j.WorkingHours, &j.Location, &j.Status, &j.SalaryNegotiable, &j.IsFeatured, &j.CreatedAt, &j.UpdatedAt, employment_type, working_hours, location, region_id, city_id,
<<<<<<< HEAD requirements, benefits, visa_support, language_level, status, is_featured, featured_until, view_count, created_at, updated_at,
&j.CompanyName, &j.CompanyLogoURL, &j.RegionName, &j.CityName, salary_negotiable, currency, work_mode
&j.ViewCount, &j.FeaturedUntil, FROM jobs WHERE id = $1
======= `
&j.CompanyName, &j.CompanyLogoURL, &j.RegionName, &j.CityName, &j.ApplicationsCount, err := s.DB.QueryRow(query, id).Scan(
>>>>>>> dev &j.ID, &j.CompanyID, &j.Title, &j.Description, &j.SalaryMin, &j.SalaryMax, &j.SalaryType,
); err != nil { &j.EmploymentType, &j.WorkingHours, &j.Location, &j.RegionID, &j.CityID,
return nil, 0, err &j.Requirements, &j.Benefits, &j.VisaSupport, &j.LanguageLevel, &j.Status, &j.IsFeatured, &j.FeaturedUntil, &j.ViewCount, &j.CreatedAt, &j.UpdatedAt,
} &j.SalaryNegotiable, &j.Currency, &j.WorkMode,
jobs = append(jobs, j) )
} if err != nil {
return nil, err
var total int }
err = s.DB.QueryRow(countQuery, args...).Scan(&total) return &j, nil
if err != nil { }
return nil, 0, err
} func (s *JobService) UpdateJob(id string, req dto.UpdateJobRequest) (*models.Job, error) {
var setClauses []string
return jobs, total, nil var args []interface{}
} argId := 1
func (s *JobService) GetJobByID(id string) (*models.Job, error) { if req.Title != nil {
var j models.Job setClauses = append(setClauses, fmt.Sprintf("title = $%d", argId))
query := ` args = append(args, *req.Title)
SELECT id, company_id, title, description, salary_min, salary_max, salary_type, argId++
employment_type, working_hours, location, region_id, city_id, }
requirements, benefits, visa_support, language_level, status, is_featured, featured_until, view_count, created_at, updated_at, if req.Description != nil {
salary_negotiable, currency, work_mode setClauses = append(setClauses, fmt.Sprintf("description = $%d", argId))
FROM jobs WHERE id = $1 args = append(args, *req.Description)
` argId++
// Added extra fields to SELECT to cover both models }
err := s.DB.QueryRow(query, id).Scan( if req.SalaryMin != nil {
&j.ID, &j.CompanyID, &j.Title, &j.Description, &j.SalaryMin, &j.SalaryMax, &j.SalaryType, setClauses = append(setClauses, fmt.Sprintf("salary_min = $%d", argId))
<<<<<<< HEAD args = append(args, *req.SalaryMin)
&j.EmploymentType, &j.WorkingHours, &j.Location, &j.RegionID, &j.CityID, argId++
&j.Requirements, &j.Benefits, &j.VisaSupport, &j.LanguageLevel, &j.Status, &j.IsFeatured, &j.FeaturedUntil, &j.ViewCount, &j.CreatedAt, &j.UpdatedAt, }
&j.SalaryNegotiable, &j.Currency, &j.WorkMode, if req.SalaryMax != nil {
======= setClauses = append(setClauses, fmt.Sprintf("salary_max = $%d", argId))
&j.EmploymentType, &j.WorkingHours, &j.Location, &j.RegionID, &j.CityID, &j.SalaryNegotiable, args = append(args, *req.SalaryMax)
&j.Requirements, &j.Benefits, &j.VisaSupport, &j.LanguageLevel, &j.Status, &j.CreatedAt, &j.UpdatedAt, argId++
>>>>>>> dev }
) if req.SalaryType != nil {
if err != nil { setClauses = append(setClauses, fmt.Sprintf("salary_type = $%d", argId))
return nil, err args = append(args, *req.SalaryType)
} argId++
return &j, nil }
} if req.Currency != nil {
setClauses = append(setClauses, fmt.Sprintf("currency = $%d", argId))
func (s *JobService) UpdateJob(id string, req dto.UpdateJobRequest) (*models.Job, error) { args = append(args, *req.Currency)
var setClauses []string argId++
var args []interface{} }
argId := 1 if req.EmploymentType != nil {
setClauses = append(setClauses, fmt.Sprintf("employment_type = $%d", argId))
if req.Title != nil { args = append(args, *req.EmploymentType)
setClauses = append(setClauses, fmt.Sprintf("title = $%d", argId)) argId++
args = append(args, *req.Title) }
argId++ if req.WorkingHours != nil {
} setClauses = append(setClauses, fmt.Sprintf("working_hours = $%d", argId))
if req.Description != nil { args = append(args, *req.WorkingHours)
setClauses = append(setClauses, fmt.Sprintf("description = $%d", argId)) argId++
args = append(args, *req.Description) }
argId++ if req.Location != nil {
} setClauses = append(setClauses, fmt.Sprintf("location = $%d", argId))
<<<<<<< HEAD args = append(args, *req.Location)
======= argId++
if req.SalaryMin != nil { }
setClauses = append(setClauses, fmt.Sprintf("salary_min = $%d", argId)) if req.RegionID != nil {
args = append(args, *req.SalaryMin) setClauses = append(setClauses, fmt.Sprintf("region_id = $%d", argId))
argId++ args = append(args, *req.RegionID)
} argId++
if req.SalaryMax != nil { }
setClauses = append(setClauses, fmt.Sprintf("salary_max = $%d", argId)) if req.CityID != nil {
args = append(args, *req.SalaryMax) setClauses = append(setClauses, fmt.Sprintf("city_id = $%d", argId))
argId++ args = append(args, *req.CityID)
} argId++
if req.SalaryType != nil { }
setClauses = append(setClauses, fmt.Sprintf("salary_type = $%d", argId)) if req.Requirements != nil {
args = append(args, *req.SalaryType) setClauses = append(setClauses, fmt.Sprintf("requirements = $%d", argId))
argId++ args = append(args, req.Requirements)
} argId++
if req.Currency != nil { }
setClauses = append(setClauses, fmt.Sprintf("currency = $%d", argId)) if req.Benefits != nil {
args = append(args, *req.Currency) setClauses = append(setClauses, fmt.Sprintf("benefits = $%d", argId))
argId++ args = append(args, req.Benefits)
} argId++
if req.EmploymentType != nil { }
setClauses = append(setClauses, fmt.Sprintf("employment_type = $%d", argId)) if req.Questions != nil {
args = append(args, *req.EmploymentType) setClauses = append(setClauses, fmt.Sprintf("questions = $%d", argId))
argId++ args = append(args, req.Questions)
} argId++
if req.WorkingHours != nil { }
setClauses = append(setClauses, fmt.Sprintf("working_hours = $%d", argId)) if req.VisaSupport != nil {
args = append(args, *req.WorkingHours) setClauses = append(setClauses, fmt.Sprintf("visa_support = $%d", argId))
argId++ args = append(args, *req.VisaSupport)
} argId++
if req.Location != nil { }
setClauses = append(setClauses, fmt.Sprintf("location = $%d", argId)) if req.LanguageLevel != nil {
args = append(args, *req.Location) setClauses = append(setClauses, fmt.Sprintf("language_level = $%d", argId))
argId++ args = append(args, *req.LanguageLevel)
} argId++
if req.RegionID != nil { }
setClauses = append(setClauses, fmt.Sprintf("region_id = $%d", argId)) if req.Status != nil {
args = append(args, *req.RegionID) setClauses = append(setClauses, fmt.Sprintf("status = $%d", argId))
argId++ args = append(args, *req.Status)
} argId++
if req.CityID != nil { }
setClauses = append(setClauses, fmt.Sprintf("city_id = $%d", argId)) if req.IsFeatured != nil {
args = append(args, *req.CityID) setClauses = append(setClauses, fmt.Sprintf("is_featured = $%d", argId))
argId++ args = append(args, *req.IsFeatured)
} argId++
if req.Requirements != nil { }
setClauses = append(setClauses, fmt.Sprintf("requirements = $%d", argId)) if req.FeaturedUntil != nil {
args = append(args, req.Requirements) setClauses = append(setClauses, fmt.Sprintf("featured_until = $%d", argId))
argId++ parsedTime, err := time.Parse(time.RFC3339, *req.FeaturedUntil)
} if err == nil {
if req.Benefits != nil { args = append(args, parsedTime)
setClauses = append(setClauses, fmt.Sprintf("benefits = $%d", argId)) } else {
args = append(args, req.Benefits) args = append(args, nil)
argId++ }
} argId++
if req.Questions != nil { }
setClauses = append(setClauses, fmt.Sprintf("questions = $%d", argId)) if req.SalaryNegotiable != nil {
args = append(args, req.Questions) setClauses = append(setClauses, fmt.Sprintf("salary_negotiable = $%d", argId))
argId++ args = append(args, *req.SalaryNegotiable)
} argId++
if req.VisaSupport != nil { }
setClauses = append(setClauses, fmt.Sprintf("visa_support = $%d", argId))
args = append(args, *req.VisaSupport) if len(setClauses) == 0 {
argId++ return s.GetJobByID(id)
} }
if req.LanguageLevel != nil {
setClauses = append(setClauses, fmt.Sprintf("language_level = $%d", argId)) setClauses = append(setClauses, "updated_at = NOW()")
args = append(args, *req.LanguageLevel)
argId++ query := fmt.Sprintf("UPDATE jobs SET %s WHERE id = $%d RETURNING id, updated_at", strings.Join(setClauses, ", "), argId)
} args = append(args, id)
>>>>>>> dev
if req.Status != nil { var j models.Job
setClauses = append(setClauses, fmt.Sprintf("status = $%d", argId)) err := s.DB.QueryRow(query, args...).Scan(&j.ID, &j.UpdatedAt)
args = append(args, *req.Status) if err != nil {
argId++ return nil, err
} }
if req.IsFeatured != nil {
setClauses = append(setClauses, fmt.Sprintf("is_featured = $%d", argId)) return s.GetJobByID(id)
args = append(args, *req.IsFeatured) }
argId++
} func (s *JobService) DeleteJob(id string) error {
if req.FeaturedUntil != nil { _, err := s.DB.Exec("DELETE FROM jobs WHERE id = $1", id)
setClauses = append(setClauses, fmt.Sprintf("featured_until = $%d", argId)) return err
// HEAD had string parsing. hml didn't show parsing logic but request field might be string. }
// Assuming ISO8601 string from DTO.
parsedTime, err := time.Parse(time.RFC3339, *req.FeaturedUntil)
if err == nil {
args = append(args, parsedTime)
} else {
// Fallback or error? For now fallback null or skip
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 req.Currency != nil {
setClauses = append(setClauses, fmt.Sprintf("currency = $%d", argId))
args = append(args, *req.Currency)
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
}