package services import ( "context" "database/sql" "encoding/json" "fmt" "strings" "time" "github.com/lib/pq" "github.com/rede5/gohorsejobs/backend/internal/dto" "github.com/rede5/gohorsejobs/backend/internal/models" ) type AdminService struct { DB *sql.DB } func NewAdminService(db *sql.DB) *AdminService { return &AdminService{DB: db} } func (s *AdminService) ListCompanies(ctx context.Context, verified *bool, page, limit int) ([]models.Company, int, error) { offset := (page - 1) * limit // Count Total // Count Total countQuery := `SELECT COUNT(*) FROM companies WHERE type != 'CANDIDATE_WORKSPACE'` var countArgs []interface{} if verified != nil { countQuery += " AND verified = $1" countArgs = append(countArgs, *verified) } var total int if err := s.DB.QueryRowContext(ctx, countQuery, countArgs...).Scan(&total); err != nil { return nil, 0, err } // Fetch Data baseQuery := ` SELECT id, name, slug, type, document, address, region_id, city_id, phone, email, website, logo_url, description, active, verified, created_at, updated_at FROM companies WHERE type != 'CANDIDATE_WORKSPACE' ` var args []interface{} if verified != nil { baseQuery += " AND verified = $1" args = append(args, *verified) } // Add pagination baseQuery += fmt.Sprintf(" ORDER BY created_at DESC LIMIT $%d OFFSET $%d", len(args)+1, len(args)+2) args = append(args, limit, offset) rows, err := s.DB.QueryContext(ctx, baseQuery, args...) if err != nil { return nil, 0, err } defer rows.Close() companies := []models.Company{} for rows.Next() { var c models.Company if err := rows.Scan( &c.ID, &c.Name, &c.Slug, &c.Type, &c.Document, &c.Address, &c.RegionID, &c.CityID, &c.Phone, &c.Email, &c.Website, &c.LogoURL, &c.Description, &c.Active, &c.Verified, &c.CreatedAt, &c.UpdatedAt, ); err != nil { return nil, 0, err } companies = append(companies, c) } return companies, total, nil } // ListUsers returns all users with pagination (for admin view) // If companyID is provided, filters users by that company. func (s *AdminService) ListUsers(ctx context.Context, page, limit int, companyID *string) ([]dto.User, int, error) { offset := (page - 1) * limit // Count Total countQuery := `SELECT COUNT(*) FROM users` var countArgs []interface{} if companyID != nil && *companyID != "" { countQuery += ` WHERE tenant_id = $1` countArgs = append(countArgs, *companyID) } var total int if err := s.DB.QueryRowContext(ctx, countQuery, countArgs...).Scan(&total); err != nil { return nil, 0, err } // Fetch Data query := ` SELECT id, COALESCE(name, full_name, identifier, ''), email, role, COALESCE(status, 'active'), created_at FROM users ` var args []interface{} if companyID != nil && *companyID != "" { query += ` WHERE tenant_id = $1` args = append(args, *companyID) } query += ` ORDER BY created_at DESC` query += fmt.Sprintf(` LIMIT $%d OFFSET $%d`, len(args)+1, len(args)+2) args = append(args, limit, offset) rows, err := s.DB.QueryContext(ctx, query, args...) if err != nil { return nil, 0, err } defer rows.Close() users := []dto.User{} for rows.Next() { var u dto.User var roleStr string if err := rows.Scan(&u.ID, &u.Name, &u.Email, &roleStr, &u.Status, &u.CreatedAt); err != nil { return nil, 0, err } u.Role = roleStr users = append(users, u) } return users, total, nil } func (s *AdminService) UpdateCompanyStatus(ctx context.Context, id string, active *bool, verified *bool) (*models.Company, error) { company, err := s.GetCompanyByID(ctx, id) if err != nil { return nil, err } if active != nil { company.Active = *active } if verified != nil { company.Verified = *verified } company.UpdatedAt = time.Now() query := ` UPDATE companies SET active = $1, verified = $2, updated_at = $3 WHERE id = $4 ` _, err = s.DB.ExecContext(ctx, query, company.Active, company.Verified, company.UpdatedAt, id) if err != nil { return nil, err } return company, nil } func (s *AdminService) DuplicateJob(ctx context.Context, id string) (*models.Job, error) { query := ` SELECT company_id, created_by, title, description, salary_min, salary_max, salary_type, employment_type, work_mode, working_hours, location, region_id, city_id, requirements, benefits, visa_support, language_level FROM jobs WHERE id = $1 ` var job models.Job if err := s.DB.QueryRowContext(ctx, query, id).Scan( &job.CompanyID, &job.CreatedBy, &job.Title, &job.Description, &job.SalaryMin, &job.SalaryMax, &job.SalaryType, &job.EmploymentType, &job.WorkMode, &job.WorkingHours, &job.Location, &job.RegionID, &job.CityID, &job.Requirements, &job.Benefits, &job.VisaSupport, &job.LanguageLevel, ); err != nil { return nil, err } job.Status = "draft" job.IsFeatured = false job.CreatedAt = time.Now() job.UpdatedAt = time.Now() insertQuery := ` INSERT INTO jobs ( company_id, created_by, title, description, salary_min, salary_max, salary_type, employment_type, work_mode, working_hours, location, region_id, city_id, requirements, benefits, visa_support, language_level, status, is_featured, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) RETURNING id ` if err := s.DB.QueryRowContext(ctx, insertQuery, job.CompanyID, job.CreatedBy, job.Title, job.Description, job.SalaryMin, job.SalaryMax, job.SalaryType, job.EmploymentType, job.WorkMode, job.WorkingHours, job.Location, job.RegionID, job.CityID, job.Requirements, job.Benefits, job.VisaSupport, job.LanguageLevel, job.Status, job.IsFeatured, job.CreatedAt, job.UpdatedAt, ).Scan(&job.ID); err != nil { return nil, err } return &job, nil } func (s *AdminService) ListTags(ctx context.Context, category *string) ([]models.Tag, error) { baseQuery := `SELECT id, name, category, active, created_at, updated_at FROM job_tags` var args []interface{} if category != nil && *category != "" { baseQuery += " WHERE category = $1" args = append(args, *category) } baseQuery += " ORDER BY name ASC" rows, err := s.DB.QueryContext(ctx, baseQuery, args...) if err != nil { return nil, err } defer rows.Close() tags := []models.Tag{} for rows.Next() { var t models.Tag if err := rows.Scan(&t.ID, &t.Name, &t.Category, &t.Active, &t.CreatedAt, &t.UpdatedAt); err != nil { return nil, err } tags = append(tags, t) } return tags, nil } func (s *AdminService) CreateTag(ctx context.Context, name string, category string) (*models.Tag, error) { if strings.TrimSpace(name) == "" { return nil, fmt.Errorf("tag name is required") } now := time.Now() tag := models.Tag{ Name: strings.TrimSpace(name), Category: category, Active: true, CreatedAt: now, UpdatedAt: now, } query := ` INSERT INTO job_tags (name, category, active, created_at, updated_at) VALUES ($1, $2, $3, $4, $5) RETURNING id ` if err := s.DB.QueryRowContext(ctx, query, tag.Name, tag.Category, tag.Active, tag.CreatedAt, tag.UpdatedAt).Scan(&tag.ID); err != nil { return nil, err } return &tag, nil } func (s *AdminService) UpdateTag(ctx context.Context, id int, name *string, active *bool) (*models.Tag, error) { tag, err := s.getTagByID(ctx, id) if err != nil { return nil, err } if name != nil { trimmed := strings.TrimSpace(*name) if trimmed != "" { tag.Name = trimmed } } if active != nil { tag.Active = *active } tagUpdatedAt := time.Now() query := ` UPDATE job_tags SET name = $1, active = $2, updated_at = $3 WHERE id = $4 ` _, err = s.DB.ExecContext(ctx, query, tag.Name, tag.Active, tagUpdatedAt, id) if err != nil { return nil, err } tag.UpdatedAt = tagUpdatedAt return tag, nil } func (s *AdminService) ListCandidates(ctx context.Context, companyID *string, page, perPage int) ([]dto.Candidate, dto.CandidateStats, dto.PaginationInfo, error) { fmt.Println("[DEBUG] Starting ListCandidates") // Default pagination values if page < 1 { page = 1 } if perPage < 1 { perPage = 10 } if perPage > 100 { perPage = 100 } // Get total count first var totalItems int countQuery := `SELECT COUNT(*) FROM users WHERE role = 'candidate'` if err := s.DB.QueryRowContext(ctx, countQuery).Scan(&totalItems); err != nil { fmt.Println("[ERROR] Count query failed:", err) return nil, dto.CandidateStats{}, dto.PaginationInfo{}, err } totalPages := (totalItems + perPage - 1) / perPage offset := (page - 1) * perPage query := ` SELECT id, full_name, email, phone, city, state, title, experience, bio, skills, avatar_url, created_at FROM users WHERE role = 'candidate' ORDER BY created_at DESC LIMIT $1 OFFSET $2 ` fmt.Println("[DEBUG] Executing query:", query) rows, err := s.DB.QueryContext(ctx, query, perPage, offset) if err != nil { fmt.Println("[ERROR] Query failed:", err) return nil, dto.CandidateStats{}, dto.PaginationInfo{}, err } defer rows.Close() candidates := make([]dto.Candidate, 0) candidateIndex := make(map[string]int) // ID is string (UUID) candidateIDs := make([]string, 0) // ID is string (UUID) stats := dto.CandidateStats{} thirtyDaysAgo := time.Now().AddDate(0, 0, -30) for rows.Next() { var ( id string // Changed to string for UUID fullName string email sql.NullString phone sql.NullString city sql.NullString state sql.NullString title sql.NullString experience sql.NullString bio sql.NullString avatarURL sql.NullString skills []string createdAt time.Time ) if err := rows.Scan( &id, &fullName, &email, &phone, &city, &state, &title, &experience, &bio, pq.Array(&skills), &avatarURL, &createdAt, ); err != nil { fmt.Println("[ERROR] Scan failed:", err) return nil, dto.CandidateStats{}, dto.PaginationInfo{}, err } location := buildLocation(city, state) candidate := dto.Candidate{ ID: id, // Check if this compiles! Name: fullName, Email: stringOrNil(email), Phone: stringOrNil(phone), Location: location, Title: stringOrNil(title), Experience: stringOrNil(experience), Bio: stringOrNil(bio), AvatarURL: stringOrNil(avatarURL), Skills: normalizeSkills(skills), Applications: []dto.CandidateApplication{}, CreatedAt: createdAt, } if createdAt.After(thirtyDaysAgo) { stats.NewCandidates++ } candidateIndex[id] = len(candidates) candidateIDs = append(candidateIDs, id) candidates = append(candidates, candidate) } fmt.Printf("[DEBUG] Found %d candidates\n", len(candidates)) // Get total stats (not paginated) stats.TotalCandidates = totalItems // Count new candidates in last 30 days var newCount int newCountQuery := `SELECT COUNT(*) FROM users WHERE role = 'candidate' AND created_at > $1` if err := s.DB.QueryRowContext(ctx, newCountQuery, thirtyDaysAgo).Scan(&newCount); err == nil { stats.NewCandidates = newCount } if len(candidateIDs) == 0 { pagination := dto.PaginationInfo{ Page: page, PerPage: perPage, TotalPages: totalPages, TotalItems: totalItems, } return candidates, stats, pagination, nil } appQuery := ` SELECT a.id, a.user_id, a.status, a.created_at, j.title, c.name FROM applications a JOIN jobs j ON j.id = a.job_id JOIN companies c ON c.id = j.company_id WHERE a.user_id = ANY($1) ORDER BY a.created_at DESC ` fmt.Println("[DEBUG] Executing appQuery") appRows, err := s.DB.QueryContext(ctx, appQuery, pq.Array(candidateIDs)) if err != nil { fmt.Println("[ERROR] AppQuery failed:", err) return nil, dto.CandidateStats{}, dto.PaginationInfo{}, err } defer appRows.Close() totalApplications := 0 hiredApplications := 0 for appRows.Next() { var ( app dto.CandidateApplication userID string // UUID ) if err := appRows.Scan( &app.ID, &userID, &app.Status, &app.AppliedAt, &app.JobTitle, &app.Company, ); err != nil { fmt.Println("[ERROR] AppList Scan failed:", err) return nil, dto.CandidateStats{}, dto.PaginationInfo{}, err } totalApplications++ if isActiveApplicationStatus(app.Status) { stats.ActiveApplications++ } if app.Status == "hired" { hiredApplications++ } if index, ok := candidateIndex[userID]; ok { candidates[index].Applications = append(candidates[index].Applications, app) } } fmt.Printf("[DEBUG] Processed %d applications\n", totalApplications) if totalApplications > 0 { stats.HiringRate = (float64(hiredApplications) / float64(totalApplications)) * 100 } pagination := dto.PaginationInfo{ Page: page, PerPage: perPage, TotalPages: totalPages, TotalItems: totalItems, } return candidates, stats, pagination, nil } func stringOrNil(value sql.NullString) *string { if !value.Valid { return nil } trimmed := strings.TrimSpace(value.String) if trimmed == "" { return nil } return &trimmed } func buildLocation(city sql.NullString, state sql.NullString) *string { parts := make([]string, 0, 2) if city.Valid && strings.TrimSpace(city.String) != "" { parts = append(parts, strings.TrimSpace(city.String)) } if state.Valid && strings.TrimSpace(state.String) != "" { parts = append(parts, strings.TrimSpace(state.String)) } if len(parts) == 0 { return nil } location := strings.Join(parts, ", ") return &location } func normalizeSkills(skills []string) []string { if len(skills) == 0 { return []string{} } normalized := make([]string, 0, len(skills)) for _, skill := range skills { trimmed := strings.TrimSpace(skill) if trimmed == "" { continue } normalized = append(normalized, trimmed) } if len(normalized) == 0 { return []string{} } return normalized } func isActiveApplicationStatus(status string) bool { switch status { case "pending", "reviewed", "shortlisted": return true default: return false } } func (s *AdminService) GetCompanyOwner(ctx context.Context, companyID string) (*dto.User, error) { query := ` SELECT id, email, full_name FROM users WHERE tenant_id = $1 AND role IN ('admin', 'company') LIMIT 1 ` var u dto.User err := s.DB.QueryRowContext(ctx, query, companyID).Scan(&u.ID, &u.Email, &u.Name) if err != nil { return nil, err } return &u, nil } func (s *AdminService) GetCompanyByID(ctx context.Context, id string) (*models.Company, error) { query := ` SELECT id, name, slug, type, document, address, region_id, city_id, phone, email, website, logo_url, description, active, verified, created_at, updated_at FROM companies WHERE id = $1 ` var c models.Company if err := s.DB.QueryRowContext(ctx, query, id).Scan( &c.ID, &c.Name, &c.Slug, &c.Type, &c.Document, &c.Address, &c.RegionID, &c.CityID, &c.Phone, &c.Email, &c.Website, &c.LogoURL, &c.Description, &c.Active, &c.Verified, &c.CreatedAt, &c.UpdatedAt, ); err != nil { return nil, err } return &c, nil } func (s *AdminService) getTagByID(ctx context.Context, id int) (*models.Tag, error) { query := `SELECT id, name, category, active, created_at, updated_at FROM job_tags WHERE id = $1` var t models.Tag if err := s.DB.QueryRowContext(ctx, query, id).Scan(&t.ID, &t.Name, &t.Category, &t.Active, &t.CreatedAt, &t.UpdatedAt); err != nil { return nil, err } return &t, nil } // GetUser fetches a user by ID func (s *AdminService) GetUser(ctx context.Context, id string) (*dto.User, error) { query := ` SELECT id, full_name, email, role, COALESCE(status, 'active'), created_at, phone, bio, avatar_url FROM users WHERE id = $1 ` var u dto.User var roleStr string var phone sql.NullString var bio sql.NullString var avatarURL sql.NullString if err := s.DB.QueryRowContext(ctx, query, id).Scan( &u.ID, &u.Name, &u.Email, &roleStr, &u.Status, &u.CreatedAt, &phone, &bio, &avatarURL, ); err != nil { return nil, err } u.Role = roleStr if phone.Valid { u.Phone = &phone.String } if bio.Valid { u.Bio = &bio.String } if avatarURL.Valid { u.AvatarUrl = &avatarURL.String } // Fetch roles roles, err := s.getUserRoles(ctx, u.ID) if err == nil { u.Roles = roles } return &u, nil } func (s *AdminService) getUserRoles(ctx context.Context, userID string) ([]string, error) { 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 := s.DB.QueryContext(ctx, query, userID) if err != nil { return nil, err } defer rows.Close() var roles []string seen := make(map[string]bool) for rows.Next() { var roleName string if err := rows.Scan(&roleName); err == nil { if !seen[roleName] { roles = append(roles, roleName) seen[roleName] = true } } } return roles, nil } // GetCompanyByUserID fetches the company associated with a user func (s *AdminService) GetCompanyByUserID(ctx context.Context, userID string) (*models.Company, error) { // First, try to find company where this user is admin // Assuming users table has company_id or companies table has admin_email // Let's check if 'users' has company_id column via error or assume architecture. // Since CreateCompany creates a user, it likely links them. // I will try to find a company by created_by = user_id IF that column exists? // Or query based on some relation. // Let's try finding company by admin_email matching user email. // Fetch user email first user, err := s.GetUser(ctx, userID) if err != nil { return nil, err } query := `SELECT id, name, slug, active, verified FROM companies WHERE email = $1` var c models.Company if err := s.DB.QueryRowContext(ctx, query, user.Email).Scan(&c.ID, &c.Name, &c.Slug, &c.Active, &c.Verified); err != nil { // Try another way? Join companies c JOIN users u ON u.company_id = c.id // If users table has company_id column... // Let's try that as fallback. query2 := ` SELECT c.id, c.name, c.slug, c.active, c.verified FROM companies c JOIN users u ON u.company_id = c.id WHERE u.id = $1 ` if err2 := s.DB.QueryRowContext(ctx, query2, userID).Scan(&c.ID, &c.Name, &c.Slug, &c.Active, &c.Verified); err2 != nil { return nil, fmt.Errorf("company not found for user %s", userID) } } return &c, nil } func (s *AdminService) UpdateCompany(ctx context.Context, id string, req dto.UpdateCompanyRequest) (*models.Company, error) { company, err := s.GetCompanyByID(ctx, id) if err != nil { return nil, err } if req.Name != nil { company.Name = *req.Name } if req.Slug != nil { company.Slug = *req.Slug } if req.Type != nil { company.Type = *req.Type } if req.Document != nil { company.Document = req.Document } if req.Address != nil { company.Address = req.Address } if req.RegionID != nil { company.RegionID = req.RegionID } if req.CityID != nil { company.CityID = req.CityID } if req.Phone != nil { company.Phone = req.Phone } if req.Email != nil { company.Email = req.Email } if req.Website != nil { company.Website = req.Website } if req.LogoURL != nil { company.LogoURL = req.LogoURL } if req.Description != nil { company.Description = req.Description } if req.Active != nil { company.Active = *req.Active } if req.Verified != nil { company.Verified = *req.Verified } company.UpdatedAt = time.Now() query := ` UPDATE companies SET name=$1, slug=$2, type=$3, document=$4, address=$5, region_id=$6, city_id=$7, phone=$8, email=$9, website=$10, logo_url=$11, description=$12, active=$13, verified=$14, updated_at=$15 WHERE id=$16 ` _, err = s.DB.ExecContext(ctx, query, company.Name, company.Slug, company.Type, company.Document, company.Address, company.RegionID, company.CityID, company.Phone, company.Email, company.Website, company.LogoURL, company.Description, company.Active, company.Verified, company.UpdatedAt, company.ID, ) if err != nil { return nil, err } return company, nil } func (s *AdminService) DeleteCompany(ctx context.Context, id string) error { // First check if exists _, err := s.GetCompanyByID(ctx, id) if err != nil { return err } tx, err := s.DB.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() // Delete jobs if _, err := tx.ExecContext(ctx, `DELETE FROM jobs WHERE company_id=$1`, id); err != nil { return err } // Delete users if _, err := tx.ExecContext(ctx, `DELETE FROM users WHERE tenant_id=$1`, id); err != nil { return err } // Delete company if _, err := tx.ExecContext(ctx, `DELETE FROM companies WHERE id=$1`, id); err != nil { return err } return tx.Commit() } // ============================================================================ // Email Templates & Settings CRUD // ============================================================================ func (s *AdminService) ListEmailTemplates(ctx context.Context) ([]dto.EmailTemplateDTO, error) { query := `SELECT id, slug, subject, body_html, variables, created_at, updated_at FROM email_templates ORDER BY slug` rows, err := s.DB.QueryContext(ctx, query) if err != nil { return nil, err } defer rows.Close() templates := []dto.EmailTemplateDTO{} for rows.Next() { var t dto.EmailTemplateDTO var varsJSON []byte if err := rows.Scan(&t.ID, &t.Slug, &t.Subject, &t.BodyHTML, &varsJSON, &t.CreatedAt, &t.UpdatedAt); err != nil { return nil, err } if len(varsJSON) > 0 { json.Unmarshal(varsJSON, &t.Variables) } templates = append(templates, t) } return templates, nil } func (s *AdminService) GetEmailTemplate(ctx context.Context, slug string) (*dto.EmailTemplateDTO, error) { query := `SELECT id, slug, subject, body_html, variables, created_at, updated_at FROM email_templates WHERE slug = $1` row := s.DB.QueryRowContext(ctx, query, slug) t := &dto.EmailTemplateDTO{} var varsJSON []byte err := row.Scan(&t.ID, &t.Slug, &t.Subject, &t.BodyHTML, &varsJSON, &t.CreatedAt, &t.UpdatedAt) if err != nil { if err == sql.ErrNoRows { return nil, nil } return nil, err } if len(varsJSON) > 0 { json.Unmarshal(varsJSON, &t.Variables) } return t, nil } func (s *AdminService) CreateEmailTemplate(ctx context.Context, req dto.CreateEmailTemplateRequest) (*dto.EmailTemplateDTO, error) { query := `INSERT INTO email_templates (slug, subject, body_html, variables, updated_at) VALUES ($1, $2, $3, $4, $5) RETURNING id, created_at` varsJSON, _ := json.Marshal(req.Variables) if varsJSON == nil { varsJSON = []byte("[]") } t := &dto.EmailTemplateDTO{ Slug: req.Slug, Subject: req.Subject, BodyHTML: req.BodyHTML, Variables: req.Variables, } err := s.DB.QueryRowContext(ctx, query, req.Slug, req.Subject, req.BodyHTML, varsJSON, time.Now()).Scan(&t.ID, &t.CreatedAt) if err != nil { return nil, err } t.UpdatedAt = t.CreatedAt return t, nil } func (s *AdminService) UpdateEmailTemplate(ctx context.Context, slug string, req dto.UpdateEmailTemplateRequest) (*dto.EmailTemplateDTO, error) { // Fetch existing existing, err := s.GetEmailTemplate(ctx, slug) if err != nil { return nil, err } if existing == nil { return nil, fmt.Errorf("template not found") } // Apply updates if req.Subject != nil { existing.Subject = *req.Subject } if req.BodyHTML != nil { existing.BodyHTML = *req.BodyHTML } if req.Variables != nil { existing.Variables = *req.Variables } varsJSON, _ := json.Marshal(existing.Variables) query := `UPDATE email_templates SET subject=$1, body_html=$2, variables=$3, updated_at=$4 WHERE slug=$5` _, err = s.DB.ExecContext(ctx, query, existing.Subject, existing.BodyHTML, varsJSON, time.Now(), slug) if err != nil { return nil, err } return s.GetEmailTemplate(ctx, slug) } func (s *AdminService) DeleteEmailTemplate(ctx context.Context, slug string) error { _, err := s.DB.ExecContext(ctx, "DELETE FROM email_templates WHERE slug=$1", slug) return err } func (s *AdminService) GetEmailSettings(ctx context.Context) (*dto.EmailSettingsDTO, error) { query := `SELECT id, provider, smtp_host, smtp_port, smtp_user, smtp_pass, smtp_secure, sender_name, sender_email, amqp_url, is_active, updated_at FROM email_settings WHERE is_active = true ORDER BY updated_at DESC LIMIT 1` row := s.DB.QueryRowContext(ctx, query) var s_ dto.EmailSettingsDTO err := row.Scan( &s_.ID, &s_.Provider, &s_.SMTPHost, &s_.SMTPPort, &s_.SMTPUser, &s_.SMTPPass, &s_.SMTPSecure, &s_.SenderName, &s_.SenderEmail, &s_.AMQPURL, &s_.IsActive, &s_.UpdatedAt, ) if err != nil { if err == sql.ErrNoRows { return nil, nil } return nil, err } return &s_, nil } func (s *AdminService) UpdateEmailSettings(ctx context.Context, req dto.UpdateEmailSettingsRequest) (*dto.EmailSettingsDTO, error) { existing, err := s.GetEmailSettings(ctx) if err != nil { return nil, err } if existing == nil { // Insert new query := `INSERT INTO email_settings (provider, smtp_host, smtp_port, smtp_user, smtp_pass, smtp_secure, sender_name, sender_email, amqp_url, is_active) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, true) RETURNING id, updated_at` newS := &dto.EmailSettingsDTO{ Provider: "smtp", SMTPSecure: true, SenderName: "GoHorse Jobs", SenderEmail: "no-reply@gohorsejobs.com", IsActive: true, } applyEmailSettingsUpdate(newS, req) err = s.DB.QueryRowContext(ctx, query, newS.Provider, newS.SMTPHost, newS.SMTPPort, newS.SMTPUser, newS.SMTPPass, newS.SMTPSecure, newS.SenderName, newS.SenderEmail, newS.AMQPURL).Scan(&newS.ID, &newS.UpdatedAt) return newS, err } // Update existing applyEmailSettingsUpdate(existing, req) query := `UPDATE email_settings SET provider=$1, smtp_host=$2, smtp_port=$3, smtp_user=$4, smtp_pass=$5, smtp_secure=$6, sender_name=$7, sender_email=$8, amqp_url=$9, updated_at=$10 WHERE id=$11` _, err = s.DB.ExecContext(ctx, query, existing.Provider, existing.SMTPHost, existing.SMTPPort, existing.SMTPUser, existing.SMTPPass, existing.SMTPSecure, existing.SenderName, existing.SenderEmail, existing.AMQPURL, time.Now(), existing.ID) if err != nil { return nil, err } return s.GetEmailSettings(ctx) } func applyEmailSettingsUpdate(s *dto.EmailSettingsDTO, req dto.UpdateEmailSettingsRequest) { if req.Provider != nil { s.Provider = *req.Provider } if req.SMTPHost != nil { s.SMTPHost = req.SMTPHost } if req.SMTPPort != nil { s.SMTPPort = req.SMTPPort } if req.SMTPUser != nil { s.SMTPUser = req.SMTPUser } if req.SMTPPass != nil { s.SMTPPass = req.SMTPPass } if req.SMTPSecure != nil { s.SMTPSecure = *req.SMTPSecure } if req.SenderName != nil { s.SenderName = *req.SenderName } if req.SenderEmail != nil { s.SenderEmail = *req.SenderEmail } if req.AMQPURL != nil { s.AMQPURL = req.AMQPURL } }