2026-03-31 19:19:04 +08:00
|
|
|
use std::collections::BTreeMap;
|
|
|
|
|
use std::sync::RwLock;
|
|
|
|
|
|
|
|
|
|
use async_trait::async_trait;
|
|
|
|
|
|
|
|
|
|
use super::types::{
|
2026-04-05 20:23:16 +08:00
|
|
|
AdjustWalletBalanceInput, AdminPaymentOrderListQuery, AdminWalletLedgerQuery,
|
|
|
|
|
AdminWalletListQuery, AdminWalletRefundRequestListQuery, CompleteAdminWalletRefundInput,
|
|
|
|
|
CreateManualWalletRechargeInput, CreateWalletRechargeOrderInput,
|
|
|
|
|
CreateWalletRechargeOrderOutcome, CreateWalletRefundRequestInput,
|
|
|
|
|
CreateWalletRefundRequestOutcome, CreditAdminPaymentOrderInput, FailAdminWalletRefundInput,
|
|
|
|
|
ProcessAdminWalletRefundInput, ProcessPaymentCallbackInput, ProcessPaymentCallbackOutcome,
|
|
|
|
|
StoredAdminPaymentCallbackPage, StoredAdminPaymentOrder, StoredAdminPaymentOrderPage,
|
|
|
|
|
StoredAdminWalletLedgerPage, StoredAdminWalletListItem, StoredAdminWalletListPage,
|
|
|
|
|
StoredAdminWalletRefundPage, StoredAdminWalletRefundRequestPage,
|
|
|
|
|
StoredAdminWalletTransactionPage, StoredWalletDailyUsageLedger,
|
|
|
|
|
StoredWalletDailyUsageLedgerPage, StoredWalletSnapshot, WalletLookupKey, WalletMutationOutcome,
|
2026-03-31 19:19:04 +08:00
|
|
|
WalletReadRepository, WalletWriteRepository,
|
|
|
|
|
};
|
|
|
|
|
use crate::DataLayerError;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Default)]
|
|
|
|
|
pub struct InMemoryWalletRepository {
|
|
|
|
|
wallets_by_id: RwLock<BTreeMap<String, StoredWalletSnapshot>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl InMemoryWalletRepository {
|
|
|
|
|
pub fn seed<I>(items: I) -> Self
|
|
|
|
|
where
|
|
|
|
|
I: IntoIterator<Item = StoredWalletSnapshot>,
|
|
|
|
|
{
|
|
|
|
|
let mut wallets_by_id = BTreeMap::new();
|
|
|
|
|
for item in items {
|
|
|
|
|
wallets_by_id.insert(item.id.clone(), item);
|
|
|
|
|
}
|
|
|
|
|
Self {
|
|
|
|
|
wallets_by_id: RwLock::new(wallets_by_id),
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-05 20:23:16 +08:00
|
|
|
|
|
|
|
|
pub(crate) fn with_wallets_mut<R>(
|
|
|
|
|
&self,
|
|
|
|
|
f: impl FnOnce(&mut BTreeMap<String, StoredWalletSnapshot>) -> R,
|
|
|
|
|
) -> R {
|
|
|
|
|
let mut wallets = self.wallets_by_id.write().expect("wallet repo lock");
|
|
|
|
|
f(&mut wallets)
|
|
|
|
|
}
|
2026-03-31 19:19:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[async_trait]
|
|
|
|
|
impl WalletReadRepository for InMemoryWalletRepository {
|
|
|
|
|
async fn find(
|
|
|
|
|
&self,
|
|
|
|
|
key: WalletLookupKey<'_>,
|
|
|
|
|
) -> Result<Option<StoredWalletSnapshot>, DataLayerError> {
|
|
|
|
|
let wallets = self.wallets_by_id.read().expect("wallet repo lock");
|
|
|
|
|
Ok(match key {
|
|
|
|
|
WalletLookupKey::WalletId(wallet_id) => wallets.get(wallet_id).cloned(),
|
|
|
|
|
WalletLookupKey::UserId(user_id) => wallets
|
|
|
|
|
.values()
|
|
|
|
|
.find(|wallet| wallet.user_id.as_deref() == Some(user_id))
|
|
|
|
|
.cloned(),
|
|
|
|
|
WalletLookupKey::ApiKeyId(api_key_id) => wallets
|
|
|
|
|
.values()
|
|
|
|
|
.find(|wallet| wallet.api_key_id.as_deref() == Some(api_key_id))
|
|
|
|
|
.cloned(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn list_wallets_by_user_ids(
|
|
|
|
|
&self,
|
|
|
|
|
user_ids: &[String],
|
|
|
|
|
) -> Result<Vec<StoredWalletSnapshot>, DataLayerError> {
|
|
|
|
|
if user_ids.is_empty() {
|
|
|
|
|
return Ok(Vec::new());
|
|
|
|
|
}
|
|
|
|
|
let user_set: std::collections::BTreeSet<&str> =
|
|
|
|
|
user_ids.iter().map(String::as_str).collect();
|
|
|
|
|
let wallets = self.wallets_by_id.read().expect("wallet repo lock");
|
|
|
|
|
Ok(wallets
|
|
|
|
|
.values()
|
|
|
|
|
.filter(|wallet| {
|
|
|
|
|
wallet
|
|
|
|
|
.user_id
|
|
|
|
|
.as_deref()
|
|
|
|
|
.map(|user_id| user_set.contains(user_id))
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
})
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn list_wallets_by_api_key_ids(
|
|
|
|
|
&self,
|
|
|
|
|
api_key_ids: &[String],
|
|
|
|
|
) -> Result<Vec<StoredWalletSnapshot>, DataLayerError> {
|
|
|
|
|
if api_key_ids.is_empty() {
|
|
|
|
|
return Ok(Vec::new());
|
|
|
|
|
}
|
|
|
|
|
let key_set: std::collections::BTreeSet<&str> =
|
|
|
|
|
api_key_ids.iter().map(String::as_str).collect();
|
|
|
|
|
let wallets = self.wallets_by_id.read().expect("wallet repo lock");
|
|
|
|
|
Ok(wallets
|
|
|
|
|
.values()
|
|
|
|
|
.filter(|wallet| {
|
|
|
|
|
wallet
|
|
|
|
|
.api_key_id
|
|
|
|
|
.as_deref()
|
|
|
|
|
.map(|api_key_id| key_set.contains(api_key_id))
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
})
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect())
|
|
|
|
|
}
|
2026-04-05 20:23:16 +08:00
|
|
|
|
|
|
|
|
async fn list_admin_wallets(
|
|
|
|
|
&self,
|
|
|
|
|
query: &AdminWalletListQuery,
|
|
|
|
|
) -> Result<StoredAdminWalletListPage, DataLayerError> {
|
|
|
|
|
let wallets = self.wallets_by_id.read().expect("wallet repo lock");
|
|
|
|
|
let mut items = wallets
|
|
|
|
|
.values()
|
|
|
|
|
.filter(|wallet| {
|
|
|
|
|
query
|
|
|
|
|
.status
|
|
|
|
|
.as_deref()
|
|
|
|
|
.is_none_or(|expected| wallet.status == expected)
|
|
|
|
|
})
|
|
|
|
|
.filter(|wallet| match query.owner_type.as_deref() {
|
|
|
|
|
Some("user") => wallet.user_id.is_some(),
|
|
|
|
|
Some("api_key") => wallet.api_key_id.is_some(),
|
|
|
|
|
_ => true,
|
|
|
|
|
})
|
|
|
|
|
.map(|wallet| StoredAdminWalletListItem {
|
|
|
|
|
id: wallet.id.clone(),
|
|
|
|
|
user_id: wallet.user_id.clone(),
|
|
|
|
|
api_key_id: wallet.api_key_id.clone(),
|
|
|
|
|
balance: wallet.balance,
|
|
|
|
|
gift_balance: wallet.gift_balance,
|
|
|
|
|
limit_mode: wallet.limit_mode.clone(),
|
|
|
|
|
currency: wallet.currency.clone(),
|
|
|
|
|
status: wallet.status.clone(),
|
|
|
|
|
total_recharged: wallet.total_recharged,
|
|
|
|
|
total_consumed: wallet.total_consumed,
|
|
|
|
|
total_refunded: wallet.total_refunded,
|
|
|
|
|
total_adjusted: wallet.total_adjusted,
|
|
|
|
|
user_name: None,
|
|
|
|
|
api_key_name: None,
|
2026-04-10 01:46:14 +08:00
|
|
|
created_at_unix_ms: None,
|
2026-04-05 20:23:16 +08:00
|
|
|
updated_at_unix_secs: Some(wallet.updated_at_unix_secs),
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
items.sort_by(|left, right| {
|
|
|
|
|
right
|
|
|
|
|
.updated_at_unix_secs
|
|
|
|
|
.cmp(&left.updated_at_unix_secs)
|
|
|
|
|
.then_with(|| right.id.cmp(&left.id))
|
|
|
|
|
});
|
|
|
|
|
let total = items.len() as u64;
|
|
|
|
|
let items = items
|
|
|
|
|
.into_iter()
|
|
|
|
|
.skip(query.offset)
|
|
|
|
|
.take(query.limit)
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
Ok(StoredAdminWalletListPage { items, total })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn list_admin_wallet_ledger(
|
|
|
|
|
&self,
|
|
|
|
|
_query: &AdminWalletLedgerQuery,
|
|
|
|
|
) -> Result<StoredAdminWalletLedgerPage, DataLayerError> {
|
|
|
|
|
Ok(StoredAdminWalletLedgerPage::default())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn list_admin_wallet_refund_requests(
|
|
|
|
|
&self,
|
|
|
|
|
_query: &AdminWalletRefundRequestListQuery,
|
|
|
|
|
) -> Result<StoredAdminWalletRefundRequestPage, DataLayerError> {
|
|
|
|
|
Ok(StoredAdminWalletRefundRequestPage::default())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn list_admin_wallet_transactions(
|
|
|
|
|
&self,
|
|
|
|
|
_wallet_id: &str,
|
|
|
|
|
_limit: usize,
|
|
|
|
|
_offset: usize,
|
|
|
|
|
) -> Result<StoredAdminWalletTransactionPage, DataLayerError> {
|
|
|
|
|
Ok(StoredAdminWalletTransactionPage::default())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn find_wallet_today_usage(
|
|
|
|
|
&self,
|
|
|
|
|
_wallet_id: &str,
|
|
|
|
|
_billing_timezone: &str,
|
|
|
|
|
) -> Result<Option<StoredWalletDailyUsageLedger>, DataLayerError> {
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn list_wallet_daily_usage_history(
|
|
|
|
|
&self,
|
|
|
|
|
_wallet_id: &str,
|
|
|
|
|
_billing_timezone: &str,
|
|
|
|
|
_limit: usize,
|
|
|
|
|
) -> Result<StoredWalletDailyUsageLedgerPage, DataLayerError> {
|
|
|
|
|
Ok(StoredWalletDailyUsageLedgerPage::default())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn list_admin_wallet_refunds(
|
|
|
|
|
&self,
|
|
|
|
|
_wallet_id: &str,
|
|
|
|
|
_limit: usize,
|
|
|
|
|
_offset: usize,
|
|
|
|
|
) -> Result<StoredAdminWalletRefundPage, DataLayerError> {
|
|
|
|
|
Ok(StoredAdminWalletRefundPage::default())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn list_admin_payment_orders(
|
|
|
|
|
&self,
|
|
|
|
|
_query: &AdminPaymentOrderListQuery,
|
|
|
|
|
) -> Result<StoredAdminPaymentOrderPage, DataLayerError> {
|
|
|
|
|
Ok(StoredAdminPaymentOrderPage::default())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn find_admin_payment_order(
|
|
|
|
|
&self,
|
|
|
|
|
_order_id: &str,
|
|
|
|
|
) -> Result<Option<StoredAdminPaymentOrder>, DataLayerError> {
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn list_wallet_payment_orders_by_user_id(
|
|
|
|
|
&self,
|
|
|
|
|
_user_id: &str,
|
|
|
|
|
_limit: usize,
|
|
|
|
|
_offset: usize,
|
|
|
|
|
) -> Result<StoredAdminPaymentOrderPage, DataLayerError> {
|
|
|
|
|
Ok(StoredAdminPaymentOrderPage::default())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn find_wallet_payment_order_by_user_id(
|
|
|
|
|
&self,
|
|
|
|
|
_user_id: &str,
|
|
|
|
|
_order_id: &str,
|
|
|
|
|
) -> Result<Option<StoredAdminPaymentOrder>, DataLayerError> {
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn find_wallet_refund(
|
|
|
|
|
&self,
|
|
|
|
|
_wallet_id: &str,
|
|
|
|
|
_refund_id: &str,
|
|
|
|
|
) -> Result<Option<super::types::StoredAdminWalletRefund>, DataLayerError> {
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn list_admin_payment_callbacks(
|
|
|
|
|
&self,
|
|
|
|
|
_payment_method: Option<&str>,
|
|
|
|
|
_limit: usize,
|
|
|
|
|
_offset: usize,
|
|
|
|
|
) -> Result<StoredAdminPaymentCallbackPage, DataLayerError> {
|
|
|
|
|
Ok(StoredAdminPaymentCallbackPage::default())
|
|
|
|
|
}
|
2026-03-31 19:19:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[async_trait]
|
|
|
|
|
impl WalletWriteRepository for InMemoryWalletRepository {
|
2026-04-05 20:23:16 +08:00
|
|
|
async fn create_wallet_recharge_order(
|
2026-03-31 19:19:04 +08:00
|
|
|
&self,
|
2026-04-05 20:23:16 +08:00
|
|
|
input: CreateWalletRechargeOrderInput,
|
|
|
|
|
) -> Result<CreateWalletRechargeOrderOutcome, DataLayerError> {
|
2026-03-31 19:19:04 +08:00
|
|
|
let mut wallets = self.wallets_by_id.write().expect("wallet repo lock");
|
2026-04-05 20:23:16 +08:00
|
|
|
let wallet = wallets
|
|
|
|
|
.values_mut()
|
|
|
|
|
.find(|wallet| wallet.user_id.as_deref() == Some(input.user_id.as_str()));
|
|
|
|
|
if wallet
|
|
|
|
|
.as_ref()
|
|
|
|
|
.is_some_and(|wallet| wallet.status != "active")
|
|
|
|
|
{
|
|
|
|
|
return Ok(CreateWalletRechargeOrderOutcome::WalletInactive);
|
2026-03-31 19:19:04 +08:00
|
|
|
}
|
2026-04-05 20:23:16 +08:00
|
|
|
let wallet_id = wallet
|
|
|
|
|
.map(|wallet| wallet.id.clone())
|
|
|
|
|
.or(input.preferred_wallet_id)
|
|
|
|
|
.unwrap_or_else(|| "wallet-memory".to_string());
|
|
|
|
|
Ok(CreateWalletRechargeOrderOutcome::Created(
|
|
|
|
|
StoredAdminPaymentOrder {
|
|
|
|
|
id: "payment-order-memory".to_string(),
|
|
|
|
|
order_no: input.order_no,
|
|
|
|
|
wallet_id,
|
|
|
|
|
user_id: Some(input.user_id),
|
|
|
|
|
amount_usd: input.amount_usd,
|
|
|
|
|
pay_amount: input.pay_amount,
|
|
|
|
|
pay_currency: input.pay_currency,
|
|
|
|
|
exchange_rate: input.exchange_rate,
|
|
|
|
|
refunded_amount_usd: 0.0,
|
|
|
|
|
refundable_amount_usd: 0.0,
|
|
|
|
|
payment_method: input.payment_method,
|
|
|
|
|
gateway_order_id: Some(input.gateway_order_id),
|
|
|
|
|
gateway_response: Some(input.gateway_response),
|
|
|
|
|
status: "pending".to_string(),
|
2026-04-10 01:46:14 +08:00
|
|
|
created_at_unix_ms: 0,
|
2026-04-05 20:23:16 +08:00
|
|
|
paid_at_unix_secs: None,
|
|
|
|
|
credited_at_unix_secs: None,
|
|
|
|
|
expires_at_unix_secs: Some(input.expires_at_unix_secs),
|
|
|
|
|
},
|
|
|
|
|
))
|
|
|
|
|
}
|
2026-03-31 19:19:04 +08:00
|
|
|
|
2026-04-05 20:23:16 +08:00
|
|
|
async fn create_wallet_refund_request(
|
|
|
|
|
&self,
|
|
|
|
|
input: CreateWalletRefundRequestInput,
|
|
|
|
|
) -> Result<CreateWalletRefundRequestOutcome, DataLayerError> {
|
|
|
|
|
let wallets = self.wallets_by_id.read().expect("wallet repo lock");
|
|
|
|
|
let Some(wallet) = wallets.get(&input.wallet_id) else {
|
|
|
|
|
return Ok(CreateWalletRefundRequestOutcome::WalletMissing);
|
|
|
|
|
};
|
|
|
|
|
if input.amount_usd > wallet.balance {
|
|
|
|
|
return Ok(CreateWalletRefundRequestOutcome::RefundAmountExceedsAvailableBalance);
|
2026-03-31 19:19:04 +08:00
|
|
|
}
|
2026-04-05 20:23:16 +08:00
|
|
|
Ok(CreateWalletRefundRequestOutcome::Created(
|
|
|
|
|
super::types::StoredAdminWalletRefund {
|
|
|
|
|
id: "refund-memory".to_string(),
|
|
|
|
|
refund_no: input.refund_no,
|
|
|
|
|
wallet_id: input.wallet_id,
|
|
|
|
|
user_id: Some(input.user_id),
|
|
|
|
|
payment_order_id: input.payment_order_id,
|
|
|
|
|
source_type: input
|
|
|
|
|
.source_type
|
|
|
|
|
.unwrap_or_else(|| "wallet_balance".to_string()),
|
|
|
|
|
source_id: input.source_id,
|
|
|
|
|
refund_mode: input
|
|
|
|
|
.refund_mode
|
|
|
|
|
.unwrap_or_else(|| "offline_payout".to_string()),
|
|
|
|
|
amount_usd: input.amount_usd,
|
|
|
|
|
status: "pending_approval".to_string(),
|
|
|
|
|
reason: input.reason,
|
|
|
|
|
failure_reason: None,
|
|
|
|
|
gateway_refund_id: None,
|
|
|
|
|
payout_method: None,
|
|
|
|
|
payout_reference: None,
|
|
|
|
|
payout_proof: None,
|
|
|
|
|
requested_by: None,
|
|
|
|
|
approved_by: None,
|
|
|
|
|
processed_by: None,
|
2026-04-10 01:46:14 +08:00
|
|
|
created_at_unix_ms: 0,
|
2026-04-05 20:23:16 +08:00
|
|
|
updated_at_unix_secs: 0,
|
|
|
|
|
processed_at_unix_secs: None,
|
|
|
|
|
completed_at_unix_secs: None,
|
|
|
|
|
},
|
|
|
|
|
))
|
|
|
|
|
}
|
2026-03-31 19:19:04 +08:00
|
|
|
|
2026-04-05 20:23:16 +08:00
|
|
|
async fn process_payment_callback(
|
|
|
|
|
&self,
|
|
|
|
|
_input: ProcessPaymentCallbackInput,
|
|
|
|
|
) -> Result<ProcessPaymentCallbackOutcome, DataLayerError> {
|
|
|
|
|
Ok(ProcessPaymentCallbackOutcome::Failed {
|
|
|
|
|
duplicate: false,
|
|
|
|
|
error: "payment callback is not supported in memory wallet repository".to_string(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn adjust_wallet_balance(
|
|
|
|
|
&self,
|
|
|
|
|
_input: AdjustWalletBalanceInput,
|
|
|
|
|
) -> Result<
|
|
|
|
|
Option<(
|
|
|
|
|
StoredWalletSnapshot,
|
|
|
|
|
super::types::StoredAdminWalletTransaction,
|
|
|
|
|
)>,
|
|
|
|
|
DataLayerError,
|
|
|
|
|
> {
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn create_manual_wallet_recharge(
|
|
|
|
|
&self,
|
|
|
|
|
_input: CreateManualWalletRechargeInput,
|
|
|
|
|
) -> Result<Option<(StoredWalletSnapshot, StoredAdminPaymentOrder)>, DataLayerError> {
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn process_admin_wallet_refund(
|
|
|
|
|
&self,
|
|
|
|
|
_input: ProcessAdminWalletRefundInput,
|
|
|
|
|
) -> Result<
|
|
|
|
|
WalletMutationOutcome<(
|
|
|
|
|
StoredWalletSnapshot,
|
|
|
|
|
super::types::StoredAdminWalletRefund,
|
|
|
|
|
super::types::StoredAdminWalletTransaction,
|
|
|
|
|
)>,
|
|
|
|
|
DataLayerError,
|
|
|
|
|
> {
|
|
|
|
|
Ok(WalletMutationOutcome::NotFound)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn complete_admin_wallet_refund(
|
|
|
|
|
&self,
|
|
|
|
|
_input: CompleteAdminWalletRefundInput,
|
|
|
|
|
) -> Result<WalletMutationOutcome<super::types::StoredAdminWalletRefund>, DataLayerError> {
|
|
|
|
|
Ok(WalletMutationOutcome::NotFound)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn fail_admin_wallet_refund(
|
|
|
|
|
&self,
|
|
|
|
|
_input: FailAdminWalletRefundInput,
|
|
|
|
|
) -> Result<
|
|
|
|
|
WalletMutationOutcome<(
|
|
|
|
|
StoredWalletSnapshot,
|
|
|
|
|
super::types::StoredAdminWalletRefund,
|
|
|
|
|
Option<super::types::StoredAdminWalletTransaction>,
|
|
|
|
|
)>,
|
|
|
|
|
DataLayerError,
|
|
|
|
|
> {
|
|
|
|
|
Ok(WalletMutationOutcome::NotFound)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn expire_admin_payment_order(
|
|
|
|
|
&self,
|
|
|
|
|
_order_id: &str,
|
|
|
|
|
) -> Result<WalletMutationOutcome<(StoredAdminPaymentOrder, bool)>, DataLayerError> {
|
|
|
|
|
Ok(WalletMutationOutcome::NotFound)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn fail_admin_payment_order(
|
|
|
|
|
&self,
|
|
|
|
|
_order_id: &str,
|
|
|
|
|
) -> Result<WalletMutationOutcome<StoredAdminPaymentOrder>, DataLayerError> {
|
|
|
|
|
Ok(WalletMutationOutcome::NotFound)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn credit_admin_payment_order(
|
|
|
|
|
&self,
|
|
|
|
|
_input: CreditAdminPaymentOrderInput,
|
|
|
|
|
) -> Result<WalletMutationOutcome<(StoredAdminPaymentOrder, bool)>, DataLayerError> {
|
|
|
|
|
Ok(WalletMutationOutcome::NotFound)
|
2026-03-31 19:19:04 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::InMemoryWalletRepository;
|
|
|
|
|
use crate::repository::wallet::{
|
2026-04-05 20:23:16 +08:00
|
|
|
AdminWalletListQuery, StoredWalletSnapshot, WalletLookupKey, WalletReadRepository,
|
2026-03-31 19:19:04 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fn sample_wallet() -> StoredWalletSnapshot {
|
|
|
|
|
StoredWalletSnapshot::new(
|
|
|
|
|
"wallet-1".to_string(),
|
|
|
|
|
Some("user-1".to_string()),
|
|
|
|
|
Some("key-1".to_string()),
|
|
|
|
|
10.0,
|
|
|
|
|
2.0,
|
|
|
|
|
"finite".to_string(),
|
|
|
|
|
"USD".to_string(),
|
|
|
|
|
"active".to_string(),
|
|
|
|
|
0.0,
|
|
|
|
|
0.0,
|
|
|
|
|
0.0,
|
|
|
|
|
0.0,
|
|
|
|
|
100,
|
|
|
|
|
)
|
|
|
|
|
.expect("wallet should build")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn finds_wallet_by_owner() {
|
|
|
|
|
let repository = InMemoryWalletRepository::seed(vec![sample_wallet()]);
|
|
|
|
|
let wallet = repository
|
|
|
|
|
.find(WalletLookupKey::UserId("user-1"))
|
|
|
|
|
.await
|
|
|
|
|
.expect("lookup should succeed")
|
|
|
|
|
.expect("wallet should exist");
|
|
|
|
|
assert_eq!(wallet.id, "wallet-1");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
2026-04-05 20:23:16 +08:00
|
|
|
async fn lists_admin_wallets_with_filters_and_pagination() {
|
|
|
|
|
let repository = InMemoryWalletRepository::seed(vec![
|
|
|
|
|
sample_wallet(),
|
|
|
|
|
StoredWalletSnapshot::new(
|
|
|
|
|
"wallet-2".to_string(),
|
|
|
|
|
Some("user-2".to_string()),
|
|
|
|
|
None,
|
|
|
|
|
3.0,
|
|
|
|
|
1.0,
|
|
|
|
|
"finite".to_string(),
|
|
|
|
|
"USD".to_string(),
|
|
|
|
|
"inactive".to_string(),
|
|
|
|
|
0.0,
|
|
|
|
|
0.0,
|
|
|
|
|
0.0,
|
|
|
|
|
0.0,
|
|
|
|
|
90,
|
|
|
|
|
)
|
|
|
|
|
.expect("wallet should build"),
|
|
|
|
|
StoredWalletSnapshot::new(
|
|
|
|
|
"wallet-3".to_string(),
|
|
|
|
|
None,
|
|
|
|
|
Some("key-3".to_string()),
|
|
|
|
|
5.0,
|
|
|
|
|
0.0,
|
|
|
|
|
"unlimited".to_string(),
|
|
|
|
|
"USD".to_string(),
|
|
|
|
|
"active".to_string(),
|
|
|
|
|
0.0,
|
|
|
|
|
0.0,
|
|
|
|
|
0.0,
|
|
|
|
|
0.0,
|
|
|
|
|
110,
|
|
|
|
|
)
|
|
|
|
|
.expect("wallet should build"),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
let page = repository
|
|
|
|
|
.list_admin_wallets(&AdminWalletListQuery {
|
|
|
|
|
status: Some("active".to_string()),
|
|
|
|
|
owner_type: Some("api_key".to_string()),
|
|
|
|
|
limit: 1,
|
|
|
|
|
offset: 0,
|
2026-03-31 19:19:04 +08:00
|
|
|
})
|
|
|
|
|
.await
|
2026-04-05 20:23:16 +08:00
|
|
|
.expect("list should succeed");
|
2026-03-31 19:19:04 +08:00
|
|
|
|
2026-04-05 20:23:16 +08:00
|
|
|
assert_eq!(page.total, 2);
|
|
|
|
|
assert_eq!(page.items.len(), 1);
|
|
|
|
|
assert_eq!(page.items[0].id, "wallet-3");
|
|
|
|
|
assert_eq!(page.items[0].updated_at_unix_secs, Some(110));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn daily_usage_queries_default_to_empty_in_memory() {
|
|
|
|
|
let repository = InMemoryWalletRepository::seed(vec![sample_wallet()]);
|
|
|
|
|
let today = repository
|
|
|
|
|
.find_wallet_today_usage("wallet-1", "Asia/Shanghai")
|
|
|
|
|
.await
|
|
|
|
|
.expect("lookup should succeed");
|
|
|
|
|
let history = repository
|
|
|
|
|
.list_wallet_daily_usage_history("wallet-1", "Asia/Shanghai", 20)
|
|
|
|
|
.await
|
|
|
|
|
.expect("history should succeed");
|
|
|
|
|
|
|
|
|
|
assert!(today.is_none());
|
|
|
|
|
assert_eq!(history.total, 0);
|
|
|
|
|
assert!(history.items.is_empty());
|
2026-03-31 19:19:04 +08:00
|
|
|
}
|
|
|
|
|
}
|