nxtgauge-backend-rust/crates/db/src/models/onboarding_state.rs

135 lines
4.4 KiB
Rust

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sqlx::{FromRow, PgPool};
use uuid::Uuid;
// ── Structs ───────────────────────────────────────────────────────────────────
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct OnboardingState {
pub id: Uuid,
pub user_id: Uuid,
pub role_id: Uuid,
pub status: String, // NOT_STARTED | IN_PROGRESS | COMPLETED
pub progress_json: Value,
pub completed_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Deserialize)]
pub struct SaveProgressPayload {
pub role_id: Uuid,
pub progress_json: Value,
}
#[derive(Debug, Deserialize)]
pub struct SubmitOnboardingPayload {
pub role_id: Uuid,
pub progress_json: Value,
}
// ── Repository ────────────────────────────────────────────────────────────────
pub struct OnboardingStateRepository;
impl OnboardingStateRepository {
/// Fetch onboarding state for a user + role. Returns None if no record exists yet.
pub async fn get(
pool: &PgPool,
user_id: Uuid,
role_id: Uuid,
) -> Result<Option<OnboardingState>, sqlx::Error> {
sqlx::query_as!(
OnboardingState,
r#"
SELECT id, user_id, role_id, status, progress_json,
completed_at, created_at, updated_at
FROM onboarding_states
WHERE user_id = $1 AND role_id = $2
"#,
user_id,
role_id,
)
.fetch_optional(pool)
.await
}
/// Upsert progress — inserts a new record or updates progress on conflict.
pub async fn save_progress(
pool: &PgPool,
user_id: Uuid,
role_id: Uuid,
progress: &Value,
) -> Result<OnboardingState, sqlx::Error> {
sqlx::query_as!(
OnboardingState,
r#"
INSERT INTO onboarding_states (user_id, role_id, status, progress_json)
VALUES ($1, $2, 'IN_PROGRESS', $3)
ON CONFLICT (user_id, role_id) DO UPDATE
SET status = CASE
WHEN onboarding_states.status = 'COMPLETED' THEN 'COMPLETED'
ELSE 'IN_PROGRESS'
END,
progress_json = EXCLUDED.progress_json,
updated_at = NOW()
RETURNING id, user_id, role_id, status, progress_json,
completed_at, created_at, updated_at
"#,
user_id,
role_id,
progress,
)
.fetch_one(pool)
.await
}
/// Mark onboarding as COMPLETED and freeze the final answers.
pub async fn complete(
pool: &PgPool,
user_id: Uuid,
role_id: Uuid,
final_answers: &Value,
) -> Result<OnboardingState, sqlx::Error> {
sqlx::query_as!(
OnboardingState,
r#"
INSERT INTO onboarding_states (user_id, role_id, status, progress_json, completed_at)
VALUES ($1, $2, 'COMPLETED', $3, NOW())
ON CONFLICT (user_id, role_id) DO UPDATE
SET status = 'COMPLETED',
progress_json = EXCLUDED.progress_json,
completed_at = NOW(),
updated_at = NOW()
RETURNING id, user_id, role_id, status, progress_json,
completed_at, created_at, updated_at
"#,
user_id,
role_id,
final_answers,
)
.fetch_one(pool)
.await
}
/// Check whether onboarding is complete for a user+role. Returns false if no record.
pub async fn is_complete(
pool: &PgPool,
user_id: Uuid,
role_id: Uuid,
) -> Result<bool, sqlx::Error> {
let status = sqlx::query_scalar!(
r#"
SELECT status FROM onboarding_states
WHERE user_id = $1 AND role_id = $2
"#,
user_id,
role_id,
)
.fetch_optional(pool)
.await?;
Ok(status.map(|s| s == "COMPLETED").unwrap_or(false))
}
}