Skip to main content

r402_server/hooks/
resource.rs

1//! Resource-server lifecycle hooks.
2
3use std::fmt::{self, Debug, Formatter};
4use std::future::Future;
5
6use compact_str::CompactString;
7use r402_facilitator::{BoxFuture, FailureRecovery};
8use r402_protocol::error::FacilitatorError;
9use r402_protocol::payment::{
10    Extensions, PaymentPayload, PaymentRequirements, SettleResponse, VerifyResponse,
11};
12
13use crate::payment_flow::SettlePhase;
14
15/// Wire payment payload with typed requirements and opaque scheme body.
16pub type WirePaymentPayload = PaymentPayload<PaymentRequirements, serde_json::Value>;
17
18/// Why a verified payment was canceled before settlement.
19///
20/// Wire-stable `snake_case` labels: `handler_threw` / `handler_failed` /
21/// `after_verify_aborted`.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23#[non_exhaustive]
24pub enum CancelReason {
25    /// Protected handler panicked or returned a transport error.
26    HandlerThrew,
27    /// Protected handler completed with a failing status (≥ 400).
28    HandlerFailed,
29    /// An `after_verify` hook aborted after a successful verify.
30    AfterVerifyAborted,
31}
32
33impl CancelReason {
34    /// Stable machine-readable label.
35    #[must_use]
36    pub const fn as_str(self) -> &'static str {
37        match self {
38            Self::HandlerThrew => "handler_threw",
39            Self::HandlerFailed => "handler_failed",
40            Self::AfterVerifyAborted => "after_verify_aborted",
41        }
42    }
43}
44
45impl fmt::Display for CancelReason {
46    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
47        f.write_str(self.as_str())
48    }
49}
50
51/// Decision from a "before verify/settle" hook.
52#[derive(Debug)]
53#[non_exhaustive]
54pub enum BeforeOpDecision<T> {
55    /// Proceed with the facilitator call.
56    Continue,
57    /// Abort the operation with a structured reason.
58    Abort {
59        /// Machine-readable reason.
60        reason: String,
61        /// Human-readable description.
62        message: String,
63    },
64    /// Short-circuit: use this local result instead of calling the facilitator.
65    Skip {
66        /// Locally produced response.
67        result: T,
68    },
69}
70
71/// In-process directive when an after-verify hook skips the resource handler.
72///
73/// Never appears on the facilitator wire; transports may use `body` as the
74/// success response when settling inline.
75#[derive(Debug, Clone, Default)]
76#[non_exhaustive]
77pub struct SkipHandlerDirective {
78    /// Optional content type for the transport response body.
79    pub content_type: Option<String>,
80    /// Optional JSON body for the transport success response.
81    pub body: Option<serde_json::Value>,
82}
83
84impl SkipHandlerDirective {
85    /// Empty directive (settle inline, default success body).
86    #[must_use]
87    pub const fn empty() -> Self {
88        Self {
89            content_type: None,
90            body: None,
91        }
92    }
93
94    /// Builder: attach a content type.
95    #[must_use]
96    pub fn with_content_type(mut self, content_type: impl Into<String>) -> Self {
97        self.content_type = Some(content_type.into());
98        self
99    }
100
101    /// Builder: attach a JSON body.
102    #[must_use]
103    pub fn with_body(mut self, body: serde_json::Value) -> Self {
104        self.body = Some(body);
105        self
106    }
107}
108
109/// Decision from an after-verify hook.
110#[derive(Debug, Clone, Default)]
111#[non_exhaustive]
112pub enum AfterVerifyDecision {
113    /// Continue to the resource handler (default).
114    #[default]
115    Continue,
116    /// Fail closed: fire cancel (`after_verify_aborted`) and reject payment.
117    Abort {
118        /// Machine-readable reason.
119        reason: String,
120        /// Human-readable description.
121        message: String,
122    },
123    /// Bypass the resource handler; transport should settle inline.
124    SkipHandler {
125        /// Optional success body for the transport.
126        response: SkipHandlerDirective,
127    },
128}
129
130/// Shared context for resource-server payment hooks.
131#[derive(Clone)]
132#[non_exhaustive]
133pub struct PaymentHookContext {
134    /// Client payment payload.
135    pub payload: WirePaymentPayload,
136    /// Matched payment requirements.
137    pub requirements: PaymentRequirements,
138}
139
140impl Debug for PaymentHookContext {
141    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
142        f.debug_struct("PaymentHookContext")
143            .field("requirements", &self.requirements)
144            .finish_non_exhaustive()
145    }
146}
147
148impl PaymentHookContext {
149    /// Constructs a payment hook context.
150    #[must_use]
151    pub const fn new(payload: WirePaymentPayload, requirements: PaymentRequirements) -> Self {
152        Self {
153            payload,
154            requirements,
155        }
156    }
157}
158
159/// Context for after-verify hooks (includes facilitator result).
160#[derive(Clone)]
161#[non_exhaustive]
162pub struct VerifyResultContext {
163    /// Base payment context.
164    pub payment: PaymentHookContext,
165    /// Facilitator (or skip/recover) verify response — always a success path
166    /// entry (`Valid` or recovered result passed to after hooks).
167    pub result: VerifyResponse,
168}
169
170impl Debug for VerifyResultContext {
171    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
172        f.debug_struct("VerifyResultContext")
173            .field("payment", &self.payment)
174            .field("result_valid", &self.result.is_valid())
175            .finish_non_exhaustive()
176    }
177}
178
179/// Context for before-settle / settle-failure hooks.
180#[derive(Clone)]
181#[non_exhaustive]
182pub struct SettleContext {
183    /// Base payment context.
184    pub payment: PaymentHookContext,
185    /// Extension IDs declared on the 402 for this settle.
186    pub declared_extensions: Extensions,
187    /// Which settle invocation is running.
188    pub phase: SettlePhase,
189    /// Resource URL from the 402 / request, when known.
190    pub resource_url: Option<CompactString>,
191}
192
193impl Debug for SettleContext {
194    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
195        f.debug_struct("SettleContext")
196            .field("phase", &self.phase)
197            .field("payment", &self.payment)
198            .finish_non_exhaustive()
199    }
200}
201
202impl SettleContext {
203    /// Constructs a settle context with empty declared extensions.
204    #[must_use]
205    pub fn new(payment: PaymentHookContext, phase: SettlePhase) -> Self {
206        Self {
207            payment,
208            declared_extensions: Extensions::new(),
209            phase,
210            resource_url: None,
211        }
212    }
213}
214
215/// Context for after-settle hooks.
216#[derive(Clone)]
217#[non_exhaustive]
218pub struct SettleResultContext {
219    /// Settle invocation that produced `result`.
220    pub settle: SettleContext,
221    /// Facilitator settle response.
222    pub result: SettleResponse,
223}
224
225impl Debug for SettleResultContext {
226    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
227        f.debug_struct("SettleResultContext")
228            .field("settle", &self.settle)
229            .field("result_success", &self.result.is_success())
230            .finish_non_exhaustive()
231    }
232}
233
234/// Context for verified-payment cancellation.
235#[derive(Clone)]
236#[non_exhaustive]
237pub struct VerifiedPaymentCanceledContext {
238    /// Base payment context.
239    pub payment: PaymentHookContext,
240    /// Cancellation reason.
241    pub reason: CancelReason,
242    /// Optional error message from the handler / hook.
243    pub error: Option<String>,
244    /// Optional transport status from a failed handler response.
245    pub response_status: Option<u16>,
246    /// Settle phases already completed for this payment.
247    pub settled_phases: Vec<SettlePhase>,
248}
249
250impl Debug for VerifiedPaymentCanceledContext {
251    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
252        f.debug_struct("VerifiedPaymentCanceledContext")
253            .field("reason", &self.reason)
254            .field("response_status", &self.response_status)
255            .field("settled_phases", &self.settled_phases)
256            .finish_non_exhaustive()
257    }
258}
259
260impl VerifiedPaymentCanceledContext {
261    /// Constructs a cancel context.
262    #[must_use]
263    pub const fn new(
264        payment: PaymentHookContext,
265        reason: CancelReason,
266        settled_phases: Vec<SettlePhase>,
267    ) -> Self {
268        Self {
269            payment,
270            reason,
271            error: None,
272            response_status: None,
273            settled_phases,
274        }
275    }
276}
277
278/// Lifecycle hooks for the resource server (transport-agnostic).
279///
280/// All methods default to no-ops. Override only what you need.
281pub trait ResourceServerHooks: Send + Sync {
282    /// Runs before facilitator verify.
283    fn before_verify<'a>(
284        &'a self,
285        _ctx: &'a PaymentHookContext,
286    ) -> impl Future<Output = BeforeOpDecision<VerifyResponse>> + Send + 'a {
287        async { BeforeOpDecision::Continue }
288    }
289
290    /// Runs after a successful verify (including skip / failure recovery).
291    fn after_verify<'a>(
292        &'a self,
293        _ctx: &'a VerifyResultContext,
294    ) -> impl Future<Output = AfterVerifyDecision> + Send + 'a {
295        async { AfterVerifyDecision::Continue }
296    }
297
298    /// Runs when facilitator verify returns an error.
299    fn on_verify_failure<'a>(
300        &'a self,
301        _ctx: &'a PaymentHookContext,
302        _error: &'a FacilitatorError,
303    ) -> impl Future<Output = FailureRecovery<VerifyResponse>> + Send + 'a {
304        async { FailureRecovery::Propagate }
305    }
306
307    /// Runs before facilitator settle.
308    fn before_settle<'a>(
309        &'a self,
310        _ctx: &'a SettleContext,
311    ) -> impl Future<Output = BeforeOpDecision<SettleResponse>> + Send + 'a {
312        async { BeforeOpDecision::Continue }
313    }
314
315    /// Runs after a successful settle (including skip / failure recovery).
316    fn after_settle<'a>(
317        &'a self,
318        _ctx: &'a SettleResultContext,
319    ) -> impl Future<Output = ()> + Send + 'a {
320        async {}
321    }
322
323    /// Runs when facilitator settle returns an error.
324    fn on_settle_failure<'a>(
325        &'a self,
326        _ctx: &'a SettleContext,
327        _error: &'a FacilitatorError,
328    ) -> impl Future<Output = FailureRecovery<SettleResponse>> + Send + 'a {
329        async { FailureRecovery::Propagate }
330    }
331
332    /// Runs when a verified payment will not be settled.
333    fn on_verified_payment_canceled<'a>(
334        &'a self,
335        _ctx: &'a VerifiedPaymentCanceledContext,
336    ) -> impl Future<Output = ()> + Send + 'a {
337        async {}
338    }
339}
340
341/// Object-safe erasure of [`ResourceServerHooks`].
342pub trait DynResourceServerHooks: Send + Sync {
343    /// See [`ResourceServerHooks::before_verify`].
344    fn before_verify<'a>(
345        &'a self,
346        ctx: &'a PaymentHookContext,
347    ) -> BoxFuture<'a, BeforeOpDecision<VerifyResponse>>;
348
349    /// See [`ResourceServerHooks::after_verify`].
350    fn after_verify<'a>(
351        &'a self,
352        ctx: &'a VerifyResultContext,
353    ) -> BoxFuture<'a, AfterVerifyDecision>;
354
355    /// See [`ResourceServerHooks::on_verify_failure`].
356    fn on_verify_failure<'a>(
357        &'a self,
358        ctx: &'a PaymentHookContext,
359        error: &'a FacilitatorError,
360    ) -> BoxFuture<'a, FailureRecovery<VerifyResponse>>;
361
362    /// See [`ResourceServerHooks::before_settle`].
363    fn before_settle<'a>(
364        &'a self,
365        ctx: &'a SettleContext,
366    ) -> BoxFuture<'a, BeforeOpDecision<SettleResponse>>;
367
368    /// See [`ResourceServerHooks::after_settle`].
369    fn after_settle<'a>(&'a self, ctx: &'a SettleResultContext) -> BoxFuture<'a, ()>;
370
371    /// See [`ResourceServerHooks::on_settle_failure`].
372    fn on_settle_failure<'a>(
373        &'a self,
374        ctx: &'a SettleContext,
375        error: &'a FacilitatorError,
376    ) -> BoxFuture<'a, FailureRecovery<SettleResponse>>;
377
378    /// See [`ResourceServerHooks::on_verified_payment_canceled`].
379    fn on_verified_payment_canceled<'a>(
380        &'a self,
381        ctx: &'a VerifiedPaymentCanceledContext,
382    ) -> BoxFuture<'a, ()>;
383}
384
385impl<T: ResourceServerHooks + ?Sized> DynResourceServerHooks for T {
386    fn before_verify<'a>(
387        &'a self,
388        ctx: &'a PaymentHookContext,
389    ) -> BoxFuture<'a, BeforeOpDecision<VerifyResponse>> {
390        Box::pin(<Self as ResourceServerHooks>::before_verify(self, ctx))
391    }
392
393    fn after_verify<'a>(
394        &'a self,
395        ctx: &'a VerifyResultContext,
396    ) -> BoxFuture<'a, AfterVerifyDecision> {
397        Box::pin(<Self as ResourceServerHooks>::after_verify(self, ctx))
398    }
399
400    fn on_verify_failure<'a>(
401        &'a self,
402        ctx: &'a PaymentHookContext,
403        error: &'a FacilitatorError,
404    ) -> BoxFuture<'a, FailureRecovery<VerifyResponse>> {
405        Box::pin(<Self as ResourceServerHooks>::on_verify_failure(
406            self, ctx, error,
407        ))
408    }
409
410    fn before_settle<'a>(
411        &'a self,
412        ctx: &'a SettleContext,
413    ) -> BoxFuture<'a, BeforeOpDecision<SettleResponse>> {
414        Box::pin(<Self as ResourceServerHooks>::before_settle(self, ctx))
415    }
416
417    fn after_settle<'a>(&'a self, ctx: &'a SettleResultContext) -> BoxFuture<'a, ()> {
418        Box::pin(<Self as ResourceServerHooks>::after_settle(self, ctx))
419    }
420
421    fn on_settle_failure<'a>(
422        &'a self,
423        ctx: &'a SettleContext,
424        error: &'a FacilitatorError,
425    ) -> BoxFuture<'a, FailureRecovery<SettleResponse>> {
426        Box::pin(<Self as ResourceServerHooks>::on_settle_failure(
427            self, ctx, error,
428        ))
429    }
430
431    fn on_verified_payment_canceled<'a>(
432        &'a self,
433        ctx: &'a VerifiedPaymentCanceledContext,
434    ) -> BoxFuture<'a, ()> {
435        Box::pin(<Self as ResourceServerHooks>::on_verified_payment_canceled(
436            self, ctx,
437        ))
438    }
439}