Files
MOHPortalTest-AllAgents-All…/qwen/go/services/services.go
2025-10-24 16:29:40 -05:00

582 lines
16 KiB
Go

package services
import (
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"time"
"mohportal/db"
"mohportal/models"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// TenantService handles tenant-related operations
type TenantService struct{}
// CreateTenant creates a new tenant
func (ts *TenantService) CreateTenant(name, slug, description, logoURL string) (*models.Tenant, error) {
tenant := &models.Tenant{
ID: uuid.New(),
Name: name,
Slug: slug,
Description: description,
LogoURL: logoURL,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
IsActive: true,
}
if err := db.DB.Create(tenant).Error; err != nil {
return nil, err
}
return tenant, nil
}
// GetTenant retrieves a tenant by ID
func (ts *TenantService) GetTenant(id uuid.UUID) (*models.Tenant, error) {
var tenant models.Tenant
if err := db.DB.First(&tenant, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("tenant not found")
}
return nil, err
}
return &tenant, nil
}
// GetTenantBySlug retrieves a tenant by slug
func (ts *TenantService) GetTenantBySlug(slug string) (*models.Tenant, error) {
var tenant models.Tenant
if err := db.DB.First(&tenant, "slug = ?", slug).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("tenant not found")
}
return nil, err
}
return &tenant, nil
}
// GetTenants retrieves all tenants (with optional filtering)
func (ts *TenantService) GetTenants(limit, offset int) ([]*models.Tenant, error) {
var tenants []*models.Tenant
query := db.DB.Limit(limit).Offset(offset)
if err := query.Find(&tenants).Error; err != nil {
return nil, err
}
return tenants, nil
}
// UpdateTenant updates an existing tenant
func (ts *TenantService) UpdateTenant(id uuid.UUID, name, slug, description, logoURL string) (*models.Tenant, error) {
var tenant models.Tenant
if err := db.DB.First(&tenant, "id = ?", id).Error; err != nil {
return nil, err
}
tenant.Name = name
tenant.Slug = slug
tenant.Description = description
tenant.LogoURL = logoURL
tenant.UpdatedAt = time.Now()
if err := db.DB.Save(&tenant).Error; err != nil {
return nil, err
}
return &tenant, nil
}
// DeleteTenant deletes a tenant
func (ts *TenantService) DeleteTenant(id uuid.UUID) error {
return db.DB.Delete(&models.Tenant{}, "id = ?", id).Error
}
// UserService handles user-related operations
type UserService struct{}
// CreateUser creates a new user
func (us *UserService) CreateUser(tenantID uuid.UUID, email, username, firstName, lastName, phone, role, password string) (*models.User, error) {
// Check if user already exists
var existingUser models.User
if err := db.DB.Where("email = ? OR username = ?", email, username).First(&existingUser).Error; err == nil {
return nil, errors.New("user with email or username already exists")
}
// Hash the password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
user := &models.User{
ID: uuid.New(),
TenantID: tenantID,
Email: email,
Username: username,
FirstName: firstName,
LastName: lastName,
Phone: phone,
Role: role,
PasswordHash: string(hashedPassword),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
IsActive: true,
}
if err := db.DB.Create(user).Error; err != nil {
return nil, err
}
return user, nil
}
// GetUser retrieves a user by ID
func (us *UserService) GetUser(id uuid.UUID) (*models.User, error) {
var user models.User
if err := db.DB.Preload("Tenant").First(&user, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("user not found")
}
return nil, err
}
return &user, nil
}
// GetUserByEmail retrieves a user by email
func (us *UserService) GetUserByEmail(email string) (*models.User, error) {
var user models.User
if err := db.DB.Preload("Tenant").First(&user, "email = ?", email).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("user not found")
}
return nil, err
}
return &user, nil
}
// AuthenticateUser authenticates a user by email and password
func (us *UserService) AuthenticateUser(email, password string) (*models.User, error) {
var user models.User
if err := db.DB.First(&user, "email = ?", email).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// Return error without specifying whether email exists to prevent timing attacks
return nil, errors.New("invalid credentials")
}
return nil, err
}
if !user.IsActive {
return nil, errors.New("user account is deactivated")
}
// Compare the password
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
return nil, errors.New("invalid credentials")
}
// Update last login
loginTime := time.Now()
user.LastLogin = &loginTime
db.DB.Save(&user)
return &user, nil
}
// UpdateUser updates an existing user
func (us *UserService) UpdateUser(id uuid.UUID, email, username, firstName, lastName, phone, role string) (*models.User, error) {
var user models.User
if err := db.DB.First(&user, "id = ?", id).Error; err != nil {
return nil, err
}
// Check if email or username is already taken by another user
var existingUser models.User
if err := db.DB.Where("email = ? AND id != ?", email, id).First(&existingUser).Error; err == nil {
return nil, errors.New("email already in use by another user")
}
if username != "" {
if err := db.DB.Where("username = ? AND id != ?", username, id).First(&existingUser).Error; err == nil {
return nil, errors.New("username already in use by another user")
}
}
user.Email = email
if username != "" {
user.Username = username
}
user.FirstName = firstName
user.LastName = lastName
user.Phone = phone
user.Role = role
user.UpdatedAt = time.Now()
if err := db.DB.Save(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
// PositionService handles job position-related operations
type PositionService struct{}
// CreatePosition creates a new job position
func (ps *PositionService) CreatePosition(tenantID, userID uuid.UUID, title, description, requirements, location, employmentType, experienceLevel string, salaryMin, salaryMax *float64) (*models.JobPosition, error) {
position := &models.JobPosition{
ID: uuid.New(),
TenantID: tenantID,
UserID: userID,
Title: title,
Description: description,
Requirements: requirements,
Location: location,
EmploymentType: employmentType,
SalaryMin: salaryMin,
SalaryMax: salaryMax,
ExperienceLevel: experienceLevel,
PostedAt: time.Now(),
Status: "open",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
IsActive: true,
}
if err := db.DB.Create(position).Error; err != nil {
return nil, err
}
return position, nil
}
// GetPosition retrieves a position by ID
func (ps *PositionService) GetPosition(id uuid.UUID) (*models.JobPosition, error) {
var position models.JobPosition
if err := db.DB.Preload("User").First(&position, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("position not found")
}
return nil, err
}
return &position, nil
}
// GetPositions retrieves positions with optional filtering
func (ps *PositionService) GetPositions(tenantID *uuid.UUID, limit, offset int, status, employmentType, experienceLevel, location string) ([]*models.JobPosition, error) {
var positions []*models.JobPosition
query := db.DB.Preload("User").Limit(limit).Offset(offset)
if tenantID != nil {
query = query.Where("tenant_id = ?", tenantID)
}
if status != "" {
query = query.Where("status = ?", status)
}
if employmentType != "" {
query = query.Where("employment_type = ?", employmentType)
}
if experienceLevel != "" {
query = query.Where("experience_level = ?", experienceLevel)
}
if location != "" {
query = query.Where("location LIKE ?", "%"+location+"%")
}
query = query.Where("is_active = ? AND status = ?", true, "open")
if err := query.Find(&positions).Error; err != nil {
return nil, err
}
return positions, nil
}
// UpdatePosition updates an existing position
func (ps *PositionService) UpdatePosition(id uuid.UUID, title, description, requirements, location, employmentType, experienceLevel string, salaryMin, salaryMax *float64) (*models.JobPosition, error) {
var position models.JobPosition
if err := db.DB.First(&position, "id = ?", id).Error; err != nil {
return nil, err
}
position.Title = title
position.Description = description
position.Requirements = requirements
position.Location = location
position.EmploymentType = employmentType
position.ExperienceLevel = experienceLevel
position.SalaryMin = salaryMin
position.SalaryMax = salaryMax
position.UpdatedAt = time.Now()
if err := db.DB.Save(&position).Error; err != nil {
return nil, err
}
return &position, nil
}
// ClosePosition closes a position (marks as filled or cancelled)
func (ps *PositionService) ClosePosition(id uuid.UUID, status string) (*models.JobPosition, error) {
var position models.JobPosition
if err := db.DB.First(&position, "id = ?", id).Error; err != nil {
return nil, err
}
if status != "closed" && status != "filled" {
return nil, errors.New("invalid status for closing position")
}
position.Status = status
closedTime := time.Now()
position.ClosedAt = &closedTime
position.UpdatedAt = time.Now()
if err := db.DB.Save(&position).Error; err != nil {
return nil, err
}
return &position, nil
}
// DeletePosition deletes a position
func (ps *PositionService) DeletePosition(id uuid.UUID) error {
return db.DB.Delete(&models.JobPosition{}, "id = ?", id).Error
}
// ResumeService handles resume-related operations
type ResumeService struct{}
// UploadResume uploads a resume file and creates a record
func (rs *ResumeService) UploadResume(userID uuid.UUID, fileHeader *multipart.FileHeader, title string) (*models.Resume, error) {
// Validate file type
allowedTypes := map[string]bool{
"application/pdf": true,
"application/msword": true,
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": true,
"text/plain": true,
}
file, err := fileHeader.Open()
if err != nil {
return nil, err
}
defer file.Close()
// Detect content type
buffer := make([]byte, 512)
_, err = file.Read(buffer)
if err != nil {
return nil, err
}
fileType := http.DetectContentType(buffer)
if !allowedTypes[fileType] {
return nil, errors.New("file type not allowed")
}
// Ensure the static/uploads directory exists
uploadDir := "./static/uploads/resumes"
if err := os.MkdirAll(uploadDir, 0755); err != nil {
return nil, err
}
// Generate unique filename
filename := fmt.Sprintf("%s_%s", userID.String(), fileHeader.Filename)
uploadPath := filepath.Join(uploadDir, filename)
// Copy file to upload directory
src, err := fileHeader.Open()
if err != nil {
return nil, err
}
defer src.Close()
dst, err := os.Create(uploadPath)
if err != nil {
return nil, err
}
defer dst.Close()
if _, err := io.Copy(dst, src); err != nil {
return nil, err
}
// Create resume record
resume := &models.Resume{
ID: uuid.New(),
UserID: userID,
Title: title,
FilePath: uploadPath,
FileType: fileType,
FileSize: fileHeader.Size,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
IsActive: true,
}
if err := db.DB.Create(resume).Error; err != nil {
// Clean up the uploaded file if DB operation fails
os.Remove(uploadPath)
return nil, err
}
return resume, nil
}
// GetResume retrieves a resume by ID
func (rs *ResumeService) GetResume(id uuid.UUID) (*models.Resume, error) {
var resume models.Resume
if err := db.DB.First(&resume, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("resume not found")
}
return nil, err
}
return &resume, nil
}
// GetResumeByUser retrieves all resumes for a user
func (rs *ResumeService) GetResumeByUser(userID uuid.UUID) ([]*models.Resume, error) {
var resumes []*models.Resume
if err := db.DB.Where("user_id = ?", userID).Find(&resumes).Error; err != nil {
return nil, err
}
return resumes, nil
}
// ApplicationService handles job application-related operations
type ApplicationService struct{}
// CreateApplication creates a new job application
func (as *ApplicationService) CreateApplication(positionID, userID uuid.UUID, resumeID *uuid.UUID, coverLetter string) (*models.Application, error) {
// Check if user already applied for this position
var existingApplication models.Application
if err := db.DB.Where("position_id = ? AND user_id = ?", positionID, userID).First(&existingApplication).Error; err == nil {
return nil, errors.New("user has already applied for this position")
}
// Verify position exists and is open
var position models.JobPosition
if err := db.DB.First(&position, "id = ? AND status = ? AND is_active = ?", positionID, "open", true).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("position not found or not open")
}
return nil, err
}
application := &models.Application{
ID: uuid.New(),
PositionID: positionID,
UserID: userID,
ResumeID: resumeID,
CoverLetter: coverLetter,
Status: "pending",
AppliedAt: time.Now(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := db.DB.Create(application).Error; err != nil {
return nil, err
}
return application, nil
}
// GetApplication retrieves an application by ID
func (as *ApplicationService) GetApplication(id uuid.UUID) (*models.Application, error) {
var application models.Application
if err := db.DB.Preload("User").Preload("Position").Preload("Resume").First(&application, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("application not found")
}
return nil, err
}
return &application, nil
}
// GetApplications retrieves applications with optional filtering
func (as *ApplicationService) GetApplications(userID, positionID *uuid.UUID, status string, limit, offset int) ([]*models.Application, error) {
var applications []*models.Application
query := db.DB.Preload("User").Preload("Position").Preload("Resume").Limit(limit).Offset(offset)
if userID != nil {
query = query.Where("user_id = ?", userID)
}
if positionID != nil {
query = query.Where("position_id = ?", positionID)
}
if status != "" {
query = query.Where("status = ?", status)
}
if err := query.Find(&applications).Error; err != nil {
return nil, err
}
return applications, nil
}
// UpdateApplication updates an application status
func (as *ApplicationService) UpdateApplication(id uuid.UUID, status string, reviewerID uuid.UUID, notes string) (*models.Application, error) {
var application models.Application
if err := db.DB.First(&application, "id = ?", id).Error; err != nil {
return nil, err
}
// Validate status
if !models.ValidStatus(status) {
return nil, errors.New("invalid status")
}
application.Status = status
application.ReviewerUserID = &reviewerID
application.Notes = notes
application.UpdatedAt = time.Now()
reviewedTime := time.Now()
application.ReviewedAt = &reviewedTime
if err := db.DB.Save(&application).Error; err != nil {
return nil, err
}
return &application, nil
}
// DeleteApplication deletes an application
func (as *ApplicationService) DeleteApplication(id uuid.UUID) error {
return db.DB.Delete(&models.Application{}, "id = ?", id).Error
}
// GetApplicationsForPosition retrieves all applications for a specific position
func (as *ApplicationService) GetApplicationsForPosition(positionID uuid.UUID, status string, limit, offset int) ([]*models.Application, error) {
var applications []*models.Application
query := db.DB.Preload("User").Preload("Resume").Limit(limit).Offset(offset).Where("position_id = ?", positionID)
if status != "" {
query = query.Where("status = ?", status)
}
if err := query.Find(&applications).Error; err != nil {
return nil, err
}
return applications, nil
}