Skip to main content

max / makenotwork

14.7 KB · 268 lines History Blame Raw
1 //! Stripe payment processing via Connect Direct Charges.
2 //!
3 //! Wraps the Stripe API for one-time purchases and recurring subscriptions
4 //! using the Direct Charges pattern: payments are created directly on the
5 //! creator's connected Stripe account with no `application_fee_amount`,
6 //! enforcing MakeNotWork's 0% platform fee promise. The only deduction
7 //! creators see is Stripe's own processing fee (~3%).
8 //!
9 //! Key responsibilities:
10 //! - Standard connected account creation and onboarding links
11 //! - One-time purchase and subscription Checkout Session creation
12 //! - Webhook signature verification (v1 and v2 thin events)
13 //! - Event extraction helpers for checkout, subscription, invoice, account,
14 //! and refund webhook events
15 //! - Subscription product and price creation on connected accounts
16
17 mod checkout;
18 mod checkout_metadata;
19 mod connect;
20 pub mod synckit_app_pricing;
21 pub mod synckit_billing;
22 mod webhooks;
23
24 pub use checkout::*;
25 pub use checkout_metadata::*;
26 pub use synckit_app_pricing::{quote_price_cents, SyncBillingInterval, ANNUAL_MULTIPLIER, MAX_CAP_BYTES, MIN_CAP_BYTES, MIN_CHARGE_CENTS};
27 pub use synckit_billing::SynckitSubResult;
28 pub use webhooks::*;
29
30 use stripe::Client;
31 use crate::config::StripeConfig;
32
33 /// Stripe client wrapper for payment operations
34 #[derive(Clone)]
35 pub struct StripeClient {
36 pub(crate) client: Client,
37 pub(crate) config: StripeConfig,
38 }
39
40 impl StripeClient {
41 /// Create a new Stripe client from configuration
42 pub fn new(config: &StripeConfig) -> Self {
43 let client = Client::new(&config.secret_key);
44 StripeClient {
45 client,
46 config: config.clone(),
47 }
48 }
49
50 /// Parse a connected account ID string into an `AccountId`.
51 ///
52 /// Account IDs are read from our own DB (`users.stripe_account_id`), so a
53 /// parse failure is an internal invariant violation rather than bad user
54 /// input — classify it `Internal` and keep the underlying error for ops.
55 pub(crate) fn parse_account_id(account_id: &str) -> Result<stripe_shared::AccountId> {
56 account_id.parse().map_err(|e| {
57 AppError::Internal(anyhow::anyhow!("Invalid Stripe account ID '{}': {}", account_id, e))
58 })
59 }
60 }
61
62 use crate::error::{AppError, Result};
63
64 /// Simplified checkout result: what handlers need from Stripe sessions.
65 pub struct CheckoutResult {
66 pub id: String,
67 pub url: Option<String>,
68 }
69
70 /// Simplified balance: what handlers need from Stripe balance.
71 pub struct BalanceSummary {
72 pub available_cents: i64,
73 pub pending_cents: i64,
74 }
75
76 /// Payment provider abstraction for checkout, connect, and webhook operations.
77 #[async_trait::async_trait]
78 pub trait PaymentProvider: Send + Sync {
79 // Checkout
80 async fn create_checkout_session(&self, params: &CheckoutParams<'_>) -> crate::error::Result<CheckoutResult>;
81 async fn create_guest_checkout_session(&self, params: &GuestCheckoutParams<'_>) -> crate::error::Result<CheckoutResult>;
82 async fn create_subscription_checkout_session(&self, params: &SubscriptionCheckoutParams<'_>) -> crate::error::Result<CheckoutResult>;
83 async fn create_tip_checkout_session(&self, params: &TipCheckoutParams<'_>) -> crate::error::Result<CheckoutResult>;
84 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<CheckoutResult>;
85 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<CheckoutResult>;
86 async fn create_synckit_app_sub_checkout_session(&self, params: &SynckitAppSubCheckoutParams<'_>) -> crate::error::Result<CheckoutResult>;
87 async fn create_cart_checkout_session(&self, params: &CartCheckoutParams<'_>) -> crate::error::Result<CheckoutResult>;
88
89 // Connect
90 async fn create_connect_account(&self, email: &str) -> crate::error::Result<String>;
91 async fn create_account_link(&self, account_id: &str, return_url: &str, refresh_url: &str) -> crate::error::Result<String>;
92 async fn fetch_account(&self, account_id: &str) -> crate::error::Result<AccountUpdate>;
93 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)>;
94 async fn get_balance(&self, account_id: &str) -> crate::error::Result<BalanceSummary>;
95
96 // Subscription lifecycle
97 async fn pause_subscription(&self, stripe_sub_id: &str, connected_account_id: &str) -> crate::error::Result<()>;
98 async fn resume_subscription(&self, stripe_sub_id: &str, connected_account_id: &str) -> crate::error::Result<()>;
99 async fn cancel_subscription(&self, stripe_sub_id: &str, connected_account_id: &str) -> crate::error::Result<()>;
100 /// Set or clear `cancel_at_period_end` on a fan subscription (for creator pause/resume).
101 async fn set_cancel_at_period_end(&self, stripe_sub_id: &str, connected_account_id: &str, cancel: bool) -> crate::error::Result<()>;
102 /// Cancel a platform-level subscription (creator tier, Fan+). Not on a connected account.
103 async fn cancel_platform_subscription(&self, stripe_sub_id: &str) -> crate::error::Result<()>;
104 /// Set or clear `cancel_at_period_end` on a platform subscription (Fan+, creator tier).
105 async fn set_platform_cancel_at_period_end(&self, stripe_sub_id: &str, cancel: bool) -> crate::error::Result<()>;
106 /// Create a Stripe-hosted billing portal session. Returns the URL to redirect to.
107 async fn create_billing_portal_session(&self, stripe_customer_id: &str, return_url: &str) -> crate::error::Result<String>;
108
109 // Refunds
110 async fn create_refund(&self, payment_intent_id: &str, connected_account_id: &str) -> crate::error::Result<()>;
111
112 // Webhooks
113 fn verify_webhook(&self, payload: &str, signature: &str) -> crate::error::Result<UntypedEvent>;
114 fn verify_webhook_v2(&self, payload: &str, signature: &str) -> crate::error::Result<serde_json::Value>;
115
116 // SyncKit v2 developer billing — one customer + subscription per app,
117 // separate from creator-tier and Fan+ subscriptions. See
118 // `synckit_billing.rs` for the rationale on per-app customers.
119 async fn create_synckit_customer(&self, developer_user_id: crate::db::UserId, email: &str, app_name: &str) -> crate::error::Result<String>;
120 async fn create_synckit_subscription(&self, customer_id: &str, app_id: crate::db::SyncAppId, app_name: &str, price_cents: i64) -> crate::error::Result<SynckitSubResult>;
121 async fn update_synckit_subscription_price(&self, subscription_id: &str, new_price_cents: i64, app_name: &str) -> crate::error::Result<()>;
122 /// Re-price an end-user SyncKit app subscription. Used by the cap-change
123 /// path; takes effect at next billing cycle (no proration).
124 async fn update_synckit_app_sub_price(&self, subscription_id: &str, new_price_cents: i64, interval: SyncBillingInterval, product_name: &str) -> crate::error::Result<()>;
125 async fn cancel_synckit_subscription(&self, subscription_id: &str) -> crate::error::Result<()>;
126 async fn create_synckit_billing_portal(&self, customer_id: &str, return_url: &str) -> crate::error::Result<String>;
127 }
128
129 #[async_trait::async_trait]
130 impl PaymentProvider for StripeClient {
131 async fn create_checkout_session(&self, params: &CheckoutParams<'_>) -> crate::error::Result<CheckoutResult> {
132 let session = StripeClient::create_checkout_session(self, params).await?;
133 Ok(CheckoutResult { id: session.id.to_string(), url: session.url })
134 }
135
136 async fn create_guest_checkout_session(&self, params: &GuestCheckoutParams<'_>) -> crate::error::Result<CheckoutResult> {
137 let session = StripeClient::create_guest_checkout_session(self, params).await?;
138 Ok(CheckoutResult { id: session.id.to_string(), url: session.url })
139 }
140
141 async fn create_subscription_checkout_session(&self, params: &SubscriptionCheckoutParams<'_>) -> crate::error::Result<CheckoutResult> {
142 let session = StripeClient::create_subscription_checkout_session(self, params).await?;
143 Ok(CheckoutResult { id: session.id.to_string(), url: session.url })
144 }
145
146 async fn create_tip_checkout_session(&self, params: &TipCheckoutParams<'_>) -> crate::error::Result<CheckoutResult> {
147 let session = StripeClient::create_tip_checkout_session(self, params).await?;
148 Ok(CheckoutResult { id: session.id.to_string(), url: session.url })
149 }
150
151 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<CheckoutResult> {
152 let session = StripeClient::create_fan_plus_checkout_session(self, price_id, user_id, success_url, cancel_url).await?;
153 Ok(CheckoutResult { id: session.id.to_string(), url: session.url })
154 }
155
156 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<CheckoutResult> {
157 let session = StripeClient::create_creator_tier_checkout_session(self, price_id, user_id, tier, success_url, cancel_url).await?;
158 Ok(CheckoutResult { id: session.id.to_string(), url: session.url })
159 }
160
161 async fn create_synckit_app_sub_checkout_session(&self, params: &SynckitAppSubCheckoutParams<'_>) -> crate::error::Result<CheckoutResult> {
162 let session = StripeClient::create_synckit_app_sub_checkout_session(self, params).await?;
163 Ok(CheckoutResult { id: session.id.to_string(), url: session.url })
164 }
165
166 async fn create_cart_checkout_session(&self, params: &CartCheckoutParams<'_>) -> crate::error::Result<CheckoutResult> {
167 let session = StripeClient::create_cart_checkout_session(self, params).await?;
168 Ok(CheckoutResult { id: session.id.to_string(), url: session.url })
169 }
170
171 async fn create_connect_account(&self, email: &str) -> crate::error::Result<String> {
172 StripeClient::create_connect_account(self, email).await
173 }
174
175 async fn create_account_link(&self, account_id: &str, return_url: &str, refresh_url: &str) -> crate::error::Result<String> {
176 StripeClient::create_account_link(self, account_id, return_url, refresh_url).await
177 }
178
179 async fn fetch_account(&self, account_id: &str) -> crate::error::Result<AccountUpdate> {
180 StripeClient::fetch_account(self, account_id).await
181 }
182
183 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)> {
184 StripeClient::create_subscription_product_and_price(self, connected_account_id, tier_name, tier_description, price_cents).await
185 }
186
187 async fn get_balance(&self, account_id: &str) -> crate::error::Result<BalanceSummary> {
188 let balance = self.get_connected_account_balance(account_id).await?;
189 let available_cents: i64 = balance
190 .available
191 .iter()
192 .filter(|b| b.currency == stripe_types::Currency::USD)
193 .map(|b| b.amount)
194 .sum();
195 let pending_cents: i64 = balance
196 .pending
197 .iter()
198 .filter(|b| b.currency == stripe_types::Currency::USD)
199 .map(|b| b.amount)
200 .sum();
201 Ok(BalanceSummary { available_cents, pending_cents })
202 }
203
204 async fn pause_subscription(&self, stripe_sub_id: &str, connected_account_id: &str) -> crate::error::Result<()> {
205 StripeClient::pause_subscription(self, stripe_sub_id, connected_account_id).await
206 }
207
208 async fn resume_subscription(&self, stripe_sub_id: &str, connected_account_id: &str) -> crate::error::Result<()> {
209 StripeClient::resume_subscription(self, stripe_sub_id, connected_account_id).await
210 }
211
212 async fn cancel_subscription(&self, stripe_sub_id: &str, connected_account_id: &str) -> crate::error::Result<()> {
213 StripeClient::cancel_subscription(self, stripe_sub_id, connected_account_id).await
214 }
215
216 async fn set_cancel_at_period_end(&self, stripe_sub_id: &str, connected_account_id: &str, cancel: bool) -> crate::error::Result<()> {
217 StripeClient::set_cancel_at_period_end(self, stripe_sub_id, connected_account_id, cancel).await
218 }
219
220 async fn cancel_platform_subscription(&self, stripe_sub_id: &str) -> crate::error::Result<()> {
221 StripeClient::cancel_platform_subscription(self, stripe_sub_id).await
222 }
223
224 async fn set_platform_cancel_at_period_end(&self, stripe_sub_id: &str, cancel: bool) -> crate::error::Result<()> {
225 StripeClient::set_platform_cancel_at_period_end(self, stripe_sub_id, cancel).await
226 }
227
228 async fn create_billing_portal_session(&self, stripe_customer_id: &str, return_url: &str) -> crate::error::Result<String> {
229 StripeClient::create_billing_portal_session(self, stripe_customer_id, return_url).await
230 }
231
232 async fn create_refund(&self, payment_intent_id: &str, connected_account_id: &str) -> crate::error::Result<()> {
233 StripeClient::create_refund(self, payment_intent_id, connected_account_id).await
234 }
235
236 fn verify_webhook(&self, payload: &str, signature: &str) -> crate::error::Result<UntypedEvent> {
237 StripeClient::verify_webhook(self, payload, signature)
238 }
239
240 fn verify_webhook_v2(&self, payload: &str, signature: &str) -> crate::error::Result<serde_json::Value> {
241 StripeClient::verify_webhook_v2(self, payload, signature)
242 }
243
244 async fn create_synckit_customer(&self, developer_user_id: crate::db::UserId, email: &str, app_name: &str) -> crate::error::Result<String> {
245 StripeClient::create_synckit_customer(self, developer_user_id, email, app_name).await
246 }
247
248 async fn create_synckit_subscription(&self, customer_id: &str, app_id: crate::db::SyncAppId, app_name: &str, price_cents: i64) -> crate::error::Result<SynckitSubResult> {
249 StripeClient::create_synckit_subscription(self, customer_id, app_id, app_name, price_cents).await
250 }
251
252 async fn update_synckit_subscription_price(&self, subscription_id: &str, new_price_cents: i64, app_name: &str) -> crate::error::Result<()> {
253 StripeClient::update_synckit_subscription_price(self, subscription_id, new_price_cents, app_name).await
254 }
255
256 async fn update_synckit_app_sub_price(&self, subscription_id: &str, new_price_cents: i64, interval: SyncBillingInterval, product_name: &str) -> crate::error::Result<()> {
257 StripeClient::update_synckit_app_sub_price(self, subscription_id, new_price_cents, interval, product_name).await
258 }
259
260 async fn cancel_synckit_subscription(&self, subscription_id: &str) -> crate::error::Result<()> {
261 StripeClient::cancel_synckit_subscription(self, subscription_id).await
262 }
263
264 async fn create_synckit_billing_portal(&self, customer_id: &str, return_url: &str) -> crate::error::Result<String> {
265 StripeClient::create_synckit_billing_portal(self, customer_id, return_url).await
266 }
267 }
268