Skip to main content

rustauth_stripe/
options.rs

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    /// Development only — do not use in production.
114    ///
115    /// Uses a no-op HTTP transport and the placeholder webhook secret `whsec_dev`.
116    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}