use axum::response::Response;
pub enum AppError {
/// Business rule validation failed (e.g., InsufficientFunds)
Domain(BankAccountError),
/// Optimistic Concurrency collision (someone else edited the stream first)
Concurrency,
/// Connection issues or internal database failures
Internal(String),
}
// Convert our application errors into Axum's response type
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, error_code, message) = match self {
AppError::Domain(BankAccountError::InsufficientFunds { available, requested }) => (
StatusCode::BAD_REQUEST,
"insufficient_funds",
format!("Requested ${requested} but only have ${available} available."),
),
AppError::Domain(BankAccountError::AccountAlreadyOpen) => (
StatusCode::CONFLICT,
"account_already_open",
"This bank account has already been opened.".to_owned(),
),
AppError::Domain(BankAccountError::AccountNotYetOpen) => (
StatusCode::BAD_REQUEST,
"account_not_open",
"The requested account is not initialized yet.".to_owned(),
),
AppError::Domain(BankAccountError::InvalidDepositAmount) => (
StatusCode::BAD_REQUEST,
"invalid_deposit_amount",
"Deposit amounts must be positive numbers.".to_owned(),
),
AppError::Concurrency => (
StatusCode::CONFLICT,
"concurrency_collision",
"The stream was modified by another request. Please retry.".to_owned(),
),
AppError::Internal(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"internal_server_error",
"The request failed. Check server logs for details.".to_owned(),
),
};
let body = serde_json::json!({
"error": error_code,
"message": message,
});
(status, Json(body)).into_response()
}
}
// Implement standard From traits for clean error propagation via '?' operator
impl From<RepositoryError<BankAccountError>> for AppError {
fn from(err: RepositoryError<BankAccountError>) -> Self {
match err {
RepositoryError::Domain(e) => AppError::Domain(e),
RepositoryError::Concurrency(_) => AppError::Concurrency,
RepositoryError::Store(e) => AppError::Internal(e.to_string()),
}
}
}