use std::str::FromStr as _; use std::sync::Arc; use anyhow::{Context as _, Result, anyhow}; use async_trait::async_trait; use serde::{Deserialize, Serialize}; use stripe::{ CancellationDetails, CancellationDetailsReason, CheckoutSession, CheckoutSessionMode, CheckoutSessionPaymentMethodCollection, CreateCheckoutSession, CreateCheckoutSessionLineItems, CreateCheckoutSessionSubscriptionData, CreateCheckoutSessionSubscriptionDataTrialSettings, CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior, CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod, CreateCustomer, Customer, CustomerId, ListCustomers, Price, PriceId, Recurring, Subscription, SubscriptionId, SubscriptionItem, SubscriptionItemId, UpdateSubscriptionItems, UpdateSubscriptionTrialSettings, UpdateSubscriptionTrialSettingsEndBehavior, UpdateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, }; use crate::stripe_client::{ CreateCustomerParams, StripeCancellationDetails, StripeCancellationDetailsReason, StripeCheckoutSession, StripeCheckoutSessionMode, StripeCheckoutSessionPaymentMethodCollection, StripeClient, StripeCreateCheckoutSessionLineItems, StripeCreateCheckoutSessionParams, StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams, StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeMeter, StripePrice, StripePriceId, StripePriceRecurring, StripeSubscription, StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId, StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior, StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateSubscriptionParams, }; pub struct RealStripeClient { client: Arc, } impl RealStripeClient { pub fn new(client: Arc) -> Self { Self { client } } } #[async_trait] impl StripeClient for RealStripeClient { async fn list_customers_by_email(&self, email: &str) -> Result> { let response = Customer::list( &self.client, &ListCustomers { email: Some(email), ..Default::default() }, ) .await?; Ok(response .data .into_iter() .map(StripeCustomer::from) .collect()) } async fn get_customer(&self, customer_id: &StripeCustomerId) -> Result { let customer_id = customer_id.try_into()?; let customer = Customer::retrieve(&self.client, &customer_id, &[]).await?; Ok(StripeCustomer::from(customer)) } async fn create_customer(&self, params: CreateCustomerParams<'_>) -> Result { let customer = Customer::create( &self.client, CreateCustomer { email: params.email, ..Default::default() }, ) .await?; Ok(StripeCustomer::from(customer)) } async fn list_subscriptions_for_customer( &self, customer_id: &StripeCustomerId, ) -> Result> { let customer_id = customer_id.try_into()?; let subscriptions = stripe::Subscription::list( &self.client, &stripe::ListSubscriptions { customer: Some(customer_id), status: None, ..Default::default() }, ) .await?; Ok(subscriptions .data .into_iter() .map(StripeSubscription::from) .collect()) } async fn get_subscription( &self, subscription_id: &StripeSubscriptionId, ) -> Result { let subscription_id = subscription_id.try_into()?; let subscription = Subscription::retrieve(&self.client, &subscription_id, &[]).await?; Ok(StripeSubscription::from(subscription)) } async fn create_subscription( &self, params: StripeCreateSubscriptionParams, ) -> Result { let customer_id = params.customer.try_into()?; let mut create_subscription = stripe::CreateSubscription::new(customer_id); create_subscription.items = Some( params .items .into_iter() .map(|item| stripe::CreateSubscriptionItems { price: item.price.map(|price| price.to_string()), quantity: item.quantity, ..Default::default() }) .collect(), ); let subscription = Subscription::create(&self.client, create_subscription).await?; Ok(StripeSubscription::from(subscription)) } async fn update_subscription( &self, subscription_id: &StripeSubscriptionId, params: UpdateSubscriptionParams, ) -> Result<()> { let subscription_id = subscription_id.try_into()?; stripe::Subscription::update( &self.client, &subscription_id, stripe::UpdateSubscription { items: params.items.map(|items| { items .into_iter() .map(|item| UpdateSubscriptionItems { price: item.price.map(|price| price.to_string()), ..Default::default() }) .collect() }), trial_settings: params.trial_settings.map(Into::into), ..Default::default() }, ) .await?; Ok(()) } async fn cancel_subscription(&self, subscription_id: &StripeSubscriptionId) -> Result<()> { let subscription_id = subscription_id.try_into()?; Subscription::cancel( &self.client, &subscription_id, stripe::CancelSubscription { invoice_now: None, ..Default::default() }, ) .await?; Ok(()) } async fn list_prices(&self) -> Result> { let response = stripe::Price::list( &self.client, &stripe::ListPrices { limit: Some(100), ..Default::default() }, ) .await?; Ok(response.data.into_iter().map(StripePrice::from).collect()) } async fn list_meters(&self) -> Result> { #[derive(Serialize)] struct Params { #[serde(skip_serializing_if = "Option::is_none")] limit: Option, } let response = self .client .get_query::, _>( "/billing/meters", Params { limit: Some(100) }, ) .await?; Ok(response.data) } async fn create_meter_event(&self, params: StripeCreateMeterEventParams<'_>) -> Result<()> { #[derive(Deserialize)] struct StripeMeterEvent { pub identifier: String, } let identifier = params.identifier; match self .client .post_form::("/billing/meter_events", params) .await { Ok(_event) => Ok(()), Err(stripe::StripeError::Stripe(error)) => { if error.http_status == 400 && error .message .as_ref() .map_or(false, |message| message.contains(identifier)) { Ok(()) } else { Err(anyhow!(stripe::StripeError::Stripe(error))) } } Err(error) => Err(anyhow!("failed to create meter event: {error:?}")), } } async fn create_checkout_session( &self, params: StripeCreateCheckoutSessionParams<'_>, ) -> Result { let params = params.try_into()?; let session = CheckoutSession::create(&self.client, params).await?; Ok(session.into()) } } impl From for StripeCustomerId { fn from(value: CustomerId) -> Self { Self(value.as_str().into()) } } impl TryFrom for CustomerId { type Error = anyhow::Error; fn try_from(value: StripeCustomerId) -> Result { Self::from_str(value.0.as_ref()).context("failed to parse Stripe customer ID") } } impl TryFrom<&StripeCustomerId> for CustomerId { type Error = anyhow::Error; fn try_from(value: &StripeCustomerId) -> Result { Self::from_str(value.0.as_ref()).context("failed to parse Stripe customer ID") } } impl From for StripeCustomer { fn from(value: Customer) -> Self { StripeCustomer { id: value.id.into(), email: value.email, } } } impl From for StripeSubscriptionId { fn from(value: SubscriptionId) -> Self { Self(value.as_str().into()) } } impl TryFrom<&StripeSubscriptionId> for SubscriptionId { type Error = anyhow::Error; fn try_from(value: &StripeSubscriptionId) -> Result { Self::from_str(value.0.as_ref()).context("failed to parse Stripe subscription ID") } } impl From for StripeSubscription { fn from(value: Subscription) -> Self { Self { id: value.id.into(), customer: value.customer.id().into(), status: value.status, current_period_start: value.current_period_start, current_period_end: value.current_period_end, items: value.items.data.into_iter().map(Into::into).collect(), cancel_at: value.cancel_at, cancellation_details: value.cancellation_details.map(Into::into), } } } impl From for StripeCancellationDetails { fn from(value: CancellationDetails) -> Self { Self { reason: value.reason.map(Into::into), } } } impl From for StripeCancellationDetailsReason { fn from(value: CancellationDetailsReason) -> Self { match value { CancellationDetailsReason::CancellationRequested => Self::CancellationRequested, CancellationDetailsReason::PaymentDisputed => Self::PaymentDisputed, CancellationDetailsReason::PaymentFailed => Self::PaymentFailed, } } } impl From for StripeSubscriptionItemId { fn from(value: SubscriptionItemId) -> Self { Self(value.as_str().into()) } } impl From for StripeSubscriptionItem { fn from(value: SubscriptionItem) -> Self { Self { id: value.id.into(), price: value.price.map(Into::into), } } } impl From for UpdateSubscriptionTrialSettings { fn from(value: StripeSubscriptionTrialSettings) -> Self { Self { end_behavior: value.end_behavior.into(), } } } impl From for UpdateSubscriptionTrialSettingsEndBehavior { fn from(value: StripeSubscriptionTrialSettingsEndBehavior) -> Self { Self { missing_payment_method: value.missing_payment_method.into(), } } } impl From for UpdateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod { fn from(value: StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod) -> Self { match value { StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel => Self::Cancel, StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::CreateInvoice => { Self::CreateInvoice } StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Pause => Self::Pause, } } } impl From for StripePriceId { fn from(value: PriceId) -> Self { Self(value.as_str().into()) } } impl TryFrom for PriceId { type Error = anyhow::Error; fn try_from(value: StripePriceId) -> Result { Self::from_str(value.0.as_ref()).context("failed to parse Stripe price ID") } } impl From for StripePrice { fn from(value: Price) -> Self { Self { id: value.id.into(), unit_amount: value.unit_amount, lookup_key: value.lookup_key, recurring: value.recurring.map(StripePriceRecurring::from), } } } impl From for StripePriceRecurring { fn from(value: Recurring) -> Self { Self { meter: value.meter } } } impl<'a> TryFrom> for CreateCheckoutSession<'a> { type Error = anyhow::Error; fn try_from(value: StripeCreateCheckoutSessionParams<'a>) -> Result { Ok(Self { customer: value .customer .map(|customer_id| customer_id.try_into()) .transpose()?, client_reference_id: value.client_reference_id, mode: value.mode.map(Into::into), line_items: value .line_items .map(|line_items| line_items.into_iter().map(Into::into).collect()), payment_method_collection: value.payment_method_collection.map(Into::into), subscription_data: value.subscription_data.map(Into::into), success_url: value.success_url, ..Default::default() }) } } impl From for CheckoutSessionMode { fn from(value: StripeCheckoutSessionMode) -> Self { match value { StripeCheckoutSessionMode::Payment => Self::Payment, StripeCheckoutSessionMode::Setup => Self::Setup, StripeCheckoutSessionMode::Subscription => Self::Subscription, } } } impl From for CreateCheckoutSessionLineItems { fn from(value: StripeCreateCheckoutSessionLineItems) -> Self { Self { price: value.price, quantity: value.quantity, ..Default::default() } } } impl From for CheckoutSessionPaymentMethodCollection { fn from(value: StripeCheckoutSessionPaymentMethodCollection) -> Self { match value { StripeCheckoutSessionPaymentMethodCollection::Always => Self::Always, StripeCheckoutSessionPaymentMethodCollection::IfRequired => Self::IfRequired, } } } impl From for CreateCheckoutSessionSubscriptionData { fn from(value: StripeCreateCheckoutSessionSubscriptionData) -> Self { Self { trial_period_days: value.trial_period_days, trial_settings: value.trial_settings.map(Into::into), metadata: value.metadata, ..Default::default() } } } impl From for CreateCheckoutSessionSubscriptionDataTrialSettings { fn from(value: StripeSubscriptionTrialSettings) -> Self { Self { end_behavior: value.end_behavior.into(), } } } impl From for CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior { fn from(value: StripeSubscriptionTrialSettingsEndBehavior) -> Self { Self { missing_payment_method: value.missing_payment_method.into(), } } } impl From for CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod { fn from(value: StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod) -> Self { match value { StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel => Self::Cancel, StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::CreateInvoice => { Self::CreateInvoice } StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Pause => Self::Pause, } } } impl From for StripeCheckoutSession { fn from(value: CheckoutSession) -> Self { Self { url: value.url } } }