kasapay_core/id.rs
1//! How a provider names one thing, and the two questions asked of every one.
2
3use std::cmp::Ordering;
4use std::fmt;
5use std::hash::{Hash, Hasher};
6use std::marker::PhantomData;
7
8/// Whose uniqueness an identifier rests on.
9///
10/// One question, asked of every identifier kasapay hands back: did the provider
11/// give us this, or did we make it up? A caller writing an identifier into a
12/// unique index — so a second webhook delivery collides instead of shipping
13/// twice — is relying on somebody's guarantee, and the two answers are worth
14/// very different things.
15///
16/// Exhaustive on purpose. There is no third answer, and an adapter that adds
17/// one has invented a guarantee nobody made.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
19pub enum IdSource {
20 /// The provider issued it, and it is unique because they say so.
21 Provider,
22 /// kasapay composed it out of the named fields, because the provider issues
23 /// none of its own. It is unique exactly as far as those fields are.
24 Derived(&'static [&'static str]),
25}
26
27/// What an identifier names — the other question, answered in the type.
28///
29/// A kind carries no data. It exists so that a payment id and a hosted form's
30/// token, both of them the provider's own and both of them a string, are not
31/// the same type and cannot be handed to each other's calls. An adapter names
32/// its own by implementing this on a unit struct of its own and writing a type
33/// alias over [`Id`], the way [`PaymentId`] is one over [`kind::Payment`].
34pub trait IdKind {
35 /// What this kind names, in words, for [`Debug`](fmt::Debug).
36 const NAMES: &'static str;
37}
38
39/// What an identifier can name.
40///
41/// A provider adapter adds its own where the concept is that provider's: a
42/// hosted checkout form belongs to iyzico's classic API rather than here.
43pub mod kind {
44 /// A payment at the provider.
45 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
46 pub struct Payment;
47
48 impl super::IdKind for Payment {
49 const NAMES: &'static str = "payment";
50 }
51
52 /// One instrument the provider holds and can charge again.
53 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
54 pub struct Instrument;
55
56 impl super::IdKind for Instrument {
57 const NAMES: &'static str = "saved instrument";
58 }
59
60 /// One refund taken off a payment.
61 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
62 pub struct Refund;
63
64 impl super::IdKind for Refund {
65 const NAMES: &'static str = "refund";
66 }
67
68 /// One delivery a provider made to a webhook address.
69 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
70 pub struct Event;
71
72 impl super::IdKind for Event {
73 const NAMES: &'static str = "webhook event";
74 }
75}
76
77/// How a provider names one thing of kind `K`.
78///
79/// Opaque on purpose: Stripe issues `pi_…`, iyzico a 64-bit integer, PayTR
80/// nothing at all, and nothing outside the adapter should read any of them.
81/// [`Display`](fmt::Display) writes the text alone, because that is what goes
82/// into a request.
83///
84/// Two facts travel with the text. `K` is what the identifier names, and it is
85/// checked by the compiler: a [`PaymentId`] cannot be passed where the token of
86/// a hosted checkout form is wanted, however alike the two strings look.
87/// [`Id::source`] is whose uniqueness it rests on — the provider's, or
88/// kasapay's own composition of fields the caller sent.
89///
90/// ```compile_fail
91/// use kasapay_core::{Id, IdKind, PaymentId};
92///
93/// struct Session;
94/// impl IdKind for Session {
95/// const NAMES: &'static str = "session";
96/// }
97///
98/// fn refund(payment: &PaymentId) {
99/// println!("{payment}");
100/// }
101///
102/// // A session is not a payment, and this does not compile.
103/// refund(&Id::<Session>::issued("tok-1"));
104/// ```
105pub struct Id<K: IdKind> {
106 key: Box<str>,
107 source: IdSource,
108 kind: PhantomData<K>,
109}
110
111impl<K: IdKind> Id<K> {
112 /// Wraps an identifier the provider issued.
113 pub fn issued(value: impl Into<Box<str>>) -> Self {
114 Self {
115 key: value.into(),
116 source: IdSource::Provider,
117 kind: PhantomData,
118 }
119 }
120
121 /// Wraps an identifier kasapay composed, naming the fields it came from.
122 ///
123 /// For a provider that issues none of its own: PayTR names a payment by
124 /// the `merchant_oid` the merchant chose and sent. `from` is what the
125 /// value's uniqueness actually rests on, and a caller reads it back
126 /// through [`Id::source`].
127 pub fn derived(value: impl Into<Box<str>>, from: &'static [&'static str]) -> Self {
128 Self {
129 key: value.into(),
130 source: IdSource::Derived(from),
131 kind: PhantomData,
132 }
133 }
134
135 /// The identifier as text.
136 #[must_use]
137 pub fn as_str(&self) -> &str {
138 &self.key
139 }
140
141 /// Whose uniqueness this identifier rests on.
142 #[must_use]
143 pub const fn source(&self) -> IdSource {
144 self.source
145 }
146}
147
148/// How the provider names a payment.
149///
150/// [`Charge::id`](crate::Charge::id) carries one, and
151/// [`Provider::charge_status`](crate::Provider::charge_status) takes one. A
152/// handle to something that is not a payment — the token of a checkout form the
153/// payer has not finished — is a different kind and will not fit here.
154pub type PaymentId = Id<kind::Payment>;
155
156/// How the provider names one refund, where it names it at all.
157///
158/// Three of the five providers here issue one — Stripe's `re_…`, Mollie's
159/// `re_…`, PayPal's own — and iyzico issues none: its refund answers the
160/// bank's `hostReference` and nothing else, which exists only once the money
161/// has gone and so cannot make the attempt idempotent. That is why
162/// [`Refund::id`](crate::Refund::id) is an `Option` rather than this type
163/// carrying a composed value in the field a real one lives in.
164pub type RefundId = Id<kind::Refund>;
165
166/// How one delivery to a webhook address is named.
167///
168/// What a caller writes into a unique index before acting on a delivery, so
169/// that the second copy of it collides instead of shipping the order again.
170/// Whether that index is a guarantee or a heuristic is
171/// [`Id::source`]: Stripe issues `evt_…` and PayPal `WH-…`, while PayTR and
172/// Mollie issue nothing and kasapay composes one out of the fields they did
173/// send — which is unique exactly as far as those fields are.
174pub type EventId = Id<kind::Event>;
175
176/// How the provider names one card it holds, so a payment need not carry one.
177///
178/// The provider keeps the card; the caller keeps this. Charging it sends the
179/// handle where a card number would otherwise go, which is the only reason a
180/// returning customer can be charged without anybody's server touching a
181/// number: Stripe issues `pm_…`, iyzico a `cardToken`, PayTR a `ctoken`.
182///
183/// # It is half of the name at two of the three
184///
185/// iyzico's `cardToken` means nothing without the `cardUserKey` whose vault it
186/// sits in, and PayTR's `ctoken` nothing without its `utoken`. That other half
187/// is the payer, and kasapay already has somewhere to put it —
188/// [`ChargeRequest::customer`](crate::ChargeRequest::customer). Stripe's
189/// `pm_…` stands alone.
190///
191/// # Nothing here creates one
192///
193/// kasapay charges a saved instrument and does not store a card, because
194/// storing one is where the card number is. iyzico's vault is filled by
195/// `POST /cardstorage/card`, which wants the number; Stripe's by
196/// `stripe.createPaymentMethod` in the payer's browser, which is the caller's
197/// page rather than their server. Either way the handle arrives from outside
198/// this library, and [`Id::issued`] is how it comes in.
199pub type InstrumentId = Id<kind::Instrument>;
200
201// Written out rather than derived: a derive would demand the same trait of `K`.
202impl<K: IdKind> fmt::Debug for Id<K> {
203 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204 f.debug_struct("Id")
205 .field("names", &K::NAMES)
206 .field("key", &self.key)
207 .field("source", &self.source)
208 .finish()
209 }
210}
211
212impl<K: IdKind> fmt::Display for Id<K> {
213 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214 f.write_str(&self.key)
215 }
216}
217
218impl<K: IdKind> Clone for Id<K> {
219 fn clone(&self) -> Self {
220 Self {
221 key: self.key.clone(),
222 source: self.source,
223 kind: PhantomData,
224 }
225 }
226}
227
228impl<K: IdKind> PartialEq for Id<K> {
229 fn eq(&self, other: &Self) -> bool {
230 self.key == other.key && self.source == other.source
231 }
232}
233
234impl<K: IdKind> Eq for Id<K> {}
235
236impl<K: IdKind> Hash for Id<K> {
237 fn hash<H: Hasher>(&self, state: &mut H) {
238 self.key.hash(state);
239 self.source.hash(state);
240 }
241}
242
243impl<K: IdKind> PartialOrd for Id<K> {
244 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
245 Some(self.cmp(other))
246 }
247}
248
249impl<K: IdKind> Ord for Id<K> {
250 fn cmp(&self, other: &Self) -> Ordering {
251 self.key
252 .cmp(&other.key)
253 .then_with(|| self.source.cmp(&other.source))
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::PaymentId;
260
261 #[test]
262 fn an_identifier_we_composed_is_not_one_the_provider_issued() {
263 let issued = PaymentId::issued("ord-1");
264 let composed = PaymentId::derived("ord-1", &["merchant_oid"]);
265 assert_eq!(issued.as_str(), composed.as_str());
266 assert_ne!(issued, composed);
267 assert_ne!(issued.source(), composed.source());
268 }
269
270 #[test]
271 fn debug_says_what_the_identifier_names() {
272 let shown = format!("{:?}", PaymentId::issued("pi_1"));
273 assert!(shown.contains("payment"), "{shown}");
274 assert!(shown.contains("pi_1"), "{shown}");
275 }
276}