One payment API over any payment provider.
Write against [Provider] and which provider takes the money becomes a
deployment decision rather than a rewrite. Five ship with this workspace —
Stripe, iyzico, PayTR, Mollie and PayPal — and a provider that lives
elsewhere is a first-class one: implement [Provider], name it with
[ProviderId::new]. Everything a caller needs is re-exported here; the
bundled adapters are behind features, one each.
= { = "0.0.4", = ["stripe", "iyzico"] }
The one thing to understand first
[Provider::charge] does not mean the money moved. It returns a [Charge]
whose [Status] is often [Status::RequiresAction], with a [NextAction]
saying what the payer must do — confirm in the browser for Stripe, follow a
deep link into iyzico's app for iyzico. Treating a returned Charge as a
completed payment is the mistake this crate is shaped to prevent.
What the trait answers
[Provider] is charge, charge_status, capture, cancel, refund,
lookup and instruments, and [Capabilities] says which of them a given
provider actually does — before there is a payment to ask about. A
capability that says yes and a call that then fails is a bug in the
adapter.
Giving money back is [Provider::refund], which answers a [Refund]
with its own life: its own identifier where the provider issues one, its
own [RefundStatus], and at iyzico's counter its own [NextAction],
because there the payer approves the refund in an app. [Status] has no
Refunded and will not grow one — no provider reports a refund as a
payment's status.
The call whose answer never arrived is [Provider::lookup], keyed by
[OrderRef] — the caller's own reference, which they had before they sent
anything. Ok(None) means the provider has no record and the charge can
safely be sent again. Two of the five can answer it; the rest say what to
do instead.
What arrives without being asked for
[Webhook] is the second trait: it takes the headers and bytes of a
[Delivery], shows they are the provider's, and says what they mean as an
[Event].
use kasapay::{Delivery, EventKind, Webhook};
# async fn handle(verifier: &dyn Webhook, headers: &[(&str, &str)], body: &[u8]) -> &'static str {
match verifier.verify(&Delivery::new(headers, body)).await {
// The identifier goes into a unique index before anything ships: the
// second delivery of an event must collide rather than ship twice.
Ok(event) if event.kind == EventKind::Captured => "ship it",
// Not an error. A provider adding an event type is normal, and
// refusing one earns days of redeliveries for something nobody wanted.
Ok(_) => "acknowledged",
// Do not act on it — and still answer the provider what the provider
// documents. Those are two different questions.
Err(_) => "acknowledged",
}
# }
Choosing a provider at runtime
use std::sync::Arc;
use kasapay::{Provider, ProviderId};
# #[cfg(all(feature = "stripe", feature = "iyzico"))]
# fn pick(
# id: ProviderId,
# stripe: kasapay::stripe::Stripe,
# iyzico: kasapay::iyzico::in_store::Client,
# )
# -> Option<Arc<dyn Provider>> {
match id {
ProviderId::STRIPE => Some(Arc::new(stripe)),
ProviderId::IYZICO => Some(Arc::new(iyzico)),
_ => None,
}
# }