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 }