Skip to main content

stateset_embedded/
subscriptions.rs

1//! Subscription management operations
2//!
3//! Comprehensive subscription system supporting:
4//! - Subscription plans with flexible billing intervals
5//! - Customer subscriptions with full lifecycle management
6//! - Trial periods and promotional pricing
7//! - Pause, resume, skip, and cancel functionality
8//! - Billing cycle tracking and payment integration
9//!
10//! # Example
11//!
12//! ```rust,ignore
13//! use stateset_embedded::{Commerce, CreateSubscriptionPlan, CreateSubscription, BillingInterval};
14//! use rust_decimal_macros::dec;
15//!
16//! let commerce = Commerce::new("./store.db")?;
17//!
18//! // Create a subscription plan
19//! let plan = commerce.subscriptions().create_plan(CreateSubscriptionPlan {
20//!     name: "Monthly Coffee Box".into(),
21//!     billing_interval: BillingInterval::Monthly,
22//!     price: dec!(29.99),
23//!     trial_days: Some(14),
24//!     ..Default::default()
25//! })?;
26//!
27//! // Activate the plan
28//! commerce.subscriptions().activate_plan(plan.id)?;
29//!
30//! // Subscribe a customer
31//! let subscription = commerce.subscriptions().subscribe(CreateSubscription {
32//!     customer_id: customer.id,
33//!     plan_id: plan.id,
34//!     ..Default::default()
35//! })?;
36//!
37//! println!("Subscription #{} created", subscription.subscription_number);
38//! # Ok::<(), stateset_embedded::CommerceError>(())
39//! ```
40
41use chrono::{DateTime, Utc};
42use stateset_core::{
43    BillingCycle, BillingCycleFilter, BillingCycleStatus, CancelSubscription, CreateBillingCycle,
44    CreateSubscription, CreateSubscriptionPlan, CustomerId, PauseSubscription, Result,
45    SkipBillingCycle, Subscription, SubscriptionEvent, SubscriptionFilter, SubscriptionId,
46    SubscriptionPlan, SubscriptionPlanFilter, UpdateSubscription, UpdateSubscriptionPlan,
47};
48use stateset_db::Database;
49use stateset_observability::Metrics;
50use std::sync::Arc;
51use uuid::Uuid;
52
53/// Subscription management interface.
54pub struct Subscriptions {
55    db: Arc<dyn Database>,
56    metrics: Metrics,
57}
58
59impl std::fmt::Debug for Subscriptions {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.debug_struct("Subscriptions").finish_non_exhaustive()
62    }
63}
64
65impl Subscriptions {
66    pub(crate) fn new(db: Arc<dyn Database>, metrics: Metrics) -> Self {
67        Self { db, metrics }
68    }
69
70    // ========================================================================
71    // Subscription Plans
72    // ========================================================================
73
74    /// Create a new subscription plan.
75    ///
76    /// # Example
77    ///
78    /// ```rust,ignore
79    /// use stateset_embedded::{Commerce, CreateSubscriptionPlan, BillingInterval};
80    /// use rust_decimal_macros::dec;
81    ///
82    /// let commerce = Commerce::new(":memory:")?;
83    ///
84    /// let plan = commerce.subscriptions().create_plan(CreateSubscriptionPlan {
85    ///     name: "Weekly Meal Kit".into(),
86    ///     billing_interval: BillingInterval::Weekly,
87    ///     price: dec!(79.99),
88    ///     trial_days: Some(7),
89    ///     ..Default::default()
90    /// })?;
91    /// # Ok::<(), stateset_embedded::CommerceError>(())
92    /// ```
93    pub fn create_plan(&self, input: CreateSubscriptionPlan) -> Result<SubscriptionPlan> {
94        self.db.subscriptions().create_plan(input)
95    }
96
97    /// Get a subscription plan by ID.
98    pub fn get_plan(&self, id: Uuid) -> Result<Option<SubscriptionPlan>> {
99        self.db.subscriptions().get_plan(id)
100    }
101
102    /// Get a subscription plan by its code.
103    pub fn get_plan_by_code(&self, code: &str) -> Result<Option<SubscriptionPlan>> {
104        self.db.subscriptions().get_plan_by_code(code)
105    }
106
107    /// List subscription plans with optional filtering.
108    ///
109    /// # Example
110    ///
111    /// ```rust,ignore
112    /// use stateset_embedded::{Commerce, SubscriptionPlanFilter, PlanStatus};
113    ///
114    /// let commerce = Commerce::new(":memory:")?;
115    ///
116    /// // List active plans
117    /// let plans = commerce.subscriptions().list_plans(SubscriptionPlanFilter {
118    ///     status: Some(PlanStatus::Active),
119    ///     ..Default::default()
120    /// })?;
121    /// # Ok::<(), stateset_embedded::CommerceError>(())
122    /// ```
123    pub fn list_plans(&self, filter: SubscriptionPlanFilter) -> Result<Vec<SubscriptionPlan>> {
124        self.db.subscriptions().list_plans(filter)
125    }
126
127    /// Update a subscription plan.
128    pub fn update_plan(&self, id: Uuid, input: UpdateSubscriptionPlan) -> Result<SubscriptionPlan> {
129        self.db.subscriptions().update_plan(id, input)
130    }
131
132    /// Activate a subscription plan (make it available for new subscriptions).
133    pub fn activate_plan(&self, id: Uuid) -> Result<SubscriptionPlan> {
134        self.db.subscriptions().activate_plan(id)
135    }
136
137    /// Archive a subscription plan (no new subscriptions, existing ones continue).
138    pub fn archive_plan(&self, id: Uuid) -> Result<SubscriptionPlan> {
139        self.db.subscriptions().archive_plan(id)
140    }
141
142    // ========================================================================
143    // Subscriptions
144    // ========================================================================
145
146    /// Create a new subscription for a customer.
147    ///
148    /// # Example
149    ///
150    /// ```rust,ignore
151    /// use stateset_embedded::{Commerce, CreateSubscription};
152    /// use uuid::Uuid;
153    ///
154    /// let commerce = Commerce::new(":memory:")?;
155    ///
156    /// let subscription = commerce.subscriptions().subscribe(CreateSubscription {
157    ///     customer_id: Uuid::new_v4(),
158    ///     plan_id: Uuid::new_v4(),
159    ///     payment_method_id: Some("pm_1234".into()),
160    ///     ..Default::default()
161    /// })?;
162    ///
163    /// println!("Created subscription #{}", subscription.subscription_number);
164    /// # Ok::<(), stateset_embedded::CommerceError>(())
165    /// ```
166    pub fn subscribe(&self, input: CreateSubscription) -> Result<Subscription> {
167        let subscription = self.db.subscriptions().create_subscription(input)?;
168        self.metrics.record_subscription_created(&subscription.id.to_string());
169        Ok(subscription)
170    }
171
172    /// Get a subscription by ID.
173    pub fn get(&self, id: SubscriptionId) -> Result<Option<Subscription>> {
174        self.db.subscriptions().get_subscription(id)
175    }
176
177    /// Get a subscription by its number (e.g., "SUB-123456").
178    pub fn get_by_number(&self, number: &str) -> Result<Option<Subscription>> {
179        self.db.subscriptions().get_subscription_by_number(number)
180    }
181
182    /// List subscriptions with optional filtering.
183    ///
184    /// # Example
185    ///
186    /// ```rust,ignore
187    /// use stateset_embedded::{Commerce, SubscriptionFilter, SubscriptionStatus};
188    /// use uuid::Uuid;
189    ///
190    /// let commerce = Commerce::new(":memory:")?;
191    ///
192    /// // List active subscriptions for a customer
193    /// let subs = commerce.subscriptions().list(SubscriptionFilter {
194    ///     customer_id: Some(Uuid::new_v4()),
195    ///     status: Some(SubscriptionStatus::Active),
196    ///     ..Default::default()
197    /// })?;
198    /// # Ok::<(), stateset_embedded::CommerceError>(())
199    /// ```
200    pub fn list(&self, filter: SubscriptionFilter) -> Result<Vec<Subscription>> {
201        self.db.subscriptions().list_subscriptions(filter)
202    }
203
204    /// Update a subscription.
205    pub fn update(&self, id: SubscriptionId, input: UpdateSubscription) -> Result<Subscription> {
206        self.db.subscriptions().update_subscription(id, input)
207    }
208
209    // ========================================================================
210    // Subscription Lifecycle
211    // ========================================================================
212
213    /// Pause a subscription.
214    ///
215    /// Pausing stops billing but preserves the subscription. The customer
216    /// can resume at any time.
217    ///
218    /// # Example
219    ///
220    /// ```rust,ignore
221    /// use stateset_embedded::{Commerce, PauseSubscription};
222    /// use uuid::Uuid;
223    /// use chrono::{Utc, Duration};
224    ///
225    /// let commerce = Commerce::new(":memory:")?;
226    ///
227    /// // Pause for 30 days
228    /// commerce.subscriptions().pause(Uuid::new_v4(), PauseSubscription {
229    ///     resume_at: Some(Utc::now() + Duration::days(30)),
230    ///     reason: Some("Customer requested vacation hold".into()),
231    /// })?;
232    /// # Ok::<(), stateset_embedded::CommerceError>(())
233    /// ```
234    pub fn pause(&self, id: SubscriptionId, input: PauseSubscription) -> Result<Subscription> {
235        self.db.subscriptions().pause_subscription(id, input)
236    }
237
238    /// Resume a paused subscription.
239    ///
240    /// This reactivates billing and creates a new billing period.
241    pub fn resume(&self, id: SubscriptionId) -> Result<Subscription> {
242        self.db.subscriptions().resume_subscription(id)
243    }
244
245    /// Cancel a subscription.
246    ///
247    /// By default, cancellation takes effect at the end of the current
248    /// billing period. Use `immediate: true` to cancel immediately.
249    ///
250    /// # Example
251    ///
252    /// ```rust,ignore
253    /// use stateset_embedded::{Commerce, CancelSubscription};
254    /// use uuid::Uuid;
255    ///
256    /// let commerce = Commerce::new(":memory:")?;
257    ///
258    /// // Cancel at end of period
259    /// commerce.subscriptions().cancel(Uuid::new_v4(), CancelSubscription {
260    ///     reason: Some("Customer found alternative".into()),
261    ///     ..Default::default()
262    /// })?;
263    ///
264    /// // Immediate cancellation
265    /// commerce.subscriptions().cancel(Uuid::new_v4(), CancelSubscription {
266    ///     immediate: Some(true),
267    ///     reason: Some("Refund requested".into()),
268    ///     ..Default::default()
269    /// })?;
270    /// # Ok::<(), stateset_embedded::CommerceError>(())
271    /// ```
272    pub fn cancel(&self, id: SubscriptionId, input: CancelSubscription) -> Result<Subscription> {
273        self.db.subscriptions().cancel_subscription(id, input)
274    }
275
276    /// Skip the next billing cycle.
277    ///
278    /// The subscription remains active, but the next billing date is
279    /// pushed forward by one interval.
280    ///
281    /// # Example
282    ///
283    /// ```rust,ignore
284    /// use stateset_embedded::{Commerce, SkipBillingCycle};
285    /// use uuid::Uuid;
286    ///
287    /// let commerce = Commerce::new(":memory:")?;
288    ///
289    /// commerce.subscriptions().skip_next_cycle(Uuid::new_v4(), SkipBillingCycle {
290    ///     reason: Some("Customer traveling".into()),
291    /// })?;
292    /// # Ok::<(), stateset_embedded::CommerceError>(())
293    /// ```
294    pub fn skip_next_cycle(
295        &self,
296        id: SubscriptionId,
297        input: SkipBillingCycle,
298    ) -> Result<Subscription> {
299        self.db.subscriptions().skip_billing_cycle(id, input)
300    }
301
302    // ========================================================================
303    // Billing Cycles
304    // ========================================================================
305
306    /// Create a billing cycle for a subscription.
307    ///
308    /// This is typically called by the billing system when processing
309    /// subscription renewals.
310    pub fn create_billing_cycle(
311        &self,
312        subscription_id: SubscriptionId,
313        cycle_number: i32,
314        period_start: DateTime<Utc>,
315        period_end: DateTime<Utc>,
316    ) -> Result<BillingCycle> {
317        self.db.subscriptions().create_billing_cycle(CreateBillingCycle {
318            subscription_id,
319            cycle_number,
320            period_start,
321            period_end,
322        })
323    }
324
325    /// Get a billing cycle by ID.
326    pub fn get_billing_cycle(&self, id: Uuid) -> Result<Option<BillingCycle>> {
327        self.db.subscriptions().get_billing_cycle(id)
328    }
329
330    /// List billing cycles with optional filtering.
331    pub fn list_billing_cycles(&self, filter: BillingCycleFilter) -> Result<Vec<BillingCycle>> {
332        self.db.subscriptions().list_billing_cycles(filter)
333    }
334
335    /// Update billing cycle status (mark as paid, failed, etc.).
336    pub fn update_billing_cycle_status(
337        &self,
338        id: Uuid,
339        status: BillingCycleStatus,
340    ) -> Result<BillingCycle> {
341        self.db.subscriptions().update_billing_cycle_status(id, status)
342    }
343
344    /// Mark a billing cycle as paid.
345    pub fn mark_cycle_paid(&self, id: Uuid) -> Result<BillingCycle> {
346        self.db.subscriptions().update_billing_cycle_status(id, BillingCycleStatus::Paid)
347    }
348
349    /// Mark a billing cycle as failed.
350    pub fn mark_cycle_failed(&self, id: Uuid) -> Result<BillingCycle> {
351        self.db.subscriptions().update_billing_cycle_status(id, BillingCycleStatus::Failed)
352    }
353
354    // ========================================================================
355    // Events
356    // ========================================================================
357
358    /// Get subscription events (audit log).
359    ///
360    /// # Example
361    ///
362    /// ```rust,ignore
363    /// use stateset_embedded::Commerce;
364    /// use uuid::Uuid;
365    ///
366    /// let commerce = Commerce::new(":memory:")?;
367    ///
368    /// let events = commerce.subscriptions().get_events(Uuid::new_v4())?;
369    /// for event in events {
370    ///     println!("{}: {}", event.event_type, event.description);
371    /// }
372    /// # Ok::<(), stateset_embedded::CommerceError>(())
373    /// ```
374    pub fn get_events(&self, subscription_id: SubscriptionId) -> Result<Vec<SubscriptionEvent>> {
375        self.db.subscriptions().get_subscription_events(subscription_id)
376    }
377
378    // ========================================================================
379    // Convenience Methods
380    // ========================================================================
381
382    /// Get all active plans.
383    pub fn get_active_plans(&self) -> Result<Vec<SubscriptionPlan>> {
384        self.list_plans(SubscriptionPlanFilter {
385            status: Some(stateset_core::PlanStatus::Active),
386            ..Default::default()
387        })
388    }
389
390    /// Get all subscriptions for a customer.
391    pub fn get_customer_subscriptions(&self, customer_id: CustomerId) -> Result<Vec<Subscription>> {
392        self.list(SubscriptionFilter { customer_id: Some(customer_id), ..Default::default() })
393    }
394
395    /// Get active subscriptions for a customer.
396    pub fn get_active_customer_subscriptions(
397        &self,
398        customer_id: CustomerId,
399    ) -> Result<Vec<Subscription>> {
400        self.list(SubscriptionFilter {
401            customer_id: Some(customer_id),
402            status: Some(stateset_core::SubscriptionStatus::Active),
403            ..Default::default()
404        })
405    }
406
407    /// Check if a subscription is active.
408    pub fn is_active(&self, id: SubscriptionId) -> Result<bool> {
409        if let Some(sub) = self.get(id)? { Ok(sub.is_active()) } else { Ok(false) }
410    }
411
412    /// Check if a subscription is in trial period.
413    pub fn is_in_trial(&self, id: SubscriptionId) -> Result<bool> {
414        if let Some(sub) = self.get(id)? { Ok(sub.is_in_trial()) } else { Ok(false) }
415    }
416
417    /// Get subscriptions due for billing.
418    ///
419    /// Returns subscriptions where `next_billing_date` is on or before the
420    /// specified date.
421    pub fn get_due_for_billing(&self, before: DateTime<Utc>) -> Result<Vec<Subscription>> {
422        let subs = self.list(SubscriptionFilter {
423            status: Some(stateset_core::SubscriptionStatus::Active),
424            ..Default::default()
425        })?;
426
427        Ok(subs
428            .into_iter()
429            .filter(|s| {
430                if let Some(next_billing) = s.next_billing_date {
431                    next_billing <= before
432                } else {
433                    false
434                }
435            })
436            .collect())
437    }
438
439    /// Get subscriptions with trials ending soon.
440    ///
441    /// Returns subscriptions in trial status where trial ends within the
442    /// specified number of days.
443    pub fn get_trials_ending(&self, within_days: i64) -> Result<Vec<Subscription>> {
444        let cutoff = Utc::now() + chrono::Duration::days(within_days);
445
446        let subs = self.list(SubscriptionFilter {
447            status: Some(stateset_core::SubscriptionStatus::Trial),
448            ..Default::default()
449        })?;
450
451        Ok(subs
452            .into_iter()
453            .filter(|s| {
454                if let Some(trial_ends) = s.trial_ends_at { trial_ends <= cutoff } else { false }
455            })
456            .collect())
457    }
458}