Skip to main content

max / makenotwork

15.2 KB · 270 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 pub(crate) fn parse_account_id(account_id: &str) -> Result<stripe_shared::AccountId> {
52 account_id.parse().map_err(|_| {
53 AppError::BadRequest("Invalid Stripe account ID format".to_string())
54 })
55 }
56 }
57
58 use crate::error::{AppError, Result};
59
60 /// Simplified checkout result: what handlers need from Stripe sessions.
61 pub struct CheckoutResult {
62 pub id: String,
63 pub url: Option<String>,
64 }
65
66 /// Simplified balance: what handlers need from Stripe balance.
67 pub struct BalanceSummary {
68 pub available_cents: i64,
69 pub pending_cents: i64,
70 }
71
72 /// Payment provider abstraction for checkout, connect, and webhook operations.
73 #[async_trait::async_trait]
74 pub trait PaymentProvider: Send + Sync {
75 // Checkout
76 async fn create_checkout_session(&self, params: &CheckoutParams<'_>) -> crate::error::Result<CheckoutResult>;
77 async fn create_guest_checkout_session(&self, params: &GuestCheckoutParams<'_>) -> crate::error::Result<CheckoutResult>;
78 async fn create_subscription_checkout_session(&self, params: &SubscriptionCheckoutParams<'_>) -> crate::error::Result<CheckoutResult>;
79 async fn create_tip_checkout_session(&self, params: &TipCheckoutParams<'_>) -> crate::error::Result<CheckoutResult>;
80 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>;
81 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>;
82 async fn create_synckit_app_sub_checkout_session(&self, product_name: &str, amount_cents: i64, interval: &str, user_id: crate::db::UserId, app_id: crate::db::SyncAppId, tier: &str, storage_limit_bytes: Option<i64>, success_url: &str, cancel_url: &str) -> crate::error::Result<CheckoutResult>;
83 async fn create_cart_checkout_session(&self, params: &CartCheckoutParams<'_>) -> crate::error::Result<CheckoutResult>;
84
85 // Connect
86 async fn create_connect_account(&self, email: &str) -> crate::error::Result<String>;
87 async fn create_account_link(&self, account_id: &str, return_url: &str, refresh_url: &str) -> crate::error::Result<String>;
88 async fn fetch_account(&self, account_id: &str) -> crate::error::Result<AccountUpdate>;
89 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)>;
90 async fn get_balance(&self, account_id: &str) -> crate::error::Result<BalanceSummary>;
91
92 // Subscription lifecycle
93 async fn pause_subscription(&self, stripe_sub_id: &str, connected_account_id: &str) -> crate::error::Result<()>;
94 async fn resume_subscription(&self, stripe_sub_id: &str, connected_account_id: &str) -> crate::error::Result<()>;
95 async fn cancel_subscription(&self, stripe_sub_id: &str, connected_account_id: &str) -> crate::error::Result<()>;
96 /// Set or clear `cancel_at_period_end` on a fan subscription (for creator pause/resume).
97 async fn set_cancel_at_period_end(&self, stripe_sub_id: &str, connected_account_id: &str, cancel: bool) -> crate::error::Result<()>;
98 /// Cancel a platform-level subscription (creator tier, Fan+). Not on a connected account.
99 async fn cancel_platform_subscription(&self, stripe_sub_id: &str) -> crate::error::Result<()>;
100 /// Set or clear `cancel_at_period_end` on a platform subscription (Fan+, creator tier).
101 async fn set_platform_cancel_at_period_end(&self, stripe_sub_id: &str, cancel: bool) -> crate::error::Result<()>;
102 /// Create a Stripe-hosted billing portal session. Returns the URL to redirect to.
103 async fn create_billing_portal_session(&self, stripe_customer_id: &str, return_url: &str) -> crate::error::Result<String>;
104
105 // Refunds
106 async fn create_refund(&self, payment_intent_id: &str, connected_account_id: &str) -> crate::error::Result<()>;
107
108 // Webhooks
109 fn verify_webhook(&self, payload: &str, signature: &str) -> crate::error::Result<UntypedEvent>;
110 fn verify_webhook_v2(&self, payload: &str, signature: &str) -> crate::error::Result<serde_json::Value>;
111
112 // SyncKit v2 developer billing — one customer + subscription per app,
113 // separate from creator-tier and Fan+ subscriptions. See
114 // `synckit_billing.rs` for the rationale on per-app customers.
115 async fn create_synckit_customer(&self, developer_user_id: crate::db::UserId, email: &str, app_name: &str) -> crate::error::Result<String>;
116 async fn create_synckit_subscription(&self, customer_id: &str, app_id: crate::db::SyncAppId, app_name: &str, price_cents: i64) -> crate::error::Result<SynckitSubResult>;
117 async fn update_synckit_subscription_price(&self, subscription_id: &str, new_price_cents: i64, app_name: &str) -> crate::error::Result<()>;
118 /// Re-price an end-user SyncKit app subscription. Used by the cap-change
119 /// path; takes effect at next billing cycle (no proration).
120 async fn update_synckit_app_sub_price(&self, subscription_id: &str, new_price_cents: i64, interval: SyncBillingInterval, product_name: &str) -> crate::error::Result<()>;
121 async fn cancel_synckit_subscription(&self, subscription_id: &str) -> crate::error::Result<()>;
122 async fn create_synckit_billing_portal(&self, customer_id: &str, return_url: &str) -> crate::error::Result<String>;
123 }
124
125 #[async_trait::async_trait]
126 impl PaymentProvider for StripeClient {
127 async fn create_checkout_session(&self, params: &CheckoutParams<'_>) -> crate::error::Result<CheckoutResult> {
128 let session = StripeClient::create_checkout_session(self, params).await?;
129 Ok(CheckoutResult { id: session.id.to_string(), url: session.url })
130 }
131
132 async fn create_guest_checkout_session(&self, params: &GuestCheckoutParams<'_>) -> crate::error::Result<CheckoutResult> {
133 let session = StripeClient::create_guest_checkout_session(self, params).await?;
134 Ok(CheckoutResult { id: session.id.to_string(), url: session.url })
135 }
136
137 async fn create_subscription_checkout_session(&self, params: &SubscriptionCheckoutParams<'_>) -> crate::error::Result<CheckoutResult> {
138 let session = StripeClient::create_subscription_checkout_session(self, params).await?;
139 Ok(CheckoutResult { id: session.id.to_string(), url: session.url })
140 }
141
142 async fn create_tip_checkout_session(&self, params: &TipCheckoutParams<'_>) -> crate::error::Result<CheckoutResult> {
143 let session = StripeClient::create_tip_checkout_session(self, params).await?;
144 Ok(CheckoutResult { id: session.id.to_string(), url: session.url })
145 }
146
147 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> {
148 let session = StripeClient::create_fan_plus_checkout_session(self, price_id, user_id, success_url, cancel_url).await?;
149 Ok(CheckoutResult { id: session.id.to_string(), url: session.url })
150 }
151
152 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> {
153 let session = StripeClient::create_creator_tier_checkout_session(self, price_id, user_id, tier, success_url, cancel_url).await?;
154 Ok(CheckoutResult { id: session.id.to_string(), url: session.url })
155 }
156
157 async fn create_synckit_app_sub_checkout_session(&self, product_name: &str, amount_cents: i64, interval: &str, user_id: crate::db::UserId, app_id: crate::db::SyncAppId, tier: &str, storage_limit_bytes: Option<i64>, success_url: &str, cancel_url: &str) -> crate::error::Result<CheckoutResult> {
158 use stripe_checkout::checkout_session::CreateCheckoutSessionLineItemsPriceDataRecurringInterval as Recurring;
159 let interval = match interval {
160 "monthly" => Recurring::Month,
161 "annual" => Recurring::Year,
162 other => return Err(crate::error::AppError::BadRequest(format!("Invalid interval '{other}'"))),
163 };
164 let session = StripeClient::create_synckit_app_sub_checkout_session(self, product_name, amount_cents, interval, user_id, app_id, tier, storage_limit_bytes, success_url, cancel_url).await?;
165 Ok(CheckoutResult { id: session.id.to_string(), url: session.url })
166 }
167
168 async fn create_cart_checkout_session(&self, params: &CartCheckoutParams<'_>) -> crate::error::Result<CheckoutResult> {
169 let session = StripeClient::create_cart_checkout_session(self, params).await?;
170 Ok(CheckoutResult { id: session.id.to_string(), url: session.url })
171 }
172
173 async fn create_connect_account(&self, email: &str) -> crate::error::Result<String> {
174 StripeClient::create_connect_account(self, email).await
175 }
176
177 async fn create_account_link(&self, account_id: &str, return_url: &str, refresh_url: &str) -> crate::error::Result<String> {
178 StripeClient::create_account_link(self, account_id, return_url, refresh_url).await
179 }
180
181 async fn fetch_account(&self, account_id: &str) -> crate::error::Result<AccountUpdate> {
182 StripeClient::fetch_account(self, account_id).await
183 }
184
185 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)> {
186 StripeClient::create_subscription_product_and_price(self, connected_account_id, tier_name, tier_description, price_cents).await
187 }
188
189 async fn get_balance(&self, account_id: &str) -> crate::error::Result<BalanceSummary> {
190 let balance = self.get_connected_account_balance(account_id).await?;
191 let available_cents: i64 = balance
192 .available
193 .iter()
194 .filter(|b| b.currency == stripe_types::Currency::USD)
195 .map(|b| b.amount)
196 .sum();
197 let pending_cents: i64 = balance
198 .pending
199 .iter()
200 .filter(|b| b.currency == stripe_types::Currency::USD)
201 .map(|b| b.amount)
202 .sum();
203 Ok(BalanceSummary { available_cents, pending_cents })
204 }
205
206 async fn pause_subscription(&self, stripe_sub_id: &str, connected_account_id: &str) -> crate::error::Result<()> {
207 StripeClient::pause_subscription(self, stripe_sub_id, connected_account_id).await
208 }
209
210 async fn resume_subscription(&self, stripe_sub_id: &str, connected_account_id: &str) -> crate::error::Result<()> {
211 StripeClient::resume_subscription(self, stripe_sub_id, connected_account_id).await
212 }
213
214 async fn cancel_subscription(&self, stripe_sub_id: &str, connected_account_id: &str) -> crate::error::Result<()> {
215 StripeClient::cancel_subscription(self, stripe_sub_id, connected_account_id).await
216 }
217
218 async fn set_cancel_at_period_end(&self, stripe_sub_id: &str, connected_account_id: &str, cancel: bool) -> crate::error::Result<()> {
219 StripeClient::set_cancel_at_period_end(self, stripe_sub_id, connected_account_id, cancel).await
220 }
221
222 async fn cancel_platform_subscription(&self, stripe_sub_id: &str) -> crate::error::Result<()> {
223 StripeClient::cancel_platform_subscription(self, stripe_sub_id).await
224 }
225
226 async fn set_platform_cancel_at_period_end(&self, stripe_sub_id: &str, cancel: bool) -> crate::error::Result<()> {
227 StripeClient::set_platform_cancel_at_period_end(self, stripe_sub_id, cancel).await
228 }
229
230 async fn create_billing_portal_session(&self, stripe_customer_id: &str, return_url: &str) -> crate::error::Result<String> {
231 StripeClient::create_billing_portal_session(self, stripe_customer_id, return_url).await
232 }
233
234 async fn create_refund(&self, payment_intent_id: &str, connected_account_id: &str) -> crate::error::Result<()> {
235 StripeClient::create_refund(self, payment_intent_id, connected_account_id).await
236 }
237
238 fn verify_webhook(&self, payload: &str, signature: &str) -> crate::error::Result<UntypedEvent> {
239 StripeClient::verify_webhook(self, payload, signature)
240 }
241
242 fn verify_webhook_v2(&self, payload: &str, signature: &str) -> crate::error::Result<serde_json::Value> {
243 StripeClient::verify_webhook_v2(self, payload, signature)
244 }
245
246 async fn create_synckit_customer(&self, developer_user_id: crate::db::UserId, email: &str, app_name: &str) -> crate::error::Result<String> {
247 StripeClient::create_synckit_customer(self, developer_user_id, email, app_name).await
248 }
249
250 async fn create_synckit_subscription(&self, customer_id: &str, app_id: crate::db::SyncAppId, app_name: &str, price_cents: i64) -> crate::error::Result<SynckitSubResult> {
251 StripeClient::create_synckit_subscription(self, customer_id, app_id, app_name, price_cents).await
252 }
253
254 async fn update_synckit_subscription_price(&self, subscription_id: &str, new_price_cents: i64, app_name: &str) -> crate::error::Result<()> {
255 StripeClient::update_synckit_subscription_price(self, subscription_id, new_price_cents, app_name).await
256 }
257
258 async fn update_synckit_app_sub_price(&self, subscription_id: &str, new_price_cents: i64, interval: SyncBillingInterval, product_name: &str) -> crate::error::Result<()> {
259 StripeClient::update_synckit_app_sub_price(self, subscription_id, new_price_cents, interval, product_name).await
260 }
261
262 async fn cancel_synckit_subscription(&self, subscription_id: &str) -> crate::error::Result<()> {
263 StripeClient::cancel_synckit_subscription(self, subscription_id).await
264 }
265
266 async fn create_synckit_billing_portal(&self, customer_id: &str, return_url: &str) -> crate::error::Result<String> {
267 StripeClient::create_synckit_billing_portal(self, customer_id, return_url).await
268 }
269 }
270