//! Walking a new creator through the first-run steps. use sqlx::PgPool; use crate::db::UserId; use crate::db::models::DbUser; use crate::error::Result; /// Users who need the next onboarding email. Returns users at a given step /// whose last email was sent more than `min_age` ago (or never). #[tracing::instrument(skip_all)] pub async fn get_onboarding_candidates( pool: &PgPool, step: i16, min_age: chrono::Duration, ) -> Result> { let cutoff = chrono::Utc::now() - min_age; // Per-tick LIMIT bounds the scheduler's input list. The caller advances // each returned user's step (so the WHERE re-excludes them), meaning the // remainder is drained on the next tick, same re-tick pattern as the // sandbox/terminated/content-removal cleanup queries. Run #14 MEDIUM: a // signup surge must not load an unbounded user vec into the lock-held tick. let users = sqlx::query_as::<_, DbUser>( "SELECT * FROM users WHERE onboarding_email_step = $1 AND (onboarding_email_sent_at IS NULL OR onboarding_email_sent_at < $2) AND suspended_at IS NULL ORDER BY onboarding_email_sent_at ASC NULLS FIRST LIMIT 1000", ) .bind(step) .bind(cutoff) .fetch_all(pool) .await?; Ok(users) } /// Advance a user's onboarding email step and record the send time. #[tracing::instrument(skip_all)] pub async fn advance_onboarding_step(pool: &PgPool, user_id: UserId, new_step: i16) -> Result<()> { sqlx::query( "UPDATE users SET onboarding_email_step = $2, onboarding_email_sent_at = NOW() WHERE id = $1", ) .bind(user_id) .bind(new_step) .execute(pool) .await?; Ok(()) } /// Advance onboarding step for multiple users in a single query. #[tracing::instrument(skip_all)] pub async fn batch_advance_onboarding_step( pool: &PgPool, user_ids: &[UserId], new_step: i16, ) -> Result<()> { sqlx::query( "UPDATE users SET onboarding_email_step = $2, onboarding_email_sent_at = NOW() WHERE id = ANY($1)", ) .bind(user_ids) .bind(new_step) .execute(pool) .await?; Ok(()) }