use serde::{Deserialize, Serialize};
use crate::error::{A2AError, A2AResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum SubscriptionStatus {
Trial,
Active,
Paused,
PastDue,
Cancelled,
}
impl std::fmt::Display for SubscriptionStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Trial => write!(f, "trial"),
Self::Active => write!(f, "active"),
Self::Paused => write!(f, "paused"),
Self::PastDue => write!(f, "past_due"),
Self::Cancelled => write!(f, "cancelled"),
}
}
}
impl SubscriptionStatus {
#[must_use]
pub const fn allowed_transitions(self) -> &'static [Self] {
match self {
Self::Trial => &[Self::Active, Self::Cancelled],
Self::Active => &[Self::Paused, Self::PastDue, Self::Cancelled],
Self::Paused => &[Self::Active, Self::Cancelled],
Self::PastDue => &[Self::Active, Self::Cancelled],
Self::Cancelled => &[],
}
}
#[must_use]
pub fn can_transition_to(self, target: Self) -> bool {
self.allowed_transitions().contains(&target)
}
#[must_use]
pub const fn is_terminal(self) -> bool {
matches!(self, Self::Cancelled)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SubscriptionTransition {
pub from: SubscriptionStatus,
pub to: SubscriptionStatus,
}
impl SubscriptionTransition {
pub fn new(from: SubscriptionStatus, to: SubscriptionStatus) -> A2AResult<Self> {
if from.can_transition_to(to) {
Ok(Self { from, to })
} else {
let allowed: Vec<&str> = from
.allowed_transitions()
.iter()
.map(|s| match s {
SubscriptionStatus::Trial => "trial",
SubscriptionStatus::Active => "active",
SubscriptionStatus::Paused => "paused",
SubscriptionStatus::PastDue => "past_due",
SubscriptionStatus::Cancelled => "cancelled",
})
.collect();
Err(A2AError::invalid_transition(from, to, &allowed))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trial_to_active() {
assert!(SubscriptionStatus::Trial.can_transition_to(SubscriptionStatus::Active));
}
#[test]
fn trial_to_cancelled() {
assert!(SubscriptionStatus::Trial.can_transition_to(SubscriptionStatus::Cancelled));
}
#[test]
fn trial_cannot_go_to_paused() {
assert!(!SubscriptionStatus::Trial.can_transition_to(SubscriptionStatus::Paused));
}
#[test]
fn trial_cannot_go_to_past_due() {
assert!(!SubscriptionStatus::Trial.can_transition_to(SubscriptionStatus::PastDue));
}
#[test]
fn active_to_paused() {
assert!(SubscriptionStatus::Active.can_transition_to(SubscriptionStatus::Paused));
}
#[test]
fn active_to_cancelled() {
assert!(SubscriptionStatus::Active.can_transition_to(SubscriptionStatus::Cancelled));
}
#[test]
fn active_to_past_due() {
assert!(SubscriptionStatus::Active.can_transition_to(SubscriptionStatus::PastDue));
}
#[test]
fn active_cannot_go_to_trial() {
assert!(!SubscriptionStatus::Active.can_transition_to(SubscriptionStatus::Trial));
}
#[test]
fn paused_to_active() {
assert!(SubscriptionStatus::Paused.can_transition_to(SubscriptionStatus::Active));
}
#[test]
fn paused_to_cancelled() {
assert!(SubscriptionStatus::Paused.can_transition_to(SubscriptionStatus::Cancelled));
}
#[test]
fn paused_cannot_go_to_trial() {
assert!(!SubscriptionStatus::Paused.can_transition_to(SubscriptionStatus::Trial));
}
#[test]
fn past_due_to_active() {
assert!(SubscriptionStatus::PastDue.can_transition_to(SubscriptionStatus::Active));
}
#[test]
fn past_due_to_cancelled() {
assert!(SubscriptionStatus::PastDue.can_transition_to(SubscriptionStatus::Cancelled));
}
#[test]
fn past_due_cannot_go_to_paused() {
assert!(!SubscriptionStatus::PastDue.can_transition_to(SubscriptionStatus::Paused));
}
#[test]
fn cancelled_is_terminal() {
assert!(SubscriptionStatus::Cancelled.is_terminal());
assert!(SubscriptionStatus::Cancelled.allowed_transitions().is_empty());
}
#[test]
fn cancelled_cannot_go_anywhere() {
assert!(!SubscriptionStatus::Cancelled.can_transition_to(SubscriptionStatus::Active));
assert!(!SubscriptionStatus::Cancelled.can_transition_to(SubscriptionStatus::Paused));
assert!(!SubscriptionStatus::Cancelled.can_transition_to(SubscriptionStatus::Trial));
}
#[test]
fn trial_is_not_terminal() {
assert!(!SubscriptionStatus::Trial.is_terminal());
}
#[test]
fn active_is_not_terminal() {
assert!(!SubscriptionStatus::Active.is_terminal());
}
#[test]
fn transition_new_valid() {
let t = SubscriptionTransition::new(
SubscriptionStatus::Active,
SubscriptionStatus::Paused,
)
.unwrap();
assert_eq!(t.from, SubscriptionStatus::Active);
assert_eq!(t.to, SubscriptionStatus::Paused);
}
#[test]
fn transition_new_invalid() {
let err = SubscriptionTransition::new(
SubscriptionStatus::Trial,
SubscriptionStatus::Paused,
)
.unwrap_err();
assert!(matches!(err, A2AError::InvalidTransition { .. }));
}
#[test]
fn transition_trial_to_active() {
let t = SubscriptionTransition::new(
SubscriptionStatus::Trial,
SubscriptionStatus::Active,
)
.unwrap();
assert_eq!(t.to, SubscriptionStatus::Active);
}
#[test]
fn status_display() {
assert_eq!(SubscriptionStatus::Trial.to_string(), "trial");
assert_eq!(SubscriptionStatus::Active.to_string(), "active");
assert_eq!(SubscriptionStatus::Paused.to_string(), "paused");
assert_eq!(SubscriptionStatus::PastDue.to_string(), "past_due");
assert_eq!(SubscriptionStatus::Cancelled.to_string(), "cancelled");
}
}