real_stripe_client.rs

  1use std::str::FromStr as _;
  2use std::sync::Arc;
  3
  4use anyhow::{Context as _, Result, anyhow};
  5use async_trait::async_trait;
  6use serde::{Deserialize, Serialize};
  7use stripe::{
  8    CancellationDetails, CancellationDetailsReason, CheckoutSession, CheckoutSessionMode,
  9    CheckoutSessionPaymentMethodCollection, CreateCheckoutSession, CreateCheckoutSessionLineItems,
 10    CreateCheckoutSessionSubscriptionData, CreateCheckoutSessionSubscriptionDataTrialSettings,
 11    CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior,
 12    CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod,
 13    CreateCustomer, Customer, CustomerId, ListCustomers, Price, PriceId, Recurring, Subscription,
 14    SubscriptionId, SubscriptionItem, SubscriptionItemId, UpdateCustomer, UpdateSubscriptionItems,
 15    UpdateSubscriptionTrialSettings, UpdateSubscriptionTrialSettingsEndBehavior,
 16    UpdateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod,
 17};
 18
 19use crate::stripe_client::{
 20    CreateCustomerParams, StripeCancellationDetails, StripeCancellationDetailsReason,
 21    StripeCheckoutSession, StripeCheckoutSessionMode, StripeCheckoutSessionPaymentMethodCollection,
 22    StripeClient, StripeCreateCheckoutSessionLineItems, StripeCreateCheckoutSessionParams,
 23    StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams,
 24    StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeMeter, StripePrice,
 25    StripePriceId, StripePriceRecurring, StripeSubscription, StripeSubscriptionId,
 26    StripeSubscriptionItem, StripeSubscriptionItemId, StripeSubscriptionTrialSettings,
 27    StripeSubscriptionTrialSettingsEndBehavior,
 28    StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateCustomerParams,
 29    UpdateSubscriptionParams,
 30};
 31
 32pub struct RealStripeClient {
 33    client: Arc<stripe::Client>,
 34}
 35
 36impl RealStripeClient {
 37    pub fn new(client: Arc<stripe::Client>) -> Self {
 38        Self { client }
 39    }
 40}
 41
 42#[async_trait]
 43impl StripeClient for RealStripeClient {
 44    async fn list_customers_by_email(&self, email: &str) -> Result<Vec<StripeCustomer>> {
 45        let response = Customer::list(
 46            &self.client,
 47            &ListCustomers {
 48                email: Some(email),
 49                ..Default::default()
 50            },
 51        )
 52        .await?;
 53
 54        Ok(response
 55            .data
 56            .into_iter()
 57            .map(StripeCustomer::from)
 58            .collect())
 59    }
 60
 61    async fn get_customer(&self, customer_id: &StripeCustomerId) -> Result<StripeCustomer> {
 62        let customer_id = customer_id.try_into()?;
 63
 64        let customer = Customer::retrieve(&self.client, &customer_id, &[]).await?;
 65
 66        Ok(StripeCustomer::from(customer))
 67    }
 68
 69    async fn create_customer(&self, params: CreateCustomerParams<'_>) -> Result<StripeCustomer> {
 70        let customer = Customer::create(
 71            &self.client,
 72            CreateCustomer {
 73                email: params.email,
 74                ..Default::default()
 75            },
 76        )
 77        .await?;
 78
 79        Ok(StripeCustomer::from(customer))
 80    }
 81
 82    async fn update_customer(
 83        &self,
 84        customer_id: &StripeCustomerId,
 85        params: UpdateCustomerParams<'_>,
 86    ) -> Result<StripeCustomer> {
 87        let customer = Customer::update(
 88            &self.client,
 89            &customer_id.try_into()?,
 90            UpdateCustomer {
 91                email: params.email,
 92                ..Default::default()
 93            },
 94        )
 95        .await?;
 96
 97        Ok(StripeCustomer::from(customer))
 98    }
 99
100    async fn list_subscriptions_for_customer(
101        &self,
102        customer_id: &StripeCustomerId,
103    ) -> Result<Vec<StripeSubscription>> {
104        let customer_id = customer_id.try_into()?;
105
106        let subscriptions = stripe::Subscription::list(
107            &self.client,
108            &stripe::ListSubscriptions {
109                customer: Some(customer_id),
110                status: None,
111                ..Default::default()
112            },
113        )
114        .await?;
115
116        Ok(subscriptions
117            .data
118            .into_iter()
119            .map(StripeSubscription::from)
120            .collect())
121    }
122
123    async fn get_subscription(
124        &self,
125        subscription_id: &StripeSubscriptionId,
126    ) -> Result<StripeSubscription> {
127        let subscription_id = subscription_id.try_into()?;
128
129        let subscription = Subscription::retrieve(&self.client, &subscription_id, &[]).await?;
130
131        Ok(StripeSubscription::from(subscription))
132    }
133
134    async fn create_subscription(
135        &self,
136        params: StripeCreateSubscriptionParams,
137    ) -> Result<StripeSubscription> {
138        let customer_id = params.customer.try_into()?;
139
140        let mut create_subscription = stripe::CreateSubscription::new(customer_id);
141        create_subscription.items = Some(
142            params
143                .items
144                .into_iter()
145                .map(|item| stripe::CreateSubscriptionItems {
146                    price: item.price.map(|price| price.to_string()),
147                    quantity: item.quantity,
148                    ..Default::default()
149                })
150                .collect(),
151        );
152
153        let subscription = Subscription::create(&self.client, create_subscription).await?;
154
155        Ok(StripeSubscription::from(subscription))
156    }
157
158    async fn update_subscription(
159        &self,
160        subscription_id: &StripeSubscriptionId,
161        params: UpdateSubscriptionParams,
162    ) -> Result<()> {
163        let subscription_id = subscription_id.try_into()?;
164
165        stripe::Subscription::update(
166            &self.client,
167            &subscription_id,
168            stripe::UpdateSubscription {
169                items: params.items.map(|items| {
170                    items
171                        .into_iter()
172                        .map(|item| UpdateSubscriptionItems {
173                            price: item.price.map(|price| price.to_string()),
174                            ..Default::default()
175                        })
176                        .collect()
177                }),
178                trial_settings: params.trial_settings.map(Into::into),
179                ..Default::default()
180            },
181        )
182        .await?;
183
184        Ok(())
185    }
186
187    async fn cancel_subscription(&self, subscription_id: &StripeSubscriptionId) -> Result<()> {
188        let subscription_id = subscription_id.try_into()?;
189
190        Subscription::cancel(
191            &self.client,
192            &subscription_id,
193            stripe::CancelSubscription {
194                invoice_now: None,
195                ..Default::default()
196            },
197        )
198        .await?;
199
200        Ok(())
201    }
202
203    async fn list_prices(&self) -> Result<Vec<StripePrice>> {
204        let response = stripe::Price::list(
205            &self.client,
206            &stripe::ListPrices {
207                limit: Some(100),
208                ..Default::default()
209            },
210        )
211        .await?;
212
213        Ok(response.data.into_iter().map(StripePrice::from).collect())
214    }
215
216    async fn list_meters(&self) -> Result<Vec<StripeMeter>> {
217        #[derive(Serialize)]
218        struct Params {
219            #[serde(skip_serializing_if = "Option::is_none")]
220            limit: Option<u64>,
221        }
222
223        let response = self
224            .client
225            .get_query::<stripe::List<StripeMeter>, _>(
226                "/billing/meters",
227                Params { limit: Some(100) },
228            )
229            .await?;
230
231        Ok(response.data)
232    }
233
234    async fn create_meter_event(&self, params: StripeCreateMeterEventParams<'_>) -> Result<()> {
235        #[derive(Deserialize)]
236        struct StripeMeterEvent {
237            pub identifier: String,
238        }
239
240        let identifier = params.identifier;
241        match self
242            .client
243            .post_form::<StripeMeterEvent, _>("/billing/meter_events", params)
244            .await
245        {
246            Ok(_event) => Ok(()),
247            Err(stripe::StripeError::Stripe(error)) => {
248                if error.http_status == 400
249                    && error
250                        .message
251                        .as_ref()
252                        .map_or(false, |message| message.contains(identifier))
253                {
254                    Ok(())
255                } else {
256                    Err(anyhow!(stripe::StripeError::Stripe(error)))
257                }
258            }
259            Err(error) => Err(anyhow!("failed to create meter event: {error:?}")),
260        }
261    }
262
263    async fn create_checkout_session(
264        &self,
265        params: StripeCreateCheckoutSessionParams<'_>,
266    ) -> Result<StripeCheckoutSession> {
267        let params = params.try_into()?;
268        let session = CheckoutSession::create(&self.client, params).await?;
269
270        Ok(session.into())
271    }
272}
273
274impl From<CustomerId> for StripeCustomerId {
275    fn from(value: CustomerId) -> Self {
276        Self(value.as_str().into())
277    }
278}
279
280impl TryFrom<StripeCustomerId> for CustomerId {
281    type Error = anyhow::Error;
282
283    fn try_from(value: StripeCustomerId) -> Result<Self, Self::Error> {
284        Self::from_str(value.0.as_ref()).context("failed to parse Stripe customer ID")
285    }
286}
287
288impl TryFrom<&StripeCustomerId> for CustomerId {
289    type Error = anyhow::Error;
290
291    fn try_from(value: &StripeCustomerId) -> Result<Self, Self::Error> {
292        Self::from_str(value.0.as_ref()).context("failed to parse Stripe customer ID")
293    }
294}
295
296impl From<Customer> for StripeCustomer {
297    fn from(value: Customer) -> Self {
298        StripeCustomer {
299            id: value.id.into(),
300            email: value.email,
301        }
302    }
303}
304
305impl From<SubscriptionId> for StripeSubscriptionId {
306    fn from(value: SubscriptionId) -> Self {
307        Self(value.as_str().into())
308    }
309}
310
311impl TryFrom<&StripeSubscriptionId> for SubscriptionId {
312    type Error = anyhow::Error;
313
314    fn try_from(value: &StripeSubscriptionId) -> Result<Self, Self::Error> {
315        Self::from_str(value.0.as_ref()).context("failed to parse Stripe subscription ID")
316    }
317}
318
319impl From<Subscription> for StripeSubscription {
320    fn from(value: Subscription) -> Self {
321        Self {
322            id: value.id.into(),
323            customer: value.customer.id().into(),
324            status: value.status,
325            current_period_start: value.current_period_start,
326            current_period_end: value.current_period_end,
327            items: value.items.data.into_iter().map(Into::into).collect(),
328            cancel_at: value.cancel_at,
329            cancellation_details: value.cancellation_details.map(Into::into),
330        }
331    }
332}
333
334impl From<CancellationDetails> for StripeCancellationDetails {
335    fn from(value: CancellationDetails) -> Self {
336        Self {
337            reason: value.reason.map(Into::into),
338        }
339    }
340}
341
342impl From<CancellationDetailsReason> for StripeCancellationDetailsReason {
343    fn from(value: CancellationDetailsReason) -> Self {
344        match value {
345            CancellationDetailsReason::CancellationRequested => Self::CancellationRequested,
346            CancellationDetailsReason::PaymentDisputed => Self::PaymentDisputed,
347            CancellationDetailsReason::PaymentFailed => Self::PaymentFailed,
348        }
349    }
350}
351
352impl From<SubscriptionItemId> for StripeSubscriptionItemId {
353    fn from(value: SubscriptionItemId) -> Self {
354        Self(value.as_str().into())
355    }
356}
357
358impl From<SubscriptionItem> for StripeSubscriptionItem {
359    fn from(value: SubscriptionItem) -> Self {
360        Self {
361            id: value.id.into(),
362            price: value.price.map(Into::into),
363        }
364    }
365}
366
367impl From<StripeSubscriptionTrialSettings> for UpdateSubscriptionTrialSettings {
368    fn from(value: StripeSubscriptionTrialSettings) -> Self {
369        Self {
370            end_behavior: value.end_behavior.into(),
371        }
372    }
373}
374
375impl From<StripeSubscriptionTrialSettingsEndBehavior>
376    for UpdateSubscriptionTrialSettingsEndBehavior
377{
378    fn from(value: StripeSubscriptionTrialSettingsEndBehavior) -> Self {
379        Self {
380            missing_payment_method: value.missing_payment_method.into(),
381        }
382    }
383}
384
385impl From<StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod>
386    for UpdateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod
387{
388    fn from(value: StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod) -> Self {
389        match value {
390            StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel => Self::Cancel,
391            StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::CreateInvoice => {
392                Self::CreateInvoice
393            }
394            StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Pause => Self::Pause,
395        }
396    }
397}
398
399impl From<PriceId> for StripePriceId {
400    fn from(value: PriceId) -> Self {
401        Self(value.as_str().into())
402    }
403}
404
405impl TryFrom<StripePriceId> for PriceId {
406    type Error = anyhow::Error;
407
408    fn try_from(value: StripePriceId) -> Result<Self, Self::Error> {
409        Self::from_str(value.0.as_ref()).context("failed to parse Stripe price ID")
410    }
411}
412
413impl From<Price> for StripePrice {
414    fn from(value: Price) -> Self {
415        Self {
416            id: value.id.into(),
417            unit_amount: value.unit_amount,
418            lookup_key: value.lookup_key,
419            recurring: value.recurring.map(StripePriceRecurring::from),
420        }
421    }
422}
423
424impl From<Recurring> for StripePriceRecurring {
425    fn from(value: Recurring) -> Self {
426        Self { meter: value.meter }
427    }
428}
429
430impl<'a> TryFrom<StripeCreateCheckoutSessionParams<'a>> for CreateCheckoutSession<'a> {
431    type Error = anyhow::Error;
432
433    fn try_from(value: StripeCreateCheckoutSessionParams<'a>) -> Result<Self, Self::Error> {
434        Ok(Self {
435            customer: value
436                .customer
437                .map(|customer_id| customer_id.try_into())
438                .transpose()?,
439            client_reference_id: value.client_reference_id,
440            mode: value.mode.map(Into::into),
441            line_items: value
442                .line_items
443                .map(|line_items| line_items.into_iter().map(Into::into).collect()),
444            payment_method_collection: value.payment_method_collection.map(Into::into),
445            subscription_data: value.subscription_data.map(Into::into),
446            success_url: value.success_url,
447            ..Default::default()
448        })
449    }
450}
451
452impl From<StripeCheckoutSessionMode> for CheckoutSessionMode {
453    fn from(value: StripeCheckoutSessionMode) -> Self {
454        match value {
455            StripeCheckoutSessionMode::Payment => Self::Payment,
456            StripeCheckoutSessionMode::Setup => Self::Setup,
457            StripeCheckoutSessionMode::Subscription => Self::Subscription,
458        }
459    }
460}
461
462impl From<StripeCreateCheckoutSessionLineItems> for CreateCheckoutSessionLineItems {
463    fn from(value: StripeCreateCheckoutSessionLineItems) -> Self {
464        Self {
465            price: value.price,
466            quantity: value.quantity,
467            ..Default::default()
468        }
469    }
470}
471
472impl From<StripeCheckoutSessionPaymentMethodCollection> for CheckoutSessionPaymentMethodCollection {
473    fn from(value: StripeCheckoutSessionPaymentMethodCollection) -> Self {
474        match value {
475            StripeCheckoutSessionPaymentMethodCollection::Always => Self::Always,
476            StripeCheckoutSessionPaymentMethodCollection::IfRequired => Self::IfRequired,
477        }
478    }
479}
480
481impl From<StripeCreateCheckoutSessionSubscriptionData> for CreateCheckoutSessionSubscriptionData {
482    fn from(value: StripeCreateCheckoutSessionSubscriptionData) -> Self {
483        Self {
484            trial_period_days: value.trial_period_days,
485            trial_settings: value.trial_settings.map(Into::into),
486            metadata: value.metadata,
487            ..Default::default()
488        }
489    }
490}
491
492impl From<StripeSubscriptionTrialSettings> for CreateCheckoutSessionSubscriptionDataTrialSettings {
493    fn from(value: StripeSubscriptionTrialSettings) -> Self {
494        Self {
495            end_behavior: value.end_behavior.into(),
496        }
497    }
498}
499
500impl From<StripeSubscriptionTrialSettingsEndBehavior>
501    for CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior
502{
503    fn from(value: StripeSubscriptionTrialSettingsEndBehavior) -> Self {
504        Self {
505            missing_payment_method: value.missing_payment_method.into(),
506        }
507    }
508}
509
510impl From<StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod>
511    for CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod
512{
513    fn from(value: StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod) -> Self {
514        match value {
515            StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel => Self::Cancel,
516            StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::CreateInvoice => {
517                Self::CreateInvoice
518            }
519            StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Pause => Self::Pause,
520        }
521    }
522}
523
524impl From<CheckoutSession> for StripeCheckoutSession {
525    fn from(value: CheckoutSession) -> Self {
526        Self { url: value.url }
527    }
528}