mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 09:50:21 +08:00
refactor: 拆分 gateway 单体为独立 crate,新增 systemd 部署方案
将 gateway 内部的 model-fetch、provider-transport、scheduler-core、 usage-runtime、video-tasks-core 模块提取为独立 crate;重构 gateway 内部模块结构(state/router/cache/data/query 等);移除大量遗留模块 文件;新增 systemd 二进制部署骨架及相关文档;更新前端 usage 相关 API 和组件。
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
use crate::gateway::async_task::CancelVideoTaskError;
|
||||
use crate::gateway::{AppState, GatewayControlDecision, GatewayError, GatewayPublicRequestContext};
|
||||
use crate::async_task::CancelVideoTaskError;
|
||||
use crate::control::GatewayControlDecision;
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use aether_data::repository::video_tasks::{
|
||||
StoredVideoTask, VideoTaskQueryFilter, VideoTaskStatus,
|
||||
};
|
||||
@@ -243,7 +245,7 @@ async fn build_local_gemini_video_operation_cancel_response(
|
||||
}
|
||||
};
|
||||
|
||||
match crate::gateway::async_task::cancel_video_task_record(state, &task.id).await {
|
||||
match crate::async_task::cancel_video_task_record(state, &task.id).await {
|
||||
Ok(_) => Json(json!({})).into_response(),
|
||||
Err(CancelVideoTaskError::NotFound) => build_ai_public_error_response(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::gateway::api::ai::public_api_format_local_path;
|
||||
use crate::gateway::handlers::{
|
||||
use crate::api::ai::public_api_format_local_path;
|
||||
use crate::handlers::{
|
||||
query_param_optional_bool, query_param_value, unix_secs_to_rfc3339,
|
||||
};
|
||||
use crate::gateway::AppState;
|
||||
use crate::AppState;
|
||||
use aether_data::repository::candidates::{
|
||||
PublicHealthTimelineBucket, RequestCandidateStatus, StoredRequestCandidate,
|
||||
};
|
||||
|
||||
@@ -7,11 +7,12 @@ use super::{
|
||||
system_config_bool, system_config_string, ApiFormatHealthMonitorOptions,
|
||||
PUBLIC_CAPABILITY_DEFINITIONS,
|
||||
};
|
||||
use crate::gateway::handlers::{
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::{
|
||||
decrypt_catalog_secret_with_fallbacks, encrypt_catalog_secret_with_fallbacks, query_param_bool,
|
||||
query_param_optional_bool, query_param_value, unix_secs_to_rfc3339,
|
||||
};
|
||||
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::body::{Body, Bytes};
|
||||
use axum::http::{self, Response};
|
||||
use axum::response::IntoResponse;
|
||||
|
||||
@@ -6,7 +6,8 @@ use axum::{
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::gateway::{AppState, GatewayError, GatewayPublicRequestContext};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
use super::announcements_shared::{
|
||||
announcements_bad_request_response, announcements_not_found_response,
|
||||
|
||||
@@ -5,7 +5,8 @@ use axum::{
|
||||
Json,
|
||||
};
|
||||
|
||||
use crate::gateway::{AppState, GatewayPublicRequestContext};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::AppState;
|
||||
|
||||
use super::super::build_unhandled_public_support_response;
|
||||
use super::announcements_shared::{
|
||||
|
||||
@@ -10,8 +10,8 @@ use aether_data::repository::announcements::{
|
||||
AnnouncementListQuery, StoredAnnouncement, StoredAnnouncementPage,
|
||||
};
|
||||
|
||||
use crate::gateway::handlers::{query_param_optional_bool, query_param_value};
|
||||
use crate::gateway::GatewayError;
|
||||
use crate::handlers::{query_param_optional_bool, query_param_value};
|
||||
use crate::GatewayError;
|
||||
|
||||
pub(super) fn parse_public_announcements_query(
|
||||
query: Option<&str>,
|
||||
|
||||
@@ -6,7 +6,8 @@ use axum::{
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::gateway::{AppState, GatewayPublicRequestContext};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::AppState;
|
||||
|
||||
use super::super::{build_unhandled_public_support_response, resolve_authenticated_local_user};
|
||||
use super::announcements_shared::{
|
||||
|
||||
@@ -301,7 +301,7 @@ pub(super) async fn maybe_build_local_auth_response(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{maybe_build_local_auth_response, AppState, GatewayPublicRequestContext};
|
||||
use crate::gateway::GatewayControlDecision;
|
||||
use crate::control::GatewayControlDecision;
|
||||
use axum::body::to_bytes;
|
||||
use axum::http::{HeaderMap, Method, StatusCode, Uri};
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ fn auth_non_empty_string(value: Option<String>) -> Option<String> {
|
||||
|
||||
pub(super) fn extract_bearer_token(headers: &http::HeaderMap) -> Option<String> {
|
||||
let value =
|
||||
crate::gateway::headers::header_value_str(headers, http::header::AUTHORIZATION.as_str())?;
|
||||
crate::headers::header_value_str(headers, http::header::AUTHORIZATION.as_str())?;
|
||||
let (scheme, token) = value.split_once(' ')?;
|
||||
if !scheme.eq_ignore_ascii_case("bearer") {
|
||||
return None;
|
||||
@@ -227,7 +227,7 @@ pub(super) fn extract_bearer_token(headers: &http::HeaderMap) -> Option<String>
|
||||
}
|
||||
|
||||
pub(super) fn extract_cookie_value(headers: &http::HeaderMap, cookie_name: &str) -> Option<String> {
|
||||
let header = crate::gateway::headers::header_value_str(headers, http::header::COOKIE.as_str())?;
|
||||
let header = crate::headers::header_value_str(headers, http::header::COOKIE.as_str())?;
|
||||
for pair in header.split(';') {
|
||||
let (name, value) = pair.trim().split_once('=')?;
|
||||
if name.trim() == cookie_name {
|
||||
@@ -241,7 +241,7 @@ pub(super) fn extract_client_device_id(
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
headers: &http::HeaderMap,
|
||||
) -> Result<String, Response<Body>> {
|
||||
let header_value = crate::gateway::headers::header_value_str(headers, "x-client-device-id");
|
||||
let header_value = crate::headers::header_value_str(headers, "x-client-device-id");
|
||||
let query_value = request_context
|
||||
.request_query_string
|
||||
.as_deref()
|
||||
@@ -268,12 +268,12 @@ pub(super) fn extract_client_device_id(
|
||||
}
|
||||
|
||||
pub(super) fn auth_user_agent(headers: &http::HeaderMap) -> Option<String> {
|
||||
crate::gateway::headers::header_value_str(headers, http::header::USER_AGENT.as_str())
|
||||
crate::headers::header_value_str(headers, http::header::USER_AGENT.as_str())
|
||||
.map(|value| value.chars().take(1000).collect())
|
||||
}
|
||||
|
||||
pub(super) fn auth_client_ip(headers: &http::HeaderMap) -> Option<String> {
|
||||
crate::gateway::headers::header_value_str(headers, "x-forwarded-for")
|
||||
crate::headers::header_value_str(headers, "x-forwarded-for")
|
||||
.and_then(|value| {
|
||||
value
|
||||
.split(',')
|
||||
@@ -283,7 +283,7 @@ pub(super) fn auth_client_ip(headers: &http::HeaderMap) -> Option<String> {
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| value.chars().take(45).collect())
|
||||
.or_else(|| {
|
||||
crate::gateway::headers::header_value_str(headers, "x-real-ip")
|
||||
crate::headers::header_value_str(headers, "x-real-ip")
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
|
||||
@@ -342,7 +342,7 @@ pub(super) async fn handle_auth_refresh(
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
headers: &http::HeaderMap,
|
||||
) -> Response<Body> {
|
||||
if crate::gateway::headers::header_value_str(headers, http::header::CONTENT_LENGTH.as_str())
|
||||
if crate::headers::header_value_str(headers, http::header::CONTENT_LENGTH.as_str())
|
||||
.as_deref()
|
||||
.is_some_and(|value| value.trim() != "0")
|
||||
{
|
||||
@@ -495,7 +495,7 @@ pub(super) async fn handle_auth_refresh(
|
||||
user_id,
|
||||
session_id,
|
||||
&session.refresh_token_hash,
|
||||
&crate::gateway::gateway_data::StoredUserSessionRecord::hash_refresh_token(
|
||||
&crate::data::state::StoredUserSessionRecord::hash_refresh_token(
|
||||
&new_refresh_token,
|
||||
),
|
||||
now,
|
||||
@@ -583,12 +583,14 @@ pub(super) async fn build_auth_login_success_response(
|
||||
)
|
||||
}
|
||||
};
|
||||
let session = match crate::gateway::gateway_data::StoredUserSessionRecord::new(
|
||||
let session = match crate::data::state::StoredUserSessionRecord::new(
|
||||
session_id,
|
||||
user.id.clone(),
|
||||
client_device_id,
|
||||
None,
|
||||
crate::gateway::gateway_data::StoredUserSessionRecord::hash_refresh_token(&refresh_token),
|
||||
crate::data::state::StoredUserSessionRecord::hash_refresh_token(
|
||||
&refresh_token,
|
||||
),
|
||||
None,
|
||||
None,
|
||||
Some(now),
|
||||
|
||||
@@ -48,10 +48,8 @@ pub(super) async fn maybe_build_local_dashboard_response(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
maybe_build_local_dashboard_response, AppState, GatewayPublicRequestContext,
|
||||
};
|
||||
use crate::gateway::GatewayControlDecision;
|
||||
use super::{maybe_build_local_dashboard_response, AppState, GatewayPublicRequestContext};
|
||||
use crate::control::GatewayControlDecision;
|
||||
use axum::body::to_bytes;
|
||||
use axum::http::{HeaderMap, Method, StatusCode, Uri};
|
||||
|
||||
|
||||
@@ -28,12 +28,12 @@ pub(super) fn models_detail_id(request_path: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
fn auth_snapshot_allows_provider_for_models(
|
||||
auth_snapshot: Option<&crate::gateway::gateway_data::StoredGatewayAuthApiKeySnapshot>,
|
||||
auth_snapshot: Option<&crate::data::auth::GatewayAuthApiKeySnapshot>,
|
||||
provider_id: &str,
|
||||
provider_name: &str,
|
||||
) -> bool {
|
||||
let Some(allowed) = auth_snapshot.and_then(
|
||||
crate::gateway::gateway_data::StoredGatewayAuthApiKeySnapshot::effective_allowed_providers,
|
||||
crate::data::auth::GatewayAuthApiKeySnapshot::effective_allowed_providers,
|
||||
) else {
|
||||
return true;
|
||||
};
|
||||
@@ -45,11 +45,11 @@ fn auth_snapshot_allows_provider_for_models(
|
||||
}
|
||||
|
||||
fn auth_snapshot_allows_model_for_models(
|
||||
auth_snapshot: Option<&crate::gateway::gateway_data::StoredGatewayAuthApiKeySnapshot>,
|
||||
auth_snapshot: Option<&crate::data::auth::GatewayAuthApiKeySnapshot>,
|
||||
global_model_name: &str,
|
||||
) -> bool {
|
||||
let Some(allowed) = auth_snapshot.and_then(
|
||||
crate::gateway::gateway_data::StoredGatewayAuthApiKeySnapshot::effective_allowed_models,
|
||||
crate::data::auth::GatewayAuthApiKeySnapshot::effective_allowed_models,
|
||||
) else {
|
||||
return true;
|
||||
};
|
||||
@@ -130,7 +130,7 @@ fn row_exposes_global_model_for_models(
|
||||
|
||||
pub(super) fn filter_rows_for_models(
|
||||
rows: Vec<StoredMinimalCandidateSelectionRow>,
|
||||
auth_snapshot: Option<&crate::gateway::gateway_data::StoredGatewayAuthApiKeySnapshot>,
|
||||
auth_snapshot: Option<&crate::data::auth::GatewayAuthApiKeySnapshot>,
|
||||
api_format: &str,
|
||||
) -> Vec<StoredMinimalCandidateSelectionRow> {
|
||||
let mut filtered = rows
|
||||
|
||||
@@ -45,7 +45,7 @@ mod tests {
|
||||
use super::{
|
||||
maybe_build_local_user_monitoring_response, AppState, GatewayPublicRequestContext,
|
||||
};
|
||||
use crate::gateway::GatewayControlDecision;
|
||||
use crate::control::GatewayControlDecision;
|
||||
use axum::body::to_bytes;
|
||||
use axum::http::{HeaderMap, Method, StatusCode, Uri};
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ use axum::{
|
||||
};
|
||||
use chrono::Utc;
|
||||
use serde_json::{json, Value};
|
||||
use sqlx::Row;
|
||||
|
||||
use crate::gateway::handlers::shared::query_param_value;
|
||||
use crate::handlers::shared::query_param_value;
|
||||
use crate::query::monitoring as monitoring_query;
|
||||
|
||||
use super::{
|
||||
build_auth_error_response, resolve_authenticated_local_user, AppState,
|
||||
@@ -124,131 +124,29 @@ pub(super) async fn handle_user_audit_logs(
|
||||
};
|
||||
|
||||
let cutoff_time = Utc::now() - chrono::Duration::days(days);
|
||||
let total = if let Some(ref event_type) = event_type {
|
||||
match sqlx::query_scalar::<_, i64>(
|
||||
r#"
|
||||
SELECT COUNT(*)
|
||||
FROM audit_logs
|
||||
WHERE user_id = $1
|
||||
AND created_at >= $2
|
||||
AND event_type = $3
|
||||
"#,
|
||||
)
|
||||
.bind(&auth.user.id)
|
||||
.bind(cutoff_time)
|
||||
.bind(event_type)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
{
|
||||
Ok(value) => usize::try_from(value.max(0)).unwrap_or(usize::MAX),
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("user audit logs count failed: {err}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match sqlx::query_scalar::<_, i64>(
|
||||
r#"
|
||||
SELECT COUNT(*)
|
||||
FROM audit_logs
|
||||
WHERE user_id = $1
|
||||
AND created_at >= $2
|
||||
"#,
|
||||
)
|
||||
.bind(&auth.user.id)
|
||||
.bind(cutoff_time)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
{
|
||||
Ok(value) => usize::try_from(value.max(0)).unwrap_or(usize::MAX),
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("user audit logs count failed: {err}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
let (items, total) = match monitoring_query::list_user_audit_logs(
|
||||
&pool,
|
||||
&auth.user.id,
|
||||
cutoff_time,
|
||||
event_type.as_deref(),
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
let detail = match err {
|
||||
crate::GatewayError::Internal(message) => message,
|
||||
other => format!("{other:?}"),
|
||||
};
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
detail,
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let rows = if let Some(ref event_type) = event_type {
|
||||
match sqlx::query(
|
||||
r#"
|
||||
SELECT id, event_type, description, ip_address, status_code, created_at
|
||||
FROM audit_logs
|
||||
WHERE user_id = $1
|
||||
AND created_at >= $2
|
||||
AND event_type = $3
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $4 OFFSET $5
|
||||
"#,
|
||||
)
|
||||
.bind(&auth.user.id)
|
||||
.bind(cutoff_time)
|
||||
.bind(event_type)
|
||||
.bind(i64::try_from(limit).unwrap_or(i64::MAX))
|
||||
.bind(i64::try_from(offset).unwrap_or(i64::MAX))
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("user audit logs read failed: {err}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match sqlx::query(
|
||||
r#"
|
||||
SELECT id, event_type, description, ip_address, status_code, created_at
|
||||
FROM audit_logs
|
||||
WHERE user_id = $1
|
||||
AND created_at >= $2
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $3 OFFSET $4
|
||||
"#,
|
||||
)
|
||||
.bind(&auth.user.id)
|
||||
.bind(cutoff_time)
|
||||
.bind(i64::try_from(limit).unwrap_or(i64::MAX))
|
||||
.bind(i64::try_from(offset).unwrap_or(i64::MAX))
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("user audit logs read failed: {err}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let items = rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
let created_at = row
|
||||
.try_get::<chrono::DateTime<chrono::Utc>, _>("created_at")
|
||||
.ok()
|
||||
.map(|value| value.to_rfc3339());
|
||||
json!({
|
||||
"id": row.try_get::<String, _>("id").ok(),
|
||||
"event_type": row.try_get::<String, _>("event_type").ok(),
|
||||
"description": row.try_get::<String, _>("description").ok(),
|
||||
"ip_address": row.try_get::<Option<String>, _>("ip_address").ok().flatten(),
|
||||
"status_code": row.try_get::<Option<i32>, _>("status_code").ok().flatten(),
|
||||
"created_at": created_at,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
build_user_monitoring_audit_logs_payload(items, total, limit, offset, event_type, days)
|
||||
}
|
||||
|
||||
@@ -3,50 +3,14 @@ use super::payment_shared::{
|
||||
NormalizedPaymentCallbackRequest,
|
||||
};
|
||||
use axum::{body::Body, http, response::Response};
|
||||
use chrono::Utc;
|
||||
use serde_json::json;
|
||||
use sqlx::Row;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::super::{build_auth_json_response, wallet_payment_order_payload_from_row};
|
||||
use super::super::build_auth_json_response;
|
||||
use super::{
|
||||
build_auth_error_response, build_payment_callback_storage_unavailable_response, AppState,
|
||||
GatewayPublicRequestContext,
|
||||
};
|
||||
|
||||
async fn update_payment_callback_failure(
|
||||
tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
callback_id: &str,
|
||||
payload: &NormalizedPaymentCallbackRequest,
|
||||
callback_payload_hash: &str,
|
||||
signature_valid: bool,
|
||||
error: &str,
|
||||
) {
|
||||
let _ = sqlx::query(
|
||||
r#"
|
||||
UPDATE payment_callbacks
|
||||
SET signature_valid = $2,
|
||||
status = 'failed',
|
||||
error_message = $3,
|
||||
payload_hash = $4,
|
||||
payload = $5,
|
||||
processed_at = NOW(),
|
||||
order_no = COALESCE($6, order_no),
|
||||
gateway_order_id = COALESCE($7, gateway_order_id)
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(callback_id)
|
||||
.bind(signature_valid)
|
||||
.bind(error)
|
||||
.bind(callback_payload_hash)
|
||||
.bind(&payload.payload)
|
||||
.bind(payload.order_no.as_deref())
|
||||
.bind(payload.gateway_order_id.as_deref())
|
||||
.execute(&mut **tx)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(super) async fn handle_payment_callback_with_postgres(
|
||||
state: &AppState,
|
||||
payment_method: &str,
|
||||
@@ -54,360 +18,75 @@ pub(super) async fn handle_payment_callback_with_postgres(
|
||||
payload: &NormalizedPaymentCallbackRequest,
|
||||
signature_valid: bool,
|
||||
) -> Response<Body> {
|
||||
let Some(pool) = state.postgres_pool() else {
|
||||
if state.postgres_pool().is_none() {
|
||||
return build_payment_callback_storage_unavailable_response();
|
||||
};
|
||||
let mut tx = match pool.begin().await {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("payment callback transaction failed: {err}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let existing_callback = match sqlx::query(
|
||||
r#"
|
||||
SELECT id, payment_order_id, status, order_no, gateway_order_id
|
||||
FROM payment_callbacks
|
||||
WHERE callback_key = $1
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.bind(&payload.callback_key)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("payment callback lookup failed: {err}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let callback_payload_hash = match payment_callback_payload_hash(&payload.payload) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(http::StatusCode::INTERNAL_SERVER_ERROR, err, false);
|
||||
return build_auth_error_response(http::StatusCode::INTERNAL_SERVER_ERROR, err, false)
|
||||
}
|
||||
};
|
||||
|
||||
let duplicate = existing_callback.is_some();
|
||||
let callback_id = if let Some(row) = existing_callback.as_ref() {
|
||||
let status = row.try_get::<String, _>("status").ok().unwrap_or_default();
|
||||
if status == "processed" {
|
||||
let order_id = row
|
||||
.try_get::<Option<String>, _>("payment_order_id")
|
||||
.ok()
|
||||
.flatten();
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_json_response(
|
||||
http::StatusCode::OK,
|
||||
json!({
|
||||
"ok": true,
|
||||
"duplicate": true,
|
||||
"credited": false,
|
||||
"order_id": order_id,
|
||||
"payment_method": payment_method,
|
||||
"request_path": request_context.request_path,
|
||||
}),
|
||||
None,
|
||||
);
|
||||
}
|
||||
row.try_get::<String, _>("id").ok().unwrap_or_default()
|
||||
} else {
|
||||
let callback_id = Uuid::new_v4().to_string();
|
||||
let insert_result = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO payment_callbacks (
|
||||
id,
|
||||
payment_order_id,
|
||||
payment_method,
|
||||
callback_key,
|
||||
order_no,
|
||||
gateway_order_id,
|
||||
payload_hash,
|
||||
signature_valid,
|
||||
status,
|
||||
payload,
|
||||
error_message,
|
||||
created_at,
|
||||
processed_at
|
||||
)
|
||||
VALUES (
|
||||
$1,
|
||||
NULL,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7,
|
||||
'received',
|
||||
$8,
|
||||
NULL,
|
||||
NOW(),
|
||||
NULL
|
||||
)
|
||||
"#,
|
||||
let outcome = match state
|
||||
.process_payment_callback(
|
||||
aether_data::repository::wallet::ProcessPaymentCallbackInput {
|
||||
payment_method: payment_method.to_string(),
|
||||
callback_key: payload.callback_key.clone(),
|
||||
order_no: payload.order_no.clone(),
|
||||
gateway_order_id: payload.gateway_order_id.clone(),
|
||||
amount_usd: payload.amount_usd,
|
||||
pay_amount: payload.pay_amount,
|
||||
pay_currency: payload.pay_currency.clone(),
|
||||
exchange_rate: payload.exchange_rate,
|
||||
payload_hash: callback_payload_hash,
|
||||
payload: payload.payload.clone(),
|
||||
signature_valid,
|
||||
},
|
||||
)
|
||||
.bind(&callback_id)
|
||||
.bind(payment_method)
|
||||
.bind(&payload.callback_key)
|
||||
.bind(payload.order_no.as_deref())
|
||||
.bind(payload.gateway_order_id.as_deref())
|
||||
.bind(&callback_payload_hash)
|
||||
.bind(signature_valid)
|
||||
.bind(&payload.payload)
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
if let Err(err) = insert_result {
|
||||
let _ = tx.rollback().await;
|
||||
.await
|
||||
{
|
||||
Ok(Some(value)) => value,
|
||||
Ok(None) => return build_payment_callback_storage_unavailable_response(),
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("payment callback create failed: {err}"),
|
||||
format!("payment callback failed: {err:?}"),
|
||||
false,
|
||||
);
|
||||
)
|
||||
}
|
||||
callback_id
|
||||
};
|
||||
|
||||
if !signature_valid {
|
||||
update_payment_callback_failure(
|
||||
&mut tx,
|
||||
&callback_id,
|
||||
payload,
|
||||
&callback_payload_hash,
|
||||
signature_valid,
|
||||
"invalid callback signature",
|
||||
)
|
||||
.await;
|
||||
let _ = tx.commit().await;
|
||||
return payment_callback_mark_failed_response(
|
||||
match outcome {
|
||||
aether_data::repository::wallet::ProcessPaymentCallbackOutcome::DuplicateProcessed {
|
||||
order_id,
|
||||
} => build_auth_json_response(
|
||||
http::StatusCode::OK,
|
||||
json!({
|
||||
"ok": true,
|
||||
"duplicate": true,
|
||||
"credited": false,
|
||||
"order_id": order_id,
|
||||
"payment_method": payment_method,
|
||||
"request_path": request_context.request_path,
|
||||
}),
|
||||
None,
|
||||
),
|
||||
aether_data::repository::wallet::ProcessPaymentCallbackOutcome::Failed {
|
||||
duplicate,
|
||||
"invalid callback signature",
|
||||
error,
|
||||
} => payment_callback_mark_failed_response(
|
||||
duplicate,
|
||||
&error,
|
||||
payment_method,
|
||||
&request_context.request_path,
|
||||
);
|
||||
}
|
||||
|
||||
let lookup_order_no = payload.order_no.clone().or_else(|| {
|
||||
existing_callback
|
||||
.as_ref()
|
||||
.and_then(|row| row.try_get::<Option<String>, _>("order_no").ok().flatten())
|
||||
});
|
||||
let lookup_gateway_order_id = payload.gateway_order_id.clone().or_else(|| {
|
||||
existing_callback.as_ref().and_then(|row| {
|
||||
row.try_get::<Option<String>, _>("gateway_order_id")
|
||||
.ok()
|
||||
.flatten()
|
||||
})
|
||||
});
|
||||
|
||||
let order_row = if let Some(order_no) = lookup_order_no.as_deref() {
|
||||
match sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
order_no,
|
||||
wallet_id,
|
||||
user_id,
|
||||
CAST(amount_usd AS DOUBLE PRECISION) AS amount_usd,
|
||||
CAST(pay_amount AS DOUBLE PRECISION) AS pay_amount,
|
||||
pay_currency,
|
||||
CAST(exchange_rate AS DOUBLE PRECISION) AS exchange_rate,
|
||||
CAST(refunded_amount_usd AS DOUBLE PRECISION) AS refunded_amount_usd,
|
||||
CAST(refundable_amount_usd AS DOUBLE PRECISION) AS refundable_amount_usd,
|
||||
payment_method,
|
||||
gateway_order_id,
|
||||
gateway_response,
|
||||
status AS effective_status,
|
||||
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM paid_at) AS BIGINT) AS paid_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM credited_at) AS BIGINT) AS credited_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM expires_at) AS BIGINT) AS expires_at_unix_secs
|
||||
FROM payment_orders
|
||||
WHERE order_no = $1
|
||||
LIMIT 1
|
||||
FOR UPDATE
|
||||
"#,
|
||||
)
|
||||
.bind(order_no)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("payment callback order lookup failed: {err}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if let Some(gateway_order_id) = lookup_gateway_order_id.as_deref() {
|
||||
match sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
order_no,
|
||||
wallet_id,
|
||||
user_id,
|
||||
CAST(amount_usd AS DOUBLE PRECISION) AS amount_usd,
|
||||
CAST(pay_amount AS DOUBLE PRECISION) AS pay_amount,
|
||||
pay_currency,
|
||||
CAST(exchange_rate AS DOUBLE PRECISION) AS exchange_rate,
|
||||
CAST(refunded_amount_usd AS DOUBLE PRECISION) AS refunded_amount_usd,
|
||||
CAST(refundable_amount_usd AS DOUBLE PRECISION) AS refundable_amount_usd,
|
||||
payment_method,
|
||||
gateway_order_id,
|
||||
gateway_response,
|
||||
status AS effective_status,
|
||||
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM paid_at) AS BIGINT) AS paid_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM credited_at) AS BIGINT) AS credited_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM expires_at) AS BIGINT) AS expires_at_unix_secs
|
||||
FROM payment_orders
|
||||
WHERE gateway_order_id = $1
|
||||
LIMIT 1
|
||||
FOR UPDATE
|
||||
"#,
|
||||
)
|
||||
.bind(gateway_order_id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("payment callback order lookup failed: {err}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let Some(order_row) = order_row else {
|
||||
update_payment_callback_failure(
|
||||
&mut tx,
|
||||
&callback_id,
|
||||
payload,
|
||||
&callback_payload_hash,
|
||||
signature_valid,
|
||||
"payment order not found",
|
||||
)
|
||||
.await;
|
||||
let _ = tx.commit().await;
|
||||
return payment_callback_mark_failed_response(
|
||||
),
|
||||
aether_data::repository::wallet::ProcessPaymentCallbackOutcome::AlreadyCredited {
|
||||
duplicate,
|
||||
"payment order not found",
|
||||
payment_method,
|
||||
&request_context.request_path,
|
||||
);
|
||||
};
|
||||
|
||||
let order_id = order_row
|
||||
.try_get::<String, _>("id")
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
let order_no = order_row
|
||||
.try_get::<String, _>("order_no")
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
let order_wallet_id = order_row
|
||||
.try_get::<String, _>("wallet_id")
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
let order_payment_method = order_row
|
||||
.try_get::<String, _>("payment_method")
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
let order_amount_usd = order_row
|
||||
.try_get::<f64, _>("amount_usd")
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
let order_status = order_row
|
||||
.try_get::<String, _>("effective_status")
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
let expires_at_unix_secs = order_row
|
||||
.try_get::<Option<i64>, _>("expires_at_unix_secs")
|
||||
.ok()
|
||||
.flatten();
|
||||
if (payload.amount_usd - order_amount_usd).abs() > f64::EPSILON {
|
||||
update_payment_callback_failure(
|
||||
&mut tx,
|
||||
&callback_id,
|
||||
payload,
|
||||
&callback_payload_hash,
|
||||
signature_valid,
|
||||
"callback amount mismatch",
|
||||
)
|
||||
.await;
|
||||
let _ = tx.commit().await;
|
||||
return payment_callback_mark_failed_response(
|
||||
duplicate,
|
||||
"callback amount mismatch",
|
||||
payment_method,
|
||||
&request_context.request_path,
|
||||
);
|
||||
}
|
||||
if !order_payment_method.eq_ignore_ascii_case(payment_method) {
|
||||
update_payment_callback_failure(
|
||||
&mut tx,
|
||||
&callback_id,
|
||||
payload,
|
||||
&callback_payload_hash,
|
||||
signature_valid,
|
||||
"payment method mismatch",
|
||||
)
|
||||
.await;
|
||||
let _ = tx.commit().await;
|
||||
return payment_callback_mark_failed_response(
|
||||
duplicate,
|
||||
"payment method mismatch",
|
||||
payment_method,
|
||||
&request_context.request_path,
|
||||
);
|
||||
}
|
||||
if order_status == "credited" {
|
||||
let _ = sqlx::query(
|
||||
r#"
|
||||
UPDATE payment_callbacks
|
||||
SET payment_order_id = $2,
|
||||
signature_valid = true,
|
||||
status = 'processed',
|
||||
error_message = NULL,
|
||||
payload_hash = $3,
|
||||
payload = $4,
|
||||
processed_at = NOW(),
|
||||
order_no = $5,
|
||||
gateway_order_id = COALESCE($6, gateway_order_id)
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(&callback_id)
|
||||
.bind(&order_id)
|
||||
.bind(&callback_payload_hash)
|
||||
.bind(&payload.payload)
|
||||
.bind(&order_no)
|
||||
.bind(payload.gateway_order_id.as_deref())
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
let _ = tx.commit().await;
|
||||
return build_auth_json_response(
|
||||
order_id,
|
||||
order_no,
|
||||
wallet_id,
|
||||
} => build_auth_json_response(
|
||||
http::StatusCode::OK,
|
||||
json!({
|
||||
"ok": true,
|
||||
@@ -416,351 +95,43 @@ WHERE id = $1
|
||||
"order_id": order_id,
|
||||
"order_no": order_no,
|
||||
"status": "credited",
|
||||
"wallet_id": order_wallet_id,
|
||||
"wallet_id": wallet_id,
|
||||
"payment_method": payment_method,
|
||||
"request_path": request_context.request_path,
|
||||
}),
|
||||
None,
|
||||
);
|
||||
}
|
||||
if matches!(order_status.as_str(), "failed" | "expired" | "refunded") {
|
||||
let error = format!("payment order is not creditable: {order_status}");
|
||||
update_payment_callback_failure(
|
||||
&mut tx,
|
||||
&callback_id,
|
||||
payload,
|
||||
&callback_payload_hash,
|
||||
signature_valid,
|
||||
&error,
|
||||
)
|
||||
.await;
|
||||
let _ = tx.commit().await;
|
||||
return payment_callback_mark_failed_response(
|
||||
),
|
||||
aether_data::repository::wallet::ProcessPaymentCallbackOutcome::Applied {
|
||||
duplicate,
|
||||
&error,
|
||||
payment_method,
|
||||
&request_context.request_path,
|
||||
);
|
||||
order_id,
|
||||
order_no,
|
||||
wallet_id,
|
||||
order,
|
||||
} => build_auth_json_response(
|
||||
http::StatusCode::OK,
|
||||
json!({
|
||||
"ok": true,
|
||||
"duplicate": duplicate,
|
||||
"credited": true,
|
||||
"order_id": order_id,
|
||||
"order_no": order_no,
|
||||
"status": order.status,
|
||||
"wallet_id": wallet_id,
|
||||
"payment_method": payment_method,
|
||||
"request_path": request_context.request_path,
|
||||
}),
|
||||
None,
|
||||
),
|
||||
}
|
||||
if order_status == "pending" {
|
||||
let now = Utc::now().timestamp();
|
||||
if expires_at_unix_secs.is_some_and(|value| value < now) {
|
||||
let _ = sqlx::query("UPDATE payment_orders SET status = 'expired' WHERE id = $1")
|
||||
.bind(&order_id)
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
update_payment_callback_failure(
|
||||
&mut tx,
|
||||
&callback_id,
|
||||
payload,
|
||||
&callback_payload_hash,
|
||||
signature_valid,
|
||||
"payment order expired",
|
||||
)
|
||||
.await;
|
||||
let _ = tx.commit().await;
|
||||
return payment_callback_mark_failed_response(
|
||||
duplicate,
|
||||
"payment order expired",
|
||||
payment_method,
|
||||
&request_context.request_path,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let wallet_row = match sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
status,
|
||||
CAST(balance AS DOUBLE PRECISION) AS balance,
|
||||
CAST(gift_balance AS DOUBLE PRECISION) AS gift_balance,
|
||||
CAST(total_recharged AS DOUBLE PRECISION) AS total_recharged
|
||||
FROM wallets
|
||||
WHERE id = $1
|
||||
LIMIT 1
|
||||
FOR UPDATE
|
||||
"#,
|
||||
)
|
||||
.bind(&order_wallet_id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("payment callback wallet lookup failed: {err}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
let Some(wallet_row) = wallet_row else {
|
||||
update_payment_callback_failure(
|
||||
&mut tx,
|
||||
&callback_id,
|
||||
payload,
|
||||
&callback_payload_hash,
|
||||
signature_valid,
|
||||
"wallet not found",
|
||||
)
|
||||
.await;
|
||||
let _ = tx.commit().await;
|
||||
return payment_callback_mark_failed_response(
|
||||
duplicate,
|
||||
"wallet not found",
|
||||
payment_method,
|
||||
&request_context.request_path,
|
||||
);
|
||||
};
|
||||
let wallet_status = wallet_row
|
||||
.try_get::<String, _>("status")
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
if wallet_status != "active" {
|
||||
update_payment_callback_failure(
|
||||
&mut tx,
|
||||
&callback_id,
|
||||
payload,
|
||||
&callback_payload_hash,
|
||||
signature_valid,
|
||||
"wallet is not active",
|
||||
)
|
||||
.await;
|
||||
let _ = tx.commit().await;
|
||||
return payment_callback_mark_failed_response(
|
||||
duplicate,
|
||||
"wallet is not active",
|
||||
payment_method,
|
||||
&request_context.request_path,
|
||||
);
|
||||
}
|
||||
|
||||
let before_recharge = wallet_row
|
||||
.try_get::<f64, _>("balance")
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
let before_gift = wallet_row
|
||||
.try_get::<f64, _>("gift_balance")
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
let before_total = before_recharge + before_gift;
|
||||
let after_recharge = before_recharge + order_amount_usd;
|
||||
let after_total = after_recharge + before_gift;
|
||||
let wallet_tx_id = Uuid::new_v4().to_string();
|
||||
|
||||
if let Err(err) = sqlx::query(
|
||||
r#"
|
||||
UPDATE wallets
|
||||
SET balance = $2,
|
||||
total_recharged = total_recharged + $3,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(&order_wallet_id)
|
||||
.bind(after_recharge)
|
||||
.bind(order_amount_usd)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
{
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("payment callback wallet update failed: {err}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(err) = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO wallet_transactions (
|
||||
id,
|
||||
wallet_id,
|
||||
category,
|
||||
reason_code,
|
||||
amount,
|
||||
balance_before,
|
||||
balance_after,
|
||||
recharge_balance_before,
|
||||
recharge_balance_after,
|
||||
gift_balance_before,
|
||||
gift_balance_after,
|
||||
link_type,
|
||||
link_id,
|
||||
operator_id,
|
||||
description,
|
||||
created_at
|
||||
)
|
||||
VALUES (
|
||||
$1,
|
||||
$2,
|
||||
'recharge',
|
||||
'topup_gateway',
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7,
|
||||
$8,
|
||||
$9,
|
||||
'payment_order',
|
||||
$10,
|
||||
NULL,
|
||||
$11,
|
||||
NOW()
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(&wallet_tx_id)
|
||||
.bind(&order_wallet_id)
|
||||
.bind(order_amount_usd)
|
||||
.bind(before_total)
|
||||
.bind(after_total)
|
||||
.bind(before_recharge)
|
||||
.bind(after_recharge)
|
||||
.bind(before_gift)
|
||||
.bind(before_gift)
|
||||
.bind(&order_id)
|
||||
.bind(format!("充值到账({payment_method})"))
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
{
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("payment callback wallet transaction failed: {err}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
let updated_order_row = match sqlx::query(
|
||||
r#"
|
||||
UPDATE payment_orders
|
||||
SET gateway_order_id = COALESCE($2, gateway_order_id),
|
||||
gateway_response = $3,
|
||||
pay_amount = COALESCE($4, pay_amount),
|
||||
pay_currency = COALESCE($5, pay_currency),
|
||||
exchange_rate = COALESCE($6, exchange_rate),
|
||||
status = 'credited',
|
||||
paid_at = COALESCE(paid_at, NOW()),
|
||||
credited_at = NOW(),
|
||||
refundable_amount_usd = amount_usd
|
||||
WHERE id = $1
|
||||
RETURNING
|
||||
id,
|
||||
order_no,
|
||||
wallet_id,
|
||||
user_id,
|
||||
CAST(amount_usd AS DOUBLE PRECISION) AS amount_usd,
|
||||
CAST(pay_amount AS DOUBLE PRECISION) AS pay_amount,
|
||||
pay_currency,
|
||||
CAST(exchange_rate AS DOUBLE PRECISION) AS exchange_rate,
|
||||
CAST(refunded_amount_usd AS DOUBLE PRECISION) AS refunded_amount_usd,
|
||||
CAST(refundable_amount_usd AS DOUBLE PRECISION) AS refundable_amount_usd,
|
||||
payment_method,
|
||||
gateway_order_id,
|
||||
gateway_response,
|
||||
status AS effective_status,
|
||||
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM paid_at) AS BIGINT) AS paid_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM credited_at) AS BIGINT) AS credited_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM expires_at) AS BIGINT) AS expires_at_unix_secs
|
||||
"#,
|
||||
)
|
||||
.bind(&order_id)
|
||||
.bind(payload.gateway_order_id.as_deref())
|
||||
.bind(&payload.payload)
|
||||
.bind(payload.pay_amount)
|
||||
.bind(payload.pay_currency.as_deref())
|
||||
.bind(payload.exchange_rate)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("payment callback order update failed: {err}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = sqlx::query(
|
||||
r#"
|
||||
UPDATE payment_callbacks
|
||||
SET payment_order_id = $2,
|
||||
signature_valid = true,
|
||||
status = 'processed',
|
||||
error_message = NULL,
|
||||
payload_hash = $3,
|
||||
payload = $4,
|
||||
processed_at = NOW(),
|
||||
order_no = $5,
|
||||
gateway_order_id = COALESCE($6, gateway_order_id)
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(&callback_id)
|
||||
.bind(&order_id)
|
||||
.bind(&callback_payload_hash)
|
||||
.bind(&payload.payload)
|
||||
.bind(&order_no)
|
||||
.bind(payload.gateway_order_id.as_deref())
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
{
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("payment callback update failed: {err}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
if let Err(err) = tx.commit().await {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("payment callback commit failed: {err}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
let updated_order_payload = match wallet_payment_order_payload_from_row(&updated_order_row) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("payment callback order payload failed: {err:?}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
build_auth_json_response(
|
||||
http::StatusCode::OK,
|
||||
json!({
|
||||
"ok": true,
|
||||
"duplicate": duplicate,
|
||||
"credited": true,
|
||||
"order_id": order_id,
|
||||
"order_no": order_no,
|
||||
"status": updated_order_payload["status"],
|
||||
"wallet_id": order_wallet_id,
|
||||
"payment_method": payment_method,
|
||||
"request_path": request_context.request_path,
|
||||
}),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{handle_payment_callback_with_postgres, AppState, NormalizedPaymentCallbackRequest};
|
||||
use crate::gateway::handlers::public::support::support_payment::PAYMENT_CALLBACK_STORAGE_UNAVAILABLE_DETAIL;
|
||||
use crate::gateway::GatewayPublicRequestContext;
|
||||
use super::{
|
||||
handle_payment_callback_with_postgres, AppState, NormalizedPaymentCallbackRequest,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::public::support::support_payment::PAYMENT_CALLBACK_STORAGE_UNAVAILABLE_DETAIL;
|
||||
use axum::body::to_bytes;
|
||||
use axum::http::{HeaderMap, Method, Uri};
|
||||
use serde_json::json;
|
||||
|
||||
@@ -31,7 +31,7 @@ pub(super) async fn maybe_build_local_payment_callback_route_response(
|
||||
));
|
||||
};
|
||||
let Some(provided_token) =
|
||||
crate::gateway::headers::header_value_str(headers, PAYMENT_CALLBACK_TOKEN_HEADER)
|
||||
crate::headers::header_value_str(headers, PAYMENT_CALLBACK_TOKEN_HEADER)
|
||||
else {
|
||||
return Some(build_auth_error_response(
|
||||
http::StatusCode::UNAUTHORIZED,
|
||||
@@ -47,7 +47,7 @@ pub(super) async fn maybe_build_local_payment_callback_route_response(
|
||||
));
|
||||
}
|
||||
let Some(signature) =
|
||||
crate::gateway::headers::header_value_str(headers, PAYMENT_CALLBACK_SIGNATURE_HEADER)
|
||||
crate::headers::header_value_str(headers, PAYMENT_CALLBACK_SIGNATURE_HEADER)
|
||||
else {
|
||||
return Some(build_auth_error_response(
|
||||
http::StatusCode::UNAUTHORIZED,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
pub(super) use super::{query_param_value, AppState, GatewayPublicRequestContext};
|
||||
use crate::gateway::handlers::admin::provider_catalog_key_supports_format;
|
||||
use crate::handlers::admin::misc_helpers::provider_catalog_key_supports_format;
|
||||
|
||||
#[path = "test_connection/route.rs"]
|
||||
mod test_connection_route;
|
||||
|
||||
@@ -157,7 +157,7 @@ pub(super) async fn maybe_build_local_test_connection_route_response(
|
||||
if transport.provider.proxy.is_some()
|
||||
|| transport.endpoint.proxy.is_some()
|
||||
|| transport.key.proxy.is_some()
|
||||
|| crate::gateway::provider_transport::resolve_transport_tls_profile(&transport).is_some()
|
||||
|| crate::provider_transport::resolve_transport_tls_profile(&transport).is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -179,7 +179,7 @@ pub(super) async fn maybe_build_local_test_connection_route_response(
|
||||
}),
|
||||
_ => return None,
|
||||
};
|
||||
if !crate::gateway::provider_transport::apply_local_body_rules(
|
||||
if !crate::provider_transport::apply_local_body_rules(
|
||||
&mut provider_request_body,
|
||||
transport.endpoint.body_rules.as_ref(),
|
||||
None,
|
||||
@@ -191,7 +191,7 @@ pub(super) async fn maybe_build_local_test_connection_route_response(
|
||||
"openai:chat" | "claude:chat" => {
|
||||
match state.resolve_local_oauth_request_auth(&transport).await {
|
||||
Ok(Some(
|
||||
crate::gateway::provider_transport::LocalResolvedOAuthRequestAuth::Header {
|
||||
crate::provider_transport::LocalResolvedOAuthRequestAuth::Header {
|
||||
name,
|
||||
value,
|
||||
},
|
||||
@@ -204,14 +204,16 @@ pub(super) async fn maybe_build_local_test_connection_route_response(
|
||||
|
||||
let auth = match format_value.as_str() {
|
||||
"openai:chat" => {
|
||||
crate::gateway::provider_transport::resolve_local_openai_chat_auth(&transport)
|
||||
crate::provider_transport::auth::resolve_local_openai_chat_auth(&transport)
|
||||
.or(oauth_auth.clone())
|
||||
}
|
||||
"claude:chat" => {
|
||||
crate::gateway::provider_transport::resolve_local_standard_auth(&transport)
|
||||
crate::provider_transport::auth::resolve_local_standard_auth(&transport)
|
||||
.or(oauth_auth.clone())
|
||||
}
|
||||
"gemini:chat" => crate::gateway::provider_transport::resolve_local_gemini_auth(&transport),
|
||||
"gemini:chat" => {
|
||||
crate::provider_transport::auth::resolve_local_gemini_auth(&transport)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let Some((auth_header, auth_value)) = auth else {
|
||||
@@ -227,7 +229,7 @@ pub(super) async fn maybe_build_local_test_connection_route_response(
|
||||
.filter(|value| !value.is_empty());
|
||||
match (format_value.as_str(), custom_path) {
|
||||
("openai:chat", Some(path)) | ("claude:chat", Some(path)) => {
|
||||
crate::gateway::provider_transport::build_passthrough_path_url(
|
||||
crate::provider_transport::url::build_passthrough_path_url(
|
||||
&transport.endpoint.base_url,
|
||||
path,
|
||||
None,
|
||||
@@ -235,31 +237,33 @@ pub(super) async fn maybe_build_local_test_connection_route_response(
|
||||
)
|
||||
}
|
||||
("gemini:chat", Some(path)) => {
|
||||
crate::gateway::provider_transport::build_passthrough_path_url(
|
||||
crate::provider_transport::url::build_passthrough_path_url(
|
||||
&transport.endpoint.base_url,
|
||||
path,
|
||||
None,
|
||||
&["key"],
|
||||
)
|
||||
}
|
||||
("openai:chat", None) => {
|
||||
Some(crate::gateway::provider_transport::build_openai_chat_url(
|
||||
&transport.endpoint.base_url,
|
||||
None,
|
||||
))
|
||||
}
|
||||
("claude:chat", None) => Some(
|
||||
crate::gateway::provider_transport::build_claude_messages_url(
|
||||
("openai:chat", None) => Some(
|
||||
crate::provider_transport::url::build_openai_chat_url(
|
||||
&transport.endpoint.base_url,
|
||||
None,
|
||||
),
|
||||
),
|
||||
("gemini:chat", None) => crate::gateway::provider_transport::build_gemini_content_url(
|
||||
&transport.endpoint.base_url,
|
||||
&model,
|
||||
false,
|
||||
None,
|
||||
("claude:chat", None) => Some(
|
||||
crate::provider_transport::url::build_claude_messages_url(
|
||||
&transport.endpoint.base_url,
|
||||
None,
|
||||
),
|
||||
),
|
||||
("gemini:chat", None) => {
|
||||
crate::provider_transport::url::build_gemini_content_url(
|
||||
&transport.endpoint.base_url,
|
||||
&model,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
@@ -271,7 +275,7 @@ pub(super) async fn maybe_build_local_test_connection_route_response(
|
||||
("content-type".to_string(), "application/json".to_string()),
|
||||
(auth_header.clone(), auth_value.clone()),
|
||||
]);
|
||||
if !crate::gateway::provider_transport::apply_local_header_rules(
|
||||
if !crate::provider_transport::apply_local_header_rules(
|
||||
&mut provider_request_headers,
|
||||
transport.endpoint.header_rules.as_ref(),
|
||||
&[auth_header.as_str(), "content-type"],
|
||||
@@ -280,7 +284,7 @@ pub(super) async fn maybe_build_local_test_connection_route_response(
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
crate::gateway::provider_transport::ensure_upstream_auth_header(
|
||||
crate::provider_transport::ensure_upstream_auth_header(
|
||||
&mut provider_request_headers,
|
||||
&auth_header,
|
||||
&auth_value,
|
||||
@@ -291,7 +295,7 @@ pub(super) async fn maybe_build_local_test_connection_route_response(
|
||||
upstream_request = upstream_request.header(name, value);
|
||||
}
|
||||
if let Some(total_ms) =
|
||||
crate::gateway::provider_transport::resolve_transport_execution_timeouts(&transport)
|
||||
crate::provider_transport::resolve_transport_execution_timeouts(&transport)
|
||||
.and_then(|timeouts| timeouts.total_ms.or(timeouts.first_byte_ms))
|
||||
{
|
||||
upstream_request = upstream_request.timeout(Duration::from_millis(total_ms));
|
||||
|
||||
@@ -5,12 +5,12 @@ use super::{
|
||||
unix_secs_to_rfc3339, validate_auth_register_password, AppState, AuthenticatedLocalUserContext,
|
||||
GatewayPublicRequestContext, PUBLIC_CAPABILITY_DEFINITIONS,
|
||||
};
|
||||
use crate::gateway::handlers::admin::{
|
||||
admin_stats_bad_request_response, build_admin_endpoint_health_status_payload,
|
||||
list_usage_for_optional_range, parse_bounded_u32, round_to, AdminStatsTimeRange,
|
||||
AdminStatsUsageFilter,
|
||||
use crate::handlers::admin::endpoints_health_helpers::build_admin_endpoint_health_status_payload;
|
||||
use crate::handlers::internal::build_management_token_payload;
|
||||
use crate::handlers::{
|
||||
admin_stats_bad_request_response, list_usage_for_optional_range, parse_bounded_u32, round_to,
|
||||
AdminStatsTimeRange, AdminStatsUsageFilter,
|
||||
};
|
||||
use crate::gateway::handlers::internal::build_management_token_payload;
|
||||
|
||||
const USERS_ME_AVAILABLE_MODELS_FETCH_LIMIT: usize = 1000;
|
||||
|
||||
|
||||
@@ -429,7 +429,7 @@ async fn resolve_users_me_api_key_snapshot_by_id(
|
||||
state: &AppState,
|
||||
user_id: &str,
|
||||
api_key_id: &str,
|
||||
) -> Result<crate::gateway::gateway_data::StoredGatewayAuthApiKeySnapshot, Response<Body>> {
|
||||
) -> Result<crate::data::auth::GatewayAuthApiKeySnapshot, Response<Body>> {
|
||||
let snapshot = match state
|
||||
.read_auth_api_key_snapshot(
|
||||
user_id,
|
||||
@@ -465,7 +465,7 @@ async fn resolve_users_me_api_key_snapshot_by_id(
|
||||
}
|
||||
|
||||
fn ensure_users_me_api_key_mutable(
|
||||
snapshot: &crate::gateway::gateway_data::StoredGatewayAuthApiKeySnapshot,
|
||||
snapshot: &crate::data::auth::GatewayAuthApiKeySnapshot,
|
||||
) -> Result<(), Response<Body>> {
|
||||
if snapshot.api_key_is_locked {
|
||||
return Err(build_auth_error_response(
|
||||
|
||||
@@ -19,7 +19,7 @@ use super::{
|
||||
query_param_value, resolve_authenticated_local_user, AppState, AuthenticatedLocalUserContext,
|
||||
GatewayPublicRequestContext,
|
||||
};
|
||||
use crate::gateway::LocalMutationOutcome;
|
||||
use crate::LocalMutationOutcome;
|
||||
|
||||
const USERS_ME_MANAGEMENT_TOKEN_PREFIX: &str = "ae_";
|
||||
const USERS_ME_MANAGEMENT_TOKEN_RANDOM_LENGTH: usize = 40;
|
||||
|
||||
@@ -73,7 +73,7 @@ fn validate_user_model_capability_settings(
|
||||
}
|
||||
|
||||
fn build_users_me_preferences_payload(
|
||||
preferences: &crate::gateway::gateway_data::StoredUserPreferenceRecord,
|
||||
preferences: &crate::data::state::StoredUserPreferenceRecord,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
"avatar_url": preferences.avatar_url,
|
||||
@@ -177,9 +177,11 @@ pub(super) async fn handle_users_me_preferences_get(
|
||||
|
||||
let preferences = match state.read_user_preferences(&auth.user.id).await {
|
||||
Ok(Some(value)) => value,
|
||||
Ok(None) => crate::gateway::gateway_data::StoredUserPreferenceRecord::default_for_user(
|
||||
&auth.user.id,
|
||||
),
|
||||
Ok(None) => {
|
||||
crate::data::state::StoredUserPreferenceRecord::default_for_user(
|
||||
&auth.user.id,
|
||||
)
|
||||
}
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
@@ -217,9 +219,11 @@ pub(super) async fn handle_users_me_preferences_put(
|
||||
|
||||
let mut preferences = match state.read_user_preferences(&auth.user.id).await {
|
||||
Ok(Some(value)) => value,
|
||||
Ok(None) => crate::gateway::gateway_data::StoredUserPreferenceRecord::default_for_user(
|
||||
&auth.user.id,
|
||||
),
|
||||
Ok(None) => {
|
||||
crate::data::state::StoredUserPreferenceRecord::default_for_user(
|
||||
&auth.user.id,
|
||||
)
|
||||
}
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::gateway::handlers::public::support::build_unhandled_public_support_response;
|
||||
use crate::handlers::public::support::build_unhandled_public_support_response;
|
||||
use axum::{body::Body, http, response::Response};
|
||||
|
||||
use super::{
|
||||
|
||||
@@ -34,7 +34,7 @@ pub(super) fn users_me_session_detail_path_matches(request_path: &str) -> bool {
|
||||
}
|
||||
|
||||
fn build_users_me_session_payload(
|
||||
session: crate::gateway::gateway_data::StoredUserSessionRecord,
|
||||
session: crate::data::state::StoredUserSessionRecord,
|
||||
current_session_id: &str,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use crate::gateway::gateway_data::StoredUserSessionRecord;
|
||||
use crate::data::state::StoredUserSessionRecord;
|
||||
|
||||
pub(super) fn format_users_me_optional_datetime_iso8601(
|
||||
value: Option<DateTime<Utc>>,
|
||||
|
||||
@@ -30,7 +30,7 @@ use self::reads::{
|
||||
build_wallet_daily_usage_payload, build_wallet_payload, build_wallet_zero_today_entry,
|
||||
handle_wallet_balance, handle_wallet_today_cost, handle_wallet_transactions,
|
||||
parse_wallet_limit, parse_wallet_offset, wallet_fixed_offset, wallet_today_billing_date_string,
|
||||
wallet_transaction_payload_from_row,
|
||||
wallet_transaction_payload_from_record,
|
||||
};
|
||||
use self::recharge::{
|
||||
handle_wallet_create_recharge, handle_wallet_recharge_detail, handle_wallet_recharge_list,
|
||||
|
||||
@@ -2,11 +2,10 @@ use super::{
|
||||
build_auth_error_response, build_auth_json_response, build_wallet_daily_usage_payload,
|
||||
build_wallet_payload, build_wallet_zero_today_entry, http, parse_wallet_limit,
|
||||
parse_wallet_offset, resolve_authenticated_local_user, unix_secs_to_rfc3339,
|
||||
wallet_fixed_offset, wallet_today_billing_date_string, wallet_transaction_payload_from_row,
|
||||
wallet_fixed_offset, wallet_today_billing_date_string, wallet_transaction_payload_from_record,
|
||||
AppState, Body, GatewayPublicRequestContext, Response, WALLET_LEGACY_TIMEZONE,
|
||||
};
|
||||
use serde_json::json;
|
||||
use sqlx::Row;
|
||||
|
||||
fn wallet_flow_sort_key(item_type: &str, payload: &serde_json::Value) -> (String, u8, String) {
|
||||
match item_type {
|
||||
@@ -100,223 +99,85 @@ pub(super) async fn handle_wallet_flow(
|
||||
};
|
||||
|
||||
let mut today_entry = build_wallet_zero_today_entry();
|
||||
let mut items = Vec::new();
|
||||
let mut total = 0_u64;
|
||||
if let Some(pool) = state.postgres_pool() {
|
||||
let today_row = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
billing_date::text AS billing_date,
|
||||
billing_timezone,
|
||||
CAST(total_cost_usd AS DOUBLE PRECISION) AS total_cost_usd,
|
||||
total_requests,
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
cache_creation_tokens,
|
||||
cache_read_tokens,
|
||||
CAST(EXTRACT(EPOCH FROM first_finalized_at) AS BIGINT) AS first_finalized_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM last_finalized_at) AS BIGINT) AS last_finalized_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM aggregated_at) AS BIGINT) AS aggregated_at_unix_secs
|
||||
FROM wallet_daily_usage_ledgers
|
||||
WHERE wallet_id = $1
|
||||
AND billing_timezone = $2
|
||||
AND billing_date = (timezone($2, now()))::date
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.bind(&wallet.id)
|
||||
.bind(WALLET_LEGACY_TIMEZONE)
|
||||
.fetch_optional(&pool)
|
||||
.await;
|
||||
if let Ok(Some(row)) = today_row {
|
||||
today_entry = build_wallet_daily_usage_payload(
|
||||
row.try_get::<Option<String>, _>("id").ok().flatten(),
|
||||
row.try_get::<String, _>("billing_date")
|
||||
.ok()
|
||||
.unwrap_or_else(wallet_today_billing_date_string),
|
||||
row.try_get::<String, _>("billing_timezone")
|
||||
.ok()
|
||||
.unwrap_or_else(|| WALLET_LEGACY_TIMEZONE.to_string()),
|
||||
row.try_get::<f64, _>("total_cost_usd")
|
||||
.ok()
|
||||
.unwrap_or_default(),
|
||||
row.try_get::<i64, _>("total_requests")
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
.max(0) as u64,
|
||||
row.try_get::<i64, _>("input_tokens")
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
.max(0) as u64,
|
||||
row.try_get::<i64, _>("output_tokens")
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
.max(0) as u64,
|
||||
row.try_get::<i64, _>("cache_creation_tokens")
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
.max(0) as u64,
|
||||
row.try_get::<i64, _>("cache_read_tokens")
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
.max(0) as u64,
|
||||
row.try_get::<Option<i64>, _>("first_finalized_at_unix_secs")
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|value| u64::try_from(value).ok())
|
||||
.and_then(unix_secs_to_rfc3339),
|
||||
row.try_get::<Option<i64>, _>("last_finalized_at_unix_secs")
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|value| u64::try_from(value).ok())
|
||||
.and_then(unix_secs_to_rfc3339),
|
||||
row.try_get::<Option<i64>, _>("aggregated_at_unix_secs")
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|value| u64::try_from(value).ok())
|
||||
.and_then(unix_secs_to_rfc3339),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
let fetch_size = offset.saturating_add(limit).min(5200);
|
||||
let tx_count_row = sqlx::query(
|
||||
r#"
|
||||
SELECT COUNT(*) AS total
|
||||
FROM wallet_transactions
|
||||
WHERE wallet_id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(&wallet.id)
|
||||
.fetch_one(&pool)
|
||||
.await;
|
||||
let tx_total = tx_count_row
|
||||
.ok()
|
||||
.and_then(|row| row.try_get::<i64, _>("total").ok())
|
||||
.unwrap_or_default()
|
||||
.max(0) as u64;
|
||||
let tx_rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
category,
|
||||
reason_code,
|
||||
CAST(amount AS DOUBLE PRECISION) AS amount,
|
||||
CAST(balance_before AS DOUBLE PRECISION) AS balance_before,
|
||||
CAST(balance_after AS DOUBLE PRECISION) AS balance_after,
|
||||
CAST(recharge_balance_before AS DOUBLE PRECISION) AS recharge_balance_before,
|
||||
CAST(recharge_balance_after AS DOUBLE PRECISION) AS recharge_balance_after,
|
||||
CAST(gift_balance_before AS DOUBLE PRECISION) AS gift_balance_before,
|
||||
CAST(gift_balance_after AS DOUBLE PRECISION) AS gift_balance_after,
|
||||
link_type,
|
||||
link_id,
|
||||
operator_id,
|
||||
description,
|
||||
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_secs
|
||||
FROM wallet_transactions
|
||||
WHERE wallet_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2
|
||||
"#,
|
||||
)
|
||||
.bind(&wallet.id)
|
||||
.bind(i64::try_from(fetch_size).ok().unwrap_or(50))
|
||||
.fetch_all(&pool)
|
||||
if let Ok(Some(today_usage)) = state
|
||||
.find_wallet_today_usage(&wallet.id, WALLET_LEGACY_TIMEZONE)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let daily_count_row = sqlx::query(
|
||||
r#"
|
||||
SELECT COUNT(*) AS total
|
||||
FROM wallet_daily_usage_ledgers
|
||||
WHERE wallet_id = $1
|
||||
AND billing_timezone = $2
|
||||
AND billing_date < (timezone($2, now()))::date
|
||||
"#,
|
||||
)
|
||||
.bind(&wallet.id)
|
||||
.bind(WALLET_LEGACY_TIMEZONE)
|
||||
.fetch_one(&pool)
|
||||
.await;
|
||||
let daily_total = daily_count_row
|
||||
.ok()
|
||||
.and_then(|row| row.try_get::<i64, _>("total").ok())
|
||||
.unwrap_or_default()
|
||||
.max(0) as u64;
|
||||
let daily_rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
billing_date::text AS billing_date,
|
||||
billing_timezone,
|
||||
CAST(total_cost_usd AS DOUBLE PRECISION) AS total_cost_usd,
|
||||
total_requests,
|
||||
input_tokens,
|
||||
output_tokens,
|
||||
cache_creation_tokens,
|
||||
cache_read_tokens,
|
||||
CAST(EXTRACT(EPOCH FROM first_finalized_at) AS BIGINT) AS first_finalized_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM last_finalized_at) AS BIGINT) AS last_finalized_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM aggregated_at) AS BIGINT) AS aggregated_at_unix_secs
|
||||
FROM wallet_daily_usage_ledgers
|
||||
WHERE wallet_id = $1
|
||||
AND billing_timezone = $2
|
||||
AND billing_date < (timezone($2, now()))::date
|
||||
ORDER BY billing_date DESC
|
||||
LIMIT $3
|
||||
"#,
|
||||
)
|
||||
.bind(&wallet.id)
|
||||
.bind(WALLET_LEGACY_TIMEZONE)
|
||||
.bind(i64::try_from(fetch_size).ok().unwrap_or(50))
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut merged = tx_rows
|
||||
.iter()
|
||||
.filter_map(|row| wallet_transaction_payload_from_row(row).ok())
|
||||
.map(|data| json!({ "type": "transaction", "data": data }))
|
||||
.collect::<Vec<_>>();
|
||||
merged.extend(daily_rows.iter().map(|row| {
|
||||
json!({
|
||||
"type": "daily_usage",
|
||||
"data": build_wallet_daily_usage_payload(
|
||||
row.try_get::<Option<String>, _>("id").ok().flatten(),
|
||||
row.try_get::<String, _>("billing_date").ok().unwrap_or_default(),
|
||||
row.try_get::<String, _>("billing_timezone").ok().unwrap_or_else(|| WALLET_LEGACY_TIMEZONE.to_string()),
|
||||
row.try_get::<f64, _>("total_cost_usd").ok().unwrap_or_default(),
|
||||
row.try_get::<i64, _>("total_requests").ok().unwrap_or_default().max(0) as u64,
|
||||
row.try_get::<i64, _>("input_tokens").ok().unwrap_or_default().max(0) as u64,
|
||||
row.try_get::<i64, _>("output_tokens").ok().unwrap_or_default().max(0) as u64,
|
||||
row.try_get::<i64, _>("cache_creation_tokens").ok().unwrap_or_default().max(0) as u64,
|
||||
row.try_get::<i64, _>("cache_read_tokens").ok().unwrap_or_default().max(0) as u64,
|
||||
row.try_get::<Option<i64>, _>("first_finalized_at_unix_secs").ok().flatten().and_then(|value| u64::try_from(value).ok()).and_then(unix_secs_to_rfc3339),
|
||||
row.try_get::<Option<i64>, _>("last_finalized_at_unix_secs").ok().flatten().and_then(|value| u64::try_from(value).ok()).and_then(unix_secs_to_rfc3339),
|
||||
row.try_get::<Option<i64>, _>("aggregated_at_unix_secs").ok().flatten().and_then(|value| u64::try_from(value).ok()).and_then(unix_secs_to_rfc3339),
|
||||
false,
|
||||
)
|
||||
})
|
||||
}));
|
||||
merged.sort_by(|left, right| {
|
||||
let left_type = left
|
||||
.get("type")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
let right_type = right
|
||||
.get("type")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
wallet_flow_sort_key(right_type, right).cmp(&wallet_flow_sort_key(left_type, left))
|
||||
});
|
||||
items = merged
|
||||
.into_iter()
|
||||
.skip(offset)
|
||||
.take(limit)
|
||||
.collect::<Vec<_>>();
|
||||
total = tx_total.saturating_add(daily_total);
|
||||
{
|
||||
today_entry = build_wallet_daily_usage_payload(
|
||||
today_usage.id,
|
||||
today_usage.billing_date,
|
||||
today_usage.billing_timezone,
|
||||
today_usage.total_cost_usd,
|
||||
today_usage.total_requests,
|
||||
today_usage.input_tokens,
|
||||
today_usage.output_tokens,
|
||||
today_usage.cache_creation_tokens,
|
||||
today_usage.cache_read_tokens,
|
||||
today_usage
|
||||
.first_finalized_at_unix_secs
|
||||
.and_then(unix_secs_to_rfc3339),
|
||||
today_usage
|
||||
.last_finalized_at_unix_secs
|
||||
.and_then(unix_secs_to_rfc3339),
|
||||
today_usage
|
||||
.aggregated_at_unix_secs
|
||||
.and_then(unix_secs_to_rfc3339),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
let fetch_size = offset.saturating_add(limit).min(5200);
|
||||
let (transactions, tx_total) = state
|
||||
.list_admin_wallet_transactions(&wallet.id, fetch_size, 0)
|
||||
.await
|
||||
.unwrap_or((Vec::new(), 0));
|
||||
let daily_page = state
|
||||
.list_wallet_daily_usage_history(&wallet.id, WALLET_LEGACY_TIMEZONE, fetch_size)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut merged = transactions
|
||||
.iter()
|
||||
.map(|record| json!({ "type": "transaction", "data": wallet_transaction_payload_from_record(record) }))
|
||||
.collect::<Vec<_>>();
|
||||
merged.extend(daily_page.items.iter().map(|entry| {
|
||||
json!({
|
||||
"type": "daily_usage",
|
||||
"data": build_wallet_daily_usage_payload(
|
||||
entry.id.clone(),
|
||||
entry.billing_date.clone(),
|
||||
entry.billing_timezone.clone(),
|
||||
entry.total_cost_usd,
|
||||
entry.total_requests,
|
||||
entry.input_tokens,
|
||||
entry.output_tokens,
|
||||
entry.cache_creation_tokens,
|
||||
entry.cache_read_tokens,
|
||||
entry.first_finalized_at_unix_secs.and_then(unix_secs_to_rfc3339),
|
||||
entry.last_finalized_at_unix_secs.and_then(unix_secs_to_rfc3339),
|
||||
entry.aggregated_at_unix_secs.and_then(unix_secs_to_rfc3339),
|
||||
false,
|
||||
)
|
||||
})
|
||||
}));
|
||||
merged.sort_by(|left, right| {
|
||||
let left_type = left
|
||||
.get("type")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
let right_type = right
|
||||
.get("type")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
wallet_flow_sort_key(right_type, right).cmp(&wallet_flow_sort_key(left_type, left))
|
||||
});
|
||||
let items = merged
|
||||
.into_iter()
|
||||
.skip(offset)
|
||||
.take(limit)
|
||||
.collect::<Vec<_>>();
|
||||
let total = tx_total.saturating_add(daily_page.total);
|
||||
|
||||
let mut payload = json!({
|
||||
"today_entry": today_entry,
|
||||
"items": items,
|
||||
|
||||
@@ -3,10 +3,9 @@ use super::{
|
||||
query_param_value, resolve_authenticated_local_user, unix_secs_to_rfc3339, AppState, Body,
|
||||
GatewayError, GatewayPublicRequestContext, Response, WALLET_LEGACY_TIMEZONE,
|
||||
};
|
||||
use crate::gateway::handlers::admin::round_to;
|
||||
use crate::handlers::round_to;
|
||||
use chrono::Utc;
|
||||
use serde_json::json;
|
||||
use sqlx::Row;
|
||||
|
||||
const WALLET_TODAY_COST_UNAVAILABLE_DETAIL: &str = "钱包今日费用数据暂不可用";
|
||||
|
||||
@@ -135,31 +134,26 @@ pub(super) fn build_wallet_zero_today_entry() -> serde_json::Value {
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn wallet_transaction_payload_from_row(
|
||||
row: &sqlx::postgres::PgRow,
|
||||
) -> Result<serde_json::Value, GatewayError> {
|
||||
let created_at = row
|
||||
.try_get::<Option<i64>, _>("created_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.and_then(|value| u64::try_from(value).ok())
|
||||
.and_then(unix_secs_to_rfc3339);
|
||||
Ok(json!({
|
||||
"id": row.try_get::<String, _>("id").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"category": row.try_get::<String, _>("category").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"reason_code": row.try_get::<String, _>("reason_code").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"amount": row.try_get::<f64, _>("amount").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"balance_before": row.try_get::<f64, _>("balance_before").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"balance_after": row.try_get::<f64, _>("balance_after").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"recharge_balance_before": row.try_get::<f64, _>("recharge_balance_before").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"recharge_balance_after": row.try_get::<f64, _>("recharge_balance_after").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"gift_balance_before": row.try_get::<f64, _>("gift_balance_before").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"gift_balance_after": row.try_get::<f64, _>("gift_balance_after").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"link_type": row.try_get::<Option<String>, _>("link_type").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"link_id": row.try_get::<Option<String>, _>("link_id").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"operator_id": row.try_get::<Option<String>, _>("operator_id").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"description": row.try_get::<Option<String>, _>("description").map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
"created_at": created_at,
|
||||
}))
|
||||
pub(super) fn wallet_transaction_payload_from_record(
|
||||
record: &aether_data::repository::wallet::StoredAdminWalletTransaction,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
"id": record.id.clone(),
|
||||
"category": record.category.clone(),
|
||||
"reason_code": record.reason_code.clone(),
|
||||
"amount": record.amount,
|
||||
"balance_before": record.balance_before,
|
||||
"balance_after": record.balance_after,
|
||||
"recharge_balance_before": record.recharge_balance_before,
|
||||
"recharge_balance_after": record.recharge_balance_after,
|
||||
"gift_balance_before": record.gift_balance_before,
|
||||
"gift_balance_after": record.gift_balance_after,
|
||||
"link_type": record.link_type.clone(),
|
||||
"link_id": record.link_id.clone(),
|
||||
"operator_id": record.operator_id.clone(),
|
||||
"description": record.description.clone(),
|
||||
"created_at": record.created_at_unix_secs.and_then(unix_secs_to_rfc3339),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn handle_wallet_balance(
|
||||
@@ -342,89 +336,23 @@ pub(super) async fn handle_wallet_transactions(
|
||||
);
|
||||
};
|
||||
|
||||
let mut total = 0_u64;
|
||||
let mut items = Vec::new();
|
||||
if let Some(pool) = state.postgres_pool() {
|
||||
let count_row = match sqlx::query(
|
||||
r#"
|
||||
SELECT COUNT(*) AS total
|
||||
FROM wallet_transactions
|
||||
WHERE wallet_id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(&wallet.id)
|
||||
.fetch_one(&pool)
|
||||
let (transactions, total) = match state
|
||||
.list_admin_wallet_transactions(&wallet.id, limit, offset)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet transaction count failed: {err}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
total = count_row
|
||||
.try_get::<i64, _>("total")
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
.max(0) as u64;
|
||||
let rows = match sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
category,
|
||||
reason_code,
|
||||
CAST(amount AS DOUBLE PRECISION) AS amount,
|
||||
CAST(balance_before AS DOUBLE PRECISION) AS balance_before,
|
||||
CAST(balance_after AS DOUBLE PRECISION) AS balance_after,
|
||||
CAST(recharge_balance_before AS DOUBLE PRECISION) AS recharge_balance_before,
|
||||
CAST(recharge_balance_after AS DOUBLE PRECISION) AS recharge_balance_after,
|
||||
CAST(gift_balance_before AS DOUBLE PRECISION) AS gift_balance_before,
|
||||
CAST(gift_balance_after AS DOUBLE PRECISION) AS gift_balance_after,
|
||||
link_type,
|
||||
link_id,
|
||||
operator_id,
|
||||
description,
|
||||
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_secs
|
||||
FROM wallet_transactions
|
||||
WHERE wallet_id = $1
|
||||
ORDER BY created_at DESC
|
||||
OFFSET $2
|
||||
LIMIT $3
|
||||
"#,
|
||||
)
|
||||
.bind(&wallet.id)
|
||||
.bind(i64::try_from(offset).ok().unwrap_or_default())
|
||||
.bind(i64::try_from(limit).ok().unwrap_or_default())
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet transaction query failed: {err}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
items = match rows
|
||||
.iter()
|
||||
.map(wallet_transaction_payload_from_row)
|
||||
.collect::<Result<Vec<_>, GatewayError>>()
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet transaction payload failed: {err:?}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet transaction lookup failed: {err:?}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
let items = transactions
|
||||
.iter()
|
||||
.map(wallet_transaction_payload_from_record)
|
||||
.collect::<Vec<_>>();
|
||||
let mut payload = json!({
|
||||
"items": items,
|
||||
"total": total,
|
||||
|
||||
@@ -262,6 +262,31 @@ pub(crate) fn wallet_payment_order_payload_from_row(
|
||||
))
|
||||
}
|
||||
|
||||
fn wallet_payment_order_payload_from_record(
|
||||
record: &aether_data::repository::wallet::StoredAdminPaymentOrder,
|
||||
) -> serde_json::Value {
|
||||
build_wallet_payment_order_payload(
|
||||
record.id.clone(),
|
||||
record.order_no.clone(),
|
||||
record.wallet_id.clone(),
|
||||
record.user_id.clone(),
|
||||
record.amount_usd,
|
||||
record.pay_amount,
|
||||
record.pay_currency.clone(),
|
||||
record.exchange_rate,
|
||||
record.refunded_amount_usd,
|
||||
record.refundable_amount_usd,
|
||||
record.payment_method.clone(),
|
||||
record.gateway_order_id.clone(),
|
||||
record.gateway_response.clone(),
|
||||
record.status.clone(),
|
||||
Some(unix_secs_to_rfc3339(record.created_at_unix_secs)).flatten(),
|
||||
record.paid_at_unix_secs.and_then(unix_secs_to_rfc3339),
|
||||
record.credited_at_unix_secs.and_then(unix_secs_to_rfc3339),
|
||||
record.expires_at_unix_secs.and_then(unix_secs_to_rfc3339),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) async fn handle_wallet_create_recharge(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
@@ -311,7 +336,7 @@ pub(super) async fn handle_wallet_create_recharge(
|
||||
}
|
||||
};
|
||||
|
||||
let Some(pool) = state.postgres_pool() else {
|
||||
if state.postgres_pool().is_none() {
|
||||
#[cfg(test)]
|
||||
{
|
||||
let Some(wallet) = wallet else {
|
||||
@@ -375,239 +400,56 @@ pub(super) async fn handle_wallet_create_recharge(
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
return build_wallet_recharge_storage_unavailable_response();
|
||||
};
|
||||
|
||||
let mut tx = match pool.begin().await {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet recharge transaction failed: {err}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let wallet_row = match sqlx::query(
|
||||
r#"
|
||||
SELECT id, status
|
||||
FROM wallets
|
||||
WHERE user_id = $1
|
||||
LIMIT 1
|
||||
FOR UPDATE
|
||||
"#,
|
||||
)
|
||||
.bind(&auth.user.id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
{
|
||||
Ok(Some(value)) => Some(value),
|
||||
Ok(None) => {
|
||||
let wallet_id = wallet
|
||||
.as_ref()
|
||||
.map(|value| value.id.clone())
|
||||
.unwrap_or_else(|| Uuid::new_v4().to_string());
|
||||
match sqlx::query(
|
||||
r#"
|
||||
INSERT INTO wallets (
|
||||
id,
|
||||
user_id,
|
||||
balance,
|
||||
gift_balance,
|
||||
limit_mode,
|
||||
currency,
|
||||
status,
|
||||
total_recharged,
|
||||
total_consumed,
|
||||
total_refunded,
|
||||
total_adjusted,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
$1,
|
||||
$2,
|
||||
0,
|
||||
0,
|
||||
'finite',
|
||||
'USD',
|
||||
'active',
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT (user_id) DO UPDATE
|
||||
SET updated_at = wallets.updated_at
|
||||
RETURNING id, status
|
||||
"#,
|
||||
)
|
||||
.bind(&wallet_id)
|
||||
.bind(&auth.user.id)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
{
|
||||
Ok(value) => Some(value),
|
||||
Err(err) => {
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet recharge wallet bootstrap failed: {err}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet recharge wallet lookup failed: {err}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
let Some(wallet_row) = wallet_row else {
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"wallet not available",
|
||||
false,
|
||||
);
|
||||
};
|
||||
let wallet_id = wallet_row
|
||||
.try_get::<String, _>("id")
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
let wallet_status = wallet_row
|
||||
.try_get::<String, _>("status")
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
if wallet_status != "active" {
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"wallet is not active",
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
let now = Utc::now();
|
||||
let order_id = Uuid::new_v4().to_string();
|
||||
let order_no = wallet_build_order_no(now);
|
||||
let expires_at = now + chrono::Duration::minutes(30);
|
||||
let (gateway_order_id, gateway_response) =
|
||||
match wallet_checkout_payload(&payload.payment_method, &order_no, expires_at) {
|
||||
Ok(value) => value,
|
||||
Err(detail) => {
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(http::StatusCode::BAD_REQUEST, detail, false);
|
||||
}
|
||||
};
|
||||
|
||||
let row = match sqlx::query(
|
||||
r#"
|
||||
INSERT INTO payment_orders (
|
||||
id,
|
||||
order_no,
|
||||
wallet_id,
|
||||
user_id,
|
||||
amount_usd,
|
||||
pay_amount,
|
||||
pay_currency,
|
||||
exchange_rate,
|
||||
refunded_amount_usd,
|
||||
refundable_amount_usd,
|
||||
payment_method,
|
||||
gateway_order_id,
|
||||
gateway_response,
|
||||
status,
|
||||
created_at,
|
||||
expires_at
|
||||
)
|
||||
VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7,
|
||||
$8,
|
||||
0,
|
||||
0,
|
||||
$9,
|
||||
$10,
|
||||
$11,
|
||||
'pending',
|
||||
NOW(),
|
||||
to_timestamp($12)
|
||||
)
|
||||
RETURNING
|
||||
id,
|
||||
order_no,
|
||||
wallet_id,
|
||||
user_id,
|
||||
CAST(amount_usd AS DOUBLE PRECISION) AS amount_usd,
|
||||
CAST(pay_amount AS DOUBLE PRECISION) AS pay_amount,
|
||||
pay_currency,
|
||||
CAST(exchange_rate AS DOUBLE PRECISION) AS exchange_rate,
|
||||
CAST(refunded_amount_usd AS DOUBLE PRECISION) AS refunded_amount_usd,
|
||||
CAST(refundable_amount_usd AS DOUBLE PRECISION) AS refundable_amount_usd,
|
||||
payment_method,
|
||||
gateway_order_id,
|
||||
gateway_response,
|
||||
status AS effective_status,
|
||||
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM paid_at) AS BIGINT) AS paid_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM credited_at) AS BIGINT) AS credited_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM expires_at) AS BIGINT) AS expires_at_unix_secs
|
||||
"#,
|
||||
)
|
||||
.bind(&order_id)
|
||||
.bind(&order_no)
|
||||
.bind(&wallet_id)
|
||||
.bind(&auth.user.id)
|
||||
.bind(payload.amount_usd)
|
||||
.bind(payload.pay_amount)
|
||||
.bind(payload.pay_currency.as_deref())
|
||||
.bind(payload.exchange_rate)
|
||||
.bind(&payload.payment_method)
|
||||
.bind(&gateway_order_id)
|
||||
.bind(&gateway_response)
|
||||
.bind(expires_at.timestamp())
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
let outcome = match state
|
||||
.create_wallet_recharge_order(
|
||||
aether_data::repository::wallet::CreateWalletRechargeOrderInput {
|
||||
preferred_wallet_id: wallet.as_ref().map(|value| value.id.clone()),
|
||||
user_id: auth.user.id.clone(),
|
||||
amount_usd: payload.amount_usd,
|
||||
pay_amount: payload.pay_amount,
|
||||
pay_currency: payload.pay_currency.clone(),
|
||||
exchange_rate: payload.exchange_rate,
|
||||
payment_method: payload.payment_method.clone(),
|
||||
gateway_order_id,
|
||||
gateway_response: gateway_response.clone(),
|
||||
order_no,
|
||||
expires_at_unix_secs: expires_at.timestamp().max(0) as u64,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Ok(Some(value)) => value,
|
||||
Ok(None) => return build_wallet_recharge_storage_unavailable_response(),
|
||||
Err(err) => {
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet recharge create failed: {err}"),
|
||||
format!("wallet recharge create failed: {err:?}"),
|
||||
false,
|
||||
);
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = tx.commit().await {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet recharge commit failed: {err}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
let order_payload = match wallet_payment_order_payload_from_row(&row) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
let order_payload = match outcome {
|
||||
aether_data::repository::wallet::CreateWalletRechargeOrderOutcome::Created(order) => {
|
||||
wallet_payment_order_payload_from_record(&order)
|
||||
}
|
||||
aether_data::repository::wallet::CreateWalletRechargeOrderOutcome::WalletInactive => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet recharge payload failed: {err:?}"),
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"wallet is not active",
|
||||
false,
|
||||
);
|
||||
)
|
||||
}
|
||||
};
|
||||
build_auth_json_response(
|
||||
@@ -658,103 +500,31 @@ pub(super) async fn handle_wallet_recharge_list(
|
||||
}
|
||||
};
|
||||
|
||||
let mut total = 0_u64;
|
||||
let mut items = Vec::new();
|
||||
if let Some(pool) = state.postgres_pool() {
|
||||
let count_row = match sqlx::query(
|
||||
r#"
|
||||
SELECT COUNT(*) AS total
|
||||
FROM payment_orders
|
||||
WHERE user_id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(&auth.user.id)
|
||||
.fetch_one(&pool)
|
||||
let (items, total) = match state
|
||||
.list_wallet_payment_orders_by_user_id(&auth.user.id, limit, offset)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet recharge count failed: {err}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
total = count_row
|
||||
.try_get::<i64, _>("total")
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
.max(0) as u64;
|
||||
let rows = match sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
order_no,
|
||||
wallet_id,
|
||||
user_id,
|
||||
CAST(amount_usd AS DOUBLE PRECISION) AS amount_usd,
|
||||
CAST(pay_amount AS DOUBLE PRECISION) AS pay_amount,
|
||||
pay_currency,
|
||||
CAST(exchange_rate AS DOUBLE PRECISION) AS exchange_rate,
|
||||
CAST(refunded_amount_usd AS DOUBLE PRECISION) AS refunded_amount_usd,
|
||||
CAST(refundable_amount_usd AS DOUBLE PRECISION) AS refundable_amount_usd,
|
||||
payment_method,
|
||||
gateway_order_id,
|
||||
gateway_response,
|
||||
CASE
|
||||
WHEN status = 'pending' AND expires_at IS NOT NULL AND expires_at < now() THEN 'expired'
|
||||
ELSE status
|
||||
END AS effective_status,
|
||||
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM paid_at) AS BIGINT) AS paid_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM credited_at) AS BIGINT) AS credited_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM expires_at) AS BIGINT) AS expires_at_unix_secs
|
||||
FROM payment_orders
|
||||
WHERE user_id = $1
|
||||
ORDER BY created_at DESC
|
||||
OFFSET $2
|
||||
LIMIT $3
|
||||
"#,
|
||||
)
|
||||
.bind(&auth.user.id)
|
||||
.bind(i64::try_from(offset).ok().unwrap_or_default())
|
||||
.bind(i64::try_from(limit).ok().unwrap_or_default())
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet recharge query failed: {err}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
items = match rows
|
||||
.iter()
|
||||
.map(wallet_payment_order_payload_from_row)
|
||||
.collect::<Result<Vec<_>, GatewayError>>()
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet recharge payload failed: {err:?}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
} else {
|
||||
#[cfg(test)]
|
||||
{
|
||||
let (test_items, test_total) =
|
||||
wallet_test_recharge_orders_for_user(&auth.user.id, limit, offset);
|
||||
items = test_items;
|
||||
total = test_total;
|
||||
{
|
||||
Ok(page) => (
|
||||
page.items
|
||||
.iter()
|
||||
.map(wallet_payment_order_payload_from_record)
|
||||
.collect::<Vec<_>>(),
|
||||
page.total,
|
||||
),
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet recharge lookup failed: {err:?}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
#[cfg(test)]
|
||||
let (items, total) = if state.postgres_pool().is_none() && items.is_empty() && total == 0 {
|
||||
wallet_test_recharge_orders_for_user(&auth.user.id, limit, offset)
|
||||
} else {
|
||||
(items, total)
|
||||
};
|
||||
|
||||
let mut payload = json!({
|
||||
"items": items,
|
||||
@@ -786,9 +556,17 @@ pub(super) async fn handle_wallet_recharge_detail(
|
||||
false,
|
||||
);
|
||||
};
|
||||
let Some(pool) = state.postgres_pool() else {
|
||||
#[cfg(test)]
|
||||
{
|
||||
match state
|
||||
.find_wallet_payment_order_by_user_id(&auth.user.id, &order_id)
|
||||
.await
|
||||
{
|
||||
Ok(Some(order)) => build_auth_json_response(
|
||||
http::StatusCode::OK,
|
||||
json!({ "order": wallet_payment_order_payload_from_record(&order) }),
|
||||
None,
|
||||
),
|
||||
Ok(None) => {
|
||||
#[cfg(test)]
|
||||
if let Some(order) = wallet_test_recharge_order_by_id(&auth.user.id, &order_id) {
|
||||
return build_auth_json_response(
|
||||
http::StatusCode::OK,
|
||||
@@ -796,72 +574,16 @@ pub(super) async fn handle_wallet_recharge_detail(
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
"Payment order not found",
|
||||
false,
|
||||
);
|
||||
};
|
||||
let row = match sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
order_no,
|
||||
wallet_id,
|
||||
user_id,
|
||||
CAST(amount_usd AS DOUBLE PRECISION) AS amount_usd,
|
||||
CAST(pay_amount AS DOUBLE PRECISION) AS pay_amount,
|
||||
pay_currency,
|
||||
CAST(exchange_rate AS DOUBLE PRECISION) AS exchange_rate,
|
||||
CAST(refunded_amount_usd AS DOUBLE PRECISION) AS refunded_amount_usd,
|
||||
CAST(refundable_amount_usd AS DOUBLE PRECISION) AS refundable_amount_usd,
|
||||
payment_method,
|
||||
gateway_order_id,
|
||||
gateway_response,
|
||||
CASE
|
||||
WHEN status = 'pending' AND expires_at IS NOT NULL AND expires_at < now() THEN 'expired'
|
||||
ELSE status
|
||||
END AS effective_status,
|
||||
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM paid_at) AS BIGINT) AS paid_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM credited_at) AS BIGINT) AS credited_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM expires_at) AS BIGINT) AS expires_at_unix_secs
|
||||
FROM payment_orders
|
||||
WHERE id = $1 AND user_id = $2
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.bind(&order_id)
|
||||
.bind(&auth.user.id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet recharge detail query failed: {err}"),
|
||||
build_auth_error_response(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
"Payment order not found",
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
let Some(row) = row else {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
"Payment order not found",
|
||||
Err(err) => build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet recharge detail lookup failed: {err:?}"),
|
||||
false,
|
||||
);
|
||||
};
|
||||
let payload = match wallet_payment_order_payload_from_row(&row) {
|
||||
Ok(value) => json!({ "order": value }),
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet recharge detail payload failed: {err:?}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
build_auth_json_response(http::StatusCode::OK, payload, None)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +139,31 @@ fn wallet_refund_payload_from_row(
|
||||
}))
|
||||
}
|
||||
|
||||
fn wallet_refund_payload_from_record(
|
||||
record: &aether_data::repository::wallet::StoredAdminWalletRefund,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
"id": record.id,
|
||||
"refund_no": record.refund_no,
|
||||
"payment_order_id": record.payment_order_id,
|
||||
"source_type": record.source_type,
|
||||
"source_id": record.source_id,
|
||||
"refund_mode": record.refund_mode,
|
||||
"amount_usd": record.amount_usd,
|
||||
"status": record.status,
|
||||
"reason": record.reason,
|
||||
"failure_reason": record.failure_reason,
|
||||
"gateway_refund_id": record.gateway_refund_id,
|
||||
"payout_method": record.payout_method,
|
||||
"payout_reference": record.payout_reference,
|
||||
"payout_proof": record.payout_proof,
|
||||
"created_at": unix_secs_to_rfc3339(record.created_at_unix_secs),
|
||||
"updated_at": unix_secs_to_rfc3339(record.updated_at_unix_secs),
|
||||
"processed_at": record.processed_at_unix_secs.and_then(unix_secs_to_rfc3339),
|
||||
"completed_at": record.completed_at_unix_secs.and_then(unix_secs_to_rfc3339),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn handle_wallet_refunds_list(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
@@ -191,102 +216,57 @@ pub(super) async fn handle_wallet_refunds_list(
|
||||
return build_auth_json_response(http::StatusCode::OK, payload, None);
|
||||
};
|
||||
|
||||
let mut total = 0_u64;
|
||||
let mut items = Vec::new();
|
||||
if let Some(pool) = state.postgres_pool() {
|
||||
let count_row = match sqlx::query(
|
||||
r#"
|
||||
SELECT COUNT(*) AS total
|
||||
FROM refund_requests
|
||||
WHERE wallet_id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(&wallet.id)
|
||||
.fetch_one(&pool)
|
||||
let (refunds, total) = match state
|
||||
.list_admin_wallet_refunds(&wallet.id, limit, offset)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet refund count failed: {err}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
total = count_row
|
||||
.try_get::<i64, _>("total")
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
.max(0) as u64;
|
||||
let rows = match sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
refund_no,
|
||||
payment_order_id,
|
||||
source_type,
|
||||
source_id,
|
||||
refund_mode,
|
||||
CAST(amount_usd AS DOUBLE PRECISION) AS amount_usd,
|
||||
status,
|
||||
reason,
|
||||
failure_reason,
|
||||
gateway_refund_id,
|
||||
payout_method,
|
||||
payout_reference,
|
||||
payout_proof,
|
||||
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM updated_at) AS BIGINT) AS updated_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM processed_at) AS BIGINT) AS processed_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM completed_at) AS BIGINT) AS completed_at_unix_secs
|
||||
FROM refund_requests
|
||||
WHERE wallet_id = $1
|
||||
ORDER BY created_at DESC
|
||||
OFFSET $2
|
||||
LIMIT $3
|
||||
"#,
|
||||
)
|
||||
.bind(&wallet.id)
|
||||
.bind(i64::try_from(offset).ok().unwrap_or_default())
|
||||
.bind(i64::try_from(limit).ok().unwrap_or_default())
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet refund query failed: {err}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
items = match rows
|
||||
.iter()
|
||||
.map(wallet_refund_payload_from_row)
|
||||
.collect::<Result<Vec<_>, GatewayError>>()
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet refund payload failed: {err:?}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet refund lookup failed: {err:?}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
let items = refunds
|
||||
.iter()
|
||||
.map(|record| {
|
||||
json!({
|
||||
"id": record.id,
|
||||
"refund_no": record.refund_no,
|
||||
"payment_order_id": record.payment_order_id,
|
||||
"source_type": record.source_type,
|
||||
"source_id": record.source_id,
|
||||
"refund_mode": record.refund_mode,
|
||||
"amount_usd": record.amount_usd,
|
||||
"status": record.status,
|
||||
"reason": record.reason,
|
||||
"failure_reason": record.failure_reason,
|
||||
"gateway_refund_id": record.gateway_refund_id,
|
||||
"payout_method": record.payout_method,
|
||||
"payout_reference": record.payout_reference,
|
||||
"payout_proof": record.payout_proof,
|
||||
"created_at": unix_secs_to_rfc3339(record.created_at_unix_secs),
|
||||
"updated_at": unix_secs_to_rfc3339(record.updated_at_unix_secs),
|
||||
"processed_at": record.processed_at_unix_secs.and_then(unix_secs_to_rfc3339),
|
||||
"completed_at": record.completed_at_unix_secs.and_then(unix_secs_to_rfc3339),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
#[cfg(test)]
|
||||
if state.postgres_pool().is_none() {
|
||||
let (items, total) = if state.postgres_pool().is_none() && items.is_empty() && total == 0 {
|
||||
let all_items = wallet_test_refunds_for_wallet(&wallet.id);
|
||||
total = all_items.len() as u64;
|
||||
items = all_items
|
||||
let total = all_items.len() as u64;
|
||||
let items = all_items
|
||||
.into_iter()
|
||||
.skip(offset)
|
||||
.take(limit)
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
(items, total)
|
||||
} else {
|
||||
(items, total)
|
||||
};
|
||||
|
||||
let mut payload = json!({
|
||||
"items": items,
|
||||
@@ -340,75 +320,29 @@ pub(super) async fn handle_wallet_refund_detail(
|
||||
false,
|
||||
);
|
||||
};
|
||||
let Some(pool) = state.postgres_pool() else {
|
||||
#[cfg(test)]
|
||||
if let Some(payload) = wallet_test_refund_by_id(&wallet.id, &refund_id) {
|
||||
return build_auth_json_response(http::StatusCode::OK, payload, None);
|
||||
}
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
"Refund request not found",
|
||||
false,
|
||||
);
|
||||
};
|
||||
let row = match sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
refund_no,
|
||||
payment_order_id,
|
||||
source_type,
|
||||
source_id,
|
||||
refund_mode,
|
||||
CAST(amount_usd AS DOUBLE PRECISION) AS amount_usd,
|
||||
status,
|
||||
reason,
|
||||
failure_reason,
|
||||
gateway_refund_id,
|
||||
payout_method,
|
||||
payout_reference,
|
||||
payout_proof,
|
||||
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM updated_at) AS BIGINT) AS updated_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM processed_at) AS BIGINT) AS processed_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM completed_at) AS BIGINT) AS completed_at_unix_secs
|
||||
FROM refund_requests
|
||||
WHERE wallet_id = $1 AND id = $2
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.bind(&wallet.id)
|
||||
.bind(&refund_id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet refund detail query failed: {err}"),
|
||||
match state.find_wallet_refund(&wallet.id, &refund_id).await {
|
||||
Ok(Some(refund)) => build_auth_json_response(
|
||||
http::StatusCode::OK,
|
||||
wallet_refund_payload_from_record(&refund),
|
||||
None,
|
||||
),
|
||||
Ok(None) => {
|
||||
#[cfg(test)]
|
||||
if let Some(payload) = wallet_test_refund_by_id(&wallet.id, &refund_id) {
|
||||
return build_auth_json_response(http::StatusCode::OK, payload, None);
|
||||
}
|
||||
build_auth_error_response(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
"Refund request not found",
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
let Some(row) = row else {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
"Refund request not found",
|
||||
Err(err) => build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet refund detail lookup failed: {err:?}"),
|
||||
false,
|
||||
);
|
||||
};
|
||||
let payload = match wallet_refund_payload_from_row(&row) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet refund detail payload failed: {err:?}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
build_auth_json_response(http::StatusCode::OK, payload, None)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn handle_wallet_create_refund(
|
||||
@@ -460,7 +394,7 @@ pub(super) async fn handle_wallet_create_refund(
|
||||
);
|
||||
};
|
||||
|
||||
let Some(pool) = state.postgres_pool() else {
|
||||
if state.postgres_pool().is_none() {
|
||||
#[cfg(test)]
|
||||
{
|
||||
if let Some(idempotency_key) = payload.idempotency_key.as_deref() {
|
||||
@@ -509,377 +443,86 @@ pub(super) async fn handle_wallet_create_refund(
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
return build_wallet_refund_storage_unavailable_response();
|
||||
};
|
||||
}
|
||||
|
||||
let mut tx = match pool.begin().await {
|
||||
Ok(value) => value,
|
||||
let outcome = match state
|
||||
.create_wallet_refund_request(
|
||||
aether_data::repository::wallet::CreateWalletRefundRequestInput {
|
||||
wallet_id: wallet.id.clone(),
|
||||
user_id: auth.user.id.clone(),
|
||||
amount_usd: payload.amount_usd,
|
||||
payment_order_id: payload.payment_order_id.clone(),
|
||||
source_type: payload.source_type.clone(),
|
||||
source_id: payload.source_id.clone(),
|
||||
refund_mode: payload.refund_mode.clone(),
|
||||
reason: payload.reason.clone(),
|
||||
idempotency_key: payload.idempotency_key.clone(),
|
||||
refund_no: wallet_build_refund_no(Utc::now()),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Some(value)) => value,
|
||||
Ok(None) => return build_wallet_refund_storage_unavailable_response(),
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet refund transaction failed: {err}"),
|
||||
format!("wallet refund create failed: {err:?}"),
|
||||
false,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let locked_wallet_row = match sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
CAST(balance AS DOUBLE PRECISION) AS balance
|
||||
FROM wallets
|
||||
WHERE id = $1
|
||||
LIMIT 1
|
||||
FOR UPDATE
|
||||
"#,
|
||||
)
|
||||
.bind(&wallet.id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet refund wallet lock failed: {err}"),
|
||||
false,
|
||||
);
|
||||
match outcome {
|
||||
aether_data::repository::wallet::CreateWalletRefundRequestOutcome::Created(refund)
|
||||
| aether_data::repository::wallet::CreateWalletRefundRequestOutcome::Duplicate(refund) => {
|
||||
build_auth_json_response(
|
||||
http::StatusCode::OK,
|
||||
wallet_refund_payload_from_record(&refund),
|
||||
None,
|
||||
)
|
||||
}
|
||||
};
|
||||
let Some(locked_wallet_row) = locked_wallet_row else {
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"当前账户尚未开通钱包,无法申请退款",
|
||||
false,
|
||||
);
|
||||
};
|
||||
let wallet_recharge_balance = locked_wallet_row
|
||||
.try_get::<f64, _>("balance")
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
let wallet_reserved_row = match sqlx::query(
|
||||
r#"
|
||||
SELECT COALESCE(CAST(SUM(amount_usd) AS DOUBLE PRECISION), 0) AS total
|
||||
FROM refund_requests
|
||||
WHERE wallet_id = $1
|
||||
AND status IN ('pending_approval', 'approved')
|
||||
"#,
|
||||
)
|
||||
.bind(&wallet.id)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet refund reserved amount lookup failed: {err}"),
|
||||
aether_data::repository::wallet::CreateWalletRefundRequestOutcome::WalletMissing => {
|
||||
build_auth_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"当前账户尚未开通钱包,无法申请退款",
|
||||
false,
|
||||
);
|
||||
)
|
||||
}
|
||||
};
|
||||
let wallet_reserved_amount = wallet_reserved_row
|
||||
.try_get::<f64, _>("total")
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
if payload.amount_usd > (wallet_recharge_balance - wallet_reserved_amount) {
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"refund amount exceeds available refundable recharge balance",
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
let mut payment_order_id = None;
|
||||
let mut source_type = payload
|
||||
.source_type
|
||||
.clone()
|
||||
.unwrap_or_else(|| "wallet_balance".to_string());
|
||||
let mut source_id = payload.source_id.clone();
|
||||
let mut refund_mode = payload
|
||||
.refund_mode
|
||||
.clone()
|
||||
.unwrap_or_else(|| "offline_payout".to_string());
|
||||
if let Some(order_id) = payload.payment_order_id.as_deref() {
|
||||
let order_row = match sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
wallet_id,
|
||||
status,
|
||||
payment_method,
|
||||
CAST(refundable_amount_usd AS DOUBLE PRECISION) AS refundable_amount_usd
|
||||
FROM payment_orders
|
||||
WHERE id = $1
|
||||
AND wallet_id = $2
|
||||
LIMIT 1
|
||||
FOR UPDATE
|
||||
"#,
|
||||
)
|
||||
.bind(order_id)
|
||||
.bind(&wallet.id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet refund payment order lookup failed: {err}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
let Some(order_row) = order_row else {
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(
|
||||
aether_data::repository::wallet::CreateWalletRefundRequestOutcome::RefundAmountExceedsAvailableBalance => {
|
||||
build_auth_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"refund amount exceeds available refundable recharge balance",
|
||||
false,
|
||||
)
|
||||
}
|
||||
aether_data::repository::wallet::CreateWalletRefundRequestOutcome::PaymentOrderNotFound => {
|
||||
build_auth_error_response(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
"Payment order not found",
|
||||
false,
|
||||
);
|
||||
};
|
||||
let status = order_row
|
||||
.try_get::<String, _>("status")
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
if status != "credited" {
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(
|
||||
)
|
||||
}
|
||||
aether_data::repository::wallet::CreateWalletRefundRequestOutcome::PaymentOrderNotRefundable => {
|
||||
build_auth_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"payment order is not refundable",
|
||||
false,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
let order_reserved_row = match sqlx::query(
|
||||
r#"
|
||||
SELECT COALESCE(CAST(SUM(amount_usd) AS DOUBLE PRECISION), 0) AS total
|
||||
FROM refund_requests
|
||||
WHERE payment_order_id = $1
|
||||
AND status IN ('pending_approval', 'approved')
|
||||
"#,
|
||||
)
|
||||
.bind(order_id)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet refund payment order reserve lookup failed: {err}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
let refundable_amount = order_row
|
||||
.try_get::<f64, _>("refundable_amount_usd")
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
let reserved_amount = order_reserved_row
|
||||
.try_get::<f64, _>("total")
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
if payload.amount_usd > (refundable_amount - reserved_amount) {
|
||||
let _ = tx.rollback().await;
|
||||
return build_auth_error_response(
|
||||
aether_data::repository::wallet::CreateWalletRefundRequestOutcome::RefundAmountExceedsAvailableOrderAmount => {
|
||||
build_auth_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"refund amount exceeds available refundable amount",
|
||||
false,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
payment_order_id = Some(order_id.to_string());
|
||||
source_type = "payment_order".to_string();
|
||||
source_id = Some(order_id.to_string());
|
||||
if payload.refund_mode.is_none() {
|
||||
let payment_method = order_row
|
||||
.try_get::<String, _>("payment_method")
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
refund_mode =
|
||||
wallet_default_refund_mode_for_payment_method(&payment_method).to_string();
|
||||
aether_data::repository::wallet::CreateWalletRefundRequestOutcome::DuplicateRejected => {
|
||||
build_auth_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"退款申请重复,请勿重复提交",
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let now = Utc::now();
|
||||
let refund_id = Uuid::new_v4().to_string();
|
||||
let refund_no = wallet_build_refund_no(now);
|
||||
let insert_result = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO refund_requests (
|
||||
id,
|
||||
refund_no,
|
||||
wallet_id,
|
||||
user_id,
|
||||
payment_order_id,
|
||||
source_type,
|
||||
source_id,
|
||||
refund_mode,
|
||||
amount_usd,
|
||||
status,
|
||||
reason,
|
||||
requested_by,
|
||||
idempotency_key,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7,
|
||||
$8,
|
||||
$9,
|
||||
'pending_approval',
|
||||
$10,
|
||||
$11,
|
||||
$12,
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
RETURNING
|
||||
id,
|
||||
refund_no,
|
||||
payment_order_id,
|
||||
source_type,
|
||||
source_id,
|
||||
refund_mode,
|
||||
CAST(amount_usd AS DOUBLE PRECISION) AS amount_usd,
|
||||
status,
|
||||
reason,
|
||||
failure_reason,
|
||||
gateway_refund_id,
|
||||
payout_method,
|
||||
payout_reference,
|
||||
payout_proof,
|
||||
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM updated_at) AS BIGINT) AS updated_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM processed_at) AS BIGINT) AS processed_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM completed_at) AS BIGINT) AS completed_at_unix_secs
|
||||
"#,
|
||||
)
|
||||
.bind(&refund_id)
|
||||
.bind(&refund_no)
|
||||
.bind(&wallet.id)
|
||||
.bind(&auth.user.id)
|
||||
.bind(payment_order_id.as_deref())
|
||||
.bind(&source_type)
|
||||
.bind(source_id.as_deref())
|
||||
.bind(&refund_mode)
|
||||
.bind(payload.amount_usd)
|
||||
.bind(payload.reason.as_deref())
|
||||
.bind(&auth.user.id)
|
||||
.bind(payload.idempotency_key.as_deref())
|
||||
.fetch_one(&mut *tx)
|
||||
.await;
|
||||
|
||||
let row = match insert_result {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
let _ = tx.rollback().await;
|
||||
if let Some(idempotency_key) = payload.idempotency_key.as_deref() {
|
||||
let existing = match sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
refund_no,
|
||||
payment_order_id,
|
||||
source_type,
|
||||
source_id,
|
||||
refund_mode,
|
||||
CAST(amount_usd AS DOUBLE PRECISION) AS amount_usd,
|
||||
status,
|
||||
reason,
|
||||
failure_reason,
|
||||
gateway_refund_id,
|
||||
payout_method,
|
||||
payout_reference,
|
||||
payout_proof,
|
||||
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM updated_at) AS BIGINT) AS updated_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM processed_at) AS BIGINT) AS processed_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM completed_at) AS BIGINT) AS completed_at_unix_secs
|
||||
FROM refund_requests
|
||||
WHERE user_id = $1
|
||||
AND idempotency_key = $2
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.bind(&auth.user.id)
|
||||
.bind(idempotency_key)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(read_err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet refund idempotency lookup failed: {read_err}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
if let Some(existing) = existing {
|
||||
let payload = match wallet_refund_payload_from_row(&existing) {
|
||||
Ok(value) => value,
|
||||
Err(payload_err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet refund payload failed: {payload_err:?}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
return build_auth_json_response(http::StatusCode::OK, payload, None);
|
||||
}
|
||||
}
|
||||
if err
|
||||
.as_database_error()
|
||||
.and_then(|value| value.code())
|
||||
.as_deref()
|
||||
== Some("23505")
|
||||
{
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"退款申请重复,请勿重复提交",
|
||||
false,
|
||||
);
|
||||
}
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet refund create failed: {err}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = tx.commit().await {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet refund commit failed: {err}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
let payload = match wallet_refund_payload_from_row(&row) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("wallet refund payload failed: {err:?}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
build_auth_json_response(http::StatusCode::OK, payload, None)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
use super::enabled_key_capability_short_names;
|
||||
use crate::gateway::handlers::{json_string_list, masked_catalog_api_key, unix_secs_to_rfc3339};
|
||||
use crate::gateway::AppState;
|
||||
use crate::handlers::{json_string_list, unix_secs_to_rfc3339};
|
||||
use crate::AppState;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
fn grouped_key_masked_label(auth_type: &str) -> &'static str {
|
||||
match auth_type.trim() {
|
||||
"service_account" | "vertex_ai" => "[Service Account]",
|
||||
"oauth" => "[OAuth Token]",
|
||||
_ => "[API Key]",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn build_admin_keys_grouped_by_format_payload(
|
||||
state: &AppState,
|
||||
) -> Option<serde_json::Value> {
|
||||
@@ -21,14 +29,22 @@ pub(crate) async fn build_admin_keys_grouped_by_format_payload(
|
||||
.iter()
|
||||
.map(|provider| provider.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let provider_by_id = providers
|
||||
let provider_metadata_by_id = providers
|
||||
.iter()
|
||||
.map(|provider| (provider.id.clone(), provider.clone()))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
.map(|provider| {
|
||||
(
|
||||
provider.id.clone(),
|
||||
(provider.name.clone(), provider.is_active),
|
||||
)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let endpoint_base_url_by_provider_and_format = state
|
||||
.list_provider_catalog_endpoints_by_provider_ids(&provider_ids)
|
||||
.await
|
||||
let (endpoints_result, keys_result) = tokio::join!(
|
||||
state.list_provider_catalog_endpoints_by_provider_ids(&provider_ids),
|
||||
state.list_provider_catalog_keys_by_provider_ids(&provider_ids),
|
||||
);
|
||||
|
||||
let endpoint_base_url_by_provider_and_format = endpoints_result
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
@@ -39,13 +55,9 @@ pub(crate) async fn build_admin_keys_grouped_by_format_payload(
|
||||
endpoint.base_url,
|
||||
)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let mut keys = state
|
||||
.list_provider_catalog_keys_by_provider_ids(&provider_ids)
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
let mut keys = keys_result.ok().unwrap_or_default();
|
||||
keys.sort_by(|left, right| {
|
||||
left.internal_priority
|
||||
.cmp(&right.internal_priority)
|
||||
@@ -60,7 +72,9 @@ pub(crate) async fn build_admin_keys_grouped_by_format_payload(
|
||||
|
||||
let mut grouped = BTreeMap::<String, Vec<serde_json::Value>>::new();
|
||||
for key in keys {
|
||||
let Some(provider) = provider_by_id.get(&key.provider_id) else {
|
||||
let Some((provider_name, provider_is_active)) =
|
||||
provider_metadata_by_id.get(&key.provider_id)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let request_count = u64::from(key.request_count.unwrap_or(0));
|
||||
@@ -113,13 +127,13 @@ pub(crate) async fn build_admin_keys_grouped_by_format_payload(
|
||||
"provider_id": key.provider_id,
|
||||
"name": key.name,
|
||||
"auth_type": key.auth_type,
|
||||
"api_key_masked": masked_catalog_api_key(state, &key),
|
||||
"api_key_masked": grouped_key_masked_label(&key.auth_type),
|
||||
"internal_priority": key.internal_priority,
|
||||
"global_priority_by_format": key.global_priority_by_format,
|
||||
"rate_multipliers": key.rate_multipliers,
|
||||
"is_active": key.is_active,
|
||||
"provider_active": provider.is_active,
|
||||
"provider_name": provider.name,
|
||||
"provider_active": provider_is_active,
|
||||
"provider_name": provider_name,
|
||||
"api_formats": api_formats,
|
||||
"capabilities": capability_names,
|
||||
"success_rate": success_rate,
|
||||
@@ -127,7 +141,7 @@ pub(crate) async fn build_admin_keys_grouped_by_format_payload(
|
||||
"request_count": request_count,
|
||||
"api_format": api_format,
|
||||
"endpoint_base_url": endpoint_base_url_by_provider_and_format
|
||||
.get(&(provider.id.clone(), api_format.clone()))
|
||||
.get(&(key.provider_id.clone(), api_format.clone()))
|
||||
.cloned(),
|
||||
"format_priority": priority_by_format
|
||||
.get(api_format)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::{module_available_from_env, system_config_bool};
|
||||
use crate::gateway::{AppState, GatewayError};
|
||||
use crate::{AppState, GatewayError};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::gateway::api::ai::admin_endpoint_signature_parts;
|
||||
use crate::gateway::handlers::{decrypt_catalog_secret_with_fallbacks, unix_secs_to_rfc3339};
|
||||
use crate::gateway::{AppState, GatewayError};
|
||||
use crate::api::ai::admin_endpoint_signature_parts;
|
||||
use crate::handlers::{decrypt_catalog_secret_with_fallbacks, unix_secs_to_rfc3339};
|
||||
use crate::{AppState, GatewayError};
|
||||
use aether_crypto::encrypt_python_fernet_plaintext;
|
||||
use aether_data::repository::global_models::{
|
||||
AdminGlobalModelListQuery, AdminProviderModelListQuery,
|
||||
@@ -172,20 +172,19 @@ pub(crate) async fn build_admin_system_stats_payload(
|
||||
.iter()
|
||||
.filter(|provider| provider.is_active)
|
||||
.count() as u64;
|
||||
let (total_users, active_users, total_api_keys, total_requests) =
|
||||
state.read_admin_system_stats().await?;
|
||||
let stats = state.read_admin_system_stats().await?;
|
||||
|
||||
Ok(json!({
|
||||
"users": {
|
||||
"total": total_users,
|
||||
"active": active_users,
|
||||
"total": stats.total_users,
|
||||
"active": stats.active_users,
|
||||
},
|
||||
"providers": {
|
||||
"total": total_providers,
|
||||
"active": active_providers,
|
||||
},
|
||||
"api_keys": total_api_keys,
|
||||
"requests": total_requests,
|
||||
"api_keys": stats.total_api_keys,
|
||||
"requests": stats.total_requests,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1504,7 +1503,7 @@ pub(crate) fn build_admin_system_config_list_item(
|
||||
}
|
||||
|
||||
pub(crate) fn build_admin_system_configs_payload(
|
||||
entries: &[crate::gateway::gateway_data::StoredSystemConfigEntry],
|
||||
entries: &[aether_data::repository::system::StoredSystemConfigEntry],
|
||||
) -> serde_json::Value {
|
||||
let has_request_record_level = entries
|
||||
.iter()
|
||||
|
||||
Reference in New Issue
Block a user