1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
//! The one error every provider reports through.
use std::error::Error as StdError;
use std::fmt;
use crate::provider::ProviderId;
/// What went wrong, in terms a caller can branch on without knowing the provider.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ErrorKind {
/// Credentials were missing, wrong, or not allowed to do this.
Auth,
/// The request was rejected before it reached the card network.
InvalidRequest,
/// The bank or the network refused the payment.
Declined,
/// The payment was not found, or no longer exists.
NotFound,
/// The provider asked us to slow down.
RateLimited,
/// The request never got a usable answer: DNS, TLS, timeout, socket.
Transport,
/// The provider answered, but with something this crate cannot read.
Malformed,
/// The answer could not be shown to have come from the provider.
///
/// A signature that does not match, or one that is missing where the
/// provider always sends it. Not a transport failure and not a decline: it
/// means the message may not be theirs, and it must never be acted on.
Untrusted,
/// The provider does not offer what was asked of it.
Unsupported,
/// The provider failed on its own side.
Provider,
}
impl ErrorKind {
/// Whether replaying the same request unchanged could plausibly succeed.
///
/// # This does not mean the retry is safe
///
/// It says the failure was not a verdict. It says nothing about whether
/// the first attempt took the money — a timeout is exactly the case where
/// nobody knows.
///
/// Replaying a **charge** is safe only where the provider offers
/// idempotency, and not every one does:
///
/// | | replaying a charge |
/// |---|---|
/// | Stripe | safe — `ChargeRequest::idempotency_key` is sent as `Idempotency-Key` |
/// | iyzico | **not documented safe** — it refuses an idempotency key, and does not say what a reused `orderId` does |
/// | PayTR | **not documented safe** — no idempotency mechanism is documented for opening a payment |
/// | Mollie | safe — `ChargeRequest::idempotency_key` is sent as `Idempotency-Key`, and Mollie replays the first answer for an hour |
/// | PayPal | safe — `ChargeRequest::idempotency_key` is sent as `PayPal-Request-Id`, and PayPal returns the first answer for a repeated key |
///
/// Where it is not safe, read the payment back before sending it again.
/// Reading is always safe — but **which call reads it back depends on what
/// you still have.** A charge that answered and then failed later leaves a
/// [`Charge::id`](crate::Charge::id), and
/// [`Provider::charge_status`](crate::Provider::charge_status) takes it. A
/// charge whose *answer never arrived* leaves nothing but the order
/// reference, and that is
/// [`Provider::lookup`](crate::Provider::lookup) — which is exactly the
/// timeout case, and exactly the two providers whose replay is not
/// documented as safe: iyzico and PayTR can both be asked.
///
/// **Replaying a capture is a narrower question, because a capture takes
/// money rather than opening a request for it.**
/// [`Provider::capture`](crate::Provider::capture) carries its own
/// `idempotency`, and what a timeout means depends on whether one was
/// sent:
///
/// | | replaying a capture, with a key | replaying a capture, without one |
/// |---|---|---|
/// | Stripe | safe — sent as `Idempotency-Key`, same as a charge | **not safe** — a second PaymentIntent capture can take the funds twice |
/// | iyzico | n/a — a key is refused outright, because iyzico accepts none | **not safe** — `classic`'s capture is `/payment/postauth`, and iyzico documents no idempotency mechanism for it |
/// | PayTR | n/a — no capture step; the hosted form takes the money as it goes | n/a |
/// | Mollie | safe — sent as `Idempotency-Key` on the captures endpoint, answered from the cache for an hour | **not safe** — a second capture against the same authorisation can take the funds twice |
/// | PayPal | safe — sent as `PayPal-Request-Id`, same as opening an order | **not safe, and PayPal documents it** — a second capture of the same order can take the funds twice |
///
/// Where it is not safe, this table's answer does not change: read the
/// payment back with
/// [`Provider::charge_status`](crate::Provider::charge_status) rather
/// than sending [`Provider::capture`](crate::Provider::capture) again. A
/// capture whose outcome `is_retryable` does not resolve is read back,
/// never resent — with a key, resending is safe but reading is still
/// simpler and costs nothing extra.
#[must_use]
pub const fn is_retryable(self) -> bool {
matches!(self, Self::RateLimited | Self::Transport | Self::Provider)
}
}
impl fmt::Display for ErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text = match self {
Self::Auth => "authentication",
Self::InvalidRequest => "invalid request",
Self::Declined => "declined",
Self::NotFound => "not found",
Self::RateLimited => "rate limited",
Self::Transport => "transport",
Self::Malformed => "malformed response",
Self::Untrusted => "unverified response",
Self::Unsupported => "unsupported",
Self::Provider => "provider failure",
};
f.write_str(text)
}
}
/// A payment operation failed.
#[derive(Debug)]
pub struct Error {
kind: ErrorKind,
provider: ProviderId,
message: Box<str>,
code: Option<Box<str>>,
source: Option<Box<dyn StdError + Send + Sync>>,
}
impl Error {
/// Builds an error attributed to a provider.
pub fn new(kind: ErrorKind, provider: ProviderId, message: impl Into<Box<str>>) -> Self {
Self {
kind,
provider,
message: message.into(),
code: None,
source: None,
}
}
/// Attaches the provider's own error code, verbatim.
#[must_use]
pub fn with_code(mut self, code: impl Into<Box<str>>) -> Self {
self.code = Some(code.into());
self
}
/// Attaches the underlying error.
#[must_use]
pub fn with_source(mut self, source: impl StdError + Send + Sync + 'static) -> Self {
self.source = Some(Box::new(source));
self
}
/// What went wrong.
#[must_use]
pub const fn kind(&self) -> ErrorKind {
self.kind
}
/// Which provider reported it.
#[must_use]
pub const fn provider(&self) -> ProviderId {
self.provider
}
/// The provider's own error code, when it gave one.
#[must_use]
pub fn code(&self) -> Option<&str> {
self.code.as_deref()
}
/// Whether replaying the same request unchanged could plausibly succeed.
#[must_use]
pub const fn is_retryable(&self) -> bool {
self.kind.is_retryable()
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {} ({})", self.provider, self.message, self.kind)?;
if let Some(code) = &self.code {
write!(f, " [{code}]")?;
}
Ok(())
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.source
.as_ref()
.map(|e| &**e as &(dyn StdError + 'static))
}
}