feat(data): 废弃 usage 表 HTTP/结算列,迁移至 settlement_snapshots 与 http_audits

- 新增迁移 20260413030000:标记 billing_status、finalized_at、request_headers 等列为 DEPRECATED
- 更新 baseline_v2.sql 同步废弃注释,BASELINE_V2_CUTOFF_VERSION 升至 20260413030000
- usage/sql.rs:inline body 阈值归零,强制所有 body 走 blob 存储;upsert 时清空 legacy header/output_price 列
- 查询层优先读 usage_settlement_snapshots 的 billing_status、finalized_at、output_price_per_1m
- runtime.rs:stale usage 处理同步写入 usage_settlement_snapshots;SELECT FOR UPDATE 改为 FOR UPDATE OF usage
- 前端:PerformanceAnalysis 页面重构为实时面板,新增 prometheus 工具函数与 monitoring API
This commit is contained in:
fawney19
2026-04-13 14:53:46 +08:00
parent e46629d11a
commit 6aa16ec792
16 changed files with 1587 additions and 87 deletions

View File

@@ -8,7 +8,7 @@ use tracing::{error, info, warn};
static MIGRATOR: Migrator = sqlx::migrate!("./migrations");
static BASELINE_V2_SQL: &str = include_str!("../bootstrap/20260413020000_baseline_v2.sql");
const BASELINE_V2_CUTOFF_VERSION: i64 = 20260413020000;
const BASELINE_V2_CUTOFF_VERSION: i64 = 20260413030000;
const MIGRATIONS_TABLE_EXISTS_SQL: &str =
"SELECT to_regclass('public._sqlx_migrations') IS NOT NULL";
const EMPTY_DATABASE_USER_TABLE_COUNT_SQL: &str = r#"
@@ -404,6 +404,7 @@ mod tests {
20260406000000,
20260410000000,
20260413020000,
20260413030000,
]
);
}
@@ -425,27 +426,47 @@ mod tests {
#[test]
fn deprecation_migration_and_baseline_mark_legacy_usage_columns() {
let migration = MIGRATOR
let settlement_migration = MIGRATOR
.iter()
.find(|migration| migration.version == 20260413020000)
.expect("deprecation migration should be embedded");
let http_migration = MIGRATOR
.iter()
.find(|migration| migration.version == 20260413030000)
.expect("http/body deprecation migration should be embedded");
assert!(migration
assert!(settlement_migration
.sql
.contains("COMMENT ON COLUMN public.usage.output_price_per_1m"));
assert!(migration
assert!(settlement_migration
.sql
.contains("COMMENT ON COLUMN public.usage.wallet_id"));
assert!(migration
assert!(settlement_migration
.sql
.contains("COMMENT ON COLUMN public.usage.username"));
assert!(migration
assert!(settlement_migration
.sql
.contains("COMMENT ON COLUMN public.usage.api_key_name"));
assert!(http_migration
.sql
.contains("COMMENT ON COLUMN public.usage.request_headers"));
assert!(http_migration
.sql
.contains("COMMENT ON COLUMN public.usage.request_body"));
assert!(http_migration
.sql
.contains("COMMENT ON COLUMN public.usage.billing_status"));
assert!(http_migration
.sql
.contains("COMMENT ON COLUMN public.usage.finalized_at"));
assert!(BASELINE_V2_SQL.contains("COMMENT ON COLUMN public.usage.output_price_per_1m"));
assert!(BASELINE_V2_SQL.contains("COMMENT ON COLUMN public.usage.wallet_id"));
assert!(BASELINE_V2_SQL.contains("COMMENT ON COLUMN public.usage.username"));
assert!(BASELINE_V2_SQL.contains("COMMENT ON COLUMN public.usage.api_key_name"));
assert!(BASELINE_V2_SQL.contains("COMMENT ON COLUMN public.usage.request_headers"));
assert!(BASELINE_V2_SQL.contains("COMMENT ON COLUMN public.usage.request_body"));
assert!(BASELINE_V2_SQL.contains("COMMENT ON COLUMN public.usage.billing_status"));
assert!(BASELINE_V2_SQL.contains("COMMENT ON COLUMN public.usage.finalized_at"));
}
#[test]
@@ -472,7 +493,10 @@ mod tests {
.map(|migration| migration.version)
.collect::<Vec<_>>();
assert_eq!(pending_versions, vec![20260410000000, 20260413020000]);
assert_eq!(
pending_versions,
vec![20260410000000, 20260413020000, 20260413030000]
);
}
#[test]

View File

@@ -10,7 +10,7 @@ const FIND_USAGE_FOR_SETTLEMENT_SQL: &str = r#"
SELECT
usage_record.request_id,
COALESCE(usage_settlement_snapshots.wallet_id, usage_record.wallet_id) AS wallet_id,
usage_record.billing_status,
COALESCE(usage_settlement_snapshots.billing_status, usage_record.billing_status) AS billing_status,
COALESCE(
CAST(usage_settlement_snapshots.wallet_balance_before AS DOUBLE PRECISION),
CAST(usage_record.wallet_balance_before AS DOUBLE PRECISION)
@@ -369,6 +369,7 @@ RETURNING CAST(monthly_used_usd AS DOUBLE PRECISION) AS monthly_used_usd
}
}
sync_usage_settlement_snapshot(&mut **tx, &settlement).await?;
sqlx::query(FINALIZE_USAGE_BILLING_SQL)
.bind(&input.request_id)
.bind(final_billing_status)
@@ -376,7 +377,6 @@ RETURNING CAST(monthly_used_usd AS DOUBLE PRECISION) AS monthly_used_usd
.execute(&mut **tx)
.await
.map_postgres_err()?;
sync_usage_settlement_snapshot(&mut **tx, &settlement).await?;
Ok(Some(settlement))
})
@@ -397,7 +397,9 @@ mod tests {
assert!(
super::FIND_USAGE_FOR_SETTLEMENT_SQL.contains("LEFT JOIN usage_settlement_snapshots")
);
assert!(super::FIND_USAGE_FOR_SETTLEMENT_SQL.contains("COALESCE("));
assert!(super::FIND_USAGE_FOR_SETTLEMENT_SQL.contains(
"COALESCE(usage_settlement_snapshots.billing_status, usage_record.billing_status)"
));
assert!(super::FIND_USAGE_FOR_SETTLEMENT_SQL.contains("FOR UPDATE OF usage_record"));
}

View File

@@ -18,7 +18,9 @@ use super::{
use crate::postgres::PostgresTransactionRunner;
use crate::{error::SqlxResultExt, DataLayerError};
const MAX_INLINE_USAGE_BODY_BYTES: usize = 16 * 1024;
// Legacy inline body columns on public.usage are deprecated. Keep the threshold at zero so
// newly captured bodies always spill to usage_body_blobs and resolve through usage_http_audits.
const MAX_INLINE_USAGE_BODY_BYTES: usize = 0;
const FIND_USAGE_BODY_BLOB_BY_REF_SQL: &str =
r#"SELECT payload_gzip FROM usage_body_blobs WHERE body_ref = $1 LIMIT 1"#;
const UPSERT_USAGE_BODY_BLOB_SQL: &str = r#"
@@ -258,7 +260,10 @@ SELECT
COALESCE("usage".cache_read_input_tokens, 0) AS cache_read_input_tokens,
COALESCE(CAST("usage".cache_creation_cost_usd AS DOUBLE PRECISION), 0) AS cache_creation_cost_usd,
COALESCE(CAST("usage".cache_read_cost_usd AS DOUBLE PRECISION), 0) AS cache_read_cost_usd,
CAST("usage".output_price_per_1m AS DOUBLE PRECISION) AS output_price_per_1m,
COALESCE(
CAST(usage_settlement_snapshots.output_price_per_1m AS DOUBLE PRECISION),
CAST("usage".output_price_per_1m AS DOUBLE PRECISION)
) AS output_price_per_1m,
COALESCE(CAST("usage".total_cost_usd AS DOUBLE PRECISION), 0) AS total_cost_usd,
COALESCE(CAST("usage".actual_total_cost_usd AS DOUBLE PRECISION), 0) AS actual_total_cost_usd,
"usage".status_code,
@@ -310,9 +315,19 @@ SELECT
CAST(usage_settlement_snapshots.price_per_request AS DOUBLE PRECISION) AS settlement_price_per_request,
CAST(EXTRACT(EPOCH FROM "usage".created_at) AS BIGINT) AS created_at_unix_ms,
CAST(
EXTRACT(EPOCH FROM COALESCE("usage".finalized_at, "usage".created_at)) AS BIGINT
EXTRACT(
EPOCH FROM COALESCE(
usage_settlement_snapshots.finalized_at,
"usage".finalized_at,
"usage".created_at
)
) AS BIGINT
) AS updated_at_unix_secs,
CAST(EXTRACT(EPOCH FROM "usage".finalized_at) AS BIGINT) AS finalized_at_unix_secs
CAST(
EXTRACT(
EPOCH FROM COALESCE(usage_settlement_snapshots.finalized_at, "usage".finalized_at)
) AS BIGINT
) AS finalized_at_unix_secs
FROM "usage"
LEFT JOIN usage_http_audits
ON usage_http_audits.request_id = "usage".request_id
@@ -356,7 +371,10 @@ SELECT
COALESCE("usage".cache_read_input_tokens, 0) AS cache_read_input_tokens,
COALESCE(CAST("usage".cache_creation_cost_usd AS DOUBLE PRECISION), 0) AS cache_creation_cost_usd,
COALESCE(CAST("usage".cache_read_cost_usd AS DOUBLE PRECISION), 0) AS cache_read_cost_usd,
CAST("usage".output_price_per_1m AS DOUBLE PRECISION) AS output_price_per_1m,
COALESCE(
CAST(usage_settlement_snapshots.output_price_per_1m AS DOUBLE PRECISION),
CAST("usage".output_price_per_1m AS DOUBLE PRECISION)
) AS output_price_per_1m,
COALESCE(CAST("usage".total_cost_usd AS DOUBLE PRECISION), 0) AS total_cost_usd,
COALESCE(CAST("usage".actual_total_cost_usd AS DOUBLE PRECISION), 0) AS actual_total_cost_usd,
"usage".status_code,
@@ -408,9 +426,19 @@ SELECT
CAST(usage_settlement_snapshots.price_per_request AS DOUBLE PRECISION) AS settlement_price_per_request,
CAST(EXTRACT(EPOCH FROM "usage".created_at) AS BIGINT) AS created_at_unix_ms,
CAST(
EXTRACT(EPOCH FROM COALESCE("usage".finalized_at, "usage".created_at)) AS BIGINT
EXTRACT(
EPOCH FROM COALESCE(
usage_settlement_snapshots.finalized_at,
"usage".finalized_at,
"usage".created_at
)
) AS BIGINT
) AS updated_at_unix_secs,
CAST(EXTRACT(EPOCH FROM "usage".finalized_at) AS BIGINT) AS finalized_at_unix_secs
CAST(
EXTRACT(
EPOCH FROM COALESCE(usage_settlement_snapshots.finalized_at, "usage".finalized_at)
) AS BIGINT
) AS finalized_at_unix_secs
FROM "usage"
LEFT JOIN usage_http_audits
ON usage_http_audits.request_id = "usage".request_id
@@ -505,7 +533,10 @@ SELECT
COALESCE("usage".cache_read_input_tokens, 0) AS cache_read_input_tokens,
COALESCE(CAST("usage".cache_creation_cost_usd AS DOUBLE PRECISION), 0) AS cache_creation_cost_usd,
COALESCE(CAST("usage".cache_read_cost_usd AS DOUBLE PRECISION), 0) AS cache_read_cost_usd,
CAST("usage".output_price_per_1m AS DOUBLE PRECISION) AS output_price_per_1m,
COALESCE(
CAST(usage_settlement_snapshots.output_price_per_1m AS DOUBLE PRECISION),
CAST("usage".output_price_per_1m AS DOUBLE PRECISION)
) AS output_price_per_1m,
COALESCE(CAST("usage".total_cost_usd AS DOUBLE PRECISION), 0) AS total_cost_usd,
COALESCE(CAST("usage".actual_total_cost_usd AS DOUBLE PRECISION), 0) AS actual_total_cost_usd,
"usage".status_code,
@@ -551,9 +582,19 @@ SELECT
CAST(usage_settlement_snapshots.price_per_request AS DOUBLE PRECISION) AS settlement_price_per_request,
CAST(EXTRACT(EPOCH FROM "usage".created_at) AS BIGINT) AS created_at_unix_ms,
CAST(
EXTRACT(EPOCH FROM COALESCE("usage".finalized_at, "usage".created_at)) AS BIGINT
EXTRACT(
EPOCH FROM COALESCE(
usage_settlement_snapshots.finalized_at,
"usage".finalized_at,
"usage".created_at
)
) AS BIGINT
) AS updated_at_unix_secs,
CAST(EXTRACT(EPOCH FROM "usage".finalized_at) AS BIGINT) AS finalized_at_unix_secs
CAST(
EXTRACT(
EPOCH FROM COALESCE(usage_settlement_snapshots.finalized_at, "usage".finalized_at)
) AS BIGINT
) AS finalized_at_unix_secs
FROM "usage"
LEFT JOIN usage_settlement_snapshots
ON usage_settlement_snapshots.request_id = "usage".request_id
@@ -591,7 +632,10 @@ SELECT
COALESCE("usage".cache_read_input_tokens, 0) AS cache_read_input_tokens,
COALESCE(CAST("usage".cache_creation_cost_usd AS DOUBLE PRECISION), 0) AS cache_creation_cost_usd,
COALESCE(CAST("usage".cache_read_cost_usd AS DOUBLE PRECISION), 0) AS cache_read_cost_usd,
CAST("usage".output_price_per_1m AS DOUBLE PRECISION) AS output_price_per_1m,
COALESCE(
CAST(usage_settlement_snapshots.output_price_per_1m AS DOUBLE PRECISION),
CAST("usage".output_price_per_1m AS DOUBLE PRECISION)
) AS output_price_per_1m,
COALESCE(CAST("usage".total_cost_usd AS DOUBLE PRECISION), 0) AS total_cost_usd,
COALESCE(CAST("usage".actual_total_cost_usd AS DOUBLE PRECISION), 0) AS actual_total_cost_usd,
"usage".status_code,
@@ -637,9 +681,19 @@ SELECT
CAST(usage_settlement_snapshots.price_per_request AS DOUBLE PRECISION) AS settlement_price_per_request,
CAST(EXTRACT(EPOCH FROM "usage".created_at) AS BIGINT) AS created_at_unix_ms,
CAST(
EXTRACT(EPOCH FROM COALESCE("usage".finalized_at, "usage".created_at)) AS BIGINT
EXTRACT(
EPOCH FROM COALESCE(
usage_settlement_snapshots.finalized_at,
"usage".finalized_at,
"usage".created_at
)
) AS BIGINT
) AS updated_at_unix_secs,
CAST(EXTRACT(EPOCH FROM "usage".finalized_at) AS BIGINT) AS finalized_at_unix_secs
CAST(
EXTRACT(
EPOCH FROM COALESCE(usage_settlement_snapshots.finalized_at, "usage".finalized_at)
) AS BIGINT
) AS finalized_at_unix_secs
FROM "usage"
LEFT JOIN usage_settlement_snapshots
ON usage_settlement_snapshots.request_id = "usage".request_id
@@ -792,7 +846,7 @@ DO UPDATE SET
cache_read_input_tokens = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.cache_read_input_tokens, "usage".cache_read_input_tokens) ELSE "usage".cache_read_input_tokens END,
cache_creation_cost_usd = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.cache_creation_cost_usd, "usage".cache_creation_cost_usd) ELSE "usage".cache_creation_cost_usd END,
cache_read_cost_usd = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.cache_read_cost_usd, "usage".cache_read_cost_usd) ELSE "usage".cache_read_cost_usd END,
output_price_per_1m = "usage".output_price_per_1m,
output_price_per_1m = NULL,
total_cost_usd = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.total_cost_usd, "usage".total_cost_usd) ELSE "usage".total_cost_usd END,
actual_total_cost_usd = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.actual_total_cost_usd, "usage".actual_total_cost_usd) ELSE "usage".actual_total_cost_usd END,
status_code = CASE WHEN "usage".billing_status = 'pending' THEN CASE
@@ -811,7 +865,7 @@ DO UPDATE SET
first_byte_time_ms = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.first_byte_time_ms, "usage".first_byte_time_ms) ELSE "usage".first_byte_time_ms END,
status = CASE WHEN "usage".billing_status = 'pending' THEN EXCLUDED.status ELSE "usage".status END,
billing_status = CASE WHEN "usage".billing_status = 'pending' THEN EXCLUDED.billing_status ELSE "usage".billing_status END,
request_headers = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.request_headers, "usage".request_headers) ELSE "usage".request_headers END,
request_headers = NULL,
request_body = CASE WHEN "usage".billing_status = 'pending' THEN CASE
WHEN EXCLUDED.request_body_compressed IS NOT NULL OR $56 THEN NULL
ELSE COALESCE(EXCLUDED.request_body, "usage".request_body)
@@ -820,7 +874,7 @@ DO UPDATE SET
WHEN EXCLUDED.request_body IS NOT NULL OR $56 THEN NULL
ELSE COALESCE(EXCLUDED.request_body_compressed, "usage".request_body_compressed)
END ELSE "usage".request_body_compressed END,
provider_request_headers = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.provider_request_headers, "usage".provider_request_headers) ELSE "usage".provider_request_headers END,
provider_request_headers = NULL,
provider_request_body = CASE WHEN "usage".billing_status = 'pending' THEN CASE
WHEN EXCLUDED.provider_request_body_compressed IS NOT NULL OR $57 THEN NULL
ELSE COALESCE(EXCLUDED.provider_request_body, "usage".provider_request_body)
@@ -829,7 +883,7 @@ DO UPDATE SET
WHEN EXCLUDED.provider_request_body IS NOT NULL OR $57 THEN NULL
ELSE COALESCE(EXCLUDED.provider_request_body_compressed, "usage".provider_request_body_compressed)
END ELSE "usage".provider_request_body_compressed END,
response_headers = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.response_headers, "usage".response_headers) ELSE "usage".response_headers END,
response_headers = NULL,
response_body = CASE WHEN "usage".billing_status = 'pending' THEN CASE
WHEN EXCLUDED.response_body_compressed IS NOT NULL OR $58 THEN NULL
ELSE COALESCE(EXCLUDED.response_body, "usage".response_body)
@@ -838,7 +892,7 @@ DO UPDATE SET
WHEN EXCLUDED.response_body IS NOT NULL OR $58 THEN NULL
ELSE COALESCE(EXCLUDED.response_body_compressed, "usage".response_body_compressed)
END ELSE "usage".response_body_compressed END,
client_response_headers = CASE WHEN "usage".billing_status = 'pending' THEN COALESCE(EXCLUDED.client_response_headers, "usage".client_response_headers) ELSE "usage".client_response_headers END,
client_response_headers = NULL,
client_response_body = CASE WHEN "usage".billing_status = 'pending' THEN CASE
WHEN EXCLUDED.client_response_body_compressed IS NOT NULL OR $59 THEN NULL
ELSE COALESCE(EXCLUDED.client_response_body, "usage".client_response_body)
@@ -1550,6 +1604,7 @@ impl SqlxUsageReadRepository {
stored.execution_path = routing_snapshot.execution_path.clone();
stored.local_execution_runtime_miss_reason =
routing_snapshot.local_execution_runtime_miss_reason.clone();
stored.output_price_per_1m = settlement_pricing_snapshot.output_price_per_1m;
stored.request_metadata = request_metadata_value;
Ok(stored)
}) as BoxFuture<'_, Result<StoredRequestUsageAudit, DataLayerError>>
@@ -1910,7 +1965,8 @@ struct UsageSettlementPricingSnapshot {
impl UsageSettlementPricingSnapshot {
fn any_present(&self) -> bool {
self.billing_snapshot_schema_version.is_some()
self.billing_status.is_some()
|| self.billing_snapshot_schema_version.is_some()
|| self.billing_snapshot_status.is_some()
|| self.rate_multiplier.is_some()
|| self.is_free_tier.is_some()
@@ -2907,11 +2963,10 @@ mod tests {
)
);
assert!(sql.contains(
"CAST(\"usage\".output_price_per_1m AS DOUBLE PRECISION) AS output_price_per_1m"
"CAST(usage_settlement_snapshots.output_price_per_1m AS DOUBLE PRECISION)"
));
assert!(sql.contains("EXTRACT(EPOCH FROM \"usage\".created_at)"));
assert!(sql.contains("COALESCE(\"usage\".finalized_at, \"usage\".created_at)"));
assert!(sql.contains("EXTRACT(EPOCH FROM \"usage\".finalized_at)"));
assert!(sql.contains("usage_settlement_snapshots.finalized_at"));
assert!(!sql.contains("CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT)"));
assert!(!sql.contains("CAST(output_price_per_1m AS DOUBLE PRECISION)"));
}
@@ -2955,17 +3010,11 @@ mod tests {
}
#[test]
fn usage_sql_keeps_list_output_price_reads_legacy_only() {
assert!(super::LIST_USAGE_AUDITS_PREFIX.contains(
"CAST(\"usage\".output_price_per_1m AS DOUBLE PRECISION) AS output_price_per_1m"
));
assert!(super::LIST_RECENT_USAGE_AUDITS_PREFIX.contains(
"CAST(\"usage\".output_price_per_1m AS DOUBLE PRECISION) AS output_price_per_1m"
));
assert!(!super::LIST_USAGE_AUDITS_PREFIX
.contains("COALESCE(\n usage_settlement_snapshots.output_price_per_1m,"));
assert!(!super::LIST_RECENT_USAGE_AUDITS_PREFIX
.contains("COALESCE(\n usage_settlement_snapshots.output_price_per_1m,"));
fn usage_sql_reads_list_output_price_from_settlement_snapshots_before_legacy_usage_column() {
assert!(super::LIST_USAGE_AUDITS_PREFIX
.contains("CAST(usage_settlement_snapshots.output_price_per_1m AS DOUBLE PRECISION)"));
assert!(super::LIST_RECENT_USAGE_AUDITS_PREFIX
.contains("CAST(usage_settlement_snapshots.output_price_per_1m AS DOUBLE PRECISION)"));
}
#[test]
@@ -3007,11 +3056,19 @@ mod tests {
}
#[test]
fn usage_sql_preserves_legacy_output_price_column_on_upsert() {
assert!(super::UPSERT_SQL.contains("output_price_per_1m = \"usage\".output_price_per_1m"));
fn usage_sql_clears_legacy_output_price_column_on_upsert() {
assert!(super::UPSERT_SQL.contains("output_price_per_1m = NULL"));
assert!(include_str!("sql.rs").contains(".bind(None::<f64>)"));
}
#[test]
fn usage_sql_clears_legacy_header_columns_on_upsert() {
assert!(super::UPSERT_SQL.contains("request_headers = NULL"));
assert!(super::UPSERT_SQL.contains("provider_request_headers = NULL"));
assert!(super::UPSERT_SQL.contains("response_headers = NULL"));
assert!(super::UPSERT_SQL.contains("client_response_headers = NULL"));
}
#[test]
fn usage_sql_detached_body_flags_clear_inline_and_compressed_columns() {
assert!(super::UPSERT_SQL
@@ -3040,14 +3097,18 @@ mod tests {
}
#[test]
fn prepare_usage_body_storage_keeps_small_payloads_inline() {
fn prepare_usage_body_storage_detaches_small_payloads_into_blob_storage() {
let payload = json!({"message": "hello"});
let storage = prepare_usage_body_storage(Some(&payload)).expect("storage should serialize");
assert!(storage.detached_blob_bytes.is_none());
assert!(storage.inline_json.is_none());
let compressed = storage
.detached_blob_bytes
.as_deref()
.expect("small payload should now be ref-backed");
assert_eq!(
storage.inline_json.as_deref(),
Some(payload.to_string().as_str())
inflate_usage_json_value(compressed).expect("payload should inflate"),
payload
);
}
@@ -3748,6 +3809,16 @@ mod tests {
);
}
#[test]
fn usage_settlement_pricing_snapshot_with_billing_status_only_is_still_persisted() {
let snapshot = UsageSettlementPricingSnapshot {
billing_status: Some("pending".to_string()),
..UsageSettlementPricingSnapshot::default()
};
assert!(snapshot.any_present());
}
#[test]
fn attach_usage_settlement_pricing_snapshot_metadata_adds_missing_values_without_overwriting() {
let metadata = attach_usage_settlement_pricing_snapshot_metadata(