- fix(seeder): add PASSWORD_PEPPER to all bcrypt hashes (admin + candidates/recruiters) - fix(seeder): add created_by field to jobs INSERT (was causing NOT NULL violation) - feat(backend): add custom job questions support in applications - feat(backend): add payment handler and Stripe routes - feat(frontend): add navbar and footer to /register and /register/user pages - feat(frontend): add custom question answers to job apply page - feat(frontend): update home page hero section and navbar buttons - feat(frontend): update auth/api lib with new endpoints - chore(db): add migration 045 for application answers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
251 lines
6.9 KiB
Go
251 lines
6.9 KiB
Go
package services
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/rede5/gohorsejobs/backend/internal/dto"
|
|
"github.com/rede5/gohorsejobs/backend/internal/models"
|
|
)
|
|
|
|
type ApplicationService struct {
|
|
DB *sql.DB
|
|
EmailService EmailService
|
|
}
|
|
|
|
func NewApplicationService(db *sql.DB, emailService EmailService) *ApplicationService {
|
|
return &ApplicationService{
|
|
DB: db,
|
|
EmailService: emailService,
|
|
}
|
|
}
|
|
|
|
func (s *ApplicationService) CreateApplication(req dto.CreateApplicationRequest) (*models.Application, error) {
|
|
query := `
|
|
INSERT INTO applications (
|
|
job_id, user_id, name, phone, line_id, whatsapp, email,
|
|
message, resume_url, documents, answers, status, created_at, updated_at
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
|
RETURNING id, created_at, updated_at
|
|
`
|
|
|
|
app := &models.Application{
|
|
JobID: req.JobID,
|
|
UserID: req.UserID,
|
|
Name: req.Name,
|
|
Phone: req.Phone,
|
|
LineID: req.LineID,
|
|
WhatsApp: req.WhatsApp,
|
|
Email: req.Email,
|
|
Message: req.Message,
|
|
ResumeURL: req.ResumeURL,
|
|
Documents: req.Documents,
|
|
Answers: req.Answers,
|
|
Status: "pending",
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
|
|
err := s.DB.QueryRow(
|
|
query,
|
|
app.JobID, app.UserID, app.Name, app.Phone, app.LineID, app.WhatsApp, app.Email,
|
|
app.Message, app.ResumeURL, app.Documents, app.Answers, app.Status, app.CreatedAt, app.UpdatedAt,
|
|
).Scan(&app.ID, &app.CreatedAt, &app.UpdatedAt)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Notify Company (Mock)
|
|
go func() {
|
|
name := ""
|
|
if app.Name != nil {
|
|
name = *app.Name
|
|
}
|
|
|
|
email := ""
|
|
if app.Email != nil {
|
|
email = *app.Email
|
|
}
|
|
|
|
phone := ""
|
|
if app.Phone != nil {
|
|
phone = *app.Phone
|
|
}
|
|
|
|
subject := fmt.Sprintf("Nova candidatura para a vaga #%s", app.JobID)
|
|
body := fmt.Sprintf("Olá,\n\nVocê recebeu uma nova candidatura de %s para a vaga #%s.\n\nEmail: %s\nTelefone: %s\n\nVerifique o painel para mais detalhes.", name, app.JobID, email, phone)
|
|
|
|
_ = s.EmailService.SendEmail("company@example.com", subject, body)
|
|
}()
|
|
|
|
return app, nil
|
|
}
|
|
|
|
func (s *ApplicationService) GetApplications(jobID string) ([]models.Application, error) {
|
|
// Simple get by Job ID
|
|
query := `
|
|
SELECT id, job_id, user_id, name, phone, line_id, whatsapp, email,
|
|
message, resume_url, documents, answers, status, created_at, updated_at
|
|
FROM applications WHERE job_id = $1
|
|
`
|
|
rows, err := s.DB.Query(query, jobID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var apps []models.Application
|
|
for rows.Next() {
|
|
var a models.Application
|
|
if err := rows.Scan(
|
|
&a.ID, &a.JobID, &a.UserID, &a.Name, &a.Phone, &a.LineID, &a.WhatsApp, &a.Email,
|
|
&a.Message, &a.ResumeURL, &a.Documents, &a.Answers, &a.Status, &a.CreatedAt, &a.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
apps = append(apps, a)
|
|
}
|
|
return apps, nil
|
|
}
|
|
|
|
func (s *ApplicationService) ListUserApplications(userID string) ([]models.ApplicationWithDetails, error) {
|
|
query := `
|
|
SELECT
|
|
a.id, a.job_id, a.user_id, a.name, a.phone, a.line_id, a.whatsapp, a.email,
|
|
a.message, a.resume_url, a.status, a.created_at, a.updated_at,
|
|
j.title, c.name
|
|
FROM applications a
|
|
JOIN jobs j ON a.job_id = j.id
|
|
LEFT JOIN companies c ON j.company_id = c.id
|
|
WHERE a.user_id = $1
|
|
ORDER BY a.created_at DESC
|
|
`
|
|
rows, err := s.DB.Query(query, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var apps []models.ApplicationWithDetails
|
|
for rows.Next() {
|
|
var a models.ApplicationWithDetails
|
|
if err := rows.Scan(
|
|
&a.ID, &a.JobID, &a.UserID, &a.Name, &a.Phone, &a.LineID, &a.WhatsApp, &a.Email,
|
|
&a.Message, &a.ResumeURL, &a.Status, &a.CreatedAt, &a.UpdatedAt,
|
|
&a.JobTitle, &a.CompanyName,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
// Some logical defaults if needed
|
|
a.CompanyID = "" // Adjusted to string from int/empty
|
|
apps = append(apps, a)
|
|
}
|
|
return apps, nil
|
|
}
|
|
|
|
func (s *ApplicationService) GetApplicationByID(id string) (*models.Application, error) {
|
|
var a models.Application
|
|
query := `
|
|
SELECT id, job_id, user_id, name, phone, line_id, whatsapp, email,
|
|
message, resume_url, documents, answers, status, created_at, updated_at
|
|
FROM applications WHERE id = $1
|
|
`
|
|
err := s.DB.QueryRow(query, id).Scan(
|
|
&a.ID, &a.JobID, &a.UserID, &a.Name, &a.Phone, &a.LineID, &a.WhatsApp, &a.Email,
|
|
&a.Message, &a.ResumeURL, &a.Documents, &a.Answers, &a.Status, &a.CreatedAt, &a.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &a, nil
|
|
}
|
|
|
|
func (s *ApplicationService) UpdateApplicationStatus(id string, req dto.UpdateApplicationStatusRequest) (*models.Application, error) {
|
|
query := `
|
|
UPDATE applications SET status = $1, updated_at = NOW()
|
|
WHERE id = $2
|
|
RETURNING updated_at
|
|
`
|
|
var updatedAt time.Time
|
|
err := s.DB.QueryRow(query, req.Status, id).Scan(&updatedAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return s.GetApplicationByID(id)
|
|
}
|
|
|
|
func (s *ApplicationService) GetApplicationsByCompany(companyID string) ([]models.Application, error) {
|
|
query := `
|
|
SELECT a.id, a.job_id, a.user_id, a.name, a.phone, a.line_id, a.whatsapp, a.email,
|
|
a.message, a.resume_url, a.documents, a.answers, a.status, a.created_at, a.updated_at
|
|
FROM applications a
|
|
JOIN jobs j ON a.job_id = j.id
|
|
WHERE j.company_id = $1
|
|
ORDER BY a.created_at DESC
|
|
`
|
|
rows, err := s.DB.Query(query, companyID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var apps []models.Application
|
|
for rows.Next() {
|
|
var a models.Application
|
|
if err := rows.Scan(
|
|
&a.ID, &a.JobID, &a.UserID, &a.Name, &a.Phone, &a.LineID, &a.WhatsApp, &a.Email,
|
|
&a.Message, &a.ResumeURL, &a.Documents, &a.Answers, &a.Status, &a.CreatedAt, &a.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
apps = append(apps, a)
|
|
}
|
|
return apps, nil
|
|
}
|
|
|
|
func (s *ApplicationService) GetApplicationsByUser(userID string) ([]models.ApplicationWithDetails, error) {
|
|
query := `
|
|
SELECT
|
|
a.id, a.job_id, a.user_id, a.name, a.phone, a.line_id, a.whatsapp, a.email,
|
|
a.message, a.resume_url, a.status, a.created_at, a.updated_at,
|
|
j.title, c.name, j.company_id
|
|
FROM applications a
|
|
JOIN jobs j ON a.job_id = j.id
|
|
LEFT JOIN companies c ON j.company_id::text = c.id::text
|
|
WHERE a.user_id = $1
|
|
ORDER BY a.created_at DESC
|
|
`
|
|
rows, err := s.DB.Query(query, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var apps []models.ApplicationWithDetails = []models.ApplicationWithDetails{}
|
|
for rows.Next() {
|
|
var a models.ApplicationWithDetails
|
|
var companyID sql.NullString
|
|
|
|
if err := rows.Scan(
|
|
&a.ID, &a.JobID, &a.UserID, &a.Name, &a.Phone, &a.LineID, &a.WhatsApp, &a.Email,
|
|
&a.Message, &a.ResumeURL, &a.Status, &a.CreatedAt, &a.UpdatedAt,
|
|
&a.JobTitle, &a.CompanyName, &companyID,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
if companyID.Valid {
|
|
a.CompanyID = companyID.String
|
|
}
|
|
apps = append(apps, a)
|
|
}
|
|
return apps, nil
|
|
}
|
|
|
|
func (s *ApplicationService) DeleteApplication(id string) error {
|
|
query := `DELETE FROM applications WHERE id = $1`
|
|
_, err := s.DB.Exec(query, id)
|
|
return err
|
|
}
|