//! Stripe payment processing via Connect Direct Charges. //! //! Wraps the Stripe API for one-time purchases and recurring subscriptions //! using the Direct Charges pattern: payments are created directly on the //! creator's connected Stripe account with no `application_fee_amount`, //! enforcing MakeNotWork's 0% platform fee promise. The only deduction //! creators see is Stripe's own processing fee (~3%). //! //! Key responsibilities: //! - Standard connected account creation and onboarding links //! - One-time purchase and subscription Checkout Session creation //! - Webhook signature verification (v1 and v2 thin events) //! - Event extraction helpers for checkout, subscription, invoice, account, //! and refund webhook events //! - Subscription product and price creation on connected accounts mod checkout; mod checkout_metadata; mod connect; pub mod synckit_app_pricing; pub mod synckit_billing; mod webhooks; pub use checkout::*; pub use checkout_metadata::*; pub use synckit_app_pricing::{quote_price_cents, SyncBillingInterval, ANNUAL_MULTIPLIER, MAX_CAP_BYTES, MIN_CAP_BYTES, MIN_CHARGE_CENTS}; pub use synckit_billing::SynckitSubResult; pub use webhooks::*; use stripe::Client; use crate::config::StripeConfig; /// Stripe client wrapper for payment operations #[derive(Clone)] pub struct StripeClient { pub(crate) client: Client, pub(crate) config: StripeConfig, } impl StripeClient { /// Create a new Stripe client from configuration pub fn new(config: &StripeConfig) -> Self { let client = Client::new(&config.secret_key); StripeClient { client, config: config.clone(), } } /// Parse a connected account ID string into an `AccountId`. /// /// Account IDs are read from our own DB (`users.stripe_account_id`), so a /// parse failure is an internal invariant violation rather than bad user /// input — classify it `Internal` and keep the underlying error for ops. pub(crate) fn parse_account_id(account_id: &str) -> Result { account_id.parse().map_err(|e| { AppError::Internal(anyhow::anyhow!("Invalid Stripe account ID '{}': {}", account_id, e)) }) } } use crate::error::{AppError, Result}; /// Simplified checkout result: what handlers need from Stripe sessions. pub struct CheckoutResult { pub id: String, pub url: Option, } /// Simplified balance: what handlers need from Stripe balance. pub struct BalanceSummary { pub available_cents: i64, pub pending_cents: i64, } /// Payment provider abstraction for checkout, connect, and webhook operations. #[async_trait::async_trait] pub trait PaymentProvider: Send + Sync { // Checkout async fn create_checkout_session(&self, params: &CheckoutParams<'_>) -> crate::error::Result; async fn create_guest_checkout_session(&self, params: &GuestCheckoutParams<'_>) -> crate::error::Result; async fn create_subscription_checkout_session(&self, params: &SubscriptionCheckoutParams<'_>) -> crate::error::Result; async fn create_tip_checkout_session(&self, params: &TipCheckoutParams<'_>) -> crate::error::Result; async fn create_fan_plus_checkout_session(&self, price_id: &str, user_id: crate::db::UserId, success_url: &str, cancel_url: &str) -> crate::error::Result; async fn create_creator_tier_checkout_session(&self, price_id: &str, user_id: crate::db::UserId, tier: &str, success_url: &str, cancel_url: &str) -> crate::error::Result; async fn create_synckit_app_sub_checkout_session(&self, params: &SynckitAppSubCheckoutParams<'_>) -> crate::error::Result; async fn create_cart_checkout_session(&self, params: &CartCheckoutParams<'_>) -> crate::error::Result; // Connect async fn create_connect_account(&self, email: &str) -> crate::error::Result; async fn create_account_link(&self, account_id: &str, return_url: &str, refresh_url: &str) -> crate::error::Result; async fn fetch_account(&self, account_id: &str) -> crate::error::Result; async fn create_subscription_product_and_price(&self, connected_account_id: &str, tier_name: &str, tier_description: Option<&str>, price_cents: i64) -> crate::error::Result<(String, String)>; async fn get_balance(&self, account_id: &str) -> crate::error::Result; // Subscription lifecycle async fn pause_subscription(&self, stripe_sub_id: &str, connected_account_id: &str) -> crate::error::Result<()>; async fn resume_subscription(&self, stripe_sub_id: &str, connected_account_id: &str) -> crate::error::Result<()>; async fn cancel_subscription(&self, stripe_sub_id: &str, connected_account_id: &str) -> crate::error::Result<()>; /// Set or clear `cancel_at_period_end` on a fan subscription (for creator pause/resume). async fn set_cancel_at_period_end(&self, stripe_sub_id: &str, connected_account_id: &str, cancel: bool) -> crate::error::Result<()>; /// Cancel a platform-level subscription (creator tier, Fan+). Not on a connected account. async fn cancel_platform_subscription(&self, stripe_sub_id: &str) -> crate::error::Result<()>; /// Set or clear `cancel_at_period_end` on a platform subscription (Fan+, creator tier). async fn set_platform_cancel_at_period_end(&self, stripe_sub_id: &str, cancel: bool) -> crate::error::Result<()>; /// Create a Stripe-hosted billing portal session. Returns the URL to redirect to. async fn create_billing_portal_session(&self, stripe_customer_id: &str, return_url: &str) -> crate::error::Result; // Refunds async fn create_refund(&self, payment_intent_id: &str, connected_account_id: &str) -> crate::error::Result<()>; // Webhooks fn verify_webhook(&self, payload: &str, signature: &str) -> crate::error::Result; fn verify_webhook_v2(&self, payload: &str, signature: &str) -> crate::error::Result; // SyncKit v2 developer billing — one customer + subscription per app, // separate from creator-tier and Fan+ subscriptions. See // `synckit_billing.rs` for the rationale on per-app customers. async fn create_synckit_customer(&self, developer_user_id: crate::db::UserId, email: &str, app_name: &str) -> crate::error::Result; async fn create_synckit_subscription(&self, customer_id: &str, app_id: crate::db::SyncAppId, app_name: &str, price_cents: i64) -> crate::error::Result; async fn update_synckit_subscription_price(&self, subscription_id: &str, new_price_cents: i64, app_name: &str) -> crate::error::Result<()>; /// Re-price an end-user SyncKit app subscription. Used by the cap-change /// path; takes effect at next billing cycle (no proration). async fn update_synckit_app_sub_price(&self, subscription_id: &str, new_price_cents: i64, interval: SyncBillingInterval, product_name: &str) -> crate::error::Result<()>; async fn cancel_synckit_subscription(&self, subscription_id: &str) -> crate::error::Result<()>; async fn create_synckit_billing_portal(&self, customer_id: &str, return_url: &str) -> crate::error::Result; } #[async_trait::async_trait] impl PaymentProvider for StripeClient { async fn create_checkout_session(&self, params: &CheckoutParams<'_>) -> crate::error::Result { let session = StripeClient::create_checkout_session(self, params).await?; Ok(CheckoutResult { id: session.id.to_string(), url: session.url }) } async fn create_guest_checkout_session(&self, params: &GuestCheckoutParams<'_>) -> crate::error::Result { let session = StripeClient::create_guest_checkout_session(self, params).await?; Ok(CheckoutResult { id: session.id.to_string(), url: session.url }) } async fn create_subscription_checkout_session(&self, params: &SubscriptionCheckoutParams<'_>) -> crate::error::Result { let session = StripeClient::create_subscription_checkout_session(self, params).await?; Ok(CheckoutResult { id: session.id.to_string(), url: session.url }) } async fn create_tip_checkout_session(&self, params: &TipCheckoutParams<'_>) -> crate::error::Result { let session = StripeClient::create_tip_checkout_session(self, params).await?; Ok(CheckoutResult { id: session.id.to_string(), url: session.url }) } async fn create_fan_plus_checkout_session(&self, price_id: &str, user_id: crate::db::UserId, success_url: &str, cancel_url: &str) -> crate::error::Result { let session = StripeClient::create_fan_plus_checkout_session(self, price_id, user_id, success_url, cancel_url).await?; Ok(CheckoutResult { id: session.id.to_string(), url: session.url }) } async fn create_creator_tier_checkout_session(&self, price_id: &str, user_id: crate::db::UserId, tier: &str, success_url: &str, cancel_url: &str) -> crate::error::Result { let session = StripeClient::create_creator_tier_checkout_session(self, price_id, user_id, tier, success_url, cancel_url).await?; Ok(CheckoutResult { id: session.id.to_string(), url: session.url }) } async fn create_synckit_app_sub_checkout_session(&self, params: &SynckitAppSubCheckoutParams<'_>) -> crate::error::Result { let session = StripeClient::create_synckit_app_sub_checkout_session(self, params).await?; Ok(CheckoutResult { id: session.id.to_string(), url: session.url }) } async fn create_cart_checkout_session(&self, params: &CartCheckoutParams<'_>) -> crate::error::Result { let session = StripeClient::create_cart_checkout_session(self, params).await?; Ok(CheckoutResult { id: session.id.to_string(), url: session.url }) } async fn create_connect_account(&self, email: &str) -> crate::error::Result { StripeClient::create_connect_account(self, email).await } async fn create_account_link(&self, account_id: &str, return_url: &str, refresh_url: &str) -> crate::error::Result { StripeClient::create_account_link(self, account_id, return_url, refresh_url).await } async fn fetch_account(&self, account_id: &str) -> crate::error::Result { StripeClient::fetch_account(self, account_id).await } async fn create_subscription_product_and_price(&self, connected_account_id: &str, tier_name: &str, tier_description: Option<&str>, price_cents: i64) -> crate::error::Result<(String, String)> { StripeClient::create_subscription_product_and_price(self, connected_account_id, tier_name, tier_description, price_cents).await } async fn get_balance(&self, account_id: &str) -> crate::error::Result { let balance = self.get_connected_account_balance(account_id).await?; let available_cents: i64 = balance .available .iter() .filter(|b| b.currency == stripe_types::Currency::USD) .map(|b| b.amount) .sum(); let pending_cents: i64 = balance .pending .iter() .filter(|b| b.currency == stripe_types::Currency::USD) .map(|b| b.amount) .sum(); Ok(BalanceSummary { available_cents, pending_cents }) } async fn pause_subscription(&self, stripe_sub_id: &str, connected_account_id: &str) -> crate::error::Result<()> { StripeClient::pause_subscription(self, stripe_sub_id, connected_account_id).await } async fn resume_subscription(&self, stripe_sub_id: &str, connected_account_id: &str) -> crate::error::Result<()> { StripeClient::resume_subscription(self, stripe_sub_id, connected_account_id).await } async fn cancel_subscription(&self, stripe_sub_id: &str, connected_account_id: &str) -> crate::error::Result<()> { StripeClient::cancel_subscription(self, stripe_sub_id, connected_account_id).await } async fn set_cancel_at_period_end(&self, stripe_sub_id: &str, connected_account_id: &str, cancel: bool) -> crate::error::Result<()> { StripeClient::set_cancel_at_period_end(self, stripe_sub_id, connected_account_id, cancel).await } async fn cancel_platform_subscription(&self, stripe_sub_id: &str) -> crate::error::Result<()> { StripeClient::cancel_platform_subscription(self, stripe_sub_id).await } async fn set_platform_cancel_at_period_end(&self, stripe_sub_id: &str, cancel: bool) -> crate::error::Result<()> { StripeClient::set_platform_cancel_at_period_end(self, stripe_sub_id, cancel).await } async fn create_billing_portal_session(&self, stripe_customer_id: &str, return_url: &str) -> crate::error::Result { StripeClient::create_billing_portal_session(self, stripe_customer_id, return_url).await } async fn create_refund(&self, payment_intent_id: &str, connected_account_id: &str) -> crate::error::Result<()> { StripeClient::create_refund(self, payment_intent_id, connected_account_id).await } fn verify_webhook(&self, payload: &str, signature: &str) -> crate::error::Result { StripeClient::verify_webhook(self, payload, signature) } fn verify_webhook_v2(&self, payload: &str, signature: &str) -> crate::error::Result { StripeClient::verify_webhook_v2(self, payload, signature) } async fn create_synckit_customer(&self, developer_user_id: crate::db::UserId, email: &str, app_name: &str) -> crate::error::Result { StripeClient::create_synckit_customer(self, developer_user_id, email, app_name).await } async fn create_synckit_subscription(&self, customer_id: &str, app_id: crate::db::SyncAppId, app_name: &str, price_cents: i64) -> crate::error::Result { StripeClient::create_synckit_subscription(self, customer_id, app_id, app_name, price_cents).await } async fn update_synckit_subscription_price(&self, subscription_id: &str, new_price_cents: i64, app_name: &str) -> crate::error::Result<()> { StripeClient::update_synckit_subscription_price(self, subscription_id, new_price_cents, app_name).await } async fn update_synckit_app_sub_price(&self, subscription_id: &str, new_price_cents: i64, interval: SyncBillingInterval, product_name: &str) -> crate::error::Result<()> { StripeClient::update_synckit_app_sub_price(self, subscription_id, new_price_cents, interval, product_name).await } async fn cancel_synckit_subscription(&self, subscription_id: &str) -> crate::error::Result<()> { StripeClient::cancel_synckit_subscription(self, subscription_id).await } async fn create_synckit_billing_portal(&self, customer_id: &str, return_url: &str) -> crate::error::Result { StripeClient::create_synckit_billing_portal(self, customer_id, return_url).await } }