backbone-mailing 0.6.1

Email marketing — mass-mail engine: audiences, subscriptions, hand-set mailing lifecycle, seeded A/B testing, per-recipient traces, cycle-44 bridge targets (Odoo mass_mailing port)
//! State machine for TraceStatus workflow
//!
//! Generated by metaphor-schema

use serde::{Deserialize, Serialize};
use std::str::FromStr;

/// States for TraceStatus workflow
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TraceStatusState {
    /// Initial state
    Outgoing,
    Process,
    Pending,
    Sent,
    Open,
    /// Final state
    Reply,
    /// Final state
    Bounce,
    /// Final state
    Error,
    /// Final state
    Cancel,
}

impl Default for TraceStatusState {
    fn default() -> Self {
        Self::Outgoing
    }
}

impl std::fmt::Display for TraceStatusState {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Outgoing => write!(f, "outgoing"),
            Self::Process => write!(f, "process"),
            Self::Pending => write!(f, "pending"),
            Self::Sent => write!(f, "sent"),
            Self::Open => write!(f, "open"),
            Self::Reply => write!(f, "reply"),
            Self::Bounce => write!(f, "bounce"),
            Self::Error => write!(f, "error"),
            Self::Cancel => write!(f, "cancel"),
        }
    }
}

impl FromStr for TraceStatusState {
    type Err = StateMachineError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "outgoing" => Ok(Self::Outgoing),
            "process" => Ok(Self::Process),
            "pending" => Ok(Self::Pending),
            "sent" => Ok(Self::Sent),
            "open" => Ok(Self::Open),
            "reply" => Ok(Self::Reply),
            "bounce" => Ok(Self::Bounce),
            "error" => Ok(Self::Error),
            "cancel" => Ok(Self::Cancel),
            _ => Err(StateMachineError::InvalidState(s.to_string())),
        }
    }
}

impl TraceStatusState {
    /// Check if this is the initial state
    pub fn is_initial(&self) -> bool {
        matches!(self, Self::Outgoing)
    }

    /// Check if this is a final state
    pub fn is_final(&self) -> bool {
        matches!(self, Self::Reply | Self::Bounce | Self::Error | Self::Cancel)
    }

    /// Get all possible states
    pub fn all() -> Vec<Self> {
        vec![
            Self::Outgoing,
            Self::Process,
            Self::Pending,
            Self::Sent,
            Self::Open,
            Self::Reply,
            Self::Bounce,
            Self::Error,
            Self::Cancel,
        ]
    }
}

/// Transitions for TraceStatus workflow
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TraceStatusTransition {
    /// outgoing -> process
    SetProcess,
    /// outgoing, process -> pending
    SetPending,
    /// outgoing, process, pending -> sent
    SetSent,
    /// outgoing, sent, process, pending -> open
    SetOpened,
    /// outgoing, sent, open, process, pending -> reply
    SetReplied,
    /// outgoing, sent, open, process, pending -> bounce
    SetBounced,
    /// outgoing, process, pending -> bounce
    SetBouncedSms,
    /// outgoing, sent, process, pending -> error
    SetFailed,
    /// outgoing -> cancel
    SetCanceled,
}

impl std::fmt::Display for TraceStatusTransition {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::SetProcess => write!(f, "set_process"),
            Self::SetPending => write!(f, "set_pending"),
            Self::SetSent => write!(f, "set_sent"),
            Self::SetOpened => write!(f, "set_opened"),
            Self::SetReplied => write!(f, "set_replied"),
            Self::SetBounced => write!(f, "set_bounced"),
            Self::SetBouncedSms => write!(f, "set_bounced_sms"),
            Self::SetFailed => write!(f, "set_failed"),
            Self::SetCanceled => write!(f, "set_canceled"),
        }
    }
}

impl FromStr for TraceStatusTransition {
    type Err = StateMachineError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "set_process" => Ok(Self::SetProcess),
            "set_pending" => Ok(Self::SetPending),
            "set_sent" => Ok(Self::SetSent),
            "set_opened" => Ok(Self::SetOpened),
            "set_replied" => Ok(Self::SetReplied),
            "set_bounced" => Ok(Self::SetBounced),
            "set_bounced_sms" => Ok(Self::SetBouncedSms),
            "set_failed" => Ok(Self::SetFailed),
            "set_canceled" => Ok(Self::SetCanceled),
            _ => Err(StateMachineError::InvalidTransition(s.to_string())),
        }
    }
}

impl TraceStatusTransition {
    /// Get the target state of this transition
    pub fn target_state(&self) -> TraceStatusState {
        match self {
            Self::SetProcess => TraceStatusState::Process,
            Self::SetPending => TraceStatusState::Pending,
            Self::SetSent => TraceStatusState::Sent,
            Self::SetOpened => TraceStatusState::Open,
            Self::SetReplied => TraceStatusState::Reply,
            Self::SetBounced => TraceStatusState::Bounce,
            Self::SetBouncedSms => TraceStatusState::Bounce,
            Self::SetFailed => TraceStatusState::Error,
            Self::SetCanceled => TraceStatusState::Cancel,
        }
    }

    /// Get all transitions
    pub fn all() -> Vec<Self> {
        vec![
            Self::SetProcess,
            Self::SetPending,
            Self::SetSent,
            Self::SetOpened,
            Self::SetReplied,
            Self::SetBounced,
            Self::SetBouncedSms,
            Self::SetFailed,
            Self::SetCanceled,
        ]
    }

    /// Get allowed roles for this transition
    pub fn allowed_roles(&self) -> &'static [&'static str] {
        match self {
            Self::SetProcess => &[],
            Self::SetPending => &[],
            Self::SetSent => &[],
            Self::SetOpened => &[],
            Self::SetReplied => &[],
            Self::SetBounced => &[],
            Self::SetBouncedSms => &[],
            Self::SetFailed => &[],
            Self::SetCanceled => &[],
        }
    }
}

use super::StateMachineError;

/// State machine for TraceStatus workflow
#[derive(Debug, Clone)]
pub struct TraceStatusStateMachine {
    current_state: TraceStatusState,
}

impl TraceStatusStateMachine {
    /// Create a new state machine with initial state
    pub fn new() -> Self {
        Self {
            current_state: TraceStatusState::default(),
        }
    }

    /// Create from an existing state
    pub fn from_state(state: TraceStatusState) -> Self {
        Self { current_state: state }
    }

    /// Get the current state
    pub fn current_state(&self) -> TraceStatusState {
        self.current_state
    }

    /// Check if a transition is allowed from the current state
    pub fn can_transition(&self, transition: TraceStatusTransition) -> bool {
        if matches!(self.current_state, TraceStatusState::Reply | TraceStatusState::Bounce | TraceStatusState::Error | TraceStatusState::Cancel) {
            return false;
        }

        match (self.current_state, transition) {
            (TraceStatusState::Outgoing, TraceStatusTransition::SetProcess) => true,
            (TraceStatusState::Outgoing, TraceStatusTransition::SetPending) => true,
            (TraceStatusState::Process, TraceStatusTransition::SetPending) => true,
            (TraceStatusState::Outgoing, TraceStatusTransition::SetSent) => true,
            (TraceStatusState::Process, TraceStatusTransition::SetSent) => true,
            (TraceStatusState::Pending, TraceStatusTransition::SetSent) => true,
            (TraceStatusState::Outgoing, TraceStatusTransition::SetOpened) => true,
            (TraceStatusState::Sent, TraceStatusTransition::SetOpened) => true,
            (TraceStatusState::Process, TraceStatusTransition::SetOpened) => true,
            (TraceStatusState::Pending, TraceStatusTransition::SetOpened) => true,
            (TraceStatusState::Outgoing, TraceStatusTransition::SetReplied) => true,
            (TraceStatusState::Sent, TraceStatusTransition::SetReplied) => true,
            (TraceStatusState::Open, TraceStatusTransition::SetReplied) => true,
            (TraceStatusState::Process, TraceStatusTransition::SetReplied) => true,
            (TraceStatusState::Pending, TraceStatusTransition::SetReplied) => true,
            (TraceStatusState::Outgoing, TraceStatusTransition::SetBounced) => true,
            (TraceStatusState::Sent, TraceStatusTransition::SetBounced) => true,
            (TraceStatusState::Open, TraceStatusTransition::SetBounced) => true,
            (TraceStatusState::Process, TraceStatusTransition::SetBounced) => true,
            (TraceStatusState::Pending, TraceStatusTransition::SetBounced) => true,
            (TraceStatusState::Outgoing, TraceStatusTransition::SetBouncedSms) => true,
            (TraceStatusState::Process, TraceStatusTransition::SetBouncedSms) => true,
            (TraceStatusState::Pending, TraceStatusTransition::SetBouncedSms) => true,
            (TraceStatusState::Outgoing, TraceStatusTransition::SetFailed) => true,
            (TraceStatusState::Sent, TraceStatusTransition::SetFailed) => true,
            (TraceStatusState::Process, TraceStatusTransition::SetFailed) => true,
            (TraceStatusState::Pending, TraceStatusTransition::SetFailed) => true,
            (TraceStatusState::Outgoing, TraceStatusTransition::SetCanceled) => true,
            _ => false,
        }
    }

    /// Check if a transition is allowed for a given role
    pub fn can_transition_with_role(&self, transition: TraceStatusTransition, role: &str) -> bool {
        if !self.can_transition(transition) {
            return false;
        }

        let allowed_roles = transition.allowed_roles();
        if allowed_roles.is_empty() {
            return true; // No role restriction
        }

        allowed_roles.iter().any(|r| *r == role || *r == "*")
    }

    /// Apply a transition, returning the new state
    pub fn transition(&mut self, transition: TraceStatusTransition) -> Result<TraceStatusState, StateMachineError> {
        if !self.can_transition(transition) {
            return Err(StateMachineError::TransitionNotAllowed {
                transition: transition.to_string(),
                from: self.current_state.to_string(),
            });
        }

        self.current_state = transition.target_state();
        Ok(self.current_state)
    }

    /// Apply a transition with role check
    pub fn transition_with_role(&mut self, transition: TraceStatusTransition, role: &str) -> Result<TraceStatusState, StateMachineError> {
        // Check basic transition validity first
        if !self.can_transition(transition) {
            return Err(StateMachineError::TransitionNotAllowed {
                transition: transition.to_string(),
                from: self.current_state.to_string(),
            });
        }

        // Check role authorization
        if !self.can_transition_with_role(transition, role) {
            return Err(StateMachineError::RoleNotAuthorized {
                role: role.to_string(),
                transition: transition.to_string(),
            });
        }

        self.current_state = transition.target_state();
        Ok(self.current_state)
    }

    /// Get all available transitions from the current state
    pub fn available_transitions(&self) -> Vec<TraceStatusTransition> {
        TraceStatusTransition::all()
            .into_iter()
            .filter(|t| self.can_transition(*t))
            .collect()
    }

    /// Get all available transitions for a given role
    pub fn available_transitions_for_role(&self, role: &str) -> Vec<TraceStatusTransition> {
        TraceStatusTransition::all()
            .into_iter()
            .filter(|t| self.can_transition_with_role(*t, role))
            .collect()
    }

    /// Attempt to transition directly to a target state.
    ///
    /// Finds any transition that leads to the target state and applies it.
    /// Returns Err if no valid transition leads from current state to target.
    pub fn transition_to_state(&mut self, target: TraceStatusState) -> Result<TraceStatusState, StateMachineError> {
        let valid = TraceStatusTransition::all().into_iter()
            .filter(|t| self.can_transition(*t))
            .find(|t| t.target_state() == target);
        match valid {
            Some(t) => self.transition(t),
            None => Err(StateMachineError::TransitionNotAllowed {
                transition: target.to_string(),
                from: self.current_state.to_string(),
            }),
        }
    }
}

impl Default for TraceStatusStateMachine {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_initial_state() {
        let sm = TraceStatusStateMachine::new();
        assert_eq!(sm.current_state(), TraceStatusState::Outgoing);
        assert!(sm.current_state().is_initial());
    }

    #[test]
    fn test_valid_transition() {
        let mut sm = TraceStatusStateMachine::from_state(TraceStatusState::Outgoing);
        assert!(sm.can_transition(TraceStatusTransition::SetProcess));
        let result = sm.transition(TraceStatusTransition::SetProcess);
        assert!(result.is_ok());
        assert_eq!(sm.current_state(), TraceStatusState::Process);
    }

    #[test]
    fn test_invalid_transition() {
        let mut sm = TraceStatusStateMachine::from_state(TraceStatusState::Process);
        // SetProcess is not valid from Process state
        let result = sm.transition(TraceStatusTransition::SetProcess);
        assert!(result.is_err());
    }

    #[test]
    fn test_state_parsing() {
        let state: TraceStatusState = "outgoing".parse().unwrap();
        assert_eq!(state, TraceStatusState::Outgoing);
    }

    #[test]
    fn test_available_transitions() {
        let sm = TraceStatusStateMachine::new();
        let available = sm.available_transitions();
        // Should have at least some transitions available from initial state
        assert!(!available.is_empty() || sm.current_state().is_final());
    }
}