1#[derive(Debug, Clone, PartialEq, Eq)]
2pub enum LicenseState {
3 Trial { days_left: i64 },
4 Licensed,
5 Limited,
6 FreeTier,
7 Expired,
8 Invalid,
9}
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum TrialStatus {
13 NotStarted,
14 Active { days_left: i64 },
15 Expired,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum KeylessState {
20 Trial,
21 FreeTier,
22 Expired,
23}
24impl KeylessState {
25 pub fn wire(&self) -> &'static str {
26 match self {
27 Self::Trial => "trial",
28 Self::FreeTier => "free_tier",
29 Self::Expired => "expired",
30 }
31 }
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum LicenseLifecycleEvent {
36 Renewed,
37 Cancelled,
38 Expired,
39 Restored,
40}
41
42pub fn resolve_state(
46 lease_status: Option<&str>,
47 lease_current: bool,
48 had_license: bool,
49 trial: &TrialStatus,
50 free_tier_enabled: bool,
51) -> LicenseState {
52 if let Some(status) = lease_status {
53 match (status, lease_current) {
54 ("active", true) => return LicenseState::Licensed,
55 ("fallback", _) => return LicenseState::Limited,
56 ("expired", _) => return LicenseState::Expired,
57 (_, false) => {} _ => {}
59 }
60 }
61 if had_license {
62 return LicenseState::Expired;
63 }
64 match trial {
65 TrialStatus::Active { days_left } => LicenseState::Trial {
66 days_left: *days_left,
67 },
68 _ if free_tier_enabled => LicenseState::FreeTier,
69 _ => LicenseState::Invalid,
70 }
71}
72
73pub fn lifecycle_event(
74 prev: &LicenseState,
75 next: &LicenseState,
76 expiry_moved_later: bool,
77) -> Option<LicenseLifecycleEvent> {
78 use LicenseState::*;
79 match (prev, next) {
80 (Licensed, Licensed) if expiry_moved_later => Some(LicenseLifecycleEvent::Renewed),
81 (Licensed, Expired) | (Licensed, Limited) => Some(LicenseLifecycleEvent::Cancelled),
82 (Expired, Licensed) | (Limited, Licensed) | (Invalid, Licensed) => {
83 Some(LicenseLifecycleEvent::Restored)
84 }
85 (_, Expired) if !matches!(prev, Expired) => Some(LicenseLifecycleEvent::Expired),
86 _ => None,
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 #[test]
94 fn active_current_lease_is_licensed() {
95 assert_eq!(
96 resolve_state(Some("active"), true, true, &TrialStatus::NotStarted, false),
97 LicenseState::Licensed
98 );
99 }
100 #[test]
101 fn fallback_is_limited() {
102 assert_eq!(
103 resolve_state(
104 Some("fallback"),
105 true,
106 true,
107 &TrialStatus::NotStarted,
108 false
109 ),
110 LicenseState::Limited
111 );
112 }
113 #[test]
114 fn no_license_trial_active_is_trial() {
115 assert_eq!(
116 resolve_state(
117 None,
118 false,
119 false,
120 &TrialStatus::Active { days_left: 5 },
121 false
122 ),
123 LicenseState::Trial { days_left: 5 }
124 );
125 }
126 #[test]
127 fn no_license_free_tier_is_free_tier() {
128 assert_eq!(
129 resolve_state(None, false, false, &TrialStatus::NotStarted, true),
130 LicenseState::FreeTier
131 );
132 }
133 #[test]
134 fn nothing_is_invalid() {
135 assert_eq!(
136 resolve_state(None, false, false, &TrialStatus::NotStarted, false),
137 LicenseState::Invalid
138 );
139 }
140 #[test]
141 fn keyless_wire_strings() {
142 assert_eq!(KeylessState::FreeTier.wire(), "free_tier");
143 }
144
145 use LicenseLifecycleEvent as E;
146 use LicenseState as S;
147 #[test]
148 fn renewed_when_licensed_and_expiry_later() {
149 assert_eq!(
150 lifecycle_event(&S::Licensed, &S::Licensed, true),
151 Some(E::Renewed)
152 );
153 assert_eq!(lifecycle_event(&S::Licensed, &S::Licensed, false), None);
154 }
155 #[test]
156 fn cancelled_on_licensed_to_expired_or_limited() {
157 assert_eq!(
158 lifecycle_event(&S::Licensed, &S::Expired, false),
159 Some(E::Cancelled)
160 );
161 assert_eq!(
162 lifecycle_event(&S::Licensed, &S::Limited, false),
163 Some(E::Cancelled)
164 );
165 }
166 #[test]
167 fn restored_on_recovery_to_licensed() {
168 assert_eq!(
169 lifecycle_event(&S::Expired, &S::Licensed, false),
170 Some(E::Restored)
171 );
172 assert_eq!(
173 lifecycle_event(&S::Limited, &S::Licensed, false),
174 Some(E::Restored)
175 );
176 assert_eq!(
177 lifecycle_event(&S::Invalid, &S::Licensed, false),
178 Some(E::Restored)
179 );
180 }
181 #[test]
182 fn expired_when_crossing_into_expired_from_non_expired() {
183 assert_eq!(
184 lifecycle_event(&S::Trial { days_left: 1 }, &S::Expired, false),
185 Some(E::Expired)
186 );
187 }
188 #[test]
189 fn no_event_on_noop_transitions() {
190 assert_eq!(
191 lifecycle_event(
192 &S::Trial { days_left: 3 },
193 &S::Trial { days_left: 2 },
194 false
195 ),
196 None
197 );
198 assert_eq!(lifecycle_event(&S::Expired, &S::Expired, false), None);
199 }
200}