Skip to main content

r402_server/
cancel.rs

1//! Verified-payment cancellation and failure-path receipts.
2
3use std::sync::atomic::{AtomicBool, Ordering};
4
5use compact_str::CompactString;
6use r402_protocol::payment::{PaymentRequirements, SettleResponse};
7use serde_json::Value;
8
9use crate::hooks::{
10    CancelReason, PaymentHookContext, VerifiedPaymentCanceledContext, WirePaymentPayload,
11};
12use crate::payment_flow::SettlePhase;
13use crate::resource::ResourceServer;
14use crate::settle::CompletedSettlement;
15
16impl ResourceServer {
17    /// Fires `on_verified_payment_canceled` for every registered hook.
18    pub async fn notify_verified_payment_canceled(
19        &self,
20        payload: &WirePaymentPayload,
21        requirements: &PaymentRequirements,
22        reason: CancelReason,
23        error: Option<&str>,
24        response_status: Option<u16>,
25    ) {
26        if self.hooks.is_empty() {
27            return;
28        }
29        let ctx = VerifiedPaymentCanceledContext {
30            payment: PaymentHookContext {
31                payload: payload.clone(),
32                requirements: requirements.clone(),
33            },
34            reason,
35            error: error.map(str::to_owned),
36            response_status,
37            settled_phases: Vec::new(),
38        };
39        for hook in &self.hooks {
40            hook.on_verified_payment_canceled(&ctx).await;
41        }
42    }
43
44    /// Builds a one-shot cancellation dispatcher (first call wins).
45    #[must_use]
46    pub fn cancellation_guard(
47        &self,
48        payload: WirePaymentPayload,
49        requirements: PaymentRequirements,
50    ) -> CancellationGuard {
51        CancellationGuard {
52            server: self.clone(),
53            payload,
54            requirements,
55            fired: AtomicBool::new(false),
56            settled_phases: Vec::new(),
57            before_handler: None,
58        }
59    }
60
61    async fn settle_canceled_payment(
62        &self,
63        ctx: &VerifiedPaymentCanceledContext,
64    ) -> Option<SettleResponse> {
65        if !ctx.settled_phases.contains(&SettlePhase::BeforeHandler) {
66            return None;
67        }
68        let scheme = self.registered_scheme(
69            ctx.payment.requirements.scheme.as_str(),
70            &ctx.payment.requirements.network,
71        )?;
72        let cancel_requirements = scheme.settle_on_cancel(ctx).await?;
73        match self
74            .settle_payment(
75                &ctx.payment.payload,
76                &cancel_requirements,
77                None,
78                SettlePhase::Cancel,
79                ctx.payment
80                    .payload
81                    .resource
82                    .as_ref()
83                    .map(|r| r.url.as_str()),
84                None,
85            )
86            .await
87        {
88            Ok(response) => Some(response),
89            Err(error) => SettleResponse::from_facilitator_error(
90                &error,
91                ctx.payment.requirements.network.to_string(),
92                "",
93            ),
94        }
95    }
96}
97
98/// Ensures `on_verified_payment_canceled` runs at most once for a payment.
99#[derive(Debug)]
100pub struct CancellationGuard {
101    server: ResourceServer,
102    payload: WirePaymentPayload,
103    requirements: PaymentRequirements,
104    fired: AtomicBool,
105    settled_phases: Vec<SettlePhase>,
106    before_handler: Option<CompletedSettlement>,
107}
108
109impl CancellationGuard {
110    /// Records settle phases already completed before the handler.
111    #[must_use]
112    pub fn with_settled_phases(mut self, phases: impl IntoIterator<Item = SettlePhase>) -> Self {
113        self.settled_phases = phases.into_iter().collect();
114        self
115    }
116
117    /// Stores the before-handler settle receipt for failure-path echo.
118    #[must_use]
119    pub fn with_before_handler(mut self, settlement: CompletedSettlement) -> Self {
120        self.before_handler = Some(settlement);
121        self
122    }
123
124    /// Settle phases recorded on this guard.
125    #[must_use]
126    pub const fn settled_phases(&self) -> &[SettlePhase] {
127        self.settled_phases.as_slice()
128    }
129
130    /// Before-handler settle receipt, when one was stored.
131    #[must_use]
132    pub const fn before_handler(&self) -> Option<&CompletedSettlement> {
133        self.before_handler.as_ref()
134    }
135
136    /// Fires cancel hooks if not already fired.
137    ///
138    /// When [`SettlePhase::BeforeHandler`] is in [`Self::settled_phases`] and
139    /// the matched scheme's `settle_on_cancel` returns requirements, settles
140    /// at [`SettlePhase::Cancel`].
141    pub async fn cancel(
142        &self,
143        reason: CancelReason,
144        error: Option<&str>,
145        response_status: Option<u16>,
146    ) -> Option<SettleResponse> {
147        if self
148            .fired
149            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
150            .is_err()
151        {
152            return None;
153        }
154        let ctx = VerifiedPaymentCanceledContext {
155            payment: PaymentHookContext {
156                payload: self.payload.clone(),
157                requirements: self.requirements.clone(),
158            },
159            reason,
160            error: error.map(str::to_owned),
161            response_status,
162            settled_phases: self.settled_phases.clone(),
163        };
164        for hook in &self.server.hooks {
165            hook.on_verified_payment_canceled(&ctx).await;
166        }
167        self.server.settle_canceled_payment(&ctx).await
168    }
169
170    /// Returns `true` when cancel has already been dispatched.
171    #[must_use]
172    pub fn has_fired(&self) -> bool {
173        self.fired.load(Ordering::SeqCst)
174    }
175}
176
177/// Settlement receipt attached when the resource handler fails after verify.
178///
179/// Preference: successful cancel settle, then failed cancel with deposit-recovery
180/// extras, then before-handler echo, else `None`.
181#[must_use]
182pub fn build_failure_path_settlement_response(
183    cancel_settlement: Option<&SettleResponse>,
184    before_handler: Option<&CompletedSettlement>,
185    payment_payload: Option<&WirePaymentPayload>,
186) -> Option<SettleResponse> {
187    if let Some(cancel) = cancel_settlement {
188        if cancel.is_success() {
189            return Some(cancel.clone());
190        }
191        return Some(build_failed_cancel_receipt(
192            cancel,
193            before_handler,
194            payment_payload,
195        ));
196    }
197    before_handler.map(|completed| completed.result.clone())
198}
199
200/// Failed cancel receipt with deposit-recovery facts in `extra`.
201fn build_failed_cancel_receipt(
202    cancel: &SettleResponse,
203    before_handler: Option<&CompletedSettlement>,
204    payment_payload: Option<&WirePaymentPayload>,
205) -> SettleResponse {
206    let SettleResponse::Failure {
207        reason,
208        message,
209        payer,
210        network,
211        extensions,
212        extension_responses,
213        extra: cancel_extra,
214        ..
215    } = cancel
216    else {
217        return cancel.clone();
218    };
219
220    let mut extra_map = match cancel_extra {
221        Some(Value::Object(map)) => map.clone(),
222        _ => serde_json::Map::new(),
223    };
224    if let Some(before) = before_handler {
225        extra_map.insert(
226            "depositTransaction".into(),
227            Value::String(settle_response_transaction(&before.result).to_owned()),
228        );
229        extra_map.insert(
230            "depositAmount".into(),
231            Value::String(
232                settle_response_amount(&before.result)
233                    .unwrap_or("")
234                    .to_owned(),
235            ),
236        );
237    }
238    if let Some(payload) = payment_payload
239        && let Some(channel_id) = payload
240            .payload
241            .get("channelId")
242            .and_then(Value::as_str)
243            .filter(|id| !id.is_empty())
244    {
245        extra_map.insert("channelId".into(), Value::String(channel_id.to_owned()));
246    }
247    let merged_extra = if extra_map.is_empty() {
248        None
249    } else {
250        Some(Value::Object(extra_map))
251    };
252    SettleResponse::Failure {
253        reason: reason.clone(),
254        message: message.clone(),
255        payer: payer.clone(),
256        transaction: CompactString::default(),
257        network: network.clone(),
258        extensions: extensions.clone(),
259        extension_responses: extension_responses.clone(),
260        extra: merged_extra,
261    }
262}
263
264fn settle_response_transaction(response: &SettleResponse) -> &str {
265    match response {
266        SettleResponse::Success { transaction, .. }
267        | SettleResponse::Failure { transaction, .. } => transaction.as_str(),
268        _ => "",
269    }
270}
271
272fn settle_response_amount(response: &SettleResponse) -> Option<&str> {
273    match response {
274        SettleResponse::Success { amount, .. } => amount.as_deref(),
275        SettleResponse::Failure { .. } | _ => None,
276    }
277}