use axum::{
extract::{Path, State},
http::HeaderMap,
Json,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use uuid::Uuid;
use crate::callback::AuthCallback;
use crate::errors::AppError;
use crate::handlers::admin::validate_system_admin;
use crate::services::EmailService;
use crate::AppState;
#[derive(Debug, Serialize, Deserialize)]
pub struct WalletLookupResponse {
pub user_id: Option<Uuid>,
pub wallet_address: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct StripeCustomerLookupResponse {
pub user_id: Option<Uuid>,
pub stripe_customer_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LinkStripeCustomerRequest {
pub user_id: Uuid,
}
pub async fn lookup_by_wallet<C: AuthCallback, E: EmailService>(
State(state): State<Arc<AppState<C, E>>>,
headers: HeaderMap,
Path(wallet_address): Path<String>,
) -> Result<Json<WalletLookupResponse>, AppError> {
let admin_id = validate_system_admin(&state, &headers).await?;
if wallet_address.len() < 32 || wallet_address.len() > 44 {
return Err(AppError::Validation("Invalid wallet address format".into()));
}
let user = state.user_repo.find_by_wallet(&wallet_address).await?;
let user_id = user.map(|u| u.id);
tracing::info!(
admin_id = %admin_id,
wallet_address = %wallet_address,
user_found = user_id.is_some(),
"Wallet lookup"
);
Ok(Json(WalletLookupResponse {
user_id,
wallet_address,
}))
}
pub async fn lookup_by_stripe_customer<C: AuthCallback, E: EmailService>(
State(state): State<Arc<AppState<C, E>>>,
headers: HeaderMap,
Path(stripe_customer_id): Path<String>,
) -> Result<Json<StripeCustomerLookupResponse>, AppError> {
let admin_id = validate_system_admin(&state, &headers).await?;
if !stripe_customer_id.starts_with("cus_") || stripe_customer_id.len() < 8 || stripe_customer_id.len() > 64 {
return Err(AppError::Validation(
"Invalid Stripe customer ID format".into(),
));
}
let user = state
.user_repo
.find_by_stripe_customer_id(&stripe_customer_id)
.await?;
let user_id = user.map(|u| u.id);
tracing::info!(
admin_id = %admin_id,
stripe_customer_id = %stripe_customer_id,
user_found = user_id.is_some(),
"Stripe customer lookup"
);
Ok(Json(StripeCustomerLookupResponse {
user_id,
stripe_customer_id,
}))
}
pub async fn link_stripe_customer<C: AuthCallback, E: EmailService>(
State(state): State<Arc<AppState<C, E>>>,
headers: HeaderMap,
Path(stripe_customer_id): Path<String>,
Json(req): Json<LinkStripeCustomerRequest>,
) -> Result<Json<StripeCustomerLookupResponse>, AppError> {
let admin_id = validate_system_admin(&state, &headers).await?;
if !stripe_customer_id.starts_with("cus_") || stripe_customer_id.len() < 8 || stripe_customer_id.len() > 64 {
return Err(AppError::Validation(
"Invalid Stripe customer ID format".into(),
));
}
let user = state
.user_repo
.find_by_id(req.user_id)
.await?
.ok_or_else(|| AppError::NotFound("User not found".into()))?;
if user
.stripe_customer_id
.as_deref()
.map(|v| v == stripe_customer_id)
.unwrap_or(false)
{
return Ok(Json(StripeCustomerLookupResponse {
user_id: Some(req.user_id),
stripe_customer_id,
}));
}
if let Some(existing) = state
.user_repo
.find_by_stripe_customer_id(&stripe_customer_id)
.await?
{
if existing.id != req.user_id {
return Err(AppError::Validation(
"Stripe customer ID is already linked to another user".into(),
));
}
}
state
.user_repo
.set_stripe_customer_id(req.user_id, &stripe_customer_id)
.await?;
tracing::info!(
admin_id = %admin_id,
user_id = %req.user_id,
stripe_customer_id = %stripe_customer_id,
"Linked Stripe customer to user"
);
Ok(Json(StripeCustomerLookupResponse {
user_id: Some(req.user_id),
stripe_customer_id,
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_wallet_lookup_response_serialization() {
let response = WalletLookupResponse {
user_id: Some(Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap()),
wallet_address: "ABC123...".to_string(),
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("user_id"));
assert!(json.contains("550e8400"));
let response = WalletLookupResponse {
user_id: None,
wallet_address: "XYZ789...".to_string(),
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"user_id\":null"));
}
}