mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-10 05:00:19 +08:00
fix: apply actual billing cost to wallet settlement
This commit is contained in:
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user