mod client;
mod events;
use autumn_web::AppState;
use autumn_web::webhook::WebhookEndpointConfig;
use crate::config::StripeConfig;
use crate::error::BillingError;
use crate::event::BillingEvent;
use crate::model::ProviderId;
use crate::provider::{
BillingProvider, CheckoutRequest, CustomerRequest, HostedSession, PaymentAttemptOutcome,
PortalRequest, ProviderFuture,
};
pub const PROVIDER_NAME: &str = "stripe";
pub struct StripeProvider {
config: StripeConfig,
client: autumn_web::http::Client,
}
impl std::fmt::Debug for StripeProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StripeProvider")
.field("api_base", &self.config.api_base)
.finish_non_exhaustive()
}
}
impl StripeProvider {
pub const NAME: &'static str = PROVIDER_NAME;
pub fn from_state(state: &AppState, config: &StripeConfig) -> Result<Self, BillingError> {
let client = autumn_web::http::Client::from_state(state).named(PROVIDER_NAME);
Self::new(config.clone(), client)
}
#[allow(
clippy::needless_pass_by_value,
reason = "public signature; `with_base_url` borrows and returns a fresh client"
)]
pub fn new(
config: StripeConfig,
client: autumn_web::http::Client,
) -> Result<Self, BillingError> {
if config
.secret_key
.as_ref()
.is_none_or(crate::config::SecretString::is_empty)
{
return Err(BillingError::Config(format!(
"{} is not set",
crate::config::STRIPE_SECRET_KEY_ENV
)));
}
let client = client.with_base_url(&config.api_base);
Ok(Self { config, client })
}
#[doc(hidden)]
pub fn parse_event_body(raw: &[u8]) -> Result<BillingEvent, BillingError> {
events::parse(raw)
}
}
impl BillingProvider for StripeProvider {
fn name(&self) -> &'static str {
PROVIDER_NAME
}
fn webhook_endpoint(
&self,
name: &str,
path: &str,
) -> Result<WebhookEndpointConfig, BillingError> {
let secret = self
.config
.webhook_secret
.as_ref()
.filter(|s| !s.is_empty())
.ok_or_else(|| {
BillingError::Config(format!(
"{} is not set",
crate::config::STRIPE_WEBHOOK_SECRET_ENV
))
})?;
let mut endpoint = WebhookEndpointConfig::stripe(name, path, secret.expose());
endpoint.max_body_bytes = crate::config::WEBHOOK_MAX_BODY_BYTES;
Ok(endpoint)
}
fn create_customer(&self, request: CustomerRequest) -> ProviderFuture<'_, ProviderId> {
Box::pin(self.customer(request))
}
fn create_checkout(&self, request: CheckoutRequest) -> ProviderFuture<'_, HostedSession> {
Box::pin(self.checkout(request))
}
fn create_portal(&self, request: PortalRequest) -> ProviderFuture<'_, HostedSession> {
Box::pin(self.portal(request))
}
fn parse_event(&self, raw: &[u8]) -> Result<BillingEvent, BillingError> {
Self::parse_event_body(raw)
}
fn retry_invoice_payment<'a>(
&'a self,
invoice: &'a ProviderId,
idempotency_key: &'a str,
) -> ProviderFuture<'a, PaymentAttemptOutcome> {
Box::pin(self.pay_invoice(invoice, idempotency_key))
}
fn cancel_subscription<'a>(&'a self, subscription: &'a ProviderId) -> ProviderFuture<'a, ()> {
Box::pin(self.cancel(subscription))
}
fn fetch_subscription<'a>(
&'a self,
subscription: &'a ProviderId,
) -> ProviderFuture<'a, Option<crate::event::SubscriptionSnapshot>> {
Box::pin(async move {
self.subscription(subscription)
.await?
.map(events::parse_subscription_object)
.transpose()
})
}
}