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}