Skip to main content

reauth_types/
billing.rs

1use serde::{Deserialize, Serialize};
2
3use crate::SubscriptionStatus;
4
5/// Plan type determines how a user subscribes.
6#[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/// Feature type for plan features.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum FeatureType {
26    Boolean,
27    Numeric,
28}
29
30/// A single feature attached to a subscription plan.
31#[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/// Subscription plan available for purchase.
41#[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/// User's current subscription details from the API.
60#[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/// Checkout session response from the billing API.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct CheckoutSession {
74    pub checkout_url: String,
75}
76
77/// Customer portal session response from the billing API.
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct PortalSession {
80    pub portal_url: String,
81}
82
83/// A single balance transaction record.
84#[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}