Skip to main content

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}