fake_stripe_client.rs

  1use std::sync::Arc;
  2
  3use anyhow::{Result, anyhow};
  4use async_trait::async_trait;
  5use chrono::{Duration, Utc};
  6use collections::HashMap;
  7use parking_lot::Mutex;
  8use uuid::Uuid;
  9
 10use crate::stripe_client::{
 11    CreateCustomerParams, StripeCheckoutSession, StripeCheckoutSessionMode,
 12    StripeCheckoutSessionPaymentMethodCollection, StripeClient,
 13    StripeCreateCheckoutSessionLineItems, StripeCreateCheckoutSessionParams,
 14    StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams,
 15    StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeMeter, StripeMeterId,
 16    StripePrice, StripePriceId, StripeSubscription, StripeSubscriptionId, StripeSubscriptionItem,
 17    StripeSubscriptionItemId, UpdateSubscriptionParams,
 18};
 19
 20#[derive(Debug, Clone)]
 21pub struct StripeCreateMeterEventCall {
 22    pub identifier: Arc<str>,
 23    pub event_name: Arc<str>,
 24    pub value: u64,
 25    pub stripe_customer_id: StripeCustomerId,
 26    pub timestamp: Option<i64>,
 27}
 28
 29#[derive(Debug, Clone)]
 30pub struct StripeCreateCheckoutSessionCall {
 31    pub customer: Option<StripeCustomerId>,
 32    pub client_reference_id: Option<String>,
 33    pub mode: Option<StripeCheckoutSessionMode>,
 34    pub line_items: Option<Vec<StripeCreateCheckoutSessionLineItems>>,
 35    pub payment_method_collection: Option<StripeCheckoutSessionPaymentMethodCollection>,
 36    pub subscription_data: Option<StripeCreateCheckoutSessionSubscriptionData>,
 37    pub success_url: Option<String>,
 38}
 39
 40pub struct FakeStripeClient {
 41    pub customers: Arc<Mutex<HashMap<StripeCustomerId, StripeCustomer>>>,
 42    pub subscriptions: Arc<Mutex<HashMap<StripeSubscriptionId, StripeSubscription>>>,
 43    pub update_subscription_calls:
 44        Arc<Mutex<Vec<(StripeSubscriptionId, UpdateSubscriptionParams)>>>,
 45    pub prices: Arc<Mutex<HashMap<StripePriceId, StripePrice>>>,
 46    pub meters: Arc<Mutex<HashMap<StripeMeterId, StripeMeter>>>,
 47    pub create_meter_event_calls: Arc<Mutex<Vec<StripeCreateMeterEventCall>>>,
 48    pub create_checkout_session_calls: Arc<Mutex<Vec<StripeCreateCheckoutSessionCall>>>,
 49}
 50
 51impl FakeStripeClient {
 52    pub fn new() -> Self {
 53        Self {
 54            customers: Arc::new(Mutex::new(HashMap::default())),
 55            subscriptions: Arc::new(Mutex::new(HashMap::default())),
 56            update_subscription_calls: Arc::new(Mutex::new(Vec::new())),
 57            prices: Arc::new(Mutex::new(HashMap::default())),
 58            meters: Arc::new(Mutex::new(HashMap::default())),
 59            create_meter_event_calls: Arc::new(Mutex::new(Vec::new())),
 60            create_checkout_session_calls: Arc::new(Mutex::new(Vec::new())),
 61        }
 62    }
 63}
 64
 65#[async_trait]
 66impl StripeClient for FakeStripeClient {
 67    async fn list_customers_by_email(&self, email: &str) -> Result<Vec<StripeCustomer>> {
 68        Ok(self
 69            .customers
 70            .lock()
 71            .values()
 72            .filter(|customer| customer.email.as_deref() == Some(email))
 73            .cloned()
 74            .collect())
 75    }
 76
 77    async fn get_customer(&self, customer_id: &StripeCustomerId) -> Result<StripeCustomer> {
 78        self.customers
 79            .lock()
 80            .get(customer_id)
 81            .cloned()
 82            .ok_or_else(|| anyhow!("no customer found for {customer_id:?}"))
 83    }
 84
 85    async fn create_customer(&self, params: CreateCustomerParams<'_>) -> Result<StripeCustomer> {
 86        let customer = StripeCustomer {
 87            id: StripeCustomerId(format!("cus_{}", Uuid::new_v4()).into()),
 88            email: params.email.map(|email| email.to_string()),
 89        };
 90
 91        self.customers
 92            .lock()
 93            .insert(customer.id.clone(), customer.clone());
 94
 95        Ok(customer)
 96    }
 97
 98    async fn list_subscriptions_for_customer(
 99        &self,
100        customer_id: &StripeCustomerId,
101    ) -> Result<Vec<StripeSubscription>> {
102        let subscriptions = self
103            .subscriptions
104            .lock()
105            .values()
106            .filter(|subscription| subscription.customer == *customer_id)
107            .cloned()
108            .collect();
109
110        Ok(subscriptions)
111    }
112
113    async fn get_subscription(
114        &self,
115        subscription_id: &StripeSubscriptionId,
116    ) -> Result<StripeSubscription> {
117        self.subscriptions
118            .lock()
119            .get(subscription_id)
120            .cloned()
121            .ok_or_else(|| anyhow!("no subscription found for {subscription_id:?}"))
122    }
123
124    async fn create_subscription(
125        &self,
126        params: StripeCreateSubscriptionParams,
127    ) -> Result<StripeSubscription> {
128        let now = Utc::now();
129
130        let subscription = StripeSubscription {
131            id: StripeSubscriptionId(format!("sub_{}", Uuid::new_v4()).into()),
132            customer: params.customer,
133            status: stripe::SubscriptionStatus::Active,
134            current_period_start: now.timestamp(),
135            current_period_end: (now + Duration::days(30)).timestamp(),
136            items: params
137                .items
138                .into_iter()
139                .map(|item| StripeSubscriptionItem {
140                    id: StripeSubscriptionItemId(format!("si_{}", Uuid::new_v4()).into()),
141                    price: item
142                        .price
143                        .and_then(|price_id| self.prices.lock().get(&price_id).cloned()),
144                })
145                .collect(),
146            cancel_at: None,
147            cancellation_details: None,
148        };
149
150        self.subscriptions
151            .lock()
152            .insert(subscription.id.clone(), subscription.clone());
153
154        Ok(subscription)
155    }
156
157    async fn update_subscription(
158        &self,
159        subscription_id: &StripeSubscriptionId,
160        params: UpdateSubscriptionParams,
161    ) -> Result<()> {
162        let subscription = self.get_subscription(subscription_id).await?;
163
164        self.update_subscription_calls
165            .lock()
166            .push((subscription.id, params));
167
168        Ok(())
169    }
170
171    async fn cancel_subscription(&self, subscription_id: &StripeSubscriptionId) -> Result<()> {
172        // TODO: Implement fake subscription cancellation.
173        let _ = subscription_id;
174
175        Ok(())
176    }
177
178    async fn list_prices(&self) -> Result<Vec<StripePrice>> {
179        let prices = self.prices.lock().values().cloned().collect();
180
181        Ok(prices)
182    }
183
184    async fn list_meters(&self) -> Result<Vec<StripeMeter>> {
185        let meters = self.meters.lock().values().cloned().collect();
186
187        Ok(meters)
188    }
189
190    async fn create_meter_event(&self, params: StripeCreateMeterEventParams<'_>) -> Result<()> {
191        self.create_meter_event_calls
192            .lock()
193            .push(StripeCreateMeterEventCall {
194                identifier: params.identifier.into(),
195                event_name: params.event_name.into(),
196                value: params.payload.value,
197                stripe_customer_id: params.payload.stripe_customer_id.clone(),
198                timestamp: params.timestamp,
199            });
200
201        Ok(())
202    }
203
204    async fn create_checkout_session(
205        &self,
206        params: StripeCreateCheckoutSessionParams<'_>,
207    ) -> Result<StripeCheckoutSession> {
208        self.create_checkout_session_calls
209            .lock()
210            .push(StripeCreateCheckoutSessionCall {
211                customer: params.customer.cloned(),
212                client_reference_id: params.client_reference_id.map(|id| id.to_string()),
213                mode: params.mode,
214                line_items: params.line_items,
215                payment_method_collection: params.payment_method_collection,
216                subscription_data: params.subscription_data,
217                success_url: params.success_url.map(|url| url.to_string()),
218            });
219
220        Ok(StripeCheckoutSession {
221            url: Some("https://checkout.stripe.com/c/pay/cs_test_1".to_string()),
222        })
223    }
224}