Skip to main content

reauth_types/
subscription.rs

1use serde::{Deserialize, Serialize};
2
3/// Subscription status values used in JWT claims and API responses.
4///
5/// Includes an `Unknown` variant with `#[serde(other)]` for forward compatibility
6/// with new status values that may be added in the future.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
8#[serde(rename_all = "snake_case")]
9pub enum SubscriptionStatus {
10    Active,
11    PastDue,
12    Canceled,
13    Trialing,
14    Incomplete,
15    IncompleteExpired,
16    Unpaid,
17    Paused,
18    #[default]
19    None,
20    /// Forward compatibility: any unrecognized status string deserializes to Unknown.
21    /// Note: serialization always produces `"unknown"` regardless of the original input,
22    /// so deserialize-then-serialize round-trips are lossy.
23    #[serde(other)]
24    Unknown,
25}
26
27impl SubscriptionStatus {
28    /// Returns true if the subscription is in an active state (active or trialing).
29    pub fn is_active(&self) -> bool {
30        matches!(self, Self::Active | Self::Trialing)
31    }
32
33    /// Returns true if the subscription is in a grace period (past due but not yet canceled).
34    pub fn is_grace_period(&self) -> bool {
35        matches!(self, Self::PastDue)
36    }
37
38    /// Returns true if the subscription allows access to paid features.
39    pub fn has_access(&self) -> bool {
40        matches!(self, Self::Active | Self::Trialing | Self::PastDue)
41    }
42}
43
44impl std::fmt::Display for SubscriptionStatus {
45    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46        let s = match self {
47            Self::Active => "active",
48            Self::PastDue => "past_due",
49            Self::Canceled => "canceled",
50            Self::Trialing => "trialing",
51            Self::Incomplete => "incomplete",
52            Self::IncompleteExpired => "incomplete_expired",
53            Self::Unpaid => "unpaid",
54            Self::Paused => "paused",
55            Self::None => "none",
56            Self::Unknown => "unknown",
57        };
58        write!(f, "{}", s)
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn test_serde_roundtrip() {
68        let status = SubscriptionStatus::Active;
69        let json = serde_json::to_string(&status).unwrap();
70        assert_eq!(json, r#""active""#);
71
72        let parsed: SubscriptionStatus = serde_json::from_str(&json).unwrap();
73        assert_eq!(parsed, status);
74    }
75
76    #[test]
77    fn test_snake_case_serialization() {
78        let status = SubscriptionStatus::PastDue;
79        let json = serde_json::to_string(&status).unwrap();
80        assert_eq!(json, r#""past_due""#);
81    }
82
83    #[test]
84    fn test_is_active() {
85        assert!(SubscriptionStatus::Active.is_active());
86        assert!(SubscriptionStatus::Trialing.is_active());
87        assert!(!SubscriptionStatus::Canceled.is_active());
88        assert!(!SubscriptionStatus::None.is_active());
89    }
90
91    #[test]
92    fn test_has_access() {
93        assert!(SubscriptionStatus::Active.has_access());
94        assert!(SubscriptionStatus::Trialing.has_access());
95        assert!(SubscriptionStatus::PastDue.has_access());
96        assert!(!SubscriptionStatus::Canceled.has_access());
97        assert!(!SubscriptionStatus::None.has_access());
98    }
99
100    #[test]
101    fn test_unknown_status_deserialization() {
102        let parsed: SubscriptionStatus = serde_json::from_str(r#""some_future_status""#).unwrap();
103        assert_eq!(parsed, SubscriptionStatus::Unknown);
104        assert!(!parsed.is_active());
105        assert!(!parsed.has_access());
106        assert!(!parsed.is_grace_period());
107    }
108
109    #[test]
110    fn test_unknown_status_round_trip_is_lossy() {
111        let parsed: SubscriptionStatus = serde_json::from_str(r#""grandfathered""#).unwrap();
112        assert_eq!(parsed, SubscriptionStatus::Unknown);
113        let re_serialized = serde_json::to_string(&parsed).unwrap();
114        assert_eq!(re_serialized, r#""unknown""#);
115    }
116}