use crate::AppState; use axum::{ extract::{Query, State}, http::StatusCode, response::IntoResponse, routing::{get, post}, Json, Router, }; use contracts::auth_middleware::AuthUser; use db::models::{ onboarding_state::OnboardingStateRepository, role::RoleRepository, }; use serde::{Deserialize, Serialize}; // ── Routers ─────────────────────────────────────────────────────────────────── pub fn onboarding_router() -> Router { Router::new() .route("/state", get(get_state)) .route("/save-progress", post(save_progress)) .route("/submit", post(submit)) } pub fn me_router() -> Router { Router::new() .route("/profile-status", get(profile_status)) } // ── DTOs ────────────────────────────────────────────────────────────────────── #[derive(Deserialize)] pub struct RoleKeyQuery { #[serde(rename = "roleKey")] pub role_key: Option, } // Accept role_key (string) so the frontend never has to know the internal UUID. #[derive(Deserialize)] pub struct SaveProgressInput { #[serde(rename = "roleKey", alias = "role_key")] pub role_key: String, pub progress_json: Option, } #[derive(Deserialize)] pub struct SubmitInput { #[serde(rename = "roleKey", alias = "role_key")] pub role_key: String, pub progress_json: Option, } #[derive(Serialize)] pub struct ProfileStatusResponse { pub onboarding_complete: bool, pub active_role: Option, pub roles: Vec, pub email_verified: bool, } // ── Handlers ────────────────────────────────────────────────────────────────── /// GET /api/onboarding/state?roleKey=COMPANY async fn get_state( auth: AuthUser, State(state): State, Query(query): Query, ) -> Result { let role_key = query .role_key .filter(|k| !k.is_empty()) .unwrap_or_else(|| auth.claims.active_role.clone()); let role = RoleRepository::get_by_key(&state.pool, &role_key) .await .map_err(|_| (StatusCode::NOT_FOUND, format!("Role '{}' not found", role_key)))?; let onboarding_state = OnboardingStateRepository::get(&state.pool, auth.user_id, role.id) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; let response = match onboarding_state { Some(s) => serde_json::json!({ "status": s.status, "currentStep": s.progress_json .get("step") .and_then(|v| v.as_i64()) .unwrap_or(0), "progress": s.progress_json, "completed_at": s.completed_at, "role_key": role_key, }), None => serde_json::json!({ "status": "NOT_STARTED", "currentStep": 0, "progress": null, "completed_at": null, "role_key": role_key, }), }; Ok((StatusCode::OK, Json(response))) } /// POST /api/onboarding/save-progress /// Body: { roleKey: "PHOTOGRAPHER", progress_json: { step: 2, total: 6, data: {...} } } async fn save_progress( auth: AuthUser, State(state): State, Json(input): Json, ) -> Result { let role = RoleRepository::get_by_key(&state.pool, &input.role_key) .await .map_err(|_| (StatusCode::NOT_FOUND, format!("Role '{}' not found", input.role_key)))?; let progress = input.progress_json.unwrap_or(serde_json::Value::Object(Default::default())); let saved = OnboardingStateRepository::save_progress( &state.pool, auth.user_id, role.id, &progress, ) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok((StatusCode::OK, Json(saved))) } /// POST /api/onboarding/submit /// Body: { roleKey: "PHOTOGRAPHER", progress_json: { ...all form values... } } async fn submit( auth: AuthUser, State(state): State, Json(input): Json, ) -> Result { let role = RoleRepository::get_by_key(&state.pool, &input.role_key) .await .map_err(|_| (StatusCode::NOT_FOUND, format!("Role '{}' not found", input.role_key)))?; let progress = input.progress_json.unwrap_or(serde_json::Value::Object(Default::default())); let completed = OnboardingStateRepository::complete( &state.pool, auth.user_id, role.id, &progress, ) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; Ok((StatusCode::OK, Json(completed))) } /// GET /api/me/profile-status async fn profile_status( auth: AuthUser, State(state): State, ) -> Result { use db::models::user::UserRepository; let user = UserRepository::get_by_id(&state.pool, auth.user_id) .await .map_err(|_| (StatusCode::UNAUTHORIZED, "User not found".to_string()))?; let roles = UserRepository::get_user_role_keys(&state.pool, auth.user_id) .await .unwrap_or_default(); let active_role = auth.claims.active_role.clone(); let active_role_opt = if active_role.is_empty() { None } else { Some(active_role.clone()) }; let onboarding_complete = if let Some(ref role_key) = active_role_opt { match RoleRepository::get_by_key(&state.pool, role_key).await { Ok(role) => OnboardingStateRepository::is_complete(&state.pool, auth.user_id, role.id) .await .unwrap_or(false), Err(_) => false, } } else { false }; Ok(( StatusCode::OK, Json(ProfileStatusResponse { onboarding_complete, active_role: active_role_opt, roles, email_verified: user.email_verified, }), )) }