Files
Aether/apps/aether-gateway/src/state/runtime/billing/finance_queries.rs
T
elky 066ea87d72 feat: revamp analytics dashboards and harden database migrations
Add dashboard and overview analytics, health monitoring, provider expense tracking, and announcement updates across the gateway and frontend.

Keep schema migrations free of historical backfills while preserving automatic backfill execution. Bound migration deadlines, run schema preparation before Compose replacement, and anonymize deleted dashboard users.

Include the current documentation cleanup and regression coverage.
2026-10-01 11:48:17 +08:00

523 lines
19 KiB
Rust

use aether_data::repository::wallet::{
AdminPaymentOrderListQuery, AdminRedeemCodeBatchListQuery, AdminRedeemCodeListQuery,
AdminWalletLedgerQuery, AdminWalletListQuery, AdminWalletRefundRequestListQuery,
StoredAdminPaymentCallback, StoredAdminPaymentOrder, StoredAdminRedeemCodeBatch,
StoredAdminRedeemCodePage, StoredAdminWalletLedgerItem, StoredAdminWalletListItem,
StoredAdminWalletRefund, StoredAdminWalletRefundRequestItem, StoredAdminWalletTransaction,
};
use crate::{
AdminWalletMutationOutcome, AdminWalletPaymentOrderRecord, AdminWalletRefundRecord, AppState,
GatewayAdminPaymentCallbackView, GatewayError,
};
impl AppState {
pub(crate) async fn list_admin_wallets(
&self,
user_id: Option<&str>,
status: Option<&str>,
owner_type: Option<&str>,
limit: usize,
offset: usize,
) -> Result<(Vec<StoredAdminWalletListItem>, u64), GatewayError> {
let page = self
.data
.list_admin_wallets(&AdminWalletListQuery {
user_id: user_id.map(ToOwned::to_owned),
status: status.map(ToOwned::to_owned),
owner_type: owner_type.map(ToOwned::to_owned),
limit,
offset,
})
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
Ok((page.items, page.total))
}
pub(crate) async fn list_admin_wallet_ledger(
&self,
category: Option<&str>,
reason_code: Option<&str>,
owner_type: Option<&str>,
limit: usize,
offset: usize,
) -> Result<(Vec<StoredAdminWalletLedgerItem>, u64), GatewayError> {
let page = self
.data
.list_admin_wallet_ledger(&AdminWalletLedgerQuery {
category: category.map(ToOwned::to_owned),
reason_code: reason_code.map(ToOwned::to_owned),
owner_type: owner_type.map(ToOwned::to_owned),
limit,
offset,
})
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
Ok((page.items, page.total))
}
pub(crate) async fn list_admin_payment_orders(
&self,
status: Option<&str>,
payment_method: Option<&str>,
limit: usize,
offset: usize,
) -> Result<Option<(Vec<AdminWalletPaymentOrderRecord>, u64)>, GatewayError> {
#[cfg(test)]
if let Some(store) = self.admin_wallet_payment_order_store.as_ref() {
let now_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
let mut items = store
.lock()
.expect("admin wallet payment order store should lock")
.values()
.filter(|order| {
payment_method.is_none_or(|expected| order.payment_method == expected)
&& status.is_none_or(|expected| {
let effective_status = if order.status == "pending"
&& order
.expires_at_unix_secs
.is_some_and(|value| value <= now_unix_secs)
{
"expired"
} else {
order.status.as_str()
};
effective_status == expected
})
})
.cloned()
.collect::<Vec<_>>();
items.sort_by(|left, right| {
right
.created_at_unix_ms
.cmp(&left.created_at_unix_ms)
.then_with(|| right.id.cmp(&left.id))
});
let total = items.len() as u64;
let items = items
.into_iter()
.skip(offset)
.take(limit)
.collect::<Vec<_>>();
return Ok(Some((items, total)));
}
let page = self
.data
.list_admin_payment_orders(&AdminPaymentOrderListQuery {
status: status.map(ToOwned::to_owned),
payment_method: payment_method.map(ToOwned::to_owned),
limit,
offset,
})
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
Ok(Some((
page.items
.into_iter()
.map(stored_admin_payment_order_to_gateway)
.collect(),
page.total,
)))
}
pub(crate) async fn list_admin_payment_callbacks(
&self,
payment_method: Option<&str>,
limit: usize,
offset: usize,
) -> Result<Option<(Vec<GatewayAdminPaymentCallbackView>, u64)>, GatewayError> {
#[cfg(test)]
if let Some(store) = self.admin_payment_callback_store.as_ref() {
let mut items = store
.lock()
.expect("admin payment callback store should lock")
.values()
.filter(|callback| {
payment_method.is_none_or(|expected| callback.payment_method == expected)
})
.cloned()
.collect::<Vec<_>>();
items.sort_by(|left, right| {
right
.created_at_unix_ms
.cmp(&left.created_at_unix_ms)
.then_with(|| right.id.cmp(&left.id))
});
let total = items.len() as u64;
let items = items
.into_iter()
.skip(offset)
.take(limit)
.map(Into::into)
.collect::<Vec<_>>();
return Ok(Some((items, total)));
}
let page = self
.data
.list_admin_payment_callbacks(payment_method, limit, offset)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
Ok(Some((
page.items
.into_iter()
.map(stored_admin_payment_callback_to_gateway)
.collect(),
page.total,
)))
}
pub(crate) async fn list_admin_wallet_transactions(
&self,
wallet_id: &str,
limit: usize,
offset: usize,
) -> Result<(Vec<StoredAdminWalletTransaction>, u64), GatewayError> {
#[cfg(test)]
if let Some(store) = self.admin_wallet_transaction_store.as_ref() {
let mut items = store
.lock()
.expect("admin wallet transaction store should lock")
.values()
.filter(|transaction| transaction.wallet_id == wallet_id)
.cloned()
.collect::<Vec<_>>();
items.sort_by(|left, right| {
right
.created_at_unix_ms
.cmp(&left.created_at_unix_ms)
.then_with(|| right.id.cmp(&left.id))
});
let total = items.len() as u64;
let items = items
.into_iter()
.skip(offset)
.take(limit)
.map(|record| StoredAdminWalletTransaction {
id: record.id,
wallet_id: record.wallet_id,
category: record.category,
reason_code: record.reason_code,
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,
link_id: record.link_id,
operator_id: record.operator_id,
operator_name: None,
operator_email: None,
description: record.description,
created_at_unix_ms: Some(record.created_at_unix_ms),
})
.collect::<Vec<_>>();
return Ok((items, total));
}
let page = self
.data
.list_admin_wallet_transactions(wallet_id, limit, offset)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
Ok((page.items, page.total))
}
pub(crate) async fn list_admin_wallet_refunds(
&self,
wallet_id: &str,
limit: usize,
offset: usize,
) -> Result<(Vec<AdminWalletRefundRecord>, u64), GatewayError> {
#[cfg(test)]
if let Some(store) = self.admin_wallet_refund_store.as_ref() {
let mut items = store
.lock()
.expect("admin wallet refund store should lock")
.values()
.filter(|refund| refund.wallet_id == wallet_id)
.cloned()
.collect::<Vec<_>>();
items.sort_by(|left, right| {
right
.created_at_unix_ms
.cmp(&left.created_at_unix_ms)
.then_with(|| right.id.cmp(&left.id))
});
let total = items.len() as u64;
let items = items
.into_iter()
.skip(offset)
.take(limit)
.collect::<Vec<_>>();
return Ok((items, total));
}
let page = self
.data
.list_admin_wallet_refunds(wallet_id, limit, offset)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
Ok((
page.items
.into_iter()
.map(stored_admin_wallet_refund_to_gateway)
.collect(),
page.total,
))
}
pub(crate) async fn list_admin_wallet_refund_requests(
&self,
status: Option<&str>,
limit: usize,
offset: usize,
) -> Result<(Vec<StoredAdminWalletRefundRequestItem>, u64), 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 wallets = wallet_store
.lock()
.expect("auth wallet store should lock")
.clone();
let mut items = refund_store
.lock()
.expect("admin wallet refund store should lock")
.values()
.filter(|refund| status.is_none_or(|expected| refund.status == expected))
.filter(|refund| {
wallets
.get(&refund.wallet_id)
.is_some_and(|wallet| wallet.user_id.is_some())
})
.cloned()
.collect::<Vec<_>>();
items.sort_by(|left, right| {
right
.created_at_unix_ms
.cmp(&left.created_at_unix_ms)
.then_with(|| right.id.cmp(&left.id))
});
let total = items.len() as u64;
let items = items
.into_iter()
.skip(offset)
.take(limit)
.filter_map(|refund| {
wallets.get(&refund.wallet_id).map(|wallet| {
StoredAdminWalletRefundRequestItem {
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,
wallet_user_id: wallet.user_id.clone(),
wallet_user_name: None,
wallet_api_key_id: wallet.api_key_id.clone(),
api_key_name: None,
wallet_status: wallet.status.clone(),
created_at_unix_ms: Some(refund.created_at_unix_ms),
updated_at_unix_secs: Some(refund.updated_at_unix_secs),
processed_at_unix_secs: refund.processed_at_unix_secs,
completed_at_unix_secs: refund.completed_at_unix_secs,
}
})
})
.collect::<Vec<_>>();
return Ok((items, total));
}
let page = self
.data
.list_admin_wallet_refund_requests(&AdminWalletRefundRequestListQuery {
status: status.map(ToOwned::to_owned),
limit,
offset,
})
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
Ok((page.items, page.total))
}
pub(crate) async fn read_admin_payment_order(
&self,
order_id: &str,
) -> Result<AdminWalletMutationOutcome<AdminWalletPaymentOrderRecord>, GatewayError> {
#[cfg(test)]
if let Some(store) = self.admin_wallet_payment_order_store.as_ref() {
return Ok(store
.lock()
.expect("admin wallet payment order store should lock")
.get(order_id)
.cloned()
.map(AdminWalletMutationOutcome::Applied)
.unwrap_or(AdminWalletMutationOutcome::NotFound));
}
if !self.has_wallet_data_reader() {
return Ok(AdminWalletMutationOutcome::Unavailable);
}
let order = self
.data
.find_admin_payment_order(order_id)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
match order {
Some(record) => Ok(AdminWalletMutationOutcome::Applied(
stored_admin_payment_order_to_gateway(record),
)),
None => Ok(AdminWalletMutationOutcome::NotFound),
}
}
pub(crate) async fn list_admin_redeem_code_batches(
&self,
status: Option<&str>,
limit: usize,
offset: usize,
) -> Result<(Vec<StoredAdminRedeemCodeBatch>, u64), GatewayError> {
let page = self
.data
.list_admin_redeem_code_batches(&AdminRedeemCodeBatchListQuery {
status: status.map(ToOwned::to_owned),
limit,
offset,
})
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
Ok((page.items, page.total))
}
pub(crate) async fn read_admin_redeem_code_batch(
&self,
batch_id: &str,
) -> Result<AdminWalletMutationOutcome<StoredAdminRedeemCodeBatch>, GatewayError> {
if !self.has_wallet_data_reader() {
return Ok(AdminWalletMutationOutcome::Unavailable);
}
match self
.data
.find_admin_redeem_code_batch(batch_id)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?
{
Some(batch) => Ok(AdminWalletMutationOutcome::Applied(batch)),
None => Ok(AdminWalletMutationOutcome::NotFound),
}
}
pub(crate) async fn list_admin_redeem_codes(
&self,
batch_id: &str,
status: Option<&str>,
limit: usize,
offset: usize,
) -> Result<StoredAdminRedeemCodePage, GatewayError> {
self.data
.list_admin_redeem_codes(&AdminRedeemCodeListQuery {
batch_id: batch_id.to_string(),
status: status.map(ToOwned::to_owned),
limit,
offset,
})
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
}
}
fn stored_admin_payment_order_to_gateway(
record: StoredAdminPaymentOrder,
) -> AdminWalletPaymentOrderRecord {
AdminWalletPaymentOrderRecord {
id: record.id,
order_no: record.order_no,
wallet_id: record.wallet_id,
user_id: record.user_id,
amount_usd: record.amount_usd,
pay_amount: record.pay_amount,
pay_currency: record.pay_currency,
exchange_rate: record.exchange_rate,
refunded_amount_usd: record.refunded_amount_usd,
refundable_amount_usd: record.refundable_amount_usd,
payment_method: record.payment_method,
gateway_order_id: record.gateway_order_id,
status: record.status,
gateway_response: record.gateway_response,
created_at_unix_ms: record.created_at_unix_ms,
paid_at_unix_secs: record.paid_at_unix_secs,
credited_at_unix_secs: record.credited_at_unix_secs,
expires_at_unix_secs: record.expires_at_unix_secs,
}
}
fn stored_admin_payment_callback_to_gateway(
record: StoredAdminPaymentCallback,
) -> GatewayAdminPaymentCallbackView {
GatewayAdminPaymentCallbackView {
id: record.id,
payment_order_id: record.payment_order_id,
payment_method: record.payment_method,
callback_key: record.callback_key,
order_no: record.order_no,
gateway_order_id: record.gateway_order_id,
payload_hash: record.payload_hash,
signature_valid: record.signature_valid,
status: record.status,
payload: record.payload,
error_message: record.error_message,
created_at_unix_ms: record.created_at_unix_ms,
processed_at_unix_secs: record.processed_at_unix_secs,
}
}
fn stored_admin_wallet_refund_to_gateway(
record: StoredAdminWalletRefund,
) -> AdminWalletRefundRecord {
AdminWalletRefundRecord {
id: record.id,
refund_no: record.refund_no,
wallet_id: record.wallet_id,
user_id: record.user_id,
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,
requested_by: record.requested_by,
approved_by: record.approved_by,
processed_by: record.processed_by,
created_at_unix_ms: record.created_at_unix_ms,
updated_at_unix_secs: record.updated_at_unix_secs,
processed_at_unix_secs: record.processed_at_unix_secs,
completed_at_unix_secs: record.completed_at_unix_secs,
}
}