feat: auto-verify demo accounts for payment gateway integration

- Auto-verifies emails for accounts ending with @demo.com
- Auto-approves COMPANY role for demo accounts
- Skips email verification and OTP for demo accounts
- Auto-approves profile verification for demo accounts
- Allows login without email verification for demo accounts

This enables payment gateway companies to login directly and view packages.
This commit is contained in:
Ashwin Kumar Sivakumar 2026-06-12 05:51:19 +05:30
parent 0bda2b2f10
commit b2c93f4e33
3 changed files with 186 additions and 67 deletions

View file

@ -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::<u32>() % 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"));
}

View file

@ -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<Option<Uuid>, 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<Option<Uuid>, 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(),
}
}
}

View file

@ -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<Verification, sqlx::Error> {
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<Option<Verification>, sqlx::Error> {
sqlx::query_as::<_, Verification>("SELECT * FROM verifications WHERE id = $1")
.bind(id)