use compact_str::CompactString;
use http::{HeaderMap, HeaderName, HeaderValue};
use r402_protocol::payment::{
PaymentRequirements, asset_decimals_from_extra, resolve_settlement_override_amount,
};
pub use r402_protocol::payment::{SettlementOverrideError, SettlementOverrides};
pub const SETTLEMENT_OVERRIDES_HEADER: &str = "Settlement-Overrides";
#[must_use]
pub const fn settlement_overrides_header_name() -> HeaderName {
HeaderName::from_static("settlement-overrides")
}
pub fn set_settlement_overrides(headers: &mut HeaderMap, overrides: &SettlementOverrides) {
if let Ok(json) = serde_json::to_string(overrides)
&& let Ok(value) = HeaderValue::from_str(&json)
{
let _ = headers.insert(settlement_overrides_header_name(), value);
}
}
pub fn set_settlement_overrides_on_response(
response: &mut axum_core::response::Response,
overrides: &SettlementOverrides,
) {
set_settlement_overrides(response.headers_mut(), overrides);
}
#[must_use]
pub fn take_settlement_overrides_header(headers: &mut HeaderMap) -> Option<SettlementOverrides> {
let value = headers.remove(settlement_overrides_header_name())?;
let s = value.to_str().ok()?;
serde_json::from_str(s).ok()
}
pub fn strip_response_settlement_overrides(response: &mut axum_core::response::Response) -> bool {
let header = take_settlement_overrides_header(response.headers_mut());
let ext = response.extensions_mut().remove::<UptoActualAmount>();
header.is_some() || ext.is_some()
}
pub fn resolve_response_settlement_amount(
response: &mut axum_core::response::Response,
requirements: &PaymentRequirements,
) -> Result<Option<String>, SettlementOverrideError> {
let header = take_settlement_overrides_header(response.headers_mut());
if let Some(ext) = response.extensions_mut().remove::<UptoActualAmount>() {
return Ok(Some(ext.into_inner().into()));
}
let Some(overrides) = header else {
return Ok(None);
};
let Some(raw) = overrides.amount.filter(|s| !s.is_empty()) else {
return Ok(None);
};
let decimals = asset_decimals_from_extra(requirements.extra.as_ref());
let resolved =
resolve_settlement_override_amount(&raw, requirements.amount.as_str(), decimals)?;
Ok(Some(resolved))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UptoActualAmount(CompactString);
impl UptoActualAmount {
pub fn new<S: Into<CompactString>>(amount: S) -> Self {
Self(amount.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
self.0.as_str()
}
#[must_use]
pub fn into_inner(self) -> CompactString {
self.0
}
}
impl AsRef<str> for UptoActualAmount {
fn as_ref(&self) -> &str {
self.0.as_str()
}
}
impl From<CompactString> for UptoActualAmount {
fn from(value: CompactString) -> Self {
Self(value)
}
}
impl From<&str> for UptoActualAmount {
fn from(value: &str) -> Self {
Self(value.into())
}
}
impl From<String> for UptoActualAmount {
fn from(value: String) -> Self {
Self(value.into())
}
}
#[must_use]
pub fn marshal_settlement_overrides(overrides: &SettlementOverrides) -> String {
serde_json::to_string(overrides).unwrap_or_default()
}
#[cfg(test)]
mod tests {
use http::HeaderMap;
use r402_protocol::payment::PaymentRequirements;
use super::*;
#[test]
fn header_roundtrip_and_strip() {
let mut headers = HeaderMap::new();
set_settlement_overrides(&mut headers, &SettlementOverrides::amount("50%"));
assert!(
headers.contains_key(settlement_overrides_header_name()),
"header must be present after set"
);
let parsed = take_settlement_overrides_header(&mut headers)
.expect("well-formed Settlement-Overrides must parse");
assert_eq!(parsed.amount.as_deref(), Some("50%"));
assert!(
!headers.contains_key(settlement_overrides_header_name()),
"take must strip the billing header"
);
}
#[test]
fn extension_beats_header() {
let mut response = axum_core::response::Response::new(axum_core::body::Body::empty());
set_settlement_overrides_on_response(&mut response, &SettlementOverrides::amount("50%"));
response
.extensions_mut()
.insert(UptoActualAmount::new("42"));
let req = PaymentRequirements::new(
"upto".into(),
"eip155:8453".parse().expect("fixture chain id"),
"1000000".into(),
"0xpay".into(),
"0xasset".into(),
300,
);
let amount = resolve_response_settlement_amount(&mut response, &req)
.expect("override resolve")
.expect("amount present");
assert_eq!(amount, "42");
assert!(
!response
.headers()
.contains_key(settlement_overrides_header_name()),
"Settlement-Overrides must be stripped even when the extension wins"
);
}
#[test]
fn strip_reports_presence_and_clears_both() {
let mut response = axum_core::response::Response::new(axum_core::body::Body::empty());
set_settlement_overrides_on_response(&mut response, &SettlementOverrides::amount("50%"));
response
.extensions_mut()
.insert(UptoActualAmount::new("42"));
assert!(
strip_response_settlement_overrides(&mut response),
"either billing signal must count as present"
);
assert!(
!response
.headers()
.contains_key(settlement_overrides_header_name()),
"strip must remove Settlement-Overrides"
);
assert!(
response.extensions().get::<UptoActualAmount>().is_none(),
"strip must remove UptoActualAmount"
);
assert!(
!strip_response_settlement_overrides(&mut response),
"second strip must report absence"
);
}
}