1use r402_facilitator::{DynFacilitator, FailureRecovery};
4use r402_protocol::error::{FacilitatorError, VerificationError};
5use r402_protocol::extension::{ExtensionRegistry, SettleContext as ExtensionSettleContext};
6use r402_protocol::payment::{
7 Extensions, PaymentPayload, PaymentRequirements, SettleRequest, SettleResponse,
8 SettlementOverrides, TypedVerifyRequest, V2, asset_decimals_from_extra,
9 resolve_settlement_override_amount,
10};
11use serde_json::Value;
12
13use crate::hooks::{
14 BeforeOpDecision, PaymentHookContext, SettleContext, SettleResultContext, WirePaymentPayload,
15 assert_additive_payload_enrichment,
16};
17use crate::payment_flow::{PaymentFlowName, SettlePhase};
18use crate::resource::ResourceServer;
19use crate::verify::to_verify_request;
20
21#[derive(Debug, Clone)]
23#[non_exhaustive]
24pub struct CompletedSettlement {
25 pub phase: SettlePhase,
27 pub flow: PaymentFlowName,
29 pub result: SettleResponse,
31 pub requirements: PaymentRequirements,
33}
34
35impl CompletedSettlement {
36 #[must_use]
38 pub const fn new(
39 phase: SettlePhase,
40 flow: PaymentFlowName,
41 result: SettleResponse,
42 requirements: PaymentRequirements,
43 ) -> Self {
44 Self {
45 phase,
46 flow,
47 result,
48 requirements,
49 }
50 }
51}
52
53impl ResourceServer {
54 pub async fn settle_payment(
64 &self,
65 payload: &WirePaymentPayload,
66 requirements: &PaymentRequirements,
67 overrides: Option<&SettlementOverrides>,
68 phase: SettlePhase,
69 resource_url: Option<&str>,
70 advertised: Option<&Extensions>,
71 ) -> Result<SettleResponse, FacilitatorError> {
72 let effective = apply_settlement_overrides(requirements, overrides)?;
73 let payload = self
74 .apply_scheme_settlement_payload_enrich(payload, &effective, phase)
75 .await?;
76 let settle_ctx = SettleContext {
77 payment: PaymentHookContext {
78 payload,
79 requirements: effective,
80 },
81 declared_extensions: advertised.cloned().unwrap_or_default(),
82 phase,
83 resource_url: resource_url.map(compact_str::CompactString::from),
84 };
85
86 let mut skipped: Option<SettleResponse> = None;
87 for hook in &self.hooks {
88 match hook.before_settle(&settle_ctx).await {
89 BeforeOpDecision::Continue => {}
90 BeforeOpDecision::Abort { reason, message } => {
91 return Err(FacilitatorError::Aborted { reason, message });
92 }
93 BeforeOpDecision::Skip { result } => {
94 skipped = Some(result);
95 break;
96 }
97 }
98 }
99
100 let mut response = if let Some(local) = skipped {
101 local
102 } else {
103 self.call_settle_with_failure_hooks(&settle_ctx).await?
104 };
105 attach_settle_extensions(&self.extensions, &settle_ctx, &mut response, resource_url)
106 .await?;
107
108 let result_ctx = SettleResultContext {
109 settle: settle_ctx,
110 result: response.clone(),
111 };
112 for hook in &self.hooks {
113 hook.after_settle(&result_ctx).await;
114 }
115
116 Ok(response)
117 }
118
119 async fn call_settle_with_failure_hooks(
120 &self,
121 settle: &SettleContext,
122 ) -> Result<SettleResponse, FacilitatorError> {
123 let request = build_settle_request(&settle.payment.payload, &settle.payment.requirements)?;
124 #[cfg(feature = "metrics")]
125 let started = std::time::Instant::now();
126 let result = match self.settle_with_pending_retry(request).await {
127 Ok(r) => Ok(r),
128 Err(error) => self.recover_settle(settle, error).await,
129 };
130 #[cfg(feature = "metrics")]
131 record_settle_metrics(&result, started.elapsed());
132 result
133 }
134
135 async fn settle_with_pending_retry(
139 &self,
140 request: SettleRequest,
141 ) -> Result<SettleResponse, FacilitatorError> {
142 let first = DynFacilitator::settle(self.facilitator.as_ref(), request.clone()).await;
143 match &first {
144 Ok(response) if response.is_retryable_settlement_pending() => {
145 DynFacilitator::settle(self.facilitator.as_ref(), request).await
146 }
147 _ => first,
148 }
149 }
150
151 async fn apply_scheme_settlement_payload_enrich(
152 &self,
153 payload: &WirePaymentPayload,
154 requirements: &PaymentRequirements,
155 phase: SettlePhase,
156 ) -> Result<WirePaymentPayload, FacilitatorError> {
157 let Some(scheme) =
158 self.registered_scheme(requirements.scheme.as_str(), &requirements.network)
159 else {
160 return Ok(payload.clone());
161 };
162 let ctx = SettleContext {
163 payment: PaymentHookContext {
164 payload: payload.clone(),
165 requirements: requirements.clone(),
166 },
167 declared_extensions: Extensions::new(),
168 phase,
169 resource_url: None,
170 };
171 let Some(enrichment) = scheme.enrich_settlement_payload(&ctx).await? else {
172 return Ok(payload.clone());
173 };
174 let mut enriched = payload.clone();
175 let Value::Object(payload_map) = &mut enriched.payload else {
176 return Err(FacilitatorError::Verification(
177 VerificationError::InvalidFormat("settlement payload is not a JSON object".into()),
178 ));
179 };
180 assert_additive_payload_enrichment(payload_map, &enrichment, scheme.scheme())
181 .map_err(FacilitatorError::internal)?;
182 for (key, value) in enrichment {
183 payload_map.insert(key, value);
184 }
185 Ok(enriched)
186 }
187
188 async fn recover_settle(
189 &self,
190 settle: &SettleContext,
191 error: FacilitatorError,
192 ) -> Result<SettleResponse, FacilitatorError> {
193 for hook in &self.hooks {
194 if let FailureRecovery::Recovered(r) = hook.on_settle_failure(settle, &error).await {
195 return Ok(r);
196 }
197 }
198 Err(error)
199 }
200}
201
202async fn attach_settle_extensions(
203 registry: &ExtensionRegistry,
204 settle: &SettleContext,
205 response: &mut SettleResponse,
206 resource_url: Option<&str>,
207) -> Result<(), FacilitatorError> {
208 if registry.is_empty() {
209 return Ok(());
210 }
211 let json_payload = json_payment_payload(&settle.payment.payload)?;
212 let extra = {
213 let mut ctx =
214 ExtensionSettleContext::new(&json_payload, &settle.payment.requirements, response);
215 if let Some(url) = resource_url.or(settle.resource_url.as_deref()) {
216 ctx = ctx.with_resource_url(url);
217 }
218 if !settle.declared_extensions.is_empty() {
219 ctx = ctx.with_advertised(&settle.declared_extensions);
220 }
221 registry.collect_settle(&ctx).await
222 };
223 response.extensions_mut().extend(extra);
224 Ok(())
225}
226
227fn json_payment_payload(
228 payload: &WirePaymentPayload,
229) -> Result<PaymentPayload<Value, Value>, FacilitatorError> {
230 let accepted = serde_json::to_value(&payload.accepted).map_err(FacilitatorError::internal)?;
231 Ok(PaymentPayload::new(accepted, payload.payload.clone())
232 .with_optional_resource(payload.resource.clone())
233 .with_extensions(payload.extensions.clone()))
234}
235
236fn build_settle_request(
237 payload: &WirePaymentPayload,
238 requirements: &PaymentRequirements,
239) -> Result<SettleRequest, FacilitatorError> {
240 let typed = TypedVerifyRequest {
241 x402_version: V2,
242 payment_payload: payload.clone(),
243 payment_requirements: requirements.clone(),
244 };
245 let verify = to_verify_request(&typed)?;
246 Ok(SettleRequest::from(verify.into_json()))
247}
248
249fn apply_settlement_overrides(
251 requirements: &PaymentRequirements,
252 overrides: Option<&SettlementOverrides>,
253) -> Result<PaymentRequirements, FacilitatorError> {
254 let Some(overrides) = overrides else {
255 return Ok(requirements.clone());
256 };
257 let Some(raw) = overrides
258 .amount
259 .as_ref()
260 .map(|s| s.trim())
261 .filter(|s| !s.is_empty())
262 else {
263 return Ok(requirements.clone());
264 };
265 let decimals = asset_decimals_from_extra(requirements.extra.as_ref());
266 let resolved = resolve_settlement_override_amount(raw, requirements.amount.as_str(), decimals)
267 .map_err(|e| {
268 FacilitatorError::Verification(VerificationError::InvalidFormat(format!(
269 "invalid settlement override: {e}"
270 )))
271 })?;
272 let mut effective = requirements.clone();
273 effective.amount = resolved.into();
274 Ok(effective)
275}
276
277#[cfg(feature = "metrics")]
278fn record_settle_metrics(
279 result: &Result<SettleResponse, FacilitatorError>,
280 elapsed: std::time::Duration,
281) {
282 let label = match result {
283 Ok(r) if r.is_success() => "success",
284 Ok(_) => "failure",
285 Err(_) => "error",
286 };
287 ::metrics::counter!(
288 r402_protocol::metrics::FACILITATOR_SETTLE_TOTAL,
289 "result" => label,
290 )
291 .increment(1);
292 ::metrics::histogram!(r402_protocol::metrics::FACILITATOR_SETTLE_DURATION_SECONDS)
293 .record(elapsed.as_secs_f64());
294}