Skip to main content

max / makenotwork

12.8 KB · 322 lines History Blame Raw
1 //! Connected account operations: onboarding, balance, product/price creation,
2 //! subscription lifecycle, refunds, and billing portal.
3
4 use stripe::StripeRequest;
5 use stripe_billing::subscription::{
6 CancelSubscription, ResumeSubscription,
7 UpdateSubscription,
8 UpdateSubscriptionPauseCollection, UpdateSubscriptionPauseCollectionBehavior,
9 };
10 use stripe_billing::billing_portal_session::CreateBillingPortalSession;
11 use stripe_connect::account::{CreateAccount, CreateAccountType, RetrieveAccount};
12 use stripe_connect::account_link::{CreateAccountLink, CreateAccountLinkType};
13 use stripe_core::balance::RetrieveForMyAccountBalance;
14 use stripe_core::refund::CreateRefund;
15 use stripe_product::product::CreateProduct;
16 use stripe_product::price::{CreatePrice, CreatePriceRecurring, CreatePriceRecurringInterval};
17 use stripe_types::Currency;
18
19 use crate::error::{AppError, Result};
20 use super::StripeClient;
21
22 fn parse_subscription_id(stripe_sub_id: &str) -> Result<stripe_shared::SubscriptionId> {
23 stripe_sub_id.parse().map_err(|e| {
24 AppError::Internal(anyhow::anyhow!("Invalid Stripe subscription ID '{}': {}", stripe_sub_id, e))
25 })
26 }
27
28 fn parse_account_id_internal(account_id: &str) -> Result<stripe_shared::AccountId> {
29 account_id.parse().map_err(|_| {
30 AppError::Internal(anyhow::anyhow!("Invalid Stripe account ID"))
31 })
32 }
33
34 impl StripeClient {
35 /// Create a Stripe Standard connected account for a creator.
36 #[tracing::instrument(skip_all, name = "payments::create_connect_account")]
37 pub async fn create_connect_account(&self, email: &str) -> Result<String> {
38 let account = CreateAccount::new()
39 .type_(CreateAccountType::Standard)
40 .email(email.to_string())
41 .send(&self.client)
42 .await
43 .map_err(|e| {
44 tracing::error!(error = ?e, "failed to create Stripe connected account");
45 AppError::BadRequest("Failed to create Stripe account".to_string())
46 })?;
47 Ok(account.id.to_string())
48 }
49
50 /// Create an Account Link for Stripe Connect onboarding.
51 #[tracing::instrument(skip_all, name = "payments::create_account_link")]
52 pub async fn create_account_link(
53 &self,
54 account_id: &str,
55 return_url: &str,
56 refresh_url: &str,
57 ) -> Result<String> {
58 // CreateAccountLink takes the account id as a plain String, not AccountId.
59 let link = CreateAccountLink::new(account_id.to_string(), CreateAccountLinkType::AccountOnboarding)
60 .return_url(return_url.to_string())
61 .refresh_url(refresh_url.to_string())
62 .send(&self.client)
63 .await
64 .map_err(|e| {
65 tracing::error!(error = ?e, "failed to create Stripe account link");
66 AppError::BadRequest("Failed to create Stripe onboarding link".to_string())
67 })?;
68 Ok(link.url)
69 }
70
71 /// Fetch a Stripe Connect account by ID.
72 #[tracing::instrument(skip_all, name = "payments::fetch_account")]
73 pub async fn fetch_account(&self, account_id: &str) -> Result<super::AccountUpdate> {
74 let account_id = Self::parse_account_id(account_id)?;
75 let account = RetrieveAccount::new(account_id)
76 .send(&self.client)
77 .await
78 .map_err(|e| {
79 tracing::error!(error = ?e, "failed to fetch Stripe account");
80 AppError::BadRequest("Failed to fetch Stripe account".to_string())
81 })?;
82
83 Ok(super::AccountUpdate {
84 account_id: account.id.to_string(),
85 charges_enabled: account.charges_enabled.unwrap_or(false),
86 payouts_enabled: account.payouts_enabled.unwrap_or(false),
87 details_submitted: account.details_submitted.unwrap_or(false),
88 })
89 }
90
91 /// Create a Product + monthly recurring Price on a connected account.
92 #[tracing::instrument(skip_all, name = "payments::create_subscription_product_and_price")]
93 pub async fn create_subscription_product_and_price(
94 &self,
95 connected_account_id: &str,
96 tier_name: &str,
97 tier_description: Option<&str>,
98 price_cents: i64,
99 ) -> Result<(String, String)> {
100 if price_cents <= 0 {
101 return Err(AppError::BadRequest("Price must be positive".to_string()));
102 }
103
104 let acct = Self::parse_account_id(connected_account_id)?;
105
106 let mut product_req = CreateProduct::new(tier_name.to_string());
107 if let Some(desc) = tier_description {
108 product_req = product_req.description(desc.to_string());
109 }
110 let product = product_req
111 .customize()
112 .account_id(acct.clone())
113 .send(&self.client)
114 .await
115 .map_err(|e| {
116 tracing::error!(error = ?e, "failed to create Stripe product");
117 AppError::BadRequest("Failed to create subscription product".to_string())
118 })?;
119
120 let price = CreatePrice::new(Currency::USD)
121 .product(product.id.to_string())
122 .unit_amount(price_cents)
123 .recurring(CreatePriceRecurring::new(CreatePriceRecurringInterval::Month))
124 .customize()
125 .account_id(acct)
126 .send(&self.client)
127 .await
128 .map_err(|e| {
129 tracing::error!(error = ?e, "failed to create Stripe price");
130 AppError::BadRequest("Failed to create subscription price".to_string())
131 })?;
132
133 Ok((product.id.to_string(), price.id.to_string()))
134 }
135
136 /// Retrieve the balance for a connected account.
137 #[tracing::instrument(skip_all, name = "payments::get_connected_account_balance")]
138 pub async fn get_connected_account_balance(&self, account_id: &str) -> Result<stripe_core::Balance> {
139 let acct = Self::parse_account_id(account_id)?;
140 RetrieveForMyAccountBalance::new()
141 .customize()
142 .account_id(acct)
143 .send(&self.client)
144 .await
145 .map_err(|e| {
146 tracing::error!(error = ?e, "failed to fetch Stripe balance");
147 AppError::BadRequest("Failed to fetch Stripe balance".to_string())
148 })
149 }
150
151 /// Pause subscription collection (void invoices) on a connected account.
152 #[tracing::instrument(skip_all, name = "payments::pause_subscription")]
153 pub async fn pause_subscription(
154 &self,
155 stripe_sub_id: &str,
156 connected_account_id: &str,
157 ) -> Result<()> {
158 let acct = parse_account_id_internal(connected_account_id)?;
159 let sub_id = parse_subscription_id(stripe_sub_id)?;
160
161 UpdateSubscription::new(sub_id)
162 .pause_collection(UpdateSubscriptionPauseCollection::new(
163 UpdateSubscriptionPauseCollectionBehavior::Void,
164 ))
165 .customize()
166 .account_id(acct)
167 .send(&self.client)
168 .await
169 .map_err(|e| {
170 tracing::error!(stripe_sub_id = %stripe_sub_id, error = ?e, "failed to pause Stripe subscription");
171 AppError::Internal(anyhow::anyhow!("Failed to pause subscription"))
172 })?;
173
174 Ok(())
175 }
176
177 /// Resume a paused subscription on a connected account.
178 ///
179 /// rc.5 exposes `POST /subscriptions/{id}/resume` as the proper way to lift
180 /// a pause; the legacy "clear `pause_collection`" trick is no longer needed.
181 #[tracing::instrument(skip_all, name = "payments::resume_subscription")]
182 pub async fn resume_subscription(
183 &self,
184 stripe_sub_id: &str,
185 connected_account_id: &str,
186 ) -> Result<()> {
187 let acct = parse_account_id_internal(connected_account_id)?;
188 let sub_id = parse_subscription_id(stripe_sub_id)?;
189
190 ResumeSubscription::new(sub_id)
191 .customize()
192 .account_id(acct)
193 .send(&self.client)
194 .await
195 .map_err(|e| {
196 tracing::error!(stripe_sub_id = %stripe_sub_id, error = ?e, "failed to resume Stripe subscription");
197 AppError::Internal(anyhow::anyhow!("Failed to resume subscription"))
198 })?;
199
200 Ok(())
201 }
202
203 /// Cancel a subscription on a connected account (permanent).
204 #[tracing::instrument(skip_all, name = "payments::cancel_subscription")]
205 pub async fn cancel_subscription(
206 &self,
207 stripe_sub_id: &str,
208 connected_account_id: &str,
209 ) -> Result<()> {
210 let acct = parse_account_id_internal(connected_account_id)?;
211 let sub_id = parse_subscription_id(stripe_sub_id)?;
212
213 CancelSubscription::new(sub_id)
214 .customize()
215 .account_id(acct)
216 .send(&self.client)
217 .await
218 .map_err(|e| {
219 tracing::error!(stripe_sub_id = %stripe_sub_id, error = ?e, "failed to cancel Stripe subscription");
220 AppError::Internal(anyhow::anyhow!("Failed to cancel subscription"))
221 })?;
222
223 Ok(())
224 }
225
226 /// Cancel a platform-level subscription (creator tier, Fan+).
227 #[tracing::instrument(skip_all, name = "payments::cancel_platform_subscription")]
228 pub async fn cancel_platform_subscription(&self, stripe_sub_id: &str) -> Result<()> {
229 let sub_id = parse_subscription_id(stripe_sub_id)?;
230 CancelSubscription::new(sub_id)
231 .send(&self.client)
232 .await
233 .map_err(|e| {
234 tracing::error!(stripe_sub_id = %stripe_sub_id, error = ?e, "failed to cancel platform subscription");
235 AppError::Internal(anyhow::anyhow!("Failed to cancel platform subscription"))
236 })?;
237 Ok(())
238 }
239
240 /// Set or clear `cancel_at_period_end` on a platform-level subscription.
241 #[tracing::instrument(skip_all, name = "payments::set_platform_cancel_at_period_end")]
242 pub async fn set_platform_cancel_at_period_end(
243 &self,
244 stripe_sub_id: &str,
245 cancel: bool,
246 ) -> Result<()> {
247 let sub_id = parse_subscription_id(stripe_sub_id)?;
248 UpdateSubscription::new(sub_id)
249 .cancel_at_period_end(cancel)
250 .send(&self.client)
251 .await
252 .map_err(|e| {
253 tracing::error!(stripe_sub_id = %stripe_sub_id, cancel = %cancel, error = ?e, "failed to set platform cancel_at_period_end");
254 AppError::Internal(anyhow::anyhow!("Failed to update subscription cancellation"))
255 })?;
256 Ok(())
257 }
258
259 /// Set or clear `cancel_at_period_end` on a connected-account subscription.
260 #[tracing::instrument(skip_all, name = "payments::set_cancel_at_period_end")]
261 pub async fn set_cancel_at_period_end(
262 &self,
263 stripe_sub_id: &str,
264 connected_account_id: &str,
265 cancel: bool,
266 ) -> Result<()> {
267 let acct = parse_account_id_internal(connected_account_id)?;
268 let sub_id = parse_subscription_id(stripe_sub_id)?;
269 UpdateSubscription::new(sub_id)
270 .cancel_at_period_end(cancel)
271 .customize()
272 .account_id(acct)
273 .send(&self.client)
274 .await
275 .map_err(|e| {
276 tracing::error!(stripe_sub_id = %stripe_sub_id, cancel = %cancel, error = ?e, "failed to set cancel_at_period_end");
277 AppError::Internal(anyhow::anyhow!("Failed to update subscription cancellation"))
278 })?;
279 Ok(())
280 }
281
282 /// Create a Stripe Billing Portal session for a customer.
283 #[tracing::instrument(skip_all, name = "payments::create_billing_portal_session")]
284 pub async fn create_billing_portal_session(
285 &self,
286 stripe_customer_id: &str,
287 return_url: &str,
288 ) -> Result<String> {
289 let session = CreateBillingPortalSession::new()
290 .customer(stripe_customer_id.to_string())
291 .return_url(return_url.to_string())
292 .send(&self.client)
293 .await
294 .map_err(|e| {
295 tracing::error!(error = ?e, "failed to create billing portal session");
296 AppError::Internal(anyhow::anyhow!("Failed to create billing portal session"))
297 })?;
298 Ok(session.url)
299 }
300
301 /// Issue a full refund for a payment on a connected account.
302 #[tracing::instrument(skip_all, name = "payments::create_refund")]
303 pub async fn create_refund(
304 &self,
305 payment_intent_id: &str,
306 connected_account_id: &str,
307 ) -> Result<()> {
308 let acct = parse_account_id_internal(connected_account_id)?;
309 CreateRefund::new()
310 .payment_intent(payment_intent_id.to_string())
311 .customize()
312 .account_id(acct)
313 .send(&self.client)
314 .await
315 .map_err(|e| {
316 tracing::error!(payment_intent_id = %payment_intent_id, error = ?e, "failed to create Stripe refund");
317 AppError::Internal(anyhow::anyhow!("Failed to create refund"))
318 })?;
319 Ok(())
320 }
321 }
322