Stripe, behind kasapay's Provider trait.
A thin adapter over async-stripe, which is generated from Stripe's own
OpenAPI document and regenerated weekly. This crate does not re-derive that
work; it maps PaymentIntents onto kasapay's Charge
and gets out of the way.
What maps onto what
- A charge is a PaymentIntent.
ChargeRequest::orderhas no Stripe field of its own, so it travels as metadata under [ORDER_METADATA_KEY]. - A PaymentIntent that still needs the payer comes back as
NextAction::ConfirmOnClientcarrying theclient_secretfor Stripe.js. - [
Stripe::refund] gives money back, and [Stripe::cancel] withdraws a payment that was never captured. Neither is on the shared trait yet. - A refunded PaymentIntent still reads
succeeded, so how much of a payment has been given back is [Stripe::refunds] summed rather than a status to read. - A declined card is not
Status::Failed. Stripe puts such a PaymentIntent back torequires_payment_method, which arrives asStatus::RequiresAction— honest, because the payer can try another card, but a caller waiting forFailedfrom Stripe waits forever. - Anything this crate does not model is reachable through
[
Stripe::client], which hands back theasync-stripeclient itself. - A saved card is a
pm_…, which stands alone rather than needing a second handle beside it — see the [saved] module.Provider::instrumentslists a customer's, the same as [Stripe::stored_cards] with the brand and last four turned into a label; [Stripe::charge_saved_card] charges one without a card number in sight, and [Stripe::forget_card] detaches one.
Retrying
Safe. ChargeRequest::idempotency_key is sent as Stripe's
Idempotency-Key, so replaying a charge with the same key takes the money
once. Without a key, a retry is a second charge.
Provider::capture's own idempotency
travels the same way — a capture is a POST too, and Stripe reads its
Idempotency-Key the same regardless of which endpoint it is sent to.
Without one, a second capture takes the funds a second time.
Which async-stripe
Exactly 1.0.0-rc.8, pinned rather than ranged. The 1.0 line is a
prerelease and a candidate has changed generated types before —
PaymentIntent gained a public field between rc.6 and rc.7 — so which one
this is built against is not left to resolution.
It costs a caller one thing: a crate that depends on async-stripe itself
has to be on the same candidate, because two exact pins at different
candidates cannot resolve together. [Stripe::client] hands the client back
so that reaching what this crate does not model needs no second dependency.
The pin becomes a range the day 1.0.0 ships.
Example
use kasapay_core::{ChargeRequest, Currency, Money, OrderRef, Provider, Secret};
use kasapay_stripe::Stripe;
# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let stripe = Stripe::new(&Secret::new(std::env::var("STRIPE_SECRET_KEY")?));
let request = ChargeRequest::builder(
OrderRef::new("ord-2026-0001"),
Money::parse("19.99", Currency::Usd)?,
)
.description("one coffee")
.build()?;
let charge = stripe.charge(&request).await?;
println!("{:?} {:?}", charge.status, charge.next_action);
# Ok(())
# }