nxtgauge-backend-rust/crates/db/src/models/job_seeker.rs
Ashwin Kumar Sivakumar 2e95c4750b fix: company job posting, job seeker profile submission, and job applications
- Company approval wrote profile status to 'ACTIVE' (company_profiles'
  own pre-verification default) using an id column that never matched
  any row, so create_job's APPROVED check always rejected newly
  approved companies. Match on user_id for user_id-keyed tables and
  write the canonical 'APPROVED' status.
- job_seeker_profiles was missing columns the job-seeker app has
  always queried (full_name, location, summary, skills,
  active_application_count, status), and the job_applications /
  job_seeker_documents tables it depends on were never migrated in —
  job seeker profile save/submit and job applications failed outright
  with "column/relation does not exist".
- Renamed the job_seeker first_name/last_name split to full_name to
  match what the frontend has always sent.
- Special-cased JOB_SEEKER in the generic profile.rs handlers (mirrors
  the existing COMPANY special-case) so the shared ProfilePage save/
  submit flow, which was routed through a user_role_profile_id-based
  path job_seeker_profiles never had, now persists correctly.
- Fixed apply_to_job's company notification query joining a
  nonexistent "companies" table instead of company_profiles.
- Fixed auto-apply cron's company status filter to match the
  corrected 'APPROVED' status.
2026-07-21 01:00:47 +05:30

221 lines
6.6 KiB
Rust

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, PgPool};
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize, FromRow, Clone)]
pub struct JobSeekerDocument {
pub id: Uuid,
pub job_seeker_id: Uuid,
pub document_type: String,
pub file_name: String,
pub file_url: String,
pub file_size: i64,
pub mime_type: String,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateJobSeekerDocumentPayload {
pub document_type: String,
pub file_name: String,
pub file_size: i64,
pub mime_type: String,
}
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct JobSeekerProfile {
pub id: Uuid,
pub user_id: Uuid,
pub full_name: Option<String>,
pub location: Option<String>,
pub summary: Option<String>,
pub experience_years: Option<i32>,
pub skills: Option<Vec<String>>,
pub resume_url: Option<String>,
pub active_application_count: i32,
pub status: String,
pub bio: Option<String>,
pub custom_data: Option<serde_json::Value>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UpsertJobSeekerProfilePayload {
pub full_name: Option<String>,
pub location: Option<String>,
pub summary: Option<String>,
pub experience_years: Option<i32>,
pub skills: Option<Vec<String>>,
pub resume_url: Option<String>,
pub bio: Option<String>,
pub custom_data: Option<serde_json::Value>,
}
pub struct JobSeekerRepository;
impl JobSeekerRepository {
pub async fn get_by_user_id(
pool: &PgPool,
user_id: Uuid,
) -> Result<Option<JobSeekerProfile>, sqlx::Error> {
let profile = sqlx::query_as::<_, JobSeekerProfile>(
r#"
SELECT
id, user_id, full_name, location, summary, experience_years,
skills, resume_url, active_application_count, status, bio, custom_data,
created_at, updated_at
FROM job_seeker_profiles
WHERE user_id = $1
"#,
)
.bind(user_id)
.fetch_optional(pool)
.await?;
Ok(profile)
}
pub async fn upsert(
pool: &PgPool,
user_id: Uuid,
payload: UpsertJobSeekerProfilePayload,
) -> Result<JobSeekerProfile, sqlx::Error> {
let profile = sqlx::query_as::<_, JobSeekerProfile>(
r#"
INSERT INTO job_seeker_profiles (
user_id, full_name, location, summary, experience_years,
skills, resume_url, bio, custom_data
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (user_id) DO UPDATE SET
full_name = EXCLUDED.full_name,
location = EXCLUDED.location,
summary = EXCLUDED.summary,
experience_years = EXCLUDED.experience_years,
skills = EXCLUDED.skills,
resume_url = EXCLUDED.resume_url,
bio = EXCLUDED.bio,
custom_data = EXCLUDED.custom_data,
updated_at = NOW()
RETURNING
id, user_id, full_name, location, summary, experience_years,
skills, resume_url, active_application_count, status, bio, custom_data,
created_at, updated_at
"#,
)
.bind(user_id)
.bind(payload.full_name)
.bind(payload.location)
.bind(payload.summary)
.bind(payload.experience_years.unwrap_or(0))
.bind(payload.skills.unwrap_or_default())
.bind(payload.resume_url)
.bind(payload.bio)
.bind(payload.custom_data)
.fetch_one(pool)
.await?;
Ok(profile)
}
pub async fn update_active_application_count(
pool: &PgPool,
id: Uuid,
delta: i32,
) -> Result<(), sqlx::Error> {
sqlx::query(
"UPDATE job_seeker_profiles SET active_application_count = active_application_count + $1 WHERE id = $2",
)
.bind(delta)
.bind(id)
.execute(pool)
.await?;
Ok(())
}
pub async fn submit_for_verification(
pool: &PgPool,
user_id: Uuid,
) -> Result<JobSeekerProfile, sqlx::Error> {
let profile = sqlx::query_as::<_, JobSeekerProfile>(
r#"
UPDATE job_seeker_profiles
SET status = 'PENDING_REVIEW', updated_at = NOW()
WHERE user_id = $1
RETURNING
id, user_id, full_name, location, summary, experience_years,
skills, resume_url, active_application_count, status, bio, custom_data,
created_at, updated_at
"#,
)
.bind(user_id)
.fetch_one(pool)
.await?;
Ok(profile)
}
pub async fn create_document(
pool: &PgPool,
job_seeker_id: Uuid,
payload: CreateJobSeekerDocumentPayload,
file_url: String,
) -> Result<JobSeekerDocument, sqlx::Error> {
let doc = sqlx::query_as::<_, JobSeekerDocument>(
r#"
INSERT INTO job_seeker_documents (
job_seeker_id, document_type, file_name, file_url, file_size, mime_type
)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING
id, job_seeker_id, document_type, file_name, file_url, file_size, mime_type, created_at
"#,
)
.bind(job_seeker_id)
.bind(payload.document_type)
.bind(payload.file_name)
.bind(file_url)
.bind(payload.file_size)
.bind(payload.mime_type)
.fetch_one(pool)
.await?;
Ok(doc)
}
pub async fn list_documents(
pool: &PgPool,
job_seeker_id: Uuid,
) -> Result<Vec<JobSeekerDocument>, sqlx::Error> {
let docs = sqlx::query_as::<_, JobSeekerDocument>(
r#"
SELECT id, job_seeker_id, document_type, file_name, file_url, file_size, mime_type, created_at
FROM job_seeker_documents
WHERE job_seeker_id = $1
ORDER BY created_at DESC
"#,
)
.bind(job_seeker_id)
.fetch_all(pool)
.await?;
Ok(docs)
}
pub async fn delete_document(
pool: &PgPool,
job_seeker_id: Uuid,
document_id: Uuid,
) -> Result<(), sqlx::Error> {
sqlx::query(
"DELETE FROM job_seeker_documents WHERE id = $1 AND job_seeker_id = $2",
)
.bind(document_id)
.bind(job_seeker_id)
.execute(pool)
.await?;
Ok(())
}
}