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}