fix: apply actual billing cost to wallet settlement

This commit is contained in:
Entropy.Xu
2026-07-01 01:12:40 +08:00
parent 778b106023
commit 6ed2f9bd0a
5 changed files with 56 additions and 47 deletions
@@ -4,8 +4,9 @@ use std::sync::{Arc, RwLock};
use async_trait::async_trait;
use super::{
plan_finite_wallet_debit, settlement_billing_status_for_usage_status,
SettlementWriteRepository, StoredUsageSettlement, UsageSettlementInput, SETTLEMENT_EPSILON_USD,
plan_finite_wallet_debit, settlement_billable_cost_usd,
settlement_billing_status_for_usage_status, SettlementWriteRepository, StoredUsageSettlement,
UsageSettlementInput, SETTLEMENT_EPSILON_USD,
};
use crate::repository::wallet::{InMemoryWalletRepository, StoredWalletSnapshot};
use crate::DataLayerError;
@@ -104,6 +105,7 @@ impl SettlementWriteRepository for InMemorySettlementRepository {
let mut final_billing_status =
settlement_billing_status_for_usage_status(&input.status).to_string();
let billable_cost_usd = settlement_billable_cost_usd(&input);
let mut settlement = self.wallets.with_mut(|wallets| {
let wallet_id = input
.api_key_id
@@ -154,16 +156,16 @@ impl SettlementWriteRepository for InMemorySettlementRepository {
if final_billing_status == "settled" {
if wallet.limit_mode.eq_ignore_ascii_case("unlimited") {
wallet.total_consumed += input.total_cost_usd;
wallet.total_consumed += billable_cost_usd;
} else {
let debit_plan = plan_finite_wallet_debit(
before_recharge,
before_gift,
input.total_cost_usd,
billable_cost_usd,
);
(wallet.balance, wallet.gift_balance) =
debit_plan.after_balances(before_recharge, before_gift);
wallet.total_consumed += input.total_cost_usd;
wallet.total_consumed += billable_cost_usd;
}
}
@@ -171,7 +173,7 @@ impl SettlementWriteRepository for InMemorySettlementRepository {
settlement.wallet_gift_balance_after = Some(wallet.gift_balance);
settlement.wallet_balance_after = Some(wallet.balance + wallet.gift_balance);
} else if final_billing_status == "settled"
&& input.total_cost_usd > SETTLEMENT_EPSILON_USD
&& billable_cost_usd > SETTLEMENT_EPSILON_USD
{
final_billing_status = "insufficient_quota".to_string();
settlement.billing_status = final_billing_status.clone();
@@ -258,7 +260,7 @@ mod tests {
status: "completed".to_string(),
billing_status: "pending".to_string(),
total_cost_usd: 3.0,
actual_total_cost_usd: 1.5,
actual_total_cost_usd: 6.0,
finalized_at_unix_secs: Some(200),
})
.await
@@ -267,8 +269,8 @@ mod tests {
assert_eq!(settlement.billing_status, "settled");
assert_eq!(settlement.wallet_balance_before, Some(12.0));
assert_eq!(settlement.wallet_balance_after, Some(9.0));
assert_eq!(settlement.provider_monthly_used_usd, Some(1.5));
assert_eq!(settlement.wallet_balance_after, Some(6.0));
assert_eq!(settlement.provider_monthly_used_usd, Some(6.0));
}
#[tokio::test]
@@ -285,7 +287,7 @@ mod tests {
status: "completed".to_string(),
billing_status: "pending".to_string(),
total_cost_usd: 3.0,
actual_total_cost_usd: 1.5,
actual_total_cost_usd: 6.0,
finalized_at_unix_secs: Some(200),
})
.await
@@ -294,7 +296,7 @@ mod tests {
assert_eq!(settlement.wallet_id.as_deref(), Some("wallet-user-1"));
assert_eq!(settlement.wallet_balance_before, Some(12.0));
assert_eq!(settlement.wallet_balance_after, Some(9.0));
assert_eq!(settlement.wallet_balance_after, Some(6.0));
}
#[tokio::test]
@@ -310,7 +312,7 @@ mod tests {
status: "cancelled".to_string(),
billing_status: "pending".to_string(),
total_cost_usd: 3.0,
actual_total_cost_usd: 1.5,
actual_total_cost_usd: 6.0,
finalized_at_unix_secs: Some(200),
})
.await
@@ -319,8 +321,8 @@ mod tests {
assert_eq!(settlement.billing_status, "settled");
assert_eq!(settlement.wallet_balance_before, Some(12.0));
assert_eq!(settlement.wallet_balance_after, Some(9.0));
assert_eq!(settlement.provider_monthly_used_usd, Some(1.5));
assert_eq!(settlement.wallet_balance_after, Some(6.0));
assert_eq!(settlement.provider_monthly_used_usd, Some(6.0));
}
#[tokio::test]
@@ -364,8 +366,8 @@ mod tests {
provider_id: Some("provider-1".to_string()),
status: "completed".to_string(),
billing_status: "pending".to_string(),
total_cost_usd: 15.0,
actual_total_cost_usd: 7.5,
total_cost_usd: 3.0,
actual_total_cost_usd: 15.0,
finalized_at_unix_secs: Some(200),
})
.await
@@ -377,7 +379,7 @@ mod tests {
assert_eq!(settlement.wallet_balance_after, Some(-3.0));
assert_eq!(settlement.wallet_recharge_balance_after, Some(-3.0));
assert_eq!(settlement.wallet_gift_balance_after, Some(0.0));
assert_eq!(settlement.provider_monthly_used_usd, Some(7.5));
assert_eq!(settlement.provider_monthly_used_usd, Some(15.0));
}
#[tokio::test]
@@ -49,6 +49,10 @@ fn settlement_billing_status_for_usage_status(status: &str) -> &'static str {
}
}
fn settlement_billable_cost_usd(input: &UsageSettlementInput) -> f64 {
input.actual_total_cost_usd.max(0.0)
}
#[allow(unused_imports)]
pub(crate) use aether_data_contracts::repository::settlement::{
SettlementRepository, SettlementWriteRepository, StoredUsageSettlement, UsageSettlementInput,
@@ -2,7 +2,7 @@ use async_trait::async_trait;
use sqlx::{mysql::MySqlRow, Row};
use super::{
finite_wallet_available_usd, plan_finite_wallet_debit,
finite_wallet_available_usd, plan_finite_wallet_debit, settlement_billable_cost_usd,
settlement_billing_status_for_usage_status, SettlementWriteRepository, StoredUsageSettlement,
UsageSettlementInput, SETTLEMENT_EPSILON_USD,
};
@@ -479,13 +479,14 @@ FOR UPDATE
settlement.wallet_gift_balance_after = Some(before_gift);
}
let billable_cost_usd = settlement_billable_cost_usd(&input);
let wallet_debit_cost_usd = if !api_key_is_standalone {
if let Some(user_id) = input.user_id.as_deref().filter(|value| !value.is_empty()) {
let quota = consume_daily_quota_mysql(
&mut tx,
user_id,
&input.request_id,
input.total_cost_usd,
billable_cost_usd,
wallet_available_usd,
wallet_can_overdraft,
updated_at,
@@ -496,13 +497,13 @@ FOR UPDATE
settlement.billing_status = final_billing_status.clone();
0.0
} else {
(input.total_cost_usd - quota.debited_usd).max(0.0)
(billable_cost_usd - quota.debited_usd).max(0.0)
}
} else {
input.total_cost_usd
billable_cost_usd
}
} else {
input.total_cost_usd
billable_cost_usd
};
if final_billing_status != "settled" {
sqlx::query(UPSERT_USAGE_SETTLEMENT_SNAPSHOT_SQL)
@@ -2,7 +2,7 @@ use async_trait::async_trait;
use sqlx::{PgPool, Row};
use super::{
finite_wallet_available_usd, plan_finite_wallet_debit,
finite_wallet_available_usd, plan_finite_wallet_debit, settlement_billable_cost_usd,
settlement_billing_status_for_usage_status, SettlementWriteRepository, StoredUsageSettlement,
UsageSettlementInput, SETTLEMENT_EPSILON_USD,
};
@@ -593,6 +593,7 @@ LIMIT 1
settlement.wallet_gift_balance_after = Some(before_gift);
}
let billable_cost_usd = settlement_billable_cost_usd(&input);
let wallet_debit_cost_usd = if !api_key_is_standalone {
if let Some(user_id) =
input.user_id.as_deref().filter(|value| !value.is_empty())
@@ -601,7 +602,7 @@ LIMIT 1
tx,
user_id,
&input.request_id,
input.total_cost_usd,
billable_cost_usd,
wallet_available_usd,
wallet_can_overdraft,
)
@@ -611,13 +612,13 @@ LIMIT 1
settlement.billing_status = final_billing_status.clone();
0.0
} else {
(input.total_cost_usd - quota.debited_usd).max(0.0)
(billable_cost_usd - quota.debited_usd).max(0.0)
}
} else {
input.total_cost_usd
billable_cost_usd
}
} else {
input.total_cost_usd
billable_cost_usd
};
if final_billing_status != "settled" {
sync_usage_settlement_snapshot(&mut **tx, &settlement).await?;
@@ -2,7 +2,7 @@ use async_trait::async_trait;
use sqlx::{sqlite::SqliteRow, Row};
use super::{
finite_wallet_available_usd, plan_finite_wallet_debit,
finite_wallet_available_usd, plan_finite_wallet_debit, settlement_billable_cost_usd,
settlement_billing_status_for_usage_status, SettlementWriteRepository, StoredUsageSettlement,
UsageSettlementInput, SETTLEMENT_EPSILON_USD,
};
@@ -490,13 +490,14 @@ LIMIT 1
settlement.wallet_gift_balance_after = Some(before_gift);
}
let billable_cost_usd = settlement_billable_cost_usd(&input);
let wallet_debit_cost_usd = if !api_key_is_standalone {
if let Some(user_id) = input.user_id.as_deref().filter(|value| !value.is_empty()) {
let quota = consume_daily_quota_sqlite(
&mut tx,
user_id,
&input.request_id,
input.total_cost_usd,
billable_cost_usd,
wallet_available_usd,
wallet_can_overdraft,
updated_at,
@@ -507,13 +508,13 @@ LIMIT 1
settlement.billing_status = final_billing_status.clone();
0.0
} else {
(input.total_cost_usd - quota.debited_usd).max(0.0)
(billable_cost_usd - quota.debited_usd).max(0.0)
}
} else {
input.total_cost_usd
billable_cost_usd
}
} else {
input.total_cost_usd
billable_cost_usd
};
if final_billing_status != "settled" {
sqlx::query(UPSERT_USAGE_SETTLEMENT_SNAPSHOT_SQL)
@@ -720,7 +721,7 @@ mod tests {
status: "completed".to_string(),
billing_status: "pending".to_string(),
total_cost_usd: 3.0,
actual_total_cost_usd: 2.0,
actual_total_cost_usd: 6.0,
finalized_at_unix_secs: Some(1_234),
})
.await
@@ -730,10 +731,10 @@ mod tests {
assert_eq!(settlement.billing_status, "settled");
assert_eq!(settlement.wallet_id.as_deref(), Some("wallet-1"));
assert_eq!(settlement.wallet_balance_before, Some(12.0));
assert_eq!(settlement.wallet_balance_after, Some(9.0));
assert_eq!(settlement.wallet_recharge_balance_after, Some(7.0));
assert_eq!(settlement.wallet_balance_after, Some(6.0));
assert_eq!(settlement.wallet_recharge_balance_after, Some(4.0));
assert_eq!(settlement.wallet_gift_balance_after, Some(2.0));
assert_eq!(settlement.provider_monthly_used_usd, Some(7.0));
assert_eq!(settlement.provider_monthly_used_usd, Some(11.0));
let wallet = sqlx::query(
"SELECT balance, gift_balance, total_consumed FROM wallets WHERE id = 'wallet-1'",
@@ -741,9 +742,9 @@ mod tests {
.fetch_one(&pool)
.await
.expect("wallet should load");
assert_eq!(wallet.try_get::<f64, _>("balance").unwrap(), 7.0);
assert_eq!(wallet.try_get::<f64, _>("balance").unwrap(), 4.0);
assert_eq!(wallet.try_get::<f64, _>("gift_balance").unwrap(), 2.0);
assert_eq!(wallet.try_get::<f64, _>("total_consumed").unwrap(), 3.0);
assert_eq!(wallet.try_get::<f64, _>("total_consumed").unwrap(), 6.0);
let second = repository
.settle_usage(UsageSettlementInput {
@@ -755,7 +756,7 @@ mod tests {
status: "completed".to_string(),
billing_status: "pending".to_string(),
total_cost_usd: 3.0,
actual_total_cost_usd: 2.0,
actual_total_cost_usd: 6.0,
finalized_at_unix_secs: Some(9_999),
})
.await
@@ -768,7 +769,7 @@ mod tests {
.fetch_one(&pool)
.await
.expect("provider should load");
assert_eq!(provider_used, 7.0);
assert_eq!(provider_used, 11.0);
}
#[tokio::test]
@@ -834,7 +835,7 @@ mod tests {
status: "completed".to_string(),
billing_status: "pending".to_string(),
total_cost_usd: 15.0,
actual_total_cost_usd: 7.5,
actual_total_cost_usd: 15.0,
finalized_at_unix_secs: Some(1_236),
})
.await
@@ -847,7 +848,7 @@ mod tests {
assert_eq!(settlement.wallet_balance_after, Some(-3.0));
assert_eq!(settlement.wallet_recharge_balance_after, Some(-3.0));
assert_eq!(settlement.wallet_gift_balance_after, Some(0.0));
assert_eq!(settlement.provider_monthly_used_usd, Some(12.5));
assert_eq!(settlement.provider_monthly_used_usd, Some(20.0));
let wallet = sqlx::query(
"SELECT balance, gift_balance, total_consumed FROM wallets WHERE id = 'wallet-1'",
@@ -883,7 +884,7 @@ mod tests {
status: "completed".to_string(),
billing_status: "pending".to_string(),
total_cost_usd: 3.0,
actual_total_cost_usd: 2.0,
actual_total_cost_usd: 6.0,
finalized_at_unix_secs: Some(1_260),
})
.await
@@ -909,7 +910,7 @@ mod tests {
.fetch_one(&pool)
.await
.expect("quota ledger should load");
assert_eq!(quota_used, 3.0);
assert_eq!(quota_used, 6.0);
}
async fn seed_settlement_rows(pool: &sqlx::SqlitePool) {
@@ -929,9 +930,9 @@ INSERT INTO "usage" (
request_id, user_id, provider_id, status, billing_status, total_cost_usd, actual_total_cost_usd
)
VALUES
('request-1', 'user-1', 'provider-1', 'completed', 'pending', 3.0, 2.0),
('request-1', 'user-1', 'provider-1', 'completed', 'pending', 3.0, 6.0),
('request-2', 'user-1', 'provider-1', 'failed', 'pending', 3.0, 2.0),
('request-overdraw', 'user-1', 'provider-1', 'completed', 'pending', 15.0, 7.5);
('request-overdraw', 'user-1', 'provider-1', 'completed', 'pending', 15.0, 15.0);
"#,
)
.execute(pool)
@@ -961,7 +962,7 @@ INSERT INTO "usage" (
total_cost_usd, actual_total_cost_usd
) VALUES (
'request-quota-covered', 'user-quota', 'key-quota', 'completed',
'pending', 3.0, 2.0
'pending', 3.0, 6.0
);
INSERT INTO billing_plans (