mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 18:07:47 +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:
@@ -0,0 +1,225 @@
|
||||
use crate::{
|
||||
AdminWalletPaymentOrderRecord, AdminWalletTransactionRecord, AppState, GatewayError,
|
||||
};
|
||||
|
||||
use super::admin_wallet_build_order_no;
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn admin_adjust_wallet_balance(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
amount_usd: f64,
|
||||
balance_type: &str,
|
||||
operator_id: Option<&str>,
|
||||
description: Option<&str>,
|
||||
) -> Result<
|
||||
Option<(
|
||||
aether_data::repository::wallet::StoredWalletSnapshot,
|
||||
AdminWalletTransactionRecord,
|
||||
)>,
|
||||
GatewayError,
|
||||
> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_wallet_store.as_ref() {
|
||||
let mut guard = store.lock().expect("auth wallet store should lock");
|
||||
let Some(wallet) = guard.get_mut(wallet_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let before_recharge = wallet.balance;
|
||||
let before_gift = wallet.gift_balance;
|
||||
let before_total = before_recharge + before_gift;
|
||||
let mut after_recharge = before_recharge;
|
||||
let mut after_gift = before_gift;
|
||||
|
||||
if amount_usd > 0.0 {
|
||||
if balance_type.eq_ignore_ascii_case("gift") {
|
||||
after_gift += amount_usd;
|
||||
} else {
|
||||
after_recharge += amount_usd;
|
||||
}
|
||||
} else {
|
||||
let mut remaining = -amount_usd;
|
||||
let consume_positive_bucket = |balance: &mut f64, to_consume: &mut f64| {
|
||||
if *to_consume <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let available = (*balance).max(0.0);
|
||||
let consumed = available.min(*to_consume);
|
||||
*balance -= consumed;
|
||||
*to_consume -= consumed;
|
||||
};
|
||||
if balance_type.eq_ignore_ascii_case("gift") {
|
||||
consume_positive_bucket(&mut after_gift, &mut remaining);
|
||||
consume_positive_bucket(&mut after_recharge, &mut remaining);
|
||||
} else {
|
||||
consume_positive_bucket(&mut after_recharge, &mut remaining);
|
||||
consume_positive_bucket(&mut after_gift, &mut remaining);
|
||||
}
|
||||
if remaining > 0.0 {
|
||||
after_recharge -= remaining;
|
||||
}
|
||||
}
|
||||
|
||||
wallet.balance = after_recharge;
|
||||
wallet.gift_balance = after_gift;
|
||||
wallet.total_adjusted += amount_usd;
|
||||
wallet.updated_at_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
|
||||
|
||||
let transaction = AdminWalletTransactionRecord {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
wallet_id: wallet.id.clone(),
|
||||
category: "adjust".to_string(),
|
||||
reason_code: "adjust_admin".to_string(),
|
||||
amount: amount_usd,
|
||||
balance_before: before_total,
|
||||
balance_after: after_recharge + after_gift,
|
||||
recharge_balance_before: before_recharge,
|
||||
recharge_balance_after: after_recharge,
|
||||
gift_balance_before: before_gift,
|
||||
gift_balance_after: after_gift,
|
||||
link_type: Some("admin_action".to_string()),
|
||||
link_id: Some(wallet.id.clone()),
|
||||
operator_id: operator_id.map(ToOwned::to_owned),
|
||||
description: Some(
|
||||
description
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or("管理员调账")
|
||||
.to_string(),
|
||||
),
|
||||
created_at_unix_secs: chrono::Utc::now().timestamp().max(0) as u64,
|
||||
};
|
||||
return Ok(Some((wallet.clone(), transaction)));
|
||||
}
|
||||
|
||||
Ok(self
|
||||
.adjust_wallet_balance(aether_data::repository::wallet::AdjustWalletBalanceInput {
|
||||
wallet_id: wallet_id.to_string(),
|
||||
amount_usd,
|
||||
balance_type: balance_type.to_string(),
|
||||
operator_id: operator_id.map(ToOwned::to_owned),
|
||||
description: description.map(ToOwned::to_owned),
|
||||
})
|
||||
.await?
|
||||
.map(|(wallet, transaction)| {
|
||||
(wallet, stored_wallet_transaction_to_gateway(transaction))
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) async fn admin_create_manual_wallet_recharge(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
amount_usd: f64,
|
||||
payment_method: &str,
|
||||
operator_id: Option<&str>,
|
||||
description: Option<&str>,
|
||||
) -> Result<
|
||||
Option<(
|
||||
aether_data::repository::wallet::StoredWalletSnapshot,
|
||||
AdminWalletPaymentOrderRecord,
|
||||
)>,
|
||||
GatewayError,
|
||||
> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_wallet_store.as_ref() {
|
||||
let mut guard = store.lock().expect("auth wallet store should lock");
|
||||
let Some(wallet) = guard.get_mut(wallet_id) else {
|
||||
return Ok(None);
|
||||
};
|
||||
wallet.balance += amount_usd;
|
||||
wallet.total_recharged += amount_usd;
|
||||
wallet.updated_at_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
|
||||
let now = chrono::Utc::now();
|
||||
let created_at = now.timestamp().max(0) as u64;
|
||||
let order = AdminWalletPaymentOrderRecord {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
order_no: admin_wallet_build_order_no(now),
|
||||
wallet_id: wallet.id.clone(),
|
||||
user_id: wallet.user_id.clone(),
|
||||
amount_usd,
|
||||
pay_amount: None,
|
||||
pay_currency: None,
|
||||
exchange_rate: None,
|
||||
refunded_amount_usd: 0.0,
|
||||
refundable_amount_usd: amount_usd,
|
||||
payment_method: payment_method.to_string(),
|
||||
gateway_order_id: None,
|
||||
status: "credited".to_string(),
|
||||
gateway_response: Some(serde_json::json!({
|
||||
"source": "manual",
|
||||
"operator_id": operator_id,
|
||||
"description": description,
|
||||
})),
|
||||
created_at_unix_secs: created_at,
|
||||
paid_at_unix_secs: Some(created_at),
|
||||
credited_at_unix_secs: Some(created_at),
|
||||
expires_at_unix_secs: None,
|
||||
};
|
||||
return Ok(Some((wallet.clone(), order)));
|
||||
}
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let order_no = admin_wallet_build_order_no(now);
|
||||
Ok(self
|
||||
.create_manual_wallet_recharge(
|
||||
aether_data::repository::wallet::CreateManualWalletRechargeInput {
|
||||
wallet_id: wallet_id.to_string(),
|
||||
amount_usd,
|
||||
payment_method: payment_method.to_string(),
|
||||
operator_id: operator_id.map(ToOwned::to_owned),
|
||||
description: description.map(ToOwned::to_owned),
|
||||
order_no,
|
||||
},
|
||||
)
|
||||
.await?
|
||||
.map(|(wallet, order)| (wallet, stored_admin_payment_order_to_gateway(order))))
|
||||
}
|
||||
}
|
||||
|
||||
fn stored_wallet_transaction_to_gateway(
|
||||
transaction: aether_data::repository::wallet::StoredAdminWalletTransaction,
|
||||
) -> AdminWalletTransactionRecord {
|
||||
AdminWalletTransactionRecord {
|
||||
id: transaction.id,
|
||||
wallet_id: transaction.wallet_id,
|
||||
category: transaction.category,
|
||||
reason_code: transaction.reason_code,
|
||||
amount: transaction.amount,
|
||||
balance_before: transaction.balance_before,
|
||||
balance_after: transaction.balance_after,
|
||||
recharge_balance_before: transaction.recharge_balance_before,
|
||||
recharge_balance_after: transaction.recharge_balance_after,
|
||||
gift_balance_before: transaction.gift_balance_before,
|
||||
gift_balance_after: transaction.gift_balance_after,
|
||||
link_type: transaction.link_type,
|
||||
link_id: transaction.link_id,
|
||||
operator_id: transaction.operator_id,
|
||||
description: transaction.description,
|
||||
created_at_unix_secs: transaction.created_at_unix_secs.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn stored_admin_payment_order_to_gateway(
|
||||
order: aether_data::repository::wallet::StoredAdminPaymentOrder,
|
||||
) -> AdminWalletPaymentOrderRecord {
|
||||
AdminWalletPaymentOrderRecord {
|
||||
id: order.id,
|
||||
order_no: order.order_no,
|
||||
wallet_id: order.wallet_id,
|
||||
user_id: order.user_id,
|
||||
amount_usd: order.amount_usd,
|
||||
pay_amount: order.pay_amount,
|
||||
pay_currency: order.pay_currency,
|
||||
exchange_rate: order.exchange_rate,
|
||||
refunded_amount_usd: order.refunded_amount_usd,
|
||||
refundable_amount_usd: order.refundable_amount_usd,
|
||||
payment_method: order.payment_method,
|
||||
gateway_order_id: order.gateway_order_id,
|
||||
status: order.status,
|
||||
gateway_response: order.gateway_response,
|
||||
created_at_unix_secs: order.created_at_unix_secs,
|
||||
paid_at_unix_secs: order.paid_at_unix_secs,
|
||||
credited_at_unix_secs: order.credited_at_unix_secs,
|
||||
expires_at_unix_secs: order.expires_at_unix_secs,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
use sqlx::Row;
|
||||
|
||||
use super::{
|
||||
AdminBillingCollectorRecord, AdminBillingRuleRecord, AdminWalletPaymentOrderRecord,
|
||||
AdminWalletRefundRecord, GatewayError,
|
||||
};
|
||||
|
||||
pub(crate) fn admin_wallet_build_order_no(now: chrono::DateTime<chrono::Utc>) -> String {
|
||||
format!(
|
||||
"po_{}_{}",
|
||||
now.format("%Y%m%d%H%M%S%6f"),
|
||||
&uuid::Uuid::new_v4().simple().to_string()[..12]
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn admin_payment_gateway_response_map(
|
||||
value: Option<serde_json::Value>,
|
||||
) -> serde_json::Map<String, serde_json::Value> {
|
||||
match value {
|
||||
Some(serde_json::Value::Object(map)) => map,
|
||||
_ => serde_json::Map::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn admin_wallet_snapshot_from_row(
|
||||
row: &sqlx::postgres::PgRow,
|
||||
) -> Result<aether_data::repository::wallet::StoredWalletSnapshot, GatewayError> {
|
||||
aether_data::repository::wallet::StoredWalletSnapshot::new(
|
||||
row.try_get("id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("user_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("api_key_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("balance")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("gift_balance")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("limit_mode")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("currency")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("status")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("total_recharged")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("total_consumed")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("total_refunded")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("total_adjusted")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
row.try_get("updated_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
)
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(super) fn admin_wallet_payment_order_from_row(
|
||||
row: &sqlx::postgres::PgRow,
|
||||
) -> Result<AdminWalletPaymentOrderRecord, GatewayError> {
|
||||
Ok(AdminWalletPaymentOrderRecord {
|
||||
id: row
|
||||
.try_get("id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
order_no: row
|
||||
.try_get("order_no")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
wallet_id: row
|
||||
.try_get("wallet_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
user_id: row
|
||||
.try_get("user_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
amount_usd: row
|
||||
.try_get("amount_usd")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
pay_amount: row
|
||||
.try_get("pay_amount")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
pay_currency: row
|
||||
.try_get("pay_currency")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
exchange_rate: row
|
||||
.try_get("exchange_rate")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
refunded_amount_usd: row
|
||||
.try_get("refunded_amount_usd")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
refundable_amount_usd: row
|
||||
.try_get("refundable_amount_usd")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
payment_method: row
|
||||
.try_get("payment_method")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
gateway_order_id: row
|
||||
.try_get("gateway_order_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
status: row
|
||||
.try_get("status")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
gateway_response: row
|
||||
.try_get("gateway_response")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
created_at_unix_secs: row
|
||||
.try_get::<i64, _>("created_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.max(0) as u64,
|
||||
paid_at_unix_secs: row
|
||||
.try_get::<Option<i64>, _>("paid_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.map(|value| value.max(0) as u64),
|
||||
credited_at_unix_secs: row
|
||||
.try_get::<Option<i64>, _>("credited_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.map(|value| value.max(0) as u64),
|
||||
expires_at_unix_secs: row
|
||||
.try_get::<Option<i64>, _>("expires_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.map(|value| value.max(0) as u64),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn admin_wallet_refund_from_row(
|
||||
row: &sqlx::postgres::PgRow,
|
||||
) -> Result<AdminWalletRefundRecord, GatewayError> {
|
||||
Ok(AdminWalletRefundRecord {
|
||||
id: row
|
||||
.try_get("id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
refund_no: row
|
||||
.try_get("refund_no")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
wallet_id: row
|
||||
.try_get("wallet_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
user_id: row
|
||||
.try_get("user_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
payment_order_id: row
|
||||
.try_get("payment_order_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
source_type: row
|
||||
.try_get("source_type")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
source_id: row
|
||||
.try_get("source_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
refund_mode: row
|
||||
.try_get("refund_mode")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
amount_usd: row
|
||||
.try_get("amount_usd")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
status: row
|
||||
.try_get("status")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
reason: row
|
||||
.try_get("reason")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
failure_reason: row
|
||||
.try_get("failure_reason")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
gateway_refund_id: row
|
||||
.try_get("gateway_refund_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
payout_method: row
|
||||
.try_get("payout_method")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
payout_reference: row
|
||||
.try_get("payout_reference")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
payout_proof: row
|
||||
.try_get("payout_proof")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
requested_by: row
|
||||
.try_get("requested_by")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
approved_by: row
|
||||
.try_get("approved_by")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
processed_by: row
|
||||
.try_get("processed_by")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
created_at_unix_secs: row
|
||||
.try_get::<i64, _>("created_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.max(0) as u64,
|
||||
updated_at_unix_secs: row
|
||||
.try_get::<i64, _>("updated_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.max(0) as u64,
|
||||
processed_at_unix_secs: row
|
||||
.try_get::<Option<i64>, _>("processed_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.map(|value| value.max(0) as u64),
|
||||
completed_at_unix_secs: row
|
||||
.try_get::<Option<i64>, _>("completed_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.map(|value| value.max(0) as u64),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn admin_billing_rule_from_row(
|
||||
row: &sqlx::postgres::PgRow,
|
||||
) -> Result<AdminBillingRuleRecord, GatewayError> {
|
||||
Ok(AdminBillingRuleRecord {
|
||||
id: row
|
||||
.try_get("id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
name: row
|
||||
.try_get("name")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
task_type: row
|
||||
.try_get("task_type")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
global_model_id: row
|
||||
.try_get("global_model_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
model_id: row
|
||||
.try_get("model_id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
expression: row
|
||||
.try_get("expression")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
variables: row
|
||||
.try_get::<Option<serde_json::Value>, _>("variables")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.unwrap_or_else(|| serde_json::json!({})),
|
||||
dimension_mappings: row
|
||||
.try_get::<Option<serde_json::Value>, _>("dimension_mappings")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.unwrap_or_else(|| serde_json::json!({})),
|
||||
is_enabled: row
|
||||
.try_get("is_enabled")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
created_at_unix_secs: row
|
||||
.try_get::<i64, _>("created_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.max(0) as u64,
|
||||
updated_at_unix_secs: row
|
||||
.try_get::<i64, _>("updated_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.max(0) as u64,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn admin_billing_collector_from_row(
|
||||
row: &sqlx::postgres::PgRow,
|
||||
) -> Result<AdminBillingCollectorRecord, GatewayError> {
|
||||
Ok(AdminBillingCollectorRecord {
|
||||
id: row
|
||||
.try_get("id")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
api_format: row
|
||||
.try_get("api_format")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
task_type: row
|
||||
.try_get("task_type")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
dimension_name: row
|
||||
.try_get("dimension_name")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
source_type: row
|
||||
.try_get("source_type")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
source_path: row
|
||||
.try_get("source_path")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
value_type: row
|
||||
.try_get("value_type")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
transform_expression: row
|
||||
.try_get("transform_expression")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
default_value: row
|
||||
.try_get("default_value")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
priority: row
|
||||
.try_get("priority")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
is_enabled: row
|
||||
.try_get("is_enabled")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?,
|
||||
created_at_unix_secs: row
|
||||
.try_get::<i64, _>("created_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.max(0) as u64,
|
||||
updated_at_unix_secs: row
|
||||
.try_get::<i64, _>("updated_at_unix_secs")
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.max(0) as u64,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
use super::super::{
|
||||
AdminBillingCollectorRecord, AdminBillingRuleRecord, AdminWalletMutationOutcome,
|
||||
AdminWalletPaymentOrderRecord, AdminWalletRefundRecord, AdminWalletTransactionRecord, AppState,
|
||||
GatewayError,
|
||||
};
|
||||
|
||||
mod balance_mutations;
|
||||
mod billing;
|
||||
mod mutations;
|
||||
mod reads;
|
||||
mod refund_lifecycle;
|
||||
|
||||
pub(super) use self::billing::{admin_payment_gateway_response_map, admin_wallet_build_order_no};
|
||||
@@ -0,0 +1,173 @@
|
||||
use aether_data::repository::wallet::{
|
||||
AdjustWalletBalanceInput, CompleteAdminWalletRefundInput, CreateManualWalletRechargeInput,
|
||||
CreateWalletRechargeOrderInput, CreateWalletRechargeOrderOutcome,
|
||||
CreateWalletRefundRequestInput, CreateWalletRefundRequestOutcome, CreditAdminPaymentOrderInput,
|
||||
FailAdminWalletRefundInput, ProcessAdminWalletRefundInput, ProcessPaymentCallbackInput,
|
||||
ProcessPaymentCallbackOutcome, WalletMutationOutcome,
|
||||
};
|
||||
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn create_wallet_recharge_order(
|
||||
&self,
|
||||
input: CreateWalletRechargeOrderInput,
|
||||
) -> Result<Option<CreateWalletRechargeOrderOutcome>, GatewayError> {
|
||||
self.data
|
||||
.create_wallet_recharge_order(input)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn create_wallet_refund_request(
|
||||
&self,
|
||||
input: CreateWalletRefundRequestInput,
|
||||
) -> Result<Option<CreateWalletRefundRequestOutcome>, GatewayError> {
|
||||
self.data
|
||||
.create_wallet_refund_request(input)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn process_payment_callback(
|
||||
&self,
|
||||
input: ProcessPaymentCallbackInput,
|
||||
) -> Result<Option<ProcessPaymentCallbackOutcome>, GatewayError> {
|
||||
self.data
|
||||
.process_payment_callback(input)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn adjust_wallet_balance(
|
||||
&self,
|
||||
input: AdjustWalletBalanceInput,
|
||||
) -> Result<
|
||||
Option<(
|
||||
aether_data::repository::wallet::StoredWalletSnapshot,
|
||||
aether_data::repository::wallet::StoredAdminWalletTransaction,
|
||||
)>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.adjust_wallet_balance(input)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn create_manual_wallet_recharge(
|
||||
&self,
|
||||
input: CreateManualWalletRechargeInput,
|
||||
) -> Result<
|
||||
Option<(
|
||||
aether_data::repository::wallet::StoredWalletSnapshot,
|
||||
aether_data::repository::wallet::StoredAdminPaymentOrder,
|
||||
)>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.create_manual_wallet_recharge(input)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn process_admin_wallet_refund(
|
||||
&self,
|
||||
input: ProcessAdminWalletRefundInput,
|
||||
) -> Result<
|
||||
Option<
|
||||
WalletMutationOutcome<(
|
||||
aether_data::repository::wallet::StoredWalletSnapshot,
|
||||
aether_data::repository::wallet::StoredAdminWalletRefund,
|
||||
aether_data::repository::wallet::StoredAdminWalletTransaction,
|
||||
)>,
|
||||
>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.process_admin_wallet_refund(input)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn complete_admin_wallet_refund(
|
||||
&self,
|
||||
input: CompleteAdminWalletRefundInput,
|
||||
) -> Result<
|
||||
Option<WalletMutationOutcome<aether_data::repository::wallet::StoredAdminWalletRefund>>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.complete_admin_wallet_refund(input)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn fail_admin_wallet_refund(
|
||||
&self,
|
||||
input: FailAdminWalletRefundInput,
|
||||
) -> Result<
|
||||
Option<
|
||||
WalletMutationOutcome<(
|
||||
aether_data::repository::wallet::StoredWalletSnapshot,
|
||||
aether_data::repository::wallet::StoredAdminWalletRefund,
|
||||
Option<aether_data::repository::wallet::StoredAdminWalletTransaction>,
|
||||
)>,
|
||||
>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.fail_admin_wallet_refund(input)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn expire_admin_payment_order(
|
||||
&self,
|
||||
order_id: &str,
|
||||
) -> Result<
|
||||
Option<
|
||||
WalletMutationOutcome<(
|
||||
aether_data::repository::wallet::StoredAdminPaymentOrder,
|
||||
bool,
|
||||
)>,
|
||||
>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.expire_admin_payment_order(order_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn fail_admin_payment_order(
|
||||
&self,
|
||||
order_id: &str,
|
||||
) -> Result<
|
||||
Option<WalletMutationOutcome<aether_data::repository::wallet::StoredAdminPaymentOrder>>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.fail_admin_payment_order(order_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn credit_admin_payment_order(
|
||||
&self,
|
||||
input: CreditAdminPaymentOrderInput,
|
||||
) -> Result<
|
||||
Option<
|
||||
WalletMutationOutcome<(
|
||||
aether_data::repository::wallet::StoredAdminPaymentOrder,
|
||||
bool,
|
||||
)>,
|
||||
>,
|
||||
GatewayError,
|
||||
> {
|
||||
self.data
|
||||
.credit_admin_payment_order(input)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn find_wallet(
|
||||
&self,
|
||||
lookup: aether_data::repository::wallet::WalletLookupKey<'_>,
|
||||
) -> Result<Option<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(store) = self.auth_wallet_store.as_ref() {
|
||||
let wallet = {
|
||||
let wallets = store.lock().expect("auth wallet store should lock");
|
||||
match lookup {
|
||||
aether_data::repository::wallet::WalletLookupKey::WalletId(wallet_id) => {
|
||||
wallets.get(wallet_id).cloned()
|
||||
}
|
||||
aether_data::repository::wallet::WalletLookupKey::UserId(user_id) => wallets
|
||||
.values()
|
||||
.find(|wallet| wallet.user_id.as_deref() == Some(user_id))
|
||||
.cloned(),
|
||||
aether_data::repository::wallet::WalletLookupKey::ApiKeyId(api_key_id) => {
|
||||
wallets
|
||||
.values()
|
||||
.find(|wallet| wallet.api_key_id.as_deref() == Some(api_key_id))
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
};
|
||||
if wallet.is_some() {
|
||||
return Ok(wallet);
|
||||
}
|
||||
}
|
||||
|
||||
self.data
|
||||
.find_wallet(lookup)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn read_wallet_snapshot_for_auth(
|
||||
&self,
|
||||
user_id: &str,
|
||||
api_key_id: &str,
|
||||
api_key_is_standalone: bool,
|
||||
) -> Result<Option<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
|
||||
let lookup = if api_key_is_standalone {
|
||||
if api_key_id.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(aether_data::repository::wallet::WalletLookupKey::ApiKeyId(
|
||||
api_key_id,
|
||||
))
|
||||
}
|
||||
} else if !user_id.trim().is_empty() {
|
||||
Some(aether_data::repository::wallet::WalletLookupKey::UserId(
|
||||
user_id,
|
||||
))
|
||||
} else if !api_key_id.trim().is_empty() {
|
||||
Some(aether_data::repository::wallet::WalletLookupKey::ApiKeyId(
|
||||
api_key_id,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let Some(lookup) = lookup else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
self.find_wallet(lookup).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_wallet_snapshots_by_user_ids(
|
||||
&self,
|
||||
user_ids: &[String],
|
||||
) -> Result<Vec<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
|
||||
self.data
|
||||
.list_wallets_by_user_ids(user_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_wallet_snapshots_by_api_key_ids(
|
||||
&self,
|
||||
api_key_ids: &[String],
|
||||
) -> Result<Vec<aether_data::repository::wallet::StoredWalletSnapshot>, GatewayError> {
|
||||
self.data
|
||||
.list_wallets_by_api_key_ids(api_key_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn find_wallet_today_usage(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
billing_timezone: &str,
|
||||
) -> Result<Option<aether_data::repository::wallet::StoredWalletDailyUsageLedger>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.find_wallet_today_usage(wallet_id, billing_timezone)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_wallet_daily_usage_history(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
billing_timezone: &str,
|
||||
limit: usize,
|
||||
) -> Result<aether_data::repository::wallet::StoredWalletDailyUsageLedgerPage, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.list_wallet_daily_usage_history(wallet_id, billing_timezone, limit)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn list_wallet_payment_orders_by_user_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<aether_data::repository::wallet::StoredAdminPaymentOrderPage, GatewayError> {
|
||||
self.data
|
||||
.list_wallet_payment_orders_by_user_id(user_id, limit, offset)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn find_wallet_payment_order_by_user_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
order_id: &str,
|
||||
) -> Result<Option<aether_data::repository::wallet::StoredAdminPaymentOrder>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.find_wallet_payment_order_by_user_id(user_id, order_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) async fn find_wallet_refund(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
refund_id: &str,
|
||||
) -> Result<Option<aether_data::repository::wallet::StoredAdminWalletRefund>, GatewayError>
|
||||
{
|
||||
self.data
|
||||
.find_wallet_refund(wallet_id, refund_id)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
use super::{
|
||||
AdminWalletMutationOutcome, AdminWalletRefundRecord, AdminWalletTransactionRecord, AppState,
|
||||
GatewayError,
|
||||
};
|
||||
|
||||
impl AppState {
|
||||
pub(crate) async fn admin_process_wallet_refund(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
refund_id: &str,
|
||||
operator_id: Option<&str>,
|
||||
) -> Result<
|
||||
AdminWalletMutationOutcome<(
|
||||
aether_data::repository::wallet::StoredWalletSnapshot,
|
||||
AdminWalletRefundRecord,
|
||||
AdminWalletTransactionRecord,
|
||||
)>,
|
||||
GatewayError,
|
||||
> {
|
||||
#[cfg(test)]
|
||||
if let (Some(wallet_store), Some(refund_store)) = (
|
||||
self.auth_wallet_store.as_ref(),
|
||||
self.admin_wallet_refund_store.as_ref(),
|
||||
) {
|
||||
let Some(wallet) = wallet_store
|
||||
.lock()
|
||||
.expect("auth wallet store should lock")
|
||||
.get(wallet_id)
|
||||
.cloned()
|
||||
else {
|
||||
return Ok(AdminWalletMutationOutcome::NotFound);
|
||||
};
|
||||
let Some(refund) = refund_store
|
||||
.lock()
|
||||
.expect("admin wallet refund store should lock")
|
||||
.get(refund_id)
|
||||
.filter(|refund| refund.wallet_id == wallet_id)
|
||||
.cloned()
|
||||
else {
|
||||
return Ok(AdminWalletMutationOutcome::NotFound);
|
||||
};
|
||||
if !matches!(refund.status.as_str(), "approved" | "pending_approval") {
|
||||
return Ok(AdminWalletMutationOutcome::Invalid(
|
||||
"refund status is not approvable".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let amount_usd = refund.amount_usd;
|
||||
let mut updated_wallet = wallet.clone();
|
||||
let before_recharge = updated_wallet.balance;
|
||||
let before_gift = updated_wallet.gift_balance;
|
||||
let before_total = before_recharge + before_gift;
|
||||
let after_recharge = before_recharge - amount_usd;
|
||||
if after_recharge < 0.0 {
|
||||
return Ok(AdminWalletMutationOutcome::Invalid(
|
||||
"refund amount exceeds refundable recharge balance".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut updated_order = None;
|
||||
if let Some(payment_order_id) = refund.payment_order_id.clone() {
|
||||
let Some(order_store) = self.admin_wallet_payment_order_store.as_ref() else {
|
||||
return Ok(AdminWalletMutationOutcome::Unavailable);
|
||||
};
|
||||
let Some(order) = order_store
|
||||
.lock()
|
||||
.expect("admin wallet payment order store should lock")
|
||||
.get(&payment_order_id)
|
||||
.cloned()
|
||||
else {
|
||||
return Ok(AdminWalletMutationOutcome::Invalid(
|
||||
"payment order not found".to_string(),
|
||||
));
|
||||
};
|
||||
if amount_usd > order.refundable_amount_usd {
|
||||
return Ok(AdminWalletMutationOutcome::Invalid(
|
||||
"refund amount exceeds refundable amount".to_string(),
|
||||
));
|
||||
}
|
||||
let mut order = order;
|
||||
order.refunded_amount_usd += amount_usd;
|
||||
order.refundable_amount_usd -= amount_usd;
|
||||
updated_order = Some(order);
|
||||
}
|
||||
|
||||
let now_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
|
||||
updated_wallet.balance = after_recharge;
|
||||
updated_wallet.total_refunded = (updated_wallet.total_refunded + amount_usd).max(0.0);
|
||||
updated_wallet.updated_at_unix_secs = now_unix_secs;
|
||||
|
||||
let transaction = AdminWalletTransactionRecord {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
wallet_id: updated_wallet.id.clone(),
|
||||
category: "refund".to_string(),
|
||||
reason_code: "refund_out".to_string(),
|
||||
amount: -amount_usd,
|
||||
balance_before: before_total,
|
||||
balance_after: after_recharge + before_gift,
|
||||
recharge_balance_before: before_recharge,
|
||||
recharge_balance_after: after_recharge,
|
||||
gift_balance_before: before_gift,
|
||||
gift_balance_after: before_gift,
|
||||
link_type: Some("refund_request".to_string()),
|
||||
link_id: Some(refund.id.clone()),
|
||||
operator_id: operator_id.map(ToOwned::to_owned),
|
||||
description: Some("退款占款".to_string()),
|
||||
created_at_unix_secs: now_unix_secs,
|
||||
};
|
||||
|
||||
let mut updated_refund = refund.clone();
|
||||
updated_refund.status = "processing".to_string();
|
||||
updated_refund.approved_by = operator_id.map(ToOwned::to_owned);
|
||||
updated_refund.processed_by = operator_id.map(ToOwned::to_owned);
|
||||
updated_refund.processed_at_unix_secs = Some(now_unix_secs);
|
||||
updated_refund.updated_at_unix_secs = now_unix_secs;
|
||||
|
||||
wallet_store
|
||||
.lock()
|
||||
.expect("auth wallet store should lock")
|
||||
.insert(updated_wallet.id.clone(), updated_wallet.clone());
|
||||
refund_store
|
||||
.lock()
|
||||
.expect("admin wallet refund store should lock")
|
||||
.insert(updated_refund.id.clone(), updated_refund.clone());
|
||||
if let Some(updated_order) = updated_order {
|
||||
self.admin_wallet_payment_order_store
|
||||
.as_ref()
|
||||
.expect("admin wallet payment order store should exist")
|
||||
.lock()
|
||||
.expect("admin wallet payment order store should lock")
|
||||
.insert(updated_order.id.clone(), updated_order);
|
||||
}
|
||||
|
||||
return Ok(AdminWalletMutationOutcome::Applied((
|
||||
updated_wallet,
|
||||
updated_refund,
|
||||
transaction,
|
||||
)));
|
||||
}
|
||||
|
||||
match self
|
||||
.process_admin_wallet_refund(
|
||||
aether_data::repository::wallet::ProcessAdminWalletRefundInput {
|
||||
wallet_id: wallet_id.to_string(),
|
||||
refund_id: refund_id.to_string(),
|
||||
operator_id: operator_id.map(ToOwned::to_owned),
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::Applied((
|
||||
wallet,
|
||||
refund,
|
||||
transaction,
|
||||
))) => Ok(AdminWalletMutationOutcome::Applied((
|
||||
wallet,
|
||||
stored_admin_wallet_refund_to_gateway(refund),
|
||||
stored_admin_wallet_transaction_to_gateway(transaction),
|
||||
))),
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::NotFound) => {
|
||||
Ok(AdminWalletMutationOutcome::NotFound)
|
||||
}
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::Invalid(detail)) => {
|
||||
Ok(AdminWalletMutationOutcome::Invalid(detail))
|
||||
}
|
||||
None => Ok(AdminWalletMutationOutcome::Unavailable),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn admin_complete_wallet_refund(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
refund_id: &str,
|
||||
gateway_refund_id: Option<&str>,
|
||||
payout_reference: Option<&str>,
|
||||
payout_proof: Option<serde_json::Value>,
|
||||
) -> Result<AdminWalletMutationOutcome<AdminWalletRefundRecord>, GatewayError> {
|
||||
#[cfg(test)]
|
||||
if let Some(refund_store) = self.admin_wallet_refund_store.as_ref() {
|
||||
let Some(refund) = refund_store
|
||||
.lock()
|
||||
.expect("admin wallet refund store should lock")
|
||||
.get(refund_id)
|
||||
.filter(|refund| refund.wallet_id == wallet_id)
|
||||
.cloned()
|
||||
else {
|
||||
return Ok(AdminWalletMutationOutcome::NotFound);
|
||||
};
|
||||
if refund.status != "processing" {
|
||||
return Ok(AdminWalletMutationOutcome::Invalid(
|
||||
"refund status must be processing before completion".to_string(),
|
||||
));
|
||||
}
|
||||
let now_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
|
||||
let mut updated_refund = refund;
|
||||
updated_refund.status = "succeeded".to_string();
|
||||
updated_refund.gateway_refund_id = gateway_refund_id.map(ToOwned::to_owned);
|
||||
updated_refund.payout_reference = payout_reference.map(ToOwned::to_owned);
|
||||
updated_refund.payout_proof = payout_proof;
|
||||
updated_refund.completed_at_unix_secs = Some(now_unix_secs);
|
||||
updated_refund.updated_at_unix_secs = now_unix_secs;
|
||||
refund_store
|
||||
.lock()
|
||||
.expect("admin wallet refund store should lock")
|
||||
.insert(updated_refund.id.clone(), updated_refund.clone());
|
||||
return Ok(AdminWalletMutationOutcome::Applied(updated_refund));
|
||||
}
|
||||
|
||||
match self
|
||||
.complete_admin_wallet_refund(
|
||||
aether_data::repository::wallet::CompleteAdminWalletRefundInput {
|
||||
wallet_id: wallet_id.to_string(),
|
||||
refund_id: refund_id.to_string(),
|
||||
gateway_refund_id: gateway_refund_id.map(ToOwned::to_owned),
|
||||
payout_reference: payout_reference.map(ToOwned::to_owned),
|
||||
payout_proof,
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::Applied(refund)) => Ok(
|
||||
AdminWalletMutationOutcome::Applied(stored_admin_wallet_refund_to_gateway(refund)),
|
||||
),
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::NotFound) => {
|
||||
Ok(AdminWalletMutationOutcome::NotFound)
|
||||
}
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::Invalid(detail)) => {
|
||||
Ok(AdminWalletMutationOutcome::Invalid(detail))
|
||||
}
|
||||
None => Ok(AdminWalletMutationOutcome::Unavailable),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn admin_fail_wallet_refund(
|
||||
&self,
|
||||
wallet_id: &str,
|
||||
refund_id: &str,
|
||||
reason: &str,
|
||||
operator_id: Option<&str>,
|
||||
) -> Result<
|
||||
AdminWalletMutationOutcome<(
|
||||
aether_data::repository::wallet::StoredWalletSnapshot,
|
||||
AdminWalletRefundRecord,
|
||||
Option<AdminWalletTransactionRecord>,
|
||||
)>,
|
||||
GatewayError,
|
||||
> {
|
||||
#[cfg(test)]
|
||||
if let (Some(wallet_store), Some(refund_store)) = (
|
||||
self.auth_wallet_store.as_ref(),
|
||||
self.admin_wallet_refund_store.as_ref(),
|
||||
) {
|
||||
let Some(wallet) = wallet_store
|
||||
.lock()
|
||||
.expect("auth wallet store should lock")
|
||||
.get(wallet_id)
|
||||
.cloned()
|
||||
else {
|
||||
return Ok(AdminWalletMutationOutcome::NotFound);
|
||||
};
|
||||
let Some(refund) = refund_store
|
||||
.lock()
|
||||
.expect("admin wallet refund store should lock")
|
||||
.get(refund_id)
|
||||
.filter(|refund| refund.wallet_id == wallet_id)
|
||||
.cloned()
|
||||
else {
|
||||
return Ok(AdminWalletMutationOutcome::NotFound);
|
||||
};
|
||||
|
||||
let now_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
|
||||
if matches!(refund.status.as_str(), "pending_approval" | "approved") {
|
||||
let mut updated_refund = refund;
|
||||
updated_refund.status = "failed".to_string();
|
||||
updated_refund.failure_reason = Some(reason.to_string());
|
||||
updated_refund.updated_at_unix_secs = now_unix_secs;
|
||||
refund_store
|
||||
.lock()
|
||||
.expect("admin wallet refund store should lock")
|
||||
.insert(updated_refund.id.clone(), updated_refund.clone());
|
||||
return Ok(AdminWalletMutationOutcome::Applied((
|
||||
wallet,
|
||||
updated_refund,
|
||||
None,
|
||||
)));
|
||||
}
|
||||
if refund.status != "processing" {
|
||||
return Ok(AdminWalletMutationOutcome::Invalid(format!(
|
||||
"cannot fail refund in status: {}",
|
||||
refund.status
|
||||
)));
|
||||
}
|
||||
|
||||
let amount_usd = refund.amount_usd;
|
||||
let before_recharge = wallet.balance;
|
||||
let before_gift = wallet.gift_balance;
|
||||
let before_total = before_recharge + before_gift;
|
||||
let after_recharge = before_recharge + amount_usd;
|
||||
|
||||
let mut updated_wallet = wallet.clone();
|
||||
updated_wallet.balance = after_recharge;
|
||||
updated_wallet.total_refunded = (updated_wallet.total_refunded - amount_usd).max(0.0);
|
||||
updated_wallet.updated_at_unix_secs = now_unix_secs;
|
||||
|
||||
let transaction = AdminWalletTransactionRecord {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
wallet_id: updated_wallet.id.clone(),
|
||||
category: "refund".to_string(),
|
||||
reason_code: "refund_revert".to_string(),
|
||||
amount: amount_usd,
|
||||
balance_before: before_total,
|
||||
balance_after: after_recharge + before_gift,
|
||||
recharge_balance_before: before_recharge,
|
||||
recharge_balance_after: after_recharge,
|
||||
gift_balance_before: before_gift,
|
||||
gift_balance_after: before_gift,
|
||||
link_type: Some("refund_request".to_string()),
|
||||
link_id: Some(refund.id.clone()),
|
||||
operator_id: operator_id.map(ToOwned::to_owned),
|
||||
description: Some("退款失败回补".to_string()),
|
||||
created_at_unix_secs: now_unix_secs,
|
||||
};
|
||||
|
||||
if let Some(payment_order_id) = refund.payment_order_id.clone() {
|
||||
let Some(order_store) = self.admin_wallet_payment_order_store.as_ref() else {
|
||||
return Ok(AdminWalletMutationOutcome::Unavailable);
|
||||
};
|
||||
let maybe_order = order_store
|
||||
.lock()
|
||||
.expect("admin wallet payment order store should lock")
|
||||
.get(&payment_order_id)
|
||||
.cloned();
|
||||
if let Some(mut order) = maybe_order {
|
||||
order.refunded_amount_usd -= amount_usd;
|
||||
order.refundable_amount_usd += amount_usd;
|
||||
order_store
|
||||
.lock()
|
||||
.expect("admin wallet payment order store should lock")
|
||||
.insert(order.id.clone(), order);
|
||||
}
|
||||
}
|
||||
|
||||
let mut updated_refund = refund;
|
||||
updated_refund.status = "failed".to_string();
|
||||
updated_refund.failure_reason = Some(reason.to_string());
|
||||
updated_refund.updated_at_unix_secs = now_unix_secs;
|
||||
|
||||
wallet_store
|
||||
.lock()
|
||||
.expect("auth wallet store should lock")
|
||||
.insert(updated_wallet.id.clone(), updated_wallet.clone());
|
||||
refund_store
|
||||
.lock()
|
||||
.expect("admin wallet refund store should lock")
|
||||
.insert(updated_refund.id.clone(), updated_refund.clone());
|
||||
|
||||
return Ok(AdminWalletMutationOutcome::Applied((
|
||||
updated_wallet,
|
||||
updated_refund,
|
||||
Some(transaction),
|
||||
)));
|
||||
}
|
||||
|
||||
match self
|
||||
.fail_admin_wallet_refund(
|
||||
aether_data::repository::wallet::FailAdminWalletRefundInput {
|
||||
wallet_id: wallet_id.to_string(),
|
||||
refund_id: refund_id.to_string(),
|
||||
reason: reason.to_string(),
|
||||
operator_id: operator_id.map(ToOwned::to_owned),
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::Applied((
|
||||
wallet,
|
||||
refund,
|
||||
transaction,
|
||||
))) => Ok(AdminWalletMutationOutcome::Applied((
|
||||
wallet,
|
||||
stored_admin_wallet_refund_to_gateway(refund),
|
||||
transaction.map(stored_admin_wallet_transaction_to_gateway),
|
||||
))),
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::NotFound) => {
|
||||
Ok(AdminWalletMutationOutcome::NotFound)
|
||||
}
|
||||
Some(aether_data::repository::wallet::WalletMutationOutcome::Invalid(detail)) => {
|
||||
Ok(AdminWalletMutationOutcome::Invalid(detail))
|
||||
}
|
||||
None => Ok(AdminWalletMutationOutcome::Unavailable),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stored_admin_wallet_refund_to_gateway(
|
||||
refund: aether_data::repository::wallet::StoredAdminWalletRefund,
|
||||
) -> AdminWalletRefundRecord {
|
||||
AdminWalletRefundRecord {
|
||||
id: refund.id,
|
||||
refund_no: refund.refund_no,
|
||||
wallet_id: refund.wallet_id,
|
||||
user_id: refund.user_id,
|
||||
payment_order_id: refund.payment_order_id,
|
||||
source_type: refund.source_type,
|
||||
source_id: refund.source_id,
|
||||
refund_mode: refund.refund_mode,
|
||||
amount_usd: refund.amount_usd,
|
||||
status: refund.status,
|
||||
reason: refund.reason,
|
||||
failure_reason: refund.failure_reason,
|
||||
gateway_refund_id: refund.gateway_refund_id,
|
||||
payout_method: refund.payout_method,
|
||||
payout_reference: refund.payout_reference,
|
||||
payout_proof: refund.payout_proof,
|
||||
requested_by: refund.requested_by,
|
||||
approved_by: refund.approved_by,
|
||||
processed_by: refund.processed_by,
|
||||
created_at_unix_secs: refund.created_at_unix_secs,
|
||||
updated_at_unix_secs: refund.updated_at_unix_secs,
|
||||
processed_at_unix_secs: refund.processed_at_unix_secs,
|
||||
completed_at_unix_secs: refund.completed_at_unix_secs,
|
||||
}
|
||||
}
|
||||
|
||||
fn stored_admin_wallet_transaction_to_gateway(
|
||||
transaction: aether_data::repository::wallet::StoredAdminWalletTransaction,
|
||||
) -> AdminWalletTransactionRecord {
|
||||
AdminWalletTransactionRecord {
|
||||
id: transaction.id,
|
||||
wallet_id: transaction.wallet_id,
|
||||
category: transaction.category,
|
||||
reason_code: transaction.reason_code,
|
||||
amount: transaction.amount,
|
||||
balance_before: transaction.balance_before,
|
||||
balance_after: transaction.balance_after,
|
||||
recharge_balance_before: transaction.recharge_balance_before,
|
||||
recharge_balance_after: transaction.recharge_balance_after,
|
||||
gift_balance_before: transaction.gift_balance_before,
|
||||
gift_balance_after: transaction.gift_balance_after,
|
||||
link_type: transaction.link_type,
|
||||
link_id: transaction.link_id,
|
||||
operator_id: transaction.operator_id,
|
||||
description: transaction.description,
|
||||
created_at_unix_secs: transaction.created_at_unix_secs.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user