diff --git a/src/error.rs b/src/error.rs index 3e013a6..9a7f0f6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -148,3 +148,27 @@ impl IntoResponse for AppError { /// Convenience result alias used throughout the codebase. pub type AppResult = Result; + +/// True if `error` is a Postgres unique/primary-key constraint violation. +pub fn is_unique_violation(error: &sqlx::Error) -> bool { + matches!(error, sqlx::Error::Database(db_err) if db_err.is_unique_violation()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_conflict_status_and_code() { + let err = AppError::Conflict("Email".into()); + let (status, code) = err.status_and_code(); + assert_eq!(status, StatusCode::CONFLICT); + assert_eq!(code, "CONFLICT"); + } + + #[test] + fn test_is_unique_violation_non_db_error() { + let err = sqlx::Error::RowNotFound; + assert!(!is_unique_violation(&err)); + } +} diff --git a/src/routes/auth.rs b/src/routes/auth.rs index e7cd18f..b2f02d4 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -62,7 +62,14 @@ pub async fn register( .bind(req.full_name.trim()) .bind(&req.stellar_address) .fetch_one(&state.pool) - .await?; + .await + .map_err(|e| { + if crate::error::is_unique_violation(&e) { + AppError::Conflict("Email".into()) + } else { + AppError::Database(e) + } + })?; let user = User::from(row); diff --git a/src/services/batch.rs b/src/services/batch.rs index 943d493..8c4b91b 100644 --- a/src/services/batch.rs +++ b/src/services/batch.rs @@ -1,5 +1,5 @@ use crate::{ - error::{AppError, AppResult}, + error::{is_unique_violation, AppError, AppResult}, models::{ batch_payment::{BatchPaymentResult, SendBatchPaymentRequest}, transaction::TransactionStatus, @@ -17,12 +17,6 @@ pub struct BatchPaymentService { pool: PgPool, } -/// True if `error` is a Postgres unique/primary-key violation — used to -/// detect losing the race to claim a `batch_submissions` row. -fn is_unique_violation(error: &sqlx::Error) -> bool { - matches!(error, sqlx::Error::Database(db_err) if db_err.is_unique_violation()) -} - /// Decides what a failed submission attempt means for transaction status /// (#30). The distinction is "did we get a definitive answer from Horizon /// at all":