kasapay_stripe/lib.rs
1//! Stripe, behind kasapay's [`Provider`](kasapay_core::Provider) trait.
2//!
3//! A thin adapter over [`async-stripe`], which is generated from Stripe's own
4//! OpenAPI document and regenerated weekly. This crate does not re-derive that
5//! work; it maps PaymentIntents onto kasapay's [`Charge`](kasapay_core::Charge)
6//! and gets out of the way.
7//!
8//! # What maps onto what
9//!
10//! - A charge is a PaymentIntent. `ChargeRequest::order` has no Stripe field of
11//! its own, so it travels as metadata under [`ORDER_METADATA_KEY`].
12//! - A PaymentIntent that still needs the payer comes back as
13//! [`NextAction::ConfirmOnClient`](kasapay_core::NextAction::ConfirmOnClient)
14//! carrying the `client_secret` for Stripe.js.
15//! - [`Stripe::refund`] gives money back, and [`Stripe::cancel`] withdraws a
16//! payment that was never captured. Neither is on the shared trait yet.
17//! - A refunded PaymentIntent still reads `succeeded`, so how much of a
18//! payment has been given back is [`Stripe::refunds`] summed rather than a
19//! status to read.
20//! - **A declined card is not [`Status::Failed`](kasapay_core::Status::Failed).**
21//! Stripe puts such a PaymentIntent back to `requires_payment_method`, which
22//! arrives as [`Status::RequiresAction`](kasapay_core::Status::RequiresAction)
23//! — honest, because the payer can try another card, but a caller waiting
24//! for `Failed` from Stripe waits forever.
25//! - Anything this crate does not model is reachable through
26//! [`Stripe::client`], which hands back the `async-stripe` client itself.
27//! - A saved card is a `pm_…`, which stands alone rather than needing a
28//! second handle beside it — see the [`saved`] module.
29//! [`Provider::instruments`](kasapay_core::Provider::instruments) lists a
30//! customer's, the same as [`Stripe::stored_cards`] with the brand and last
31//! four turned into a label; [`Stripe::charge_saved_card`] charges one
32//! without a card number in sight, and [`Stripe::forget_card`] detaches one.
33//!
34//! # Retrying
35//!
36//! Safe. `ChargeRequest::idempotency_key` is sent as Stripe's
37//! `Idempotency-Key`, so replaying a charge with the same key takes the money
38//! once. Without a key, a retry is a second charge.
39//!
40//! [`Provider::capture`](kasapay_core::Provider::capture)'s own `idempotency`
41//! travels the same way — a capture is a `POST` too, and Stripe reads its
42//! `Idempotency-Key` the same regardless of which endpoint it is sent to.
43//! Without one, a second capture takes the funds a second time.
44//!
45//! # Which `async-stripe`
46//!
47//! Exactly `1.0.0-rc.8`, pinned rather than ranged. The 1.0 line is a
48//! prerelease and a candidate has changed generated types before —
49//! `PaymentIntent` gained a public field between rc.6 and rc.7 — so which one
50//! this is built against is not left to resolution.
51//!
52//! It costs a caller one thing: a crate that depends on `async-stripe` itself
53//! has to be on the same candidate, because two exact pins at different
54//! candidates cannot resolve together. [`Stripe::client`] hands the client back
55//! so that reaching what this crate does not model needs no second dependency.
56//! The pin becomes a range the day 1.0.0 ships.
57//!
58//! # Example
59//!
60//! ```no_run
61//! use kasapay_core::{ChargeRequest, Currency, Money, OrderRef, Provider, Secret};
62//! use kasapay_stripe::Stripe;
63//!
64//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
65//! let stripe = Stripe::new(&Secret::new(std::env::var("STRIPE_SECRET_KEY")?));
66//!
67//! let request = ChargeRequest::builder(
68//! OrderRef::new("ord-2026-0001"),
69//! Money::parse("19.99", Currency::Usd)?,
70//! )
71//! .description("one coffee")
72//! .build()?;
73//!
74//! let charge = stripe.charge(&request).await?;
75//! println!("{:?} {:?}", charge.status, charge.next_action);
76//! # Ok(())
77//! # }
78//! ```
79//!
80//! [`async-stripe`]: https://docs.rs/async-stripe
81
82mod client;
83mod convert;
84pub mod saved;
85mod webhook;
86
87#[doc(inline)]
88pub use crate::client::{
89 DEFAULT_TIMEOUT, ORDER_METADATA_KEY, REFUND_REASON_METADATA_KEY, Refund, RefundState, Stripe,
90};
91#[doc(inline)]
92pub use crate::webhook::{DEFAULT_TOLERANCE, Webhooks};