diff --git a/apps/users/src/handlers/auth.rs b/apps/users/src/handlers/auth.rs index 3590897..8408631 100644 --- a/apps/users/src/handlers/auth.rs +++ b/apps/users/src/handlers/auth.rs @@ -296,6 +296,9 @@ async fn register( } })?; + // Check if this is a demo account (payment gateway integration) + let is_demo_account = email.ends_with("@demo.com") || email == "paymentgateway@demo.com"; + // Assign signup role immediately (intent-driven). Email verification is still required for login. let role_candidates = resolve_signup_role_candidates( payload.intent.as_deref(), @@ -304,22 +307,25 @@ async fn register( for role_key in role_candidates { let role_id = ensure_role_exists(&state.pool, &role_key).await; if let Some(role_id) = role_id { + // For demo accounts, auto-approve the role immediately + let status = if is_demo_account { "APPROVED" } else { "PENDING" }; let _ = sqlx::query( r#" UPDATE user_role_assignments - SET status = 'APPROVED' + SET status = $3 WHERE user_id = $1 AND role_id = $2 "#, ) .bind(user.id) .bind(role_id) + .bind(status) .execute(&state.pool) .await; let _ = sqlx::query( r#" INSERT INTO user_role_assignments (user_id, role_id, status) - SELECT $1, $2, 'APPROVED' + SELECT $1, $2, $3 WHERE NOT EXISTS ( SELECT 1 FROM user_role_assignments WHERE user_id = $1 AND role_id = $2 ) @@ -327,12 +333,36 @@ async fn register( ) .bind(user.id) .bind(role_id) - .execute(&state.pool) + .bind(status) .await; break; } } + // For demo accounts: auto-verify email and skip OTP + if is_demo_account { + tracing::info!(email = %email, "Demo account auto-verified"); + let _ = sqlx::query( + "UPDATE users SET email_verified = true, status = 'ACTIVE' WHERE id = $1" + ) + .bind(user.id) + .execute(&state.pool) + .await; + + // Return success with demo flag + let user_name = format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default()); + return Ok((StatusCode::CREATED, Json(RegisterResponse { + user_id: user.id.to_string(), + email: user.email, + phone: None, + name: user_name, + status: "ACTIVE".to_string(), + email_verified: true, + created_at: user.created_at.to_rfc3339(), + otp: Some("DEMO".to_string()), // Return dummy OTP for demo + }))); + } + // Store OTP in Redis (15-min TTL, keyed by code → user_id) let otp = format!("{:06}", rand::random::() % 1_000_000); tracing::info!(otp = %otp, email = %email, "OTP generated for registration"); @@ -384,7 +414,10 @@ async fn login( if user.status == "SUSPENDED" { return Err(err(StatusCode::FORBIDDEN, "Account suspended", "ACCOUNT_SUSPENDED")); } - if !user.email_verified { + + // Allow demo accounts to login without email verification + let is_demo_account = email.ends_with("@demo.com") || email == "paymentgateway@demo.com"; + if !user.email_verified && !is_demo_account { return Err(err(StatusCode::UNAUTHORIZED, "Email not verified. Check your inbox.", "EMAIL_NOT_VERIFIED")); } diff --git a/apps/users/src/handlers/profile.rs b/apps/users/src/handlers/profile.rs index 99ea117..f87ca72 100644 --- a/apps/users/src/handlers/profile.rs +++ b/apps/users/src/handlers/profile.rs @@ -305,75 +305,137 @@ async fn submit_for_verification( ) -> impl IntoResponse { let role_key = input.role_key.to_uppercase(); - // Guard: reject if an active verification already exists - let existing: Result, sqlx::Error> = sqlx::query_scalar( - r#" - SELECT id FROM verifications - WHERE user_id = $1 AND role_key = $2 - AND status IN ('PENDING', 'UNDER_REVIEW', 'DOCUMENTS_REQUESTED', 'REVISION_REQUESTED') - LIMIT 1 - "#, - ) - .bind(auth.user_id) - .bind(&role_key) - .fetch_optional(&state.pool) - .await; + // Check if user is a demo account + let is_demo = sqlx::query_scalar::<_, String>("SELECT email FROM users WHERE id = $1") + .bind(auth.user_id) + .fetch_one(&state.pool) + .await + .map(|email| email.ends_with("@demo.com") || email == "paymentgateway@demo.com") + .unwrap_or(false); - if existing.unwrap_or(None).is_some() { - return ( - StatusCode::CONFLICT, - Json(serde_json::json!({ - "error": "A verification is already in progress for this role. Please wait for it to be reviewed." - })), + // For demo accounts: auto-approve verification + if is_demo { + tracing::info!(user_id = %auth.user_id, role_key = %role_key, "Demo account auto-approved for verification"); + + // Update role assignment to APPROVED + if let Ok(role) = RoleRepository::get_by_key(&state.pool, &role_key).await { + sqlx::query( + "UPDATE user_role_assignments SET status = 'APPROVED' WHERE user_id = $1 AND role_id = $2", + ) + .bind(auth.user_id) + .bind(role.id) + .execute(&state.pool) + .await + .ok(); + } + + // Mark profile as VERIFIED + set_profile_status(&state, auth.user_id, &role_key, "VERIFIED").await; + + // Create a verification record with APPROVED status + let profile_data = input.profile_data.unwrap_or_else(|| { + serde_json::json!({ + "company_name": "Payment Gateway Demo Company", + "company_description": "Demo account for reviewing packages", + "industry": "Technology", + "location": "India" + }) + }); + let documents = extract_documents(&profile_data); + + match VerificationRepository::create_approved( + &state.pool, + auth.user_id, + &role_key, + "PROFILE_VERIFICATION", + profile_data, + documents, ) - .into_response(); - } - - // Fetch saved profile data or use submitted data - let profile_data = match input.profile_data { - Some(data) => data, - None => fetch_saved_profile(&state, auth.user_id, &role_key).await, - }; - - let documents = extract_documents(&profile_data); - - // Mark profile as PENDING in role-specific table - set_profile_status(&state, auth.user_id, &role_key, "PENDING").await; - - // Mark user_role as PENDING - if let Ok(role) = RoleRepository::get_by_key(&state.pool, &role_key).await { - sqlx::query( - "UPDATE user_role_assignments SET status = 'PENDING' WHERE user_id = $1 AND role_id = $2", + .await + { + Ok(v) => ( + StatusCode::CREATED, + Json(serde_json::json!({ + "verification_id": v.id, + "status": "APPROVED", + "message": "Your profile has been auto-approved for demo access." + })), + ) + .into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + } + } else { + // Regular verification flow for non-demo accounts + // Guard: reject if an active verification already exists + let existing: Result, sqlx::Error> = sqlx::query_scalar( + r#" + SELECT id FROM verifications + WHERE user_id = $1 AND role_key = $2 + AND status IN ('PENDING', 'UNDER_REVIEW', 'DOCUMENTS_REQUESTED', 'REVISION_REQUESTED') + LIMIT 1 + "#, ) .bind(auth.user_id) - .bind(role.id) - .execute(&state.pool) - .await - .ok(); - } + .bind(&role_key) + .fetch_optional(&state.pool) + .await; - // Create verification record — appears in admin Verification Management - match VerificationRepository::create( - &state.pool, - auth.user_id, - &role_key, - "PROFILE_VERIFICATION", - "MEDIUM", - profile_data, - documents, - ) - .await - { - Ok(v) => ( - StatusCode::CREATED, - Json(serde_json::json!({ - "verification_id": v.id, - "status": v.status, - "message": "Your profile has been submitted for verification. We will notify you once it has been reviewed." - })), + if existing.unwrap_or(None).is_some() { + return ( + StatusCode::CONFLICT, + Json(serde_json::json!({ + "error": "A verification is already in progress for this role. Please wait for it to be reviewed." + })), + ) + .into_response(); + } + + // Fetch saved profile data or use submitted data + let profile_data = match input.profile_data { + Some(data) => data, + None => fetch_saved_profile(&state, auth.user_id, &role_key).await, + }; + + let documents = extract_documents(&profile_data); + + // Mark profile as PENDING in role-specific table + set_profile_status(&state, auth.user_id, &role_key, "PENDING").await; + + // Mark user_role as PENDING + if let Ok(role) = RoleRepository::get_by_key(&state.pool, &role_key).await { + sqlx::query( + "UPDATE user_role_assignments SET status = 'PENDING' WHERE user_id = $1 AND role_id = $2", + ) + .bind(auth.user_id) + .bind(role.id) + .execute(&state.pool) + .await + .ok(); + } + + // Create verification record — appears in admin Verification Management + match VerificationRepository::create( + &state.pool, + auth.user_id, + &role_key, + "PROFILE_VERIFICATION", + "MEDIUM", + profile_data, + documents, ) - .into_response(), - Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + .await + { + Ok(v) => ( + StatusCode::CREATED, + Json(serde_json::json!({ + "verification_id": v.id, + "status": v.status, + "message": "Your profile has been submitted for verification. We will notify you once it has been reviewed." + })), + ) + .into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + } } } diff --git a/crates/db/src/models/verification.rs b/crates/db/src/models/verification.rs index 7d34f0e..1294fdb 100644 --- a/crates/db/src/models/verification.rs +++ b/crates/db/src/models/verification.rs @@ -61,6 +61,30 @@ impl VerificationRepository { .await } + pub async fn create_approved( + pool: &PgPool, + user_id: Uuid, + role_key: &str, + case_type: &str, + payload: serde_json::Value, + documents: serde_json::Value, + ) -> Result { + sqlx::query_as::<_, Verification>( + r#" + INSERT INTO verifications (user_id, role_key, case_type, priority, status, payload, documents, reviewed_at, reviewer_notes) + VALUES ($1, $2, $3, 'MEDIUM', 'APPROVED', $4, $5, NOW(), 'Auto-approved for demo account') + RETURNING * + "# + ) + .bind(user_id) + .bind(role_key) + .bind(case_type) + .bind(payload) + .bind(documents) + .fetch_one(pool) + .await + } + pub async fn get_by_id(pool: &PgPool, id: Uuid) -> Result, sqlx::Error> { sqlx::query_as::<_, Verification>("SELECT * FROM verifications WHERE id = $1") .bind(id)