1use std::future::Future;
2use std::sync::Arc;
3
4use rustauth_core::api::ApiRequest;
5use rustauth_core::context::AuthContext;
6use rustauth_core::db::{Session, User};
7use rustauth_core::env::logger::Logger;
8use rustauth_core::error::RustAuthError;
9use rustauth_core::plugin::PluginSchemaContribution;
10use serde_json::json;
11
12use crate::models::{StripeEvent, StripeSubscription, Subscription};
13use crate::stripe_api::StripeClient;
14
15#[non_exhaustive]
16#[derive(Clone)]
17pub struct StripeOptions {
18 pub(crate) stripe_client: StripeClient,
19 pub(crate) stripe_webhook_secret: String,
20 pub(crate) create_customer_on_sign_up: bool,
21 pub(crate) subscription: Option<SubscriptionOptions>,
22 pub(crate) organization: Option<OrganizationStripeOptions>,
23 pub(crate) on_event: Option<StripeEventHook>,
24 pub(crate) on_customer_create: Option<CustomerCreateHook>,
25 pub(crate) get_customer_create_params: Option<GetCustomerCreateParamsHook>,
26 pub(crate) schema: Vec<PluginSchemaContribution>,
27}
28
29type StripeEventHook = Arc<
30 dyn Fn(StripeEvent) -> crate::stripe_api::BoxFuture<'static, Result<(), RustAuthError>>
31 + Send
32 + Sync,
33>;
34type CustomerCreateHook = Arc<
35 dyn Fn(
36 CustomerCreateInput,
37 CustomerCreateContext,
38 ) -> crate::stripe_api::BoxFuture<'static, Result<(), RustAuthError>>
39 + Send
40 + Sync,
41>;
42type GetCustomerCreateParamsHook = Arc<
43 dyn Fn(
44 CustomerCreateParamsInput,
45 CustomerCreateContext,
46 )
47 -> crate::stripe_api::BoxFuture<'static, Result<serde_json::Value, RustAuthError>>
48 + Send
49 + Sync,
50>;
51
52#[derive(Clone)]
53pub struct CustomerCreateContext {
54 pub base_url: Option<String>,
55 pub request_path: Option<String>,
56 pub logger: Logger,
57}
58
59impl std::fmt::Debug for CustomerCreateContext {
60 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 formatter
62 .debug_struct("CustomerCreateContext")
63 .field("base_url", &self.base_url)
64 .field("request_path", &self.request_path)
65 .finish_non_exhaustive()
66 }
67}
68
69impl CustomerCreateContext {
70 pub fn from_auth_context(context: &AuthContext) -> Self {
71 Self {
72 base_url: Some(context.base_url.clone()),
73 request_path: None,
74 logger: context.logger.clone(),
75 }
76 }
77
78 pub fn database_hook(request_path: Option<String>, logger: &Logger) -> Self {
79 Self {
80 base_url: None,
81 request_path,
82 logger: logger.clone(),
83 }
84 }
85}
86
87#[derive(Debug, Clone, PartialEq)]
88pub struct CustomerCreateInput {
89 pub stripe_customer: serde_json::Value,
90 pub user: User,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct CustomerCreateParamsInput {
95 pub user: User,
96}
97
98impl StripeOptions {
99 pub fn new(stripe_client: StripeClient, stripe_webhook_secret: impl Into<String>) -> Self {
100 Self {
101 stripe_client,
102 stripe_webhook_secret: stripe_webhook_secret.into(),
103 create_customer_on_sign_up: false,
104 subscription: None,
105 organization: None,
106 on_event: None,
107 on_customer_create: None,
108 get_customer_create_params: None,
109 schema: Vec::new(),
110 }
111 }
112
113 pub fn dev() -> Self {
117 use std::sync::Arc;
118
119 use crate::stripe_api::{
120 StripeRequest, StripeResponse, StripeTransport, StripeTransportFuture,
121 };
122
123 struct DevStripeTransport;
124
125 impl StripeTransport for DevStripeTransport {
126 fn send<'a>(&'a self, _request: StripeRequest) -> StripeTransportFuture<'a> {
127 Box::pin(async move {
128 Ok(StripeResponse {
129 status: 200,
130 body: serde_json::json!({}),
131 })
132 })
133 }
134 }
135
136 Self::new(
137 StripeClient::with_transport("sk_test_dev", Arc::new(DevStripeTransport)),
138 "whsec_dev",
139 )
140 }
141
142 pub fn create_customer_on_sign_up(mut self, enabled: bool) -> Self {
143 self.create_customer_on_sign_up = enabled;
144 self
145 }
146
147 pub fn subscription(mut self, subscription: SubscriptionOptions) -> Self {
148 self.subscription = Some(subscription);
149 self
150 }
151
152 pub fn organization(mut self, organization: OrganizationStripeOptions) -> Self {
153 self.organization = Some(organization);
154 self
155 }
156
157 pub fn schema(mut self, contribution: PluginSchemaContribution) -> Self {
158 self.schema.push(contribution);
159 self
160 }
161
162 pub fn on_event<F, Fut>(mut self, hook: F) -> Self
163 where
164 F: Fn(StripeEvent) -> Fut + Send + Sync + 'static,
165 Fut: Future<Output = Result<(), RustAuthError>> + Send + 'static,
166 {
167 self.on_event = Some(Arc::new(move |event| Box::pin(hook(event))));
168 self
169 }
170
171 pub fn on_customer_create<F, Fut>(mut self, hook: F) -> Self
172 where
173 F: Fn(CustomerCreateInput, CustomerCreateContext) -> Fut + Send + Sync + 'static,
174 Fut: Future<Output = Result<(), RustAuthError>> + Send + 'static,
175 {
176 self.on_customer_create = Some(Arc::new(move |input, ctx| Box::pin(hook(input, ctx))));
177 self
178 }
179
180 pub fn get_customer_create_params<F, Fut>(mut self, hook: F) -> Self
181 where
182 F: Fn(CustomerCreateParamsInput, CustomerCreateContext) -> Fut + Send + Sync + 'static,
183 Fut: Future<Output = Result<serde_json::Value, RustAuthError>> + Send + 'static,
184 {
185 self.get_customer_create_params =
186 Some(Arc::new(move |input, ctx| Box::pin(hook(input, ctx))));
187 self
188 }
189
190 pub fn to_metadata(&self) -> serde_json::Value {
191 json!({
192 "subscription": self.subscription.as_ref().map(|subscription| json!({
193 "enabled": subscription.enabled,
194 "plans": subscription.plans.iter().map(|plan| plan.name.clone()).collect::<Vec<_>>()
195 })),
196 "organization": self.organization.as_ref().map(|organization| json!({
197 "enabled": organization.enabled
198 })),
199 "createCustomerOnSignUp": self.create_customer_on_sign_up
200 })
201 }
202}
203
204#[non_exhaustive]
205#[derive(Clone)]
206pub struct SubscriptionOptions {
207 pub(crate) enabled: bool,
208 pub(crate) plans: Arc<Vec<StripePlan>>,
209 pub(crate) get_plans: Option<GetPlansHook>,
210 pub(crate) require_email_verification: bool,
211 pub(crate) authorize_reference: Option<AuthorizeReferenceHook>,
212 pub(crate) on_subscription_complete: Option<SubscriptionLifecycleHook>,
213 pub(crate) on_subscription_created: Option<SubscriptionLifecycleHook>,
214 pub(crate) on_subscription_update: Option<SubscriptionUpdateHook>,
215 pub(crate) on_subscription_cancel: Option<SubscriptionLifecycleHook>,
216 pub(crate) on_subscription_deleted: Option<SubscriptionLifecycleHook>,
217 pub(crate) get_checkout_session_params: Option<GetCheckoutSessionParamsHook>,
218}
219
220type GetPlansHook = Arc<
221 dyn Fn() -> crate::stripe_api::BoxFuture<'static, Result<Vec<StripePlan>, RustAuthError>>
222 + Send
223 + Sync,
224>;
225
226type AuthorizeReferenceHook = Arc<
227 dyn Fn(
228 AuthorizeReferenceInput,
229 &AuthContext,
230 ) -> crate::stripe_api::BoxFuture<'static, Result<bool, RustAuthError>>
231 + Send
232 + Sync,
233>;
234
235type SubscriptionLifecycleHook = Arc<
236 dyn Fn(
237 SubscriptionLifecycleInput,
238 ) -> crate::stripe_api::BoxFuture<'static, Result<(), RustAuthError>>
239 + Send
240 + Sync,
241>;
242
243type SubscriptionUpdateHook = Arc<
244 dyn Fn(
245 SubscriptionUpdateInput,
246 ) -> crate::stripe_api::BoxFuture<'static, Result<(), RustAuthError>>
247 + Send
248 + Sync,
249>;
250type GetCheckoutSessionParamsHook = Arc<
251 dyn Fn(
252 CheckoutSessionParamsInput,
253 &ApiRequest,
254 &AuthContext,
255 )
256 -> crate::stripe_api::BoxFuture<'static, Result<serde_json::Value, RustAuthError>>
257 + Send
258 + Sync,
259>;
260
261#[derive(Debug, Clone, PartialEq)]
262pub struct SubscriptionLifecycleInput {
263 pub event: StripeEvent,
264 pub subscription: Subscription,
265 pub stripe_subscription: Option<StripeSubscription>,
266 pub plan: Option<StripePlan>,
267 pub cancellation_details: Option<serde_json::Value>,
268}
269
270#[derive(Debug, Clone, PartialEq)]
271pub struct SubscriptionUpdateInput {
272 pub event: StripeEvent,
273 pub subscription: Subscription,
274}
275
276#[derive(Debug, Clone, PartialEq)]
277pub struct CheckoutSessionParamsInput {
278 pub user: User,
279 pub session: Session,
280 pub plan: StripePlan,
281 pub subscription: Subscription,
282}
283
284#[derive(Debug, Clone, PartialEq, Eq)]
285pub struct AuthorizeReferenceInput {
286 pub user_id: String,
287 pub user: User,
288 pub session: Session,
289 pub reference_id: String,
290 pub action: AuthorizeReferenceAction,
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq)]
294pub enum AuthorizeReferenceAction {
295 UpgradeSubscription,
296 ListSubscription,
297 CancelSubscription,
298 RestoreSubscription,
299 BillingPortal,
300}
301
302impl AuthorizeReferenceAction {
303 pub fn as_str(self) -> &'static str {
304 match self {
305 Self::UpgradeSubscription => "upgrade-subscription",
306 Self::ListSubscription => "list-subscription",
307 Self::CancelSubscription => "cancel-subscription",
308 Self::RestoreSubscription => "restore-subscription",
309 Self::BillingPortal => "billing-portal",
310 }
311 }
312}
313
314impl SubscriptionOptions {
315 pub fn enabled(plans: Vec<StripePlan>) -> Self {
316 Self {
317 enabled: true,
318 plans: Arc::new(plans),
319 get_plans: None,
320 require_email_verification: false,
321 authorize_reference: None,
322 on_subscription_complete: None,
323 on_subscription_created: None,
324 on_subscription_update: None,
325 on_subscription_cancel: None,
326 on_subscription_deleted: None,
327 get_checkout_session_params: None,
328 }
329 }
330
331 pub fn enabled_dynamic<F, Fut>(provider: F) -> Self
332 where
333 F: Fn() -> Fut + Send + Sync + 'static,
334 Fut: Future<Output = Result<Vec<StripePlan>, RustAuthError>> + Send + 'static,
335 {
336 Self {
337 get_plans: Some(Arc::new(move || Box::pin(provider()))),
338 ..Self::enabled(Vec::new())
339 }
340 }
341
342 pub fn plans_provider<F, Fut>(mut self, provider: F) -> Self
343 where
344 F: Fn() -> Fut + Send + Sync + 'static,
345 Fut: Future<Output = Result<Vec<StripePlan>, RustAuthError>> + Send + 'static,
346 {
347 self.get_plans = Some(Arc::new(move || Box::pin(provider())));
348 self
349 }
350
351 pub async fn resolve_plans(&self) -> Result<Self, RustAuthError> {
352 let Some(provider) = &self.get_plans else {
353 return Ok(self.clone());
354 };
355 let plans = provider().await?;
356 let mut resolved = self.clone();
357 resolved.plans = Arc::new(plans);
358 Ok(resolved)
359 }
360
361 pub fn require_email_verification(mut self, enabled: bool) -> Self {
362 self.require_email_verification = enabled;
363 self
364 }
365
366 pub fn authorize_reference<F, Fut>(mut self, hook: F) -> Self
367 where
368 F: Fn(AuthorizeReferenceInput, &AuthContext) -> Fut + Send + Sync + 'static,
369 Fut: Future<Output = Result<bool, RustAuthError>> + Send + 'static,
370 {
371 self.authorize_reference = Some(Arc::new(move |input, ctx| Box::pin(hook(input, ctx))));
372 self
373 }
374
375 pub fn get_checkout_session_params<F, Fut>(mut self, hook: F) -> Self
376 where
377 F: Fn(CheckoutSessionParamsInput, &ApiRequest, &AuthContext) -> Fut + Send + Sync + 'static,
378 Fut: Future<Output = Result<serde_json::Value, RustAuthError>> + Send + 'static,
379 {
380 self.get_checkout_session_params = Some(Arc::new(move |input, request, ctx| {
381 Box::pin(hook(input, request, ctx))
382 }));
383 self
384 }
385
386 pub fn on_subscription_complete<F, Fut>(mut self, hook: F) -> Self
387 where
388 F: Fn(SubscriptionLifecycleInput) -> Fut + Send + Sync + 'static,
389 Fut: Future<Output = Result<(), RustAuthError>> + Send + 'static,
390 {
391 self.on_subscription_complete = Some(Arc::new(move |input| Box::pin(hook(input))));
392 self
393 }
394
395 pub fn on_subscription_created<F, Fut>(mut self, hook: F) -> Self
396 where
397 F: Fn(SubscriptionLifecycleInput) -> Fut + Send + Sync + 'static,
398 Fut: Future<Output = Result<(), RustAuthError>> + Send + 'static,
399 {
400 self.on_subscription_created = Some(Arc::new(move |input| Box::pin(hook(input))));
401 self
402 }
403
404 pub fn on_subscription_update<F, Fut>(mut self, hook: F) -> Self
405 where
406 F: Fn(SubscriptionUpdateInput) -> Fut + Send + Sync + 'static,
407 Fut: Future<Output = Result<(), RustAuthError>> + Send + 'static,
408 {
409 self.on_subscription_update = Some(Arc::new(move |input| Box::pin(hook(input))));
410 self
411 }
412
413 pub fn on_subscription_cancel<F, Fut>(mut self, hook: F) -> Self
414 where
415 F: Fn(SubscriptionLifecycleInput) -> Fut + Send + Sync + 'static,
416 Fut: Future<Output = Result<(), RustAuthError>> + Send + 'static,
417 {
418 self.on_subscription_cancel = Some(Arc::new(move |input| Box::pin(hook(input))));
419 self
420 }
421
422 pub fn on_subscription_deleted<F, Fut>(mut self, hook: F) -> Self
423 where
424 F: Fn(SubscriptionLifecycleInput) -> Fut + Send + Sync + 'static,
425 Fut: Future<Output = Result<(), RustAuthError>> + Send + 'static,
426 {
427 self.on_subscription_deleted = Some(Arc::new(move |input| Box::pin(hook(input))));
428 self
429 }
430}
431
432#[non_exhaustive]
433#[derive(Debug, Clone, PartialEq)]
434pub struct StripePlan {
435 pub(crate) name: String,
436 pub(crate) price_id: Option<String>,
437 pub(crate) lookup_key: Option<String>,
438 pub(crate) annual_discount_price_id: Option<String>,
439 pub(crate) annual_discount_lookup_key: Option<String>,
440 pub(crate) limits: Option<serde_json::Value>,
441 pub(crate) group: Option<String>,
442 pub(crate) seat_price_id: Option<String>,
443 pub(crate) proration_behavior: Option<String>,
444 pub(crate) line_items: Vec<serde_json::Value>,
445 pub(crate) free_trial: Option<FreeTrialOptions>,
446}
447
448impl StripePlan {
449 pub fn new(name: impl Into<String>) -> Self {
450 Self {
451 name: name.into(),
452 price_id: None,
453 lookup_key: None,
454 annual_discount_price_id: None,
455 annual_discount_lookup_key: None,
456 limits: None,
457 group: None,
458 seat_price_id: None,
459 proration_behavior: None,
460 line_items: Vec::new(),
461 free_trial: None,
462 }
463 }
464
465 pub fn name(&self) -> &str {
466 &self.name
467 }
468
469 pub fn price_id(mut self, price_id: impl Into<String>) -> Self {
470 self.price_id = Some(price_id.into());
471 self
472 }
473
474 pub fn lookup_key(mut self, lookup_key: impl Into<String>) -> Self {
475 self.lookup_key = Some(lookup_key.into());
476 self
477 }
478
479 pub fn annual_discount_price_id(mut self, price_id: impl Into<String>) -> Self {
480 self.annual_discount_price_id = Some(price_id.into());
481 self
482 }
483
484 pub fn annual_discount_lookup_key(mut self, lookup_key: impl Into<String>) -> Self {
485 self.annual_discount_lookup_key = Some(lookup_key.into());
486 self
487 }
488
489 pub fn seat_price_id(mut self, price_id: impl Into<String>) -> Self {
490 self.seat_price_id = Some(price_id.into());
491 self
492 }
493
494 pub fn limits(mut self, limits: serde_json::Value) -> Self {
495 self.limits = Some(limits);
496 self
497 }
498
499 pub fn group(mut self, group: impl Into<String>) -> Self {
500 self.group = Some(group.into());
501 self
502 }
503
504 pub fn line_item(mut self, line_item: serde_json::Value) -> Self {
505 self.line_items.push(line_item);
506 self
507 }
508
509 pub fn proration_behavior(mut self, proration_behavior: impl Into<String>) -> Self {
510 self.proration_behavior = Some(proration_behavior.into());
511 self
512 }
513
514 pub fn free_trial(mut self, free_trial: FreeTrialOptions) -> Self {
515 self.free_trial = Some(free_trial);
516 self
517 }
518}
519
520#[non_exhaustive]
521#[derive(Clone)]
522pub struct FreeTrialOptions {
523 pub(crate) days: i64,
524 pub(crate) on_trial_start: Option<TrialStartHook>,
525 pub(crate) on_trial_end: Option<TrialLifecycleHook>,
526 pub(crate) on_trial_expired: Option<TrialLifecycleHook>,
527}
528
529type TrialStartHook = Arc<
530 dyn Fn(Subscription) -> crate::stripe_api::BoxFuture<'static, Result<(), RustAuthError>>
531 + Send
532 + Sync,
533>;
534type TrialLifecycleHook = Arc<
535 dyn Fn(
536 Subscription,
537 &AuthContext,
538 ) -> crate::stripe_api::BoxFuture<'static, Result<(), RustAuthError>>
539 + Send
540 + Sync,
541>;
542
543impl std::fmt::Debug for FreeTrialOptions {
544 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
545 formatter
546 .debug_struct("FreeTrialOptions")
547 .field("days", &self.days)
548 .finish_non_exhaustive()
549 }
550}
551
552impl PartialEq for FreeTrialOptions {
553 fn eq(&self, other: &Self) -> bool {
554 self.days == other.days
555 }
556}
557
558impl Eq for FreeTrialOptions {}
559
560impl FreeTrialOptions {
561 pub fn new(days: i64) -> Self {
562 Self {
563 days,
564 on_trial_start: None,
565 on_trial_end: None,
566 on_trial_expired: None,
567 }
568 }
569
570 pub fn on_trial_start<F, Fut>(mut self, hook: F) -> Self
571 where
572 F: Fn(Subscription) -> Fut + Send + Sync + 'static,
573 Fut: Future<Output = Result<(), RustAuthError>> + Send + 'static,
574 {
575 self.on_trial_start = Some(Arc::new(move |subscription| Box::pin(hook(subscription))));
576 self
577 }
578
579 pub fn on_trial_end<F, Fut>(mut self, hook: F) -> Self
580 where
581 F: Fn(Subscription, &AuthContext) -> Fut + Send + Sync + 'static,
582 Fut: Future<Output = Result<(), RustAuthError>> + Send + 'static,
583 {
584 self.on_trial_end = Some(Arc::new(move |subscription, ctx| {
585 Box::pin(hook(subscription, ctx))
586 }));
587 self
588 }
589
590 pub fn on_trial_expired<F, Fut>(mut self, hook: F) -> Self
591 where
592 F: Fn(Subscription, &AuthContext) -> Fut + Send + Sync + 'static,
593 Fut: Future<Output = Result<(), RustAuthError>> + Send + 'static,
594 {
595 self.on_trial_expired = Some(Arc::new(move |subscription, ctx| {
596 Box::pin(hook(subscription, ctx))
597 }));
598 self
599 }
600}
601
602#[non_exhaustive]
603#[derive(Clone)]
604pub struct OrganizationStripeOptions {
605 pub(crate) enabled: bool,
606 pub(crate) get_customer_create_params: Option<GetOrganizationCustomerCreateParamsHook>,
607 pub(crate) on_customer_create: Option<OrganizationCustomerCreateHook>,
608}
609
610type GetOrganizationCustomerCreateParamsHook = Arc<
611 dyn Fn(
612 OrganizationCustomerCreateParamsInput,
613 CustomerCreateContext,
614 )
615 -> crate::stripe_api::BoxFuture<'static, Result<serde_json::Value, RustAuthError>>
616 + Send
617 + Sync,
618>;
619type OrganizationCustomerCreateHook = Arc<
620 dyn Fn(
621 OrganizationCustomerCreateInput,
622 CustomerCreateContext,
623 ) -> crate::stripe_api::BoxFuture<'static, Result<(), RustAuthError>>
624 + Send
625 + Sync,
626>;
627
628#[derive(Debug, Clone, PartialEq)]
629pub struct OrganizationCustomerCreateParamsInput {
630 pub organization: serde_json::Value,
631}
632
633#[derive(Debug, Clone, PartialEq)]
634pub struct OrganizationCustomerCreateInput {
635 pub stripe_customer: serde_json::Value,
636 pub organization: serde_json::Value,
637}
638
639impl OrganizationStripeOptions {
640 pub fn enabled() -> Self {
641 Self {
642 enabled: true,
643 get_customer_create_params: None,
644 on_customer_create: None,
645 }
646 }
647
648 pub fn get_customer_create_params<F, Fut>(mut self, hook: F) -> Self
649 where
650 F: Fn(OrganizationCustomerCreateParamsInput, CustomerCreateContext) -> Fut
651 + Send
652 + Sync
653 + 'static,
654 Fut: Future<Output = Result<serde_json::Value, RustAuthError>> + Send + 'static,
655 {
656 self.get_customer_create_params =
657 Some(Arc::new(move |input, ctx| Box::pin(hook(input, ctx))));
658 self
659 }
660
661 pub fn on_customer_create<F, Fut>(mut self, hook: F) -> Self
662 where
663 F: Fn(OrganizationCustomerCreateInput, CustomerCreateContext) -> Fut
664 + Send
665 + Sync
666 + 'static,
667 Fut: Future<Output = Result<(), RustAuthError>> + Send + 'static,
668 {
669 self.on_customer_create = Some(Arc::new(move |input, ctx| Box::pin(hook(input, ctx))));
670 self
671 }
672}