use chrono::{Duration, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; use uuid::Uuid; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MockStripeCustomer { pub id: String, pub email: String, pub name: Option, pub metadata: HashMap, pub created: i64, pub default_source: Option, pub invoice_settings: MockInvoiceSettings, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MockInvoiceSettings { pub default_payment_method: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MockStripeSubscription { pub id: String, pub customer: String, pub status: SubscriptionStatus, pub current_period_start: i64, pub current_period_end: i64, pub items: MockSubscriptionItems, pub metadata: HashMap, pub cancel_at_period_end: bool, pub canceled_at: Option, pub trial_start: Option, pub trial_end: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum SubscriptionStatus { Active, PastDue, Unpaid, Canceled, Incomplete, IncompleteExpired, Trialing, Paused, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MockSubscriptionItems { pub data: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MockSubscriptionItem { pub id: String, pub price: MockPrice, pub quantity: u32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MockPrice { pub id: String, pub product: String, pub unit_amount: i64, pub currency: String, pub recurring: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MockRecurring { pub interval: String, pub interval_count: u32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MockStripeInvoice { pub id: String, pub customer: String, pub subscription: Option, pub status: InvoiceStatus, pub amount_due: i64, pub amount_paid: i64, pub amount_remaining: i64, pub currency: String, pub created: i64, pub due_date: Option, pub paid: bool, pub lines: MockInvoiceLines, pub hosted_invoice_url: Option, pub invoice_pdf: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum InvoiceStatus { Draft, Open, Paid, Uncollectible, Void, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MockInvoiceLines { pub data: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MockInvoiceLine { pub id: String, pub amount: i64, pub currency: String, pub description: Option, pub quantity: u32, pub price: MockPrice, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MockStripePaymentIntent { pub id: String, pub amount: i64, pub currency: String, pub status: PaymentIntentStatus, pub customer: Option, pub payment_method: Option, pub client_secret: String, pub created: i64, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum PaymentIntentStatus { RequiresPaymentMethod, RequiresConfirmation, RequiresAction, Processing, RequiresCapture, Canceled, Succeeded, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MockStripePaymentMethod { pub id: String, pub customer: Option, pub payment_type: String, pub card: Option, pub created: i64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MockCard { pub brand: String, pub last4: String, pub exp_month: u32, pub exp_year: u32, pub funding: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MockStripeEvent { pub id: String, pub event_type: String, pub created: i64, pub data: MockEventData, pub livemode: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MockEventData { pub object: serde_json::Value, } pub struct MockStripeClient { customers: Arc>>, subscriptions: Arc>>, invoices: Arc>>, payment_intents: Arc>>, payment_methods: Arc>>, events: Arc>>, prices: Arc>>, should_fail: Arc>, failure_code: Arc>>, } impl MockStripeClient { pub fn new() -> Self { let client = Self { customers: Arc::new(RwLock::new(HashMap::new())), subscriptions: Arc::new(RwLock::new(HashMap::new())), invoices: Arc::new(RwLock::new(HashMap::new())), payment_intents: Arc::new(RwLock::new(HashMap::new())), payment_methods: Arc::new(RwLock::new(HashMap::new())), events: Arc::new(RwLock::new(Vec::new())), prices: Arc::new(RwLock::new(HashMap::new())), should_fail: Arc::new(RwLock::new(false)), failure_code: Arc::new(RwLock::new(None)), }; tokio::spawn({ let prices = client.prices.clone(); async move { let mut p = prices.write().await; p.insert( "price_free".to_string(), MockPrice { id: "price_free".to_string(), product: "prod_free".to_string(), unit_amount: 0, currency: "usd".to_string(), recurring: Some(MockRecurring { interval: "month".to_string(), interval_count: 1, }), }, ); p.insert( "price_starter".to_string(), MockPrice { id: "price_starter".to_string(), product: "prod_starter".to_string(), unit_amount: 2900, currency: "usd".to_string(), recurring: Some(MockRecurring { interval: "month".to_string(), interval_count: 1, }), }, ); p.insert( "price_pro".to_string(), MockPrice { id: "price_pro".to_string(), product: "prod_pro".to_string(), unit_amount: 4900, currency: "usd".to_string(), recurring: Some(MockRecurring { interval: "month".to_string(), interval_count: 1, }), }, ); p.insert( "price_enterprise".to_string(), MockPrice { id: "price_enterprise".to_string(), product: "prod_enterprise".to_string(), unit_amount: 19900, currency: "usd".to_string(), recurring: Some(MockRecurring { interval: "month".to_string(), interval_count: 1, }), }, ); } }); client } pub async fn set_should_fail(&self, should_fail: bool, code: Option) { let mut fail = self.should_fail.write().await; *fail = should_fail; let mut failure_code = self.failure_code.write().await; *failure_code = code; } async fn check_failure(&self) -> Result<(), MockStripeError> { let should_fail = *self.should_fail.read().await; if should_fail { let code = self.failure_code.read().await.clone(); return Err(MockStripeError::ApiError( code.unwrap_or_else(|| "card_declined".to_string()), )); } Ok(()) } pub async fn create_customer( &self, email: &str, name: Option<&str>, metadata: HashMap, ) -> Result { self.check_failure().await?; let customer = MockStripeCustomer { id: format!("cus_{}", generate_stripe_id()), email: email.to_string(), name: name.map(|s| s.to_string()), metadata, created: Utc::now().timestamp(), default_source: None, invoice_settings: MockInvoiceSettings { default_payment_method: None, }, }; let mut customers = self.customers.write().await; customers.insert(customer.id.clone(), customer.clone()); Ok(customer) } pub async fn get_customer(&self, customer_id: &str) -> Result { self.check_failure().await?; let customers = self.customers.read().await; customers .get(customer_id) .cloned() .ok_or_else(|| MockStripeError::NotFound(format!("Customer {customer_id} not found"))) } pub async fn update_customer( &self, customer_id: &str, email: Option<&str>, name: Option<&str>, metadata: Option>, ) -> Result { self.check_failure().await?; let mut customers = self.customers.write().await; let customer = customers .get_mut(customer_id) .ok_or_else(|| MockStripeError::NotFound(format!("Customer {customer_id} not found")))?; if let Some(e) = email { customer.email = e.to_string(); } if let Some(n) = name { customer.name = Some(n.to_string()); } if let Some(m) = metadata { customer.metadata = m; } Ok(customer.clone()) } pub async fn delete_customer(&self, customer_id: &str) -> Result<(), MockStripeError> { self.check_failure().await?; let mut customers = self.customers.write().await; customers .remove(customer_id) .ok_or_else(|| MockStripeError::NotFound(format!("Customer {customer_id} not found")))?; Ok(()) } pub async fn create_subscription( &self, customer_id: &str, price_id: &str, metadata: HashMap, trial_days: Option, ) -> Result { self.check_failure().await?; let customers = self.customers.read().await; if !customers.contains_key(customer_id) { return Err(MockStripeError::NotFound(format!( "Customer {customer_id} not found" ))); } drop(customers); let prices = self.prices.read().await; let price = prices .get(price_id) .cloned() .ok_or_else(|| MockStripeError::NotFound(format!("Price {price_id} not found")))?; drop(prices); let now = Utc::now(); let (trial_start, trial_end, status) = if let Some(days) = trial_days { let ts = now.timestamp(); let te = (now + Duration::days(days as i64)).timestamp(); (Some(ts), Some(te), SubscriptionStatus::Trialing) } else { (None, None, SubscriptionStatus::Active) }; let period_end = now + Duration::days(30); let subscription = MockStripeSubscription { id: format!("sub_{}", generate_stripe_id()), customer: customer_id.to_string(), status, current_period_start: now.timestamp(), current_period_end: period_end.timestamp(), items: MockSubscriptionItems { data: vec![MockSubscriptionItem { id: format!("si_{}", generate_stripe_id()), price, quantity: 1, }], }, metadata, cancel_at_period_end: false, canceled_at: None, trial_start, trial_end, }; let mut subscriptions = self.subscriptions.write().await; subscriptions.insert(subscription.id.clone(), subscription.clone()); self.emit_event("customer.subscription.created", &subscription).await; Ok(subscription) } pub async fn get_subscription( &self, subscription_id: &str, ) -> Result { self.check_failure().await?; let subscriptions = self.subscriptions.read().await; subscriptions .get(subscription_id) .cloned() .ok_or_else(|| MockStripeError::NotFound(format!("Subscription {subscription_id} not found"))) } pub async fn update_subscription( &self, subscription_id: &str, price_id: Option<&str>, cancel_at_period_end: Option, ) -> Result { self.check_failure().await?; let mut subscriptions = self.subscriptions.write().await; let subscription = subscriptions.get_mut(subscription_id).ok_or_else(|| { MockStripeError::NotFound(format!("Subscription {subscription_id} not found")) })?; if let Some(pid) = price_id { let prices = self.prices.read().await; let price = prices .get(pid) .cloned() .ok_or_else(|| MockStripeError::NotFound(format!("Price {pid} not found")))?; subscription.items.data[0].price = price; } if let Some(cancel) = cancel_at_period_end { subscription.cancel_at_period_end = cancel; } let result = subscription.clone(); drop(subscriptions); self.emit_event("customer.subscription.updated", &result).await; Ok(result) } pub async fn cancel_subscription( &self, subscription_id: &str, immediately: bool, ) -> Result { self.check_failure().await?; let mut subscriptions = self.subscriptions.write().await; let subscription = subscriptions.get_mut(subscription_id).ok_or_else(|| { MockStripeError::NotFound(format!("Subscription {subscription_id} not found")) })?; if immediately { subscription.status = SubscriptionStatus::Canceled; subscription.canceled_at = Some(Utc::now().timestamp()); } else { subscription.cancel_at_period_end = true; } let result = subscription.clone(); drop(subscriptions); self.emit_event("customer.subscription.deleted", &result).await; Ok(result) } pub async fn create_invoice( &self, customer_id: &str, subscription_id: Option<&str>, amount: i64, ) -> Result { self.check_failure().await?; let customers = self.customers.read().await; if !customers.contains_key(customer_id) { return Err(MockStripeError::NotFound(format!( "Customer {customer_id} not found" ))); } drop(customers); let invoice_id = format!("in_{}", generate_stripe_id()); let invoice = MockStripeInvoice { id: invoice_id.clone(), customer: customer_id.to_string(), subscription: subscription_id.map(|s| s.to_string()), status: InvoiceStatus::Draft, amount_due: amount, amount_paid: 0, amount_remaining: amount, currency: "usd".to_string(), created: Utc::now().timestamp(), due_date: Some((Utc::now() + Duration::days(30)).timestamp()), paid: false, lines: MockInvoiceLines { data: vec![MockInvoiceLine { id: format!("il_{}", generate_stripe_id()), amount, currency: "usd".to_string(), description: Some("Subscription".to_string()), quantity: 1, price: MockPrice { id: "price_auto".to_string(), product: "prod_auto".to_string(), unit_amount: amount, currency: "usd".to_string(), recurring: None, }, }], }, hosted_invoice_url: Some(format!("https://invoice.stripe.com/i/{invoice_id}")), invoice_pdf: Some(format!("https://invoice.stripe.com/i/{invoice_id}/pdf")), }; let mut invoices = self.invoices.write().await; invoices.insert(invoice.id.clone(), invoice.clone()); Ok(invoice) } pub async fn get_invoice(&self, invoice_id: &str) -> Result { self.check_failure().await?; let invoices = self.invoices.read().await; invoices .get(invoice_id) .cloned() .ok_or_else(|| MockStripeError::NotFound(format!("Invoice {invoice_id} not found"))) } pub async fn finalize_invoice(&self, invoice_id: &str) -> Result { self.check_failure().await?; let mut invoices = self.invoices.write().await; let invoice = invoices .get_mut(invoice_id) .ok_or_else(|| MockStripeError::NotFound(format!("Invoice {invoice_id} not found")))?; invoice.status = InvoiceStatus::Open; let result = invoice.clone(); drop(invoices); self.emit_event("invoice.finalized", &result).await; Ok(result) } pub async fn pay_invoice(&self, invoice_id: &str) -> Result { self.check_failure().await?; let mut invoices = self.invoices.write().await; let invoice = invoices .get_mut(invoice_id) .ok_or_else(|| MockStripeError::NotFound(format!("Invoice {invoice_id} not found")))?; invoice.status = InvoiceStatus::Paid; invoice.paid = true; invoice.amount_paid = invoice.amount_due; invoice.amount_remaining = 0; let result = invoice.clone(); drop(invoices); self.emit_event("invoice.paid", &result).await; Ok(result) } pub async fn void_invoice(&self, invoice_id: &str) -> Result { self.check_failure().await?; let mut invoices = self.invoices.write().await; let invoice = invoices .get_mut(invoice_id) .ok_or_else(|| MockStripeError::NotFound(format!("Invoice {invoice_id} not found")))?; invoice.status = InvoiceStatus::Void; let result = invoice.clone(); drop(invoices); self.emit_event("invoice.voided", &result).await; Ok(result) } pub async fn create_payment_intent( &self, amount: i64, currency: &str, customer_id: Option<&str>, ) -> Result { self.check_failure().await?; let payment_intent = MockStripePaymentIntent { id: format!("pi_{}", generate_stripe_id()), amount, currency: currency.to_string(), status: PaymentIntentStatus::RequiresPaymentMethod, customer: customer_id.map(|s| s.to_string()), payment_method: None, client_secret: format!("pi_{}_secret_{}", generate_stripe_id(), generate_stripe_id()), created: Utc::now().timestamp(), }; let mut payment_intents = self.payment_intents.write().await; payment_intents.insert(payment_intent.id.clone(), payment_intent.clone()); Ok(payment_intent) } pub async fn confirm_payment_intent( &self, payment_intent_id: &str, payment_method_id: &str, ) -> Result { self.check_failure().await?; let mut payment_intents = self.payment_intents.write().await; let pi = payment_intents.get_mut(payment_intent_id).ok_or_else(|| { MockStripeError::NotFound(format!("PaymentIntent {payment_intent_id} not found")) })?; pi.payment_method = Some(payment_method_id.to_string()); pi.status = PaymentIntentStatus::Succeeded; let result = pi.clone(); drop(payment_intents); self.emit_event("payment_intent.succeeded", &result).await; Ok(result) } pub async fn create_payment_method( &self, card_brand: &str, last4: &str, exp_month: u32, exp_year: u32, ) -> Result { self.check_failure().await?; let pm = MockStripePaymentMethod { id: format!("pm_{}", generate_stripe_id()), customer: None, payment_type: "card".to_string(), card: Some(MockCard { brand: card_brand.to_string(), last4: last4.to_string(), exp_month, exp_year, funding: "credit".to_string(), }), created: Utc::now().timestamp(), }; let mut payment_methods = self.payment_methods.write().await; payment_methods.insert(pm.id.clone(), pm.clone()); Ok(pm) } pub async fn attach_payment_method( &self, payment_method_id: &str, customer_id: &str, ) -> Result { self.check_failure().await?; let customers = self.customers.read().await; if !customers.contains_key(customer_id) { return Err(MockStripeError::NotFound(format!( "Customer {customer_id} not found" ))); } drop(customers); let mut payment_methods = self.payment_methods.write().await; let pm = payment_methods.get_mut(payment_method_id).ok_or_else(|| { MockStripeError::NotFound(format!("PaymentMethod {payment_method_id} not found")) })?; pm.customer = Some(customer_id.to_string()); Ok(pm.clone()) } pub async fn get_events(&self, limit: usize) -> Vec { let events = self.events.read().await; events.iter().rev().take(limit).cloned().collect() } async fn emit_event(&self, event_type: &str, data: &T) { let event = MockStripeEvent { id: format!("evt_{}", generate_stripe_id()), event_type: event_type.to_string(), created: Utc::now().timestamp(), data: MockEventData { object: serde_json::to_value(data).unwrap_or(serde_json::Value::Null), }, livemode: false, }; let mut events = self.events.write().await; events.push(event); if events.len() > 1000 { events.drain(0..100); } } } impl Default for MockStripeClient { fn default() -> Self { Self::new() } } fn generate_stripe_id() -> String { let uuid = Uuid::new_v4(); uuid.to_string().replace('-', "")[..24].to_string() } #[derive(Debug, Clone)] pub enum MockStripeError { NotFound(String), ApiError(String), InvalidRequest(String), AuthenticationError, RateLimitError, NetworkError(String), } impl std::fmt::Display for MockStripeError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::NotFound(msg) => write!(f, "Not found: {msg}"), Self::ApiError(code) => write!(f, "API error: {code}"), Self::InvalidRequest(msg) => write!(f, "Invalid request: {msg}"), Self::AuthenticationError => write!(f, "Authentication failed"), Self::RateLimitError => write!(f, "Rate limit exceeded"), Self::NetworkError(msg) => write!(f, "Network error: {msg}"), } } } impl std::error::Error for MockStripeError {} #[derive(Debug, Clone)] pub struct BillingTestScenario { pub name: String, pub description: String, pub steps: Vec, } #[derive(Debug, Clone)] pub struct TestStep { pub action: TestAction, pub expected_result: ExpectedResult, pub delay_ms: Option, } #[derive(Debug, Clone)] pub enum TestAction { CreateCustomer { email: String, name: Option }, CreateSubscription { plan: String, trial_days: Option }, UpgradeSubscription { new_plan: String }, DowngradeSubscription { new_plan: String }, CancelSubscription { immediately: bool }, SimulatePaymentFailure, SimulatePaymentSuccess, ProcessInvoice, ApplyDiscount { code: String }, RecordUsage { metric: String, quantity: i64 }, CheckQuota { metric: String }, } #[derive(Debug, Clone)] pub enum ExpectedResult { Success, SubscriptionStatus(SubscriptionStatus), InvoiceStatus(InvoiceStatus), QuotaExceeded, QuotaAvailable { remaining: i64 }, Error { code: String }, } pub fn create_standard_test_scenarios() -> Vec { vec![ BillingTestScenario { name: "New Customer Signup".to_string(), description: "Test new customer creating account and subscribing".to_string(), steps: vec![ TestStep { action: TestAction::CreateCustomer { email: "test@example.com".to_string(), name: Some("Test User".to_string()), }, expected_result: ExpectedResult::Success, delay_ms: None, }, TestStep { action: TestAction::CreateSubscription { plan: "starter".to_string(), trial_days: Some(14), }, expected_result: ExpectedResult::SubscriptionStatus(SubscriptionStatus::Trialing), delay_ms: None, }, ], }, BillingTestScenario { name: "Plan Upgrade".to_string(), description: "Test customer upgrading from starter to pro".to_string(), steps: vec![ TestStep { action: TestAction::CreateCustomer { email: "upgrade@example.com".to_string(), name: None, }, expected_result: ExpectedResult::Success, delay_ms: None, }, TestStep { action: TestAction::CreateSubscription { plan: "starter".to_string(), trial_days: None, }, expected_result: ExpectedResult::SubscriptionStatus(SubscriptionStatus::Active), delay_ms: None, }, TestStep { action: TestAction::UpgradeSubscription { new_plan: "pro".to_string(), }, expected_result: ExpectedResult::Success, delay_ms: Some(100), }, ], }, BillingTestScenario { name: "Payment Failure Recovery".to_string(), description: "Test handling payment failure and recovery".to_string(), steps: vec![ TestStep { action: TestAction::CreateCustomer { email: "failure@example.com".to_string(), name: None, }, expected_result: ExpectedResult::Success, delay_ms: None, }, TestStep { action: TestAction::CreateSubscription { plan: "pro".to_string(), trial_days: None, }, expected_result: ExpectedResult::SubscriptionStatus(SubscriptionStatus::Active), delay_ms: None, }, TestStep { action: TestAction::SimulatePaymentFailure, expected_result: ExpectedResult::SubscriptionStatus(SubscriptionStatus::PastDue), delay_ms: Some(50), }, TestStep { action: TestAction::SimulatePaymentSuccess, expected_result: ExpectedResult::SubscriptionStatus(SubscriptionStatus::Active), delay_ms: Some(50), }, ], }, ] }