use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SubscriptionStatus {
Trialing,
Active,
Incomplete,
IncompleteExpired,
PastDue,
Canceled,
Unpaid,
Paused,
}
#[derive(Debug, Clone, Serialize)]
pub struct SubscriptionInfo {
pub stripe_subscription_id: String,
pub plan: String,
pub status: SubscriptionStatus,
pub trial_ends_at: Option<chrono::DateTime<chrono::Utc>>,
pub cancel_at_period_end: bool,
pub current_period_end: chrono::DateTime<chrono::Utc>,
pub stripe_connect_account_id: Option<String>,
}
impl SubscriptionInfo {
pub fn on_trial(&self) -> bool {
self.status == SubscriptionStatus::Trialing
}
pub fn subscribed(&self) -> bool {
matches!(
self.status,
SubscriptionStatus::Active | SubscriptionStatus::Trialing
)
}
pub fn on_grace_period(&self) -> bool {
self.cancel_at_period_end && self.subscribed()
}
}
pub fn plan_satisfies(tenant_plan: &str, required_plan: &str) -> bool {
const TIERS: &[&str] = &["free", "pro", "enterprise"];
let tenant_rank = TIERS.iter().position(|&p| p == tenant_plan);
let required_rank = TIERS.iter().position(|&p| p == required_plan);
match (tenant_rank, required_rank) {
(Some(t), Some(r)) => t >= r,
_ => tenant_plan == required_plan,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plan_satisfies_enterprise_satisfies_pro_and_free() {
assert!(plan_satisfies("enterprise", "enterprise"));
assert!(plan_satisfies("enterprise", "pro"));
assert!(plan_satisfies("enterprise", "free"));
}
#[test]
fn plan_satisfies_pro_satisfies_free_but_not_enterprise() {
assert!(plan_satisfies("pro", "pro"));
assert!(plan_satisfies("pro", "free"));
assert!(!plan_satisfies("pro", "enterprise"));
}
#[test]
fn plan_satisfies_free_satisfies_only_itself() {
assert!(plan_satisfies("free", "free"));
assert!(!plan_satisfies("free", "pro"));
assert!(!plan_satisfies("free", "enterprise"));
}
#[test]
fn plan_satisfies_unknown_plans_match_exact() {
assert!(plan_satisfies("custom", "custom"));
assert!(!plan_satisfies("custom", "pro"));
assert!(!plan_satisfies("pro", "custom"));
}
#[test]
fn subscription_status_serializes_to_snake_case() {
assert_eq!(
serde_json::to_string(&SubscriptionStatus::PastDue).unwrap(),
"\"past_due\""
);
assert_eq!(
serde_json::to_string(&SubscriptionStatus::IncompleteExpired).unwrap(),
"\"incomplete_expired\""
);
}
}