1use serde::{Deserialize, Serialize};
2
3use crate::SubscriptionStatus;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum PlanType {
9 SelfService,
10 Sales,
11}
12
13impl std::fmt::Display for PlanType {
14 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15 match self {
16 Self::SelfService => write!(f, "self_service"),
17 Self::Sales => write!(f, "sales"),
18 }
19 }
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum FeatureType {
26 Boolean,
27 Numeric,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct PlanFeature {
33 pub code: String,
34 pub name: String,
35 pub feature_type: FeatureType,
36 pub numeric_value: Option<i64>,
37 pub unit_label: Option<String>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct SubscriptionPlan {
43 pub id: String,
44 pub code: String,
45 pub name: String,
46 pub description: Option<String>,
47 pub price_cents: i64,
48 pub currency: String,
49 pub interval: String,
50 pub interval_count: i32,
51 pub trial_days: i32,
52 pub features: Vec<PlanFeature>,
53 pub display_order: i32,
54 pub credits_amount: i32,
55 pub plan_type: PlanType,
56 pub contact_url: Option<String>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct UserSubscription {
62 pub id: Option<String>,
63 pub plan_code: Option<String>,
64 pub plan_name: Option<String>,
65 pub status: SubscriptionStatus,
66 pub current_period_end: Option<i64>,
67 pub trial_end: Option<i64>,
68 pub cancel_at_period_end: Option<bool>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct CheckoutSession {
74 pub checkout_url: String,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct PortalSession {
80 pub portal_url: String,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct BalanceTransaction {
86 pub id: String,
87 pub amount_delta: i64,
88 pub reason: serde_json::Value,
89 pub balance_after: i64,
90 pub created_at: String,
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn test_plan_type_serde() {
99 let self_service = PlanType::SelfService;
100 let json = serde_json::to_string(&self_service).unwrap();
101 assert_eq!(json, r#""self_service""#);
102
103 let parsed: PlanType = serde_json::from_str(&json).unwrap();
104 assert_eq!(parsed, self_service);
105
106 let sales = PlanType::Sales;
107 let json = serde_json::to_string(&sales).unwrap();
108 assert_eq!(json, r#""sales""#);
109 }
110
111 #[test]
112 fn test_feature_type_serde() {
113 let boolean = FeatureType::Boolean;
114 let json = serde_json::to_string(&boolean).unwrap();
115 assert_eq!(json, r#""boolean""#);
116
117 let numeric = FeatureType::Numeric;
118 let json = serde_json::to_string(&numeric).unwrap();
119 assert_eq!(json, r#""numeric""#);
120 }
121
122 #[test]
123 fn test_subscription_plan_serde() {
124 let plan = SubscriptionPlan {
125 id: "plan_123".to_string(),
126 code: "pro".to_string(),
127 name: "Pro Plan".to_string(),
128 description: Some("Best for professionals".to_string()),
129 price_cents: 999,
130 currency: "USD".to_string(),
131 interval: "monthly".to_string(),
132 interval_count: 1,
133 trial_days: 14,
134 features: vec![
135 PlanFeature {
136 code: "max_users".to_string(),
137 name: "Max Users".to_string(),
138 feature_type: FeatureType::Numeric,
139 numeric_value: Some(100),
140 unit_label: Some("users".to_string()),
141 },
142 PlanFeature {
143 code: "webhooks".to_string(),
144 name: "Webhooks".to_string(),
145 feature_type: FeatureType::Boolean,
146 numeric_value: None,
147 unit_label: None,
148 },
149 ],
150 display_order: 1,
151 credits_amount: 1000,
152 plan_type: PlanType::SelfService,
153 contact_url: None,
154 };
155
156 let json = serde_json::to_string(&plan).unwrap();
157 let parsed: SubscriptionPlan = serde_json::from_str(&json).unwrap();
158
159 assert_eq!(parsed.code, "pro");
160 assert_eq!(parsed.price_cents, 999);
161 assert_eq!(parsed.plan_type, PlanType::SelfService);
162 assert_eq!(parsed.features.len(), 2);
163 assert_eq!(parsed.features[0].feature_type, FeatureType::Numeric);
164 assert_eq!(parsed.features[0].numeric_value, Some(100));
165 }
166
167 #[test]
168 fn test_subscription_plan_sales_type() {
169 let plan = SubscriptionPlan {
170 id: "plan_456".to_string(),
171 code: "enterprise".to_string(),
172 name: "Enterprise".to_string(),
173 description: None,
174 price_cents: 0,
175 currency: "USD".to_string(),
176 interval: "custom".to_string(),
177 interval_count: 1,
178 trial_days: 0,
179 features: vec![],
180 display_order: 99,
181 credits_amount: 0,
182 plan_type: PlanType::Sales,
183 contact_url: Some("https://example.com/contact".to_string()),
184 };
185
186 let json = serde_json::to_string(&plan).unwrap();
187 let parsed: SubscriptionPlan = serde_json::from_str(&json).unwrap();
188
189 assert_eq!(parsed.plan_type, PlanType::Sales);
190 assert_eq!(
191 parsed.contact_url,
192 Some("https://example.com/contact".to_string())
193 );
194 }
195
196 #[test]
197 fn test_user_subscription_serde() {
198 let sub = UserSubscription {
199 id: Some("sub_123".to_string()),
200 plan_code: Some("pro".to_string()),
201 plan_name: Some("Pro Plan".to_string()),
202 status: SubscriptionStatus::Active,
203 current_period_end: Some(1735689600),
204 trial_end: None,
205 cancel_at_period_end: Some(false),
206 };
207
208 let json = serde_json::to_string(&sub).unwrap();
209 let parsed: UserSubscription = serde_json::from_str(&json).unwrap();
210
211 assert_eq!(parsed.status, SubscriptionStatus::Active);
212 assert_eq!(parsed.plan_code, Some("pro".to_string()));
213 }
214
215 #[test]
216 fn test_user_subscription_none_status() {
217 let sub = UserSubscription {
218 id: None,
219 plan_code: None,
220 plan_name: None,
221 status: SubscriptionStatus::None,
222 current_period_end: None,
223 trial_end: None,
224 cancel_at_period_end: None,
225 };
226
227 let json = serde_json::to_string(&sub).unwrap();
228 let parsed: UserSubscription = serde_json::from_str(&json).unwrap();
229
230 assert_eq!(parsed.status, SubscriptionStatus::None);
231 assert!(parsed.id.is_none());
232 }
233
234 #[test]
235 fn test_checkout_session_serde() {
236 let session = CheckoutSession {
237 checkout_url: "https://checkout.stripe.com/cs_test_123".to_string(),
238 };
239
240 let json = serde_json::to_string(&session).unwrap();
241 let parsed: CheckoutSession = serde_json::from_str(&json).unwrap();
242
243 assert_eq!(
244 parsed.checkout_url,
245 "https://checkout.stripe.com/cs_test_123"
246 );
247 }
248
249 #[test]
250 fn test_portal_session_serde() {
251 let session = PortalSession {
252 portal_url: "https://billing.stripe.com/p/session_123".to_string(),
253 };
254
255 let json = serde_json::to_string(&session).unwrap();
256 let parsed: PortalSession = serde_json::from_str(&json).unwrap();
257
258 assert_eq!(
259 parsed.portal_url,
260 "https://billing.stripe.com/p/session_123"
261 );
262 }
263
264 #[test]
265 fn test_balance_transaction_serde() {
266 let tx = BalanceTransaction {
267 id: "tx_123".to_string(),
268 amount_delta: -50,
269 reason: serde_json::json!({"type": "charge", "note": "API call"}),
270 balance_after: 950,
271 created_at: "2024-01-15T10:30:00Z".to_string(),
272 };
273
274 let json = serde_json::to_string(&tx).unwrap();
275 let parsed: BalanceTransaction = serde_json::from_str(&json).unwrap();
276
277 assert_eq!(parsed.amount_delta, -50);
278 assert_eq!(parsed.balance_after, 950);
279 }
280
281 #[test]
282 fn test_plan_type_display() {
283 assert_eq!(PlanType::SelfService.to_string(), "self_service");
284 assert_eq!(PlanType::Sales.to_string(), "sales");
285 }
286
287 #[test]
288 fn test_plan_deserializes_from_api_json() {
289 let api_json = r#"{
290 "id": "550e8400-e29b-41d4-a716-446655440000",
291 "code": "pro",
292 "name": "Pro Plan",
293 "description": "For professionals",
294 "price_cents": 999,
295 "currency": "USD",
296 "interval": "monthly",
297 "interval_count": 1,
298 "trial_days": 14,
299 "features": [
300 {
301 "code": "max_users",
302 "name": "Max Users",
303 "feature_type": "numeric",
304 "numeric_value": 100,
305 "unit_label": "users"
306 }
307 ],
308 "display_order": 1,
309 "credits_amount": 1000,
310 "plan_type": "self_service",
311 "contact_url": null
312 }"#;
313
314 let plan: SubscriptionPlan = serde_json::from_str(api_json).unwrap();
315 assert_eq!(plan.code, "pro");
316 assert_eq!(plan.plan_type, PlanType::SelfService);
317 assert!(plan.contact_url.is_none());
318 }
319}