gohorsejobs/backend/internal/services/seeder_service.go
Tiago Yamamoto 0876584499 feat(seeder): add realistic job templates with all new fields
Replace placeholder job data with 10 varied templates covering:
- employment_type (full-time, part-time, contract, dispatch)
- work_mode (remote, hybrid, onsite)
- salary_min/max, salary_type, currency (BRL/USD/EUR/GBP/JPY)
- salary_negotiable, language_level, visa_support
- date_posted, realistic descriptions and locations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 13:06:32 -06:00

369 lines
13 KiB
Go

package services
import (
"context"
"database/sql"
"fmt"
"math/rand"
"os"
"strings"
"time"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
)
type SeederService struct {
DB *sql.DB
}
func NewSeederService(db *sql.DB) *SeederService {
return &SeederService{DB: db}
}
// SendEvent is a helper to stream logs via SSE
func (s *SeederService) SendEvent(logChan chan string, msg string) {
if logChan != nil {
logChan <- msg
} else {
fmt.Println("[SEEDER]", msg)
}
}
func (s *SeederService) Reset(ctx context.Context) error {
// Dangerous operation: Truncate tables
queries := []string{
"TRUNCATE TABLE applications CASCADE",
"TRUNCATE TABLE jobs CASCADE",
"TRUNCATE TABLE companies CASCADE",
"TRUNCATE TABLE users CASCADE",
// Add other tables as needed
}
tx, err := s.DB.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
for _, q := range queries {
if _, err := tx.ExecContext(ctx, q); err != nil {
// Ignore if table doesn't exist, but log it
fmt.Printf("Error executing %s: %v\n", q, err)
}
}
// Re-create SuperAdmin if needed, or leave it to manual registration
// For dev, it's nice to have a default admin.
return tx.Commit()
}
func (s *SeederService) Seed(ctx context.Context, logChan chan string) error {
s.SendEvent(logChan, "🚀 Starting Database Seed...")
// 0. Auto-Create Superadmin from Env
adminEmail := os.Getenv("ADMIN_EMAIL")
adminPass := os.Getenv("ADMIN_PASSWORD")
if adminEmail != "" && adminPass != "" {
s.SendEvent(logChan, fmt.Sprintf("🛡️ Found ADMIN_EMAIL env. Creating Superadmin: %s", adminEmail))
// Hash password
hash, err := bcrypt.GenerateFromPassword([]byte(adminPass), bcrypt.DefaultCost)
if err == nil {
tx, err := s.DB.BeginTx(ctx, nil)
if err == nil {
adminID := uuid.New().String()
_, execErr := tx.ExecContext(ctx, `
INSERT INTO users (id, identifier, full_name, password_hash, role, created_at, updated_at)
VALUES ($1, $2, 'System Master', $3, 'superadmin', NOW(), NOW())
ON CONFLICT (identifier) DO UPDATE
SET password_hash = EXCLUDED.password_hash,
role = 'superadmin',
updated_at = NOW()
`, adminID, adminEmail, string(hash))
if execErr == nil {
tx.Commit()
s.SendEvent(logChan, "✅ Superadmin created/verified successfully.")
} else {
tx.Rollback()
s.SendEvent(logChan, fmt.Sprintf("❌ Failed to create Superadmin: %v", execErr))
}
}
}
}
// Create Random Source
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
tx, err := s.DB.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
// 1. Create Users (Candidates & Recruiters)
s.SendEvent(logChan, "👤 Creating Users...")
candidates := []string{"Alice Johnson", "Bob Smith", "Charlie Brown", "Diana Prince", "Evan Wright"}
var candidateIDs []string
passwordHash, _ := bcrypt.GenerateFromPassword([]byte("password123"), bcrypt.DefaultCost)
for _, name := range candidates {
id := uuid.New().String()
// Using email format as identifier
identifier := strings.ToLower(strings.ReplaceAll(name, " ", ".")) + "@example.com"
// UPSERT to ensure we get the correct ID back if it exists
row := tx.QueryRowContext(ctx, `
INSERT INTO users (id, identifier, full_name, password_hash, role, created_at, updated_at)
VALUES ($1, $2, $3, $4, 'candidate', NOW(), NOW())
ON CONFLICT (identifier) DO UPDATE SET updated_at = NOW()
RETURNING id
`, id, identifier, name, string(passwordHash))
if err := row.Scan(&id); err != nil {
s.SendEvent(logChan, fmt.Sprintf("❌ Error creating candidate %s: %v", name, err))
continue
}
candidateIDs = append(candidateIDs, id)
s.SendEvent(logChan, fmt.Sprintf(" - Created Candidate: %s (%s)", name, identifier))
}
// 2. Create Companies & Recruiters
s.SendEvent(logChan, "🏢 Creating Companies...")
companyNames := []string{"TechCorp", "InnovateX", "GlobalSolutions", "CodeFactory", "DesignStudio"}
var companyIDs []string
for _, compName := range companyNames {
// Create Recruiter
recruiterID := uuid.New().String()
recIdentifier := "hr@" + strings.ToLower(compName) + ".com"
row := tx.QueryRowContext(ctx, `
INSERT INTO users (id, identifier, full_name, password_hash, role, created_at, updated_at)
VALUES ($1, $2, $3, $4, 'recruiter', NOW(), NOW())
ON CONFLICT (identifier) DO UPDATE SET updated_at = NOW()
RETURNING id
`, recruiterID, recIdentifier, compName+" Recruiter", string(passwordHash))
if err := row.Scan(&recruiterID); err != nil {
s.SendEvent(logChan, fmt.Sprintf("❌ Error creating recruiter %s: %v", recIdentifier, err))
continue
}
// Create Company
compID := uuid.New().String()
slug := strings.ToLower(compName)
row = tx.QueryRowContext(ctx, `
INSERT INTO companies (id, name, slug, type, verified, active, created_at, updated_at)
VALUES ($1, $2, $3, 'COMPANY', true, true, NOW(), NOW())
ON CONFLICT (slug) DO UPDATE SET updated_at = NOW()
RETURNING id
`, compID, compName, slug)
if err := row.Scan(&compID); err != nil {
s.SendEvent(logChan, fmt.Sprintf("❌ Error creating company %s: %v", compName, err))
continue
}
companyIDs = append(companyIDs, compID)
// Link Recruiter - Use 'admin' as role (per schema constraint: 'admin', 'recruiter')
_, err = tx.ExecContext(ctx, `
INSERT INTO user_companies (user_id, company_id, role)
VALUES ($1, $2, 'admin')
ON CONFLICT DO NOTHING
`, recruiterID, compID)
if err != nil {
s.SendEvent(logChan, fmt.Sprintf("⚠️ Failed to link recruiter: %v", err))
}
s.SendEvent(logChan, fmt.Sprintf(" - Created Company: %s (HR: %s)", compName, recIdentifier))
}
// 3. Create Jobs
s.SendEvent(logChan, "💼 Creating Jobs...")
type jobTemplate struct {
Title string
Description string
EmploymentType string
WorkMode string
Location string
SalaryMin float64
SalaryMax float64
SalaryType string
Currency string
Negotiable bool
LanguageLevel string
VisaSupport bool
Status string
}
jobTemplates := []jobTemplate{
{
Title: "Senior Software Engineer",
Description: "We are looking for a Senior Software Engineer to join our growing engineering team. You will design, build, and maintain efficient, reusable, and reliable Go or TypeScript services. Strong understanding of distributed systems and cloud infrastructure required.",
EmploymentType: "full-time",
WorkMode: "remote",
Location: "Remote — Worldwide",
SalaryMin: 8000, SalaryMax: 14000, SalaryType: "monthly", Currency: "USD",
Negotiable: false, LanguageLevel: "none", VisaSupport: false, Status: "open",
},
{
Title: "Frontend Developer (React / Next.js)",
Description: "Join our product team as a Frontend Developer. You will own the UI layer of our SaaS platform, building responsive and accessible interfaces using React and Next.js. Experience with TypeScript and Tailwind CSS is a plus.",
EmploymentType: "full-time",
WorkMode: "hybrid",
Location: "São Paulo, SP — Brasil",
SalaryMin: 6000, SalaryMax: 10000, SalaryType: "monthly", Currency: "BRL",
Negotiable: false, LanguageLevel: "none", VisaSupport: false, Status: "open",
},
{
Title: "Backend Engineer — Go",
Description: "We need a Backend Engineer experienced in Go to help us scale our API layer. You will work with PostgreSQL, Redis, and gRPC. Clean architecture and test-driven development are core to how we work.",
EmploymentType: "contract",
WorkMode: "remote",
Location: "Remote — Latin America",
SalaryMin: 60, SalaryMax: 90, SalaryType: "hourly", Currency: "USD",
Negotiable: true, LanguageLevel: "none", VisaSupport: false, Status: "open",
},
{
Title: "Product Manager",
Description: "We are hiring a Product Manager to lead our core product squad. You will define the roadmap, work closely with engineering and design, and ensure we ship features that customers love. Prior B2B SaaS experience preferred.",
EmploymentType: "full-time",
WorkMode: "onsite",
Location: "Tokyo, Japan",
SalaryMin: 500000, SalaryMax: 800000, SalaryType: "monthly", Currency: "JPY",
Negotiable: false, LanguageLevel: "N3", VisaSupport: true, Status: "open",
},
{
Title: "UX / Product Designer",
Description: "Looking for a talented UX Designer to shape user experiences across our mobile and web products. You will conduct user research, create wireframes and prototypes, and collaborate with frontend engineers to deliver polished interfaces.",
EmploymentType: "part-time",
WorkMode: "hybrid",
Location: "Berlin, Germany",
SalaryMin: 2500, SalaryMax: 4000, SalaryType: "monthly", Currency: "EUR",
Negotiable: false, LanguageLevel: "none", VisaSupport: true, Status: "open",
},
{
Title: "DevOps / Platform Engineer",
Description: "We are seeking a Platform Engineer to own our cloud infrastructure on AWS. You will manage Kubernetes clusters, CI/CD pipelines, monitoring, and cost optimization. Terraform and Helm experience required.",
EmploymentType: "full-time",
WorkMode: "remote",
Location: "Remote — Europe",
SalaryMin: 70000, SalaryMax: 100000, SalaryType: "yearly", Currency: "GBP",
Negotiable: false, LanguageLevel: "none", VisaSupport: false, Status: "open",
},
{
Title: "Data Analyst",
Description: "Join our data team as a Data Analyst. You will work with large datasets using SQL and Python, build dashboards in Metabase, and support business decisions with clear, actionable insights.",
EmploymentType: "full-time",
WorkMode: "onsite",
Location: "São Paulo, SP — Brasil",
SalaryMin: 5000, SalaryMax: 8000, SalaryType: "monthly", Currency: "BRL",
Negotiable: true, LanguageLevel: "none", VisaSupport: false, Status: "open",
},
{
Title: "Mobile Developer (iOS / Swift)",
Description: "We are looking for an iOS Developer to build and maintain our native iOS app. You will work with Swift, SwiftUI, and REST APIs. Experience publishing apps to the App Store is required.",
EmploymentType: "contract",
WorkMode: "remote",
Location: "Remote — Americas",
SalaryMin: 50, SalaryMax: 80, SalaryType: "hourly", Currency: "USD",
Negotiable: false, LanguageLevel: "none", VisaSupport: false, Status: "open",
},
{
Title: "Dispatch Warehouse Associate",
Description: "We are hiring Dispatch Associates to join our logistics operations team in Tokyo. No prior experience required — full on-the-job training provided. Great opportunity to build a career in supply chain.",
EmploymentType: "dispatch",
WorkMode: "onsite",
Location: "Tokyo, Japan",
SalaryMin: 180000, SalaryMax: 220000, SalaryType: "monthly", Currency: "JPY",
Negotiable: false, LanguageLevel: "N4", VisaSupport: true, Status: "open",
},
{
Title: "QA Engineer",
Description: "We need a QA Engineer to ensure the quality of our web and mobile products. You will write automated tests with Playwright and Cypress, perform exploratory testing, and collaborate with developers to fix bugs fast.",
EmploymentType: "full-time",
WorkMode: "hybrid",
Location: "Remote — Brazil",
SalaryMin: 4500, SalaryMax: 7500, SalaryType: "monthly", Currency: "BRL",
Negotiable: false, LanguageLevel: "none", VisaSupport: false, Status: "open",
},
}
var jobIDs []string
for i, compID := range companyIDs {
numJobs := rnd.Intn(2) + 2
for j := 0; j < numJobs; j++ {
tmpl := jobTemplates[(i*3+j)%len(jobTemplates)]
jobID := uuid.New().String()
_, err := tx.ExecContext(ctx, `
INSERT INTO jobs (
id, company_id, title, description,
employment_type, work_mode, location,
salary_min, salary_max, salary_type, currency, salary_negotiable,
language_level, visa_support,
status, date_posted, created_at, updated_at
) VALUES (
$1, $2, $3, $4,
$5, $6, $7,
$8, $9, $10, $11, $12,
$13, $14,
$15, NOW(), NOW(), NOW()
) ON CONFLICT DO NOTHING
`,
jobID, compID, tmpl.Title, tmpl.Description,
tmpl.EmploymentType, tmpl.WorkMode, tmpl.Location,
tmpl.SalaryMin, tmpl.SalaryMax, tmpl.SalaryType, tmpl.Currency, tmpl.Negotiable,
tmpl.LanguageLevel, tmpl.VisaSupport,
tmpl.Status,
)
if err != nil {
s.SendEvent(logChan, fmt.Sprintf("❌ Error creating job %s: %v", tmpl.Title, err))
continue
}
jobIDs = append(jobIDs, jobID)
s.SendEvent(logChan, fmt.Sprintf(" - Posted Job: %s (%s/%s) at %s", tmpl.Title, tmpl.EmploymentType, tmpl.WorkMode, compID[:8]))
}
}
// 4. Create Applications
s.SendEvent(logChan, "📝 Creating Applications...")
for _, candID := range candidateIDs {
// Apply to 1-3 random jobs
numApps := rnd.Intn(3) + 1
for i := 0; i < numApps; i++ {
if len(jobIDs) == 0 {
break
}
jobID := jobIDs[rnd.Intn(len(jobIDs))]
appID := uuid.New().String()
_, err := tx.ExecContext(ctx, `
INSERT INTO applications (id, job_id, user_id, status, created_at, updated_at)
VALUES ($1, $2, $3, 'applied', NOW(), NOW())
ON CONFLICT DO NOTHING
`, appID, jobID, candID)
if err == nil {
s.SendEvent(logChan, fmt.Sprintf(" - Candidate %s applied to Job %s", candID[:8], jobID[:8]))
}
}
}
if err := tx.Commit(); err != nil {
return err
}
s.SendEvent(logChan, "✅ Seed Completed Successfully!")
return nil
}