mockforge_data/
persona_lifecycle.rs

1//! Time-Aware Personas ("Life Events")
2//!
3//! This module provides lifecycle state management for personas that evolve over pseudo-time.
4//! Supports prebuilt lifecycle scenarios (new signup, power user, churn risk) and time-based
5//! state transitions.
6
7use chrono::{DateTime, Duration, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// Lifecycle state for a persona
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum LifecycleState {
15    /// New user signup - fresh account, no history
16    NewSignup,
17    /// Active user - regular usage
18    Active,
19    /// Power user - high activity, many orders
20    PowerUser,
21    /// Churn risk - declining activity, potential to leave
22    ChurnRisk,
23    /// Churned - user has left
24    Churned,
25    /// Upgrade pending - user has requested upgrade
26    UpgradePending,
27    /// Payment failed - payment issue detected
28    PaymentFailed,
29}
30
31impl LifecycleState {
32    /// Get a human-readable name for the state
33    pub fn name(&self) -> &'static str {
34        match self {
35            LifecycleState::NewSignup => "New Signup",
36            LifecycleState::Active => "Active",
37            LifecycleState::PowerUser => "Power User",
38            LifecycleState::ChurnRisk => "Churn Risk",
39            LifecycleState::Churned => "Churned",
40            LifecycleState::UpgradePending => "Upgrade Pending",
41            LifecycleState::PaymentFailed => "Payment Failed",
42        }
43    }
44
45    /// Check if this state is a terminal state (no further transitions)
46    pub fn is_terminal(&self) -> bool {
47        matches!(self, LifecycleState::Churned)
48    }
49}
50
51/// Rule for transitioning between lifecycle states
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct TransitionRule {
54    /// Target state to transition to
55    pub to: LifecycleState,
56    /// Time threshold in days before transition can occur
57    pub after_days: Option<u64>,
58    /// Optional condition that must be met (e.g., "payment_failed_count > 2")
59    pub condition: Option<String>,
60    /// Optional callback to apply when transitioning
61    pub on_transition: Option<String>,
62}
63
64/// Persona lifecycle manager
65///
66/// Manages the lifecycle state of a persona, including state transitions
67/// based on pseudo-time and conditions.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct PersonaLifecycle {
70    /// Persona ID
71    pub persona_id: String,
72    /// Current lifecycle state
73    pub current_state: LifecycleState,
74    /// History of state transitions
75    pub state_history: Vec<(DateTime<Utc>, LifecycleState)>,
76    /// Transition rules for this persona
77    pub transition_rules: Vec<TransitionRule>,
78    /// State entered at time
79    pub state_entered_at: DateTime<Utc>,
80    /// Additional metadata for lifecycle tracking
81    #[serde(default)]
82    pub metadata: HashMap<String, serde_json::Value>,
83}
84
85impl PersonaLifecycle {
86    /// Create a new persona lifecycle with initial state
87    pub fn new(persona_id: String, initial_state: LifecycleState) -> Self {
88        let now = Utc::now();
89        Self {
90            persona_id,
91            current_state: initial_state,
92            state_history: vec![(now, initial_state)],
93            transition_rules: Vec::new(),
94            state_entered_at: now,
95            metadata: HashMap::new(),
96        }
97    }
98
99    /// Create a new persona lifecycle with transition rules
100    pub fn with_rules(
101        persona_id: String,
102        initial_state: LifecycleState,
103        transition_rules: Vec<TransitionRule>,
104    ) -> Self {
105        let mut lifecycle = Self::new(persona_id, initial_state);
106        lifecycle.transition_rules = transition_rules;
107        lifecycle
108    }
109
110    /// Check if a transition should occur based on elapsed time
111    ///
112    /// Returns the target state if a transition should occur, None otherwise.
113    pub fn transition_if_elapsed(
114        &self,
115        current_time: DateTime<Utc>,
116    ) -> Option<(LifecycleState, &TransitionRule)> {
117        let elapsed_days = (current_time - self.state_entered_at).num_days() as u64;
118
119        for rule in &self.transition_rules {
120            // Check if time threshold is met
121            if let Some(after_days) = rule.after_days {
122                if elapsed_days >= after_days {
123                    // Check if condition is met (if specified)
124                    if let Some(ref condition) = rule.condition {
125                        if !self.evaluate_condition(condition) {
126                            continue;
127                        }
128                    }
129                    return Some((rule.to, rule));
130                }
131            }
132        }
133
134        None
135    }
136
137    /// Evaluate a condition string against the persona's metadata
138    ///
139    /// Simple condition evaluation (e.g., "payment_failed_count > 2")
140    fn evaluate_condition(&self, condition: &str) -> bool {
141        // Simple condition parser - supports basic comparisons
142        // Format: "field operator value"
143        // Operators: >, <, >=, <=, ==, !=
144
145        let parts: Vec<&str> = condition.split_whitespace().collect();
146        if parts.len() != 3 {
147            return false;
148        }
149
150        let field = parts[0];
151        let operator = parts[1];
152        let value_str = parts[2];
153
154        // Get field value from metadata
155        let field_value = self.metadata.get(field).and_then(|v| {
156            if let Some(num) = v.as_u64() {
157                Some(num as i64)
158            } else {
159                v.as_i64()
160            }
161        });
162
163        let value = value_str.parse::<i64>().ok();
164
165        match (field_value, value) {
166            (Some(fv), Some(v)) => match operator {
167                ">" => fv > v,
168                "<" => fv < v,
169                ">=" => fv >= v,
170                "<=" => fv <= v,
171                "==" => fv == v,
172                "!=" => fv != v,
173                _ => false,
174            },
175            _ => false,
176        }
177    }
178
179    /// Apply lifecycle effects to persona traits
180    ///
181    /// Updates persona traits based on the current lifecycle state.
182    pub fn apply_lifecycle_effects(&self) -> HashMap<String, String> {
183        let mut traits = HashMap::new();
184
185        match self.current_state {
186            LifecycleState::NewSignup => {
187                traits.insert("account_age".to_string(), "0".to_string());
188                traits.insert("order_count".to_string(), "0".to_string());
189                traits.insert("loyalty_level".to_string(), "bronze".to_string());
190            }
191            LifecycleState::Active => {
192                traits.insert("loyalty_level".to_string(), "silver".to_string());
193                traits.insert("engagement_level".to_string(), "medium".to_string());
194            }
195            LifecycleState::PowerUser => {
196                traits.insert("loyalty_level".to_string(), "gold".to_string());
197                traits.insert("engagement_level".to_string(), "high".to_string());
198                traits.insert("order_frequency".to_string(), "high".to_string());
199            }
200            LifecycleState::ChurnRisk => {
201                traits.insert("engagement_level".to_string(), "low".to_string());
202                traits.insert("last_active_days".to_string(), "30+".to_string());
203            }
204            LifecycleState::Churned => {
205                traits.insert("status".to_string(), "inactive".to_string());
206                traits.insert("engagement_level".to_string(), "none".to_string());
207            }
208            LifecycleState::UpgradePending => {
209                traits.insert("upgrade_status".to_string(), "pending".to_string());
210            }
211            LifecycleState::PaymentFailed => {
212                traits.insert("payment_status".to_string(), "failed".to_string());
213                traits.insert("account_status".to_string(), "restricted".to_string());
214            }
215        }
216
217        traits
218    }
219
220    /// Transition to a new state
221    pub fn transition_to(&mut self, new_state: LifecycleState, transition_time: DateTime<Utc>) {
222        if self.current_state == new_state {
223            return;
224        }
225
226        self.state_history.push((transition_time, new_state));
227        self.current_state = new_state;
228        self.state_entered_at = transition_time;
229    }
230
231    /// Get the duration in the current state
232    pub fn current_state_duration(&self, current_time: DateTime<Utc>) -> Duration {
233        current_time - self.state_entered_at
234    }
235
236    /// Add metadata for lifecycle tracking
237    pub fn set_metadata(&mut self, key: String, value: serde_json::Value) {
238        self.metadata.insert(key, value);
239    }
240
241    /// Get metadata value
242    pub fn get_metadata(&self, key: &str) -> Option<&serde_json::Value> {
243        self.metadata.get(key)
244    }
245
246    /// Create a lifecycle from a preset
247    pub fn from_preset(preset: LifecyclePreset, persona_id: String) -> PersonaLifecycle {
248        match preset {
249            LifecyclePreset::Subscription => LifecycleScenarios::subscription_preset(persona_id),
250            LifecyclePreset::Loan => LifecycleScenarios::loan_preset(persona_id),
251            LifecyclePreset::OrderFulfillment => {
252                LifecycleScenarios::order_fulfillment_preset(persona_id)
253            }
254            LifecyclePreset::UserEngagement => {
255                LifecycleScenarios::user_engagement_preset(persona_id)
256            }
257        }
258    }
259}
260
261/// Lifecycle preset types
262///
263/// Predefined lifecycle patterns for common business scenarios.
264#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
265#[serde(rename_all = "snake_case")]
266pub enum LifecyclePreset {
267    /// Subscription lifecycle: NEW → ACTIVE → PAST_DUE → CANCELED
268    Subscription,
269    /// Loan lifecycle: APPLICATION → APPROVED → ACTIVE → PAST_DUE → DEFAULTED
270    Loan,
271    /// Order fulfillment lifecycle: PENDING → PROCESSING → SHIPPED → DELIVERED → COMPLETED
272    OrderFulfillment,
273    /// User engagement lifecycle: NEW → ACTIVE → CHURN_RISK → CHURNED
274    UserEngagement,
275}
276
277impl LifecyclePreset {
278    /// Get all available presets
279    pub fn all() -> Vec<Self> {
280        vec![
281            LifecyclePreset::Subscription,
282            LifecyclePreset::Loan,
283            LifecyclePreset::OrderFulfillment,
284            LifecyclePreset::UserEngagement,
285        ]
286    }
287
288    /// Get human-readable name
289    pub fn name(&self) -> &'static str {
290        match self {
291            LifecyclePreset::Subscription => "Subscription",
292            LifecyclePreset::Loan => "Loan",
293            LifecyclePreset::OrderFulfillment => "Order Fulfillment",
294            LifecyclePreset::UserEngagement => "User Engagement",
295        }
296    }
297
298    /// Get description
299    pub fn description(&self) -> &'static str {
300        match self {
301            LifecyclePreset::Subscription => "Subscription lifecycle: NEW → ACTIVE → PAST_DUE → CANCELED",
302            LifecyclePreset::Loan => "Loan lifecycle: APPLICATION → APPROVED → ACTIVE → PAST_DUE → DEFAULTED",
303            LifecyclePreset::OrderFulfillment => "Order fulfillment lifecycle: PENDING → PROCESSING → SHIPPED → DELIVERED → COMPLETED",
304            LifecyclePreset::UserEngagement => "User engagement lifecycle: NEW → ACTIVE → CHURN_RISK → CHURNED",
305        }
306    }
307}
308
309/// Extended lifecycle states for presets
310///
311/// These states extend the base LifecycleState enum with preset-specific states
312/// for subscription, loan, and order fulfillment lifecycles.
313#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
314#[serde(rename_all = "snake_case")]
315pub enum ExtendedLifecycleState {
316    // Base states
317    /// New user signup state
318    #[serde(rename = "new_signup")]
319    NewSignup,
320    /// Active user state
321    Active,
322    /// Power user state with high activity
323    #[serde(rename = "power_user")]
324    PowerUser,
325    /// Churn risk state indicating potential user departure
326    #[serde(rename = "churn_risk")]
327    ChurnRisk,
328    /// Churned state - user has left
329    Churned,
330    /// Upgrade pending state
331    #[serde(rename = "upgrade_pending")]
332    UpgradePending,
333    /// Payment failed state
334    #[serde(rename = "payment_failed")]
335    PaymentFailed,
336
337    // Subscription states
338    /// New subscription state
339    #[serde(rename = "subscription_new")]
340    SubscriptionNew,
341    /// Active subscription state
342    #[serde(rename = "subscription_active")]
343    SubscriptionActive,
344    /// Subscription past due state
345    #[serde(rename = "subscription_past_due")]
346    SubscriptionPastDue,
347    /// Subscription canceled state
348    #[serde(rename = "subscription_canceled")]
349    SubscriptionCanceled,
350
351    // Loan states
352    /// Loan application state
353    #[serde(rename = "loan_application")]
354    LoanApplication,
355    /// Loan approved state
356    #[serde(rename = "loan_approved")]
357    LoanApproved,
358    /// Loan active state
359    #[serde(rename = "loan_active")]
360    LoanActive,
361    /// Loan past due state
362    #[serde(rename = "loan_past_due")]
363    LoanPastDue,
364    /// Loan defaulted state
365    #[serde(rename = "loan_defaulted")]
366    LoanDefaulted,
367
368    // Order fulfillment states
369    /// Order pending state
370    #[serde(rename = "order_pending")]
371    OrderPending,
372    /// Order processing state
373    #[serde(rename = "order_processing")]
374    OrderProcessing,
375    /// Order shipped state
376    #[serde(rename = "order_shipped")]
377    OrderShipped,
378    /// Order delivered state
379    #[serde(rename = "order_delivered")]
380    OrderDelivered,
381    /// Order completed state
382    #[serde(rename = "order_completed")]
383    OrderCompleted,
384}
385
386/// Prebuilt lifecycle scenarios
387pub struct LifecycleScenarios;
388
389impl LifecycleScenarios {
390    /// New signup scenario - fresh user with no history
391    pub fn new_signup_scenario(persona_id: String) -> PersonaLifecycle {
392        let rules = vec![TransitionRule {
393            to: LifecycleState::Active,
394            after_days: Some(7),
395            condition: None,
396            on_transition: None,
397        }];
398
399        PersonaLifecycle::with_rules(persona_id, LifecycleState::NewSignup, rules)
400    }
401
402    /// Power user scenario - high activity, many orders
403    pub fn power_user_scenario(persona_id: String) -> PersonaLifecycle {
404        let rules = vec![TransitionRule {
405            to: LifecycleState::ChurnRisk,
406            after_days: Some(90),
407            condition: Some("order_count < 5".to_string()),
408            on_transition: None,
409        }];
410
411        PersonaLifecycle::with_rules(persona_id, LifecycleState::PowerUser, rules)
412    }
413
414    /// Churn risk scenario - declining activity, failed payments
415    pub fn churn_risk_scenario(persona_id: String) -> PersonaLifecycle {
416        let rules = vec![
417            TransitionRule {
418                to: LifecycleState::Churned,
419                after_days: Some(30),
420                condition: Some("payment_failed_count > 2".to_string()),
421                on_transition: None,
422            },
423            TransitionRule {
424                to: LifecycleState::Active,
425                after_days: Some(7),
426                condition: Some("payment_failed_count == 0".to_string()),
427                on_transition: None,
428            },
429        ];
430
431        PersonaLifecycle::with_rules(persona_id, LifecycleState::ChurnRisk, rules)
432    }
433
434    /// Active user scenario - regular usage
435    pub fn active_scenario(persona_id: String) -> PersonaLifecycle {
436        let rules = vec![
437            TransitionRule {
438                to: LifecycleState::PowerUser,
439                after_days: Some(30),
440                condition: Some("order_count > 10".to_string()),
441                on_transition: None,
442            },
443            TransitionRule {
444                to: LifecycleState::ChurnRisk,
445                after_days: Some(60),
446                condition: Some("last_active_days > 30".to_string()),
447                on_transition: None,
448            },
449        ];
450
451        PersonaLifecycle::with_rules(persona_id, LifecycleState::Active, rules)
452    }
453
454    /// Create a subscription lifecycle preset
455    ///
456    /// States: NEW → ACTIVE → PAST_DUE → CANCELED
457    pub fn subscription_preset(persona_id: String) -> PersonaLifecycle {
458        // For subscription, we'll use the base lifecycle states and map them
459        // NEW -> NewSignup, ACTIVE -> Active, PAST_DUE -> PaymentFailed, CANCELED -> Churned
460        let rules = vec![
461            TransitionRule {
462                to: LifecycleState::Active,
463                after_days: Some(0), // Immediately active after creation
464                condition: None,
465                on_transition: None,
466            },
467            TransitionRule {
468                to: LifecycleState::PaymentFailed,
469                after_days: Some(30), // Past due after 30 days
470                condition: Some("payment_failed_count > 0".to_string()),
471                on_transition: None,
472            },
473            TransitionRule {
474                to: LifecycleState::Churned,
475                after_days: Some(60), // Canceled after 60 days of past due
476                condition: Some("payment_failed_count > 2".to_string()),
477                on_transition: None,
478            },
479        ];
480
481        PersonaLifecycle::with_rules(persona_id, LifecycleState::NewSignup, rules)
482    }
483
484    /// Create a loan lifecycle preset
485    ///
486    /// States: APPLICATION → APPROVED → ACTIVE → PAST_DUE → DEFAULTED
487    pub fn loan_preset(persona_id: String) -> PersonaLifecycle {
488        let rules = vec![
489            TransitionRule {
490                to: LifecycleState::Active, // APPROVED -> ACTIVE
491                after_days: Some(7),        // Approved after 7 days
492                condition: Some("credit_score > 650".to_string()),
493                on_transition: None,
494            },
495            TransitionRule {
496                to: LifecycleState::PaymentFailed, // ACTIVE -> PAST_DUE
497                after_days: Some(90),              // Past due after 90 days
498                condition: Some("payment_failed_count > 0".to_string()),
499                on_transition: None,
500            },
501            TransitionRule {
502                to: LifecycleState::Churned, // PAST_DUE -> DEFAULTED
503                after_days: Some(120),       // Defaulted after 120 days
504                condition: Some("payment_failed_count > 3".to_string()),
505                on_transition: None,
506            },
507        ];
508
509        PersonaLifecycle::with_rules(persona_id, LifecycleState::NewSignup, rules)
510    }
511
512    /// Create an order fulfillment lifecycle preset
513    ///
514    /// States: PENDING → PROCESSING → SHIPPED → DELIVERED → COMPLETED
515    pub fn order_fulfillment_preset(persona_id: String) -> PersonaLifecycle {
516        let rules = vec![
517            TransitionRule {
518                to: LifecycleState::Active, // PENDING -> PROCESSING (using Active as processing)
519                after_days: Some(0),        // Processing starts immediately
520                condition: None,
521                on_transition: None,
522            },
523            TransitionRule {
524                to: LifecycleState::PowerUser, // PROCESSING -> SHIPPED (using PowerUser as shipped)
525                after_days: Some(1),           // Shipped after 1 day
526                condition: Some("inventory_available == true".to_string()),
527                on_transition: None,
528            },
529            TransitionRule {
530                to: LifecycleState::UpgradePending, // SHIPPED -> DELIVERED (using UpgradePending as delivered)
531                after_days: Some(3),                // Delivered after 3 days
532                condition: None,
533                on_transition: None,
534            },
535            TransitionRule {
536                to: LifecycleState::Churned, // DELIVERED -> COMPLETED (using Churned as completed - terminal state)
537                after_days: Some(7),         // Completed after 7 days
538                condition: None,
539                on_transition: None,
540            },
541        ];
542
543        PersonaLifecycle::with_rules(persona_id, LifecycleState::NewSignup, rules)
544    }
545
546    /// User Engagement lifecycle preset
547    ///
548    /// Models user engagement progression: NEW → ACTIVE → CHURN_RISK → CHURNED
549    /// States: NewSignup → Active → ChurnRisk → Churned
550    pub fn user_engagement_preset(persona_id: String) -> PersonaLifecycle {
551        let rules = vec![
552            TransitionRule {
553                to: LifecycleState::Active, // NEW → ACTIVE
554                after_days: Some(7),        // Active after 7 days of engagement
555                condition: Some("login_count >= 3".to_string()),
556                on_transition: None,
557            },
558            TransitionRule {
559                to: LifecycleState::ChurnRisk, // ACTIVE → CHURN_RISK
560                after_days: Some(90),          // Churn risk after 90 days of inactivity
561                condition: Some("last_login_days_ago > 30".to_string()),
562                on_transition: None,
563            },
564            TransitionRule {
565                to: LifecycleState::Churned, // CHURN_RISK → CHURNED
566                after_days: Some(60),        // Churned after 60 days in churn risk
567                condition: Some("last_login_days_ago > 90".to_string()),
568                on_transition: None,
569            },
570        ];
571
572        PersonaLifecycle::with_rules(persona_id, LifecycleState::NewSignup, rules)
573    }
574}
575
576#[cfg(test)]
577mod tests {
578    use super::*;
579
580    // =========================================================================
581    // LifecycleState tests
582    // =========================================================================
583
584    #[test]
585    fn test_lifecycle_state_name() {
586        assert_eq!(LifecycleState::NewSignup.name(), "New Signup");
587        assert_eq!(LifecycleState::Active.name(), "Active");
588        assert_eq!(LifecycleState::PowerUser.name(), "Power User");
589        assert_eq!(LifecycleState::ChurnRisk.name(), "Churn Risk");
590        assert_eq!(LifecycleState::Churned.name(), "Churned");
591        assert_eq!(LifecycleState::UpgradePending.name(), "Upgrade Pending");
592        assert_eq!(LifecycleState::PaymentFailed.name(), "Payment Failed");
593    }
594
595    #[test]
596    fn test_lifecycle_state_is_terminal() {
597        assert!(LifecycleState::Churned.is_terminal());
598        assert!(!LifecycleState::Active.is_terminal());
599        assert!(!LifecycleState::NewSignup.is_terminal());
600    }
601
602    #[test]
603    fn test_lifecycle_state_eq() {
604        assert_eq!(LifecycleState::Active, LifecycleState::Active);
605        assert_ne!(LifecycleState::Active, LifecycleState::Churned);
606    }
607
608    #[test]
609    fn test_lifecycle_state_clone() {
610        let state = LifecycleState::PowerUser;
611        let cloned = state;
612        assert_eq!(cloned, LifecycleState::PowerUser);
613    }
614
615    #[test]
616    fn test_lifecycle_state_serialize() {
617        let state = LifecycleState::NewSignup;
618        let json = serde_json::to_string(&state).unwrap();
619        assert_eq!(json, "\"new_signup\"");
620    }
621
622    #[test]
623    fn test_lifecycle_state_deserialize() {
624        let json = "\"active\"";
625        let state: LifecycleState = serde_json::from_str(json).unwrap();
626        assert_eq!(state, LifecycleState::Active);
627    }
628
629    #[test]
630    fn test_lifecycle_state_debug() {
631        let debug_str = format!("{:?}", LifecycleState::ChurnRisk);
632        assert!(debug_str.contains("ChurnRisk"));
633    }
634
635    // =========================================================================
636    // TransitionRule tests
637    // =========================================================================
638
639    #[test]
640    fn test_transition_rule_creation() {
641        let rule = TransitionRule {
642            to: LifecycleState::Active,
643            after_days: Some(7),
644            condition: Some("order_count > 5".to_string()),
645            on_transition: None,
646        };
647        assert_eq!(rule.to, LifecycleState::Active);
648        assert_eq!(rule.after_days, Some(7));
649    }
650
651    #[test]
652    fn test_transition_rule_clone() {
653        let rule = TransitionRule {
654            to: LifecycleState::Churned,
655            after_days: Some(30),
656            condition: None,
657            on_transition: None,
658        };
659        let cloned = rule.clone();
660        assert_eq!(cloned.to, LifecycleState::Churned);
661    }
662
663    #[test]
664    fn test_transition_rule_serialize() {
665        let rule = TransitionRule {
666            to: LifecycleState::Active,
667            after_days: Some(14),
668            condition: None,
669            on_transition: None,
670        };
671        let json = serde_json::to_string(&rule).unwrap();
672        assert!(json.contains("active"));
673        assert!(json.contains("14"));
674    }
675
676    // =========================================================================
677    // PersonaLifecycle tests
678    // =========================================================================
679
680    #[test]
681    fn test_persona_lifecycle_new() {
682        let lifecycle = PersonaLifecycle::new("user-123".to_string(), LifecycleState::NewSignup);
683        assert_eq!(lifecycle.persona_id, "user-123");
684        assert_eq!(lifecycle.current_state, LifecycleState::NewSignup);
685        assert_eq!(lifecycle.state_history.len(), 1);
686    }
687
688    #[test]
689    fn test_persona_lifecycle_with_rules() {
690        let rules = vec![TransitionRule {
691            to: LifecycleState::Active,
692            after_days: Some(7),
693            condition: None,
694            on_transition: None,
695        }];
696        let lifecycle =
697            PersonaLifecycle::with_rules("user-123".to_string(), LifecycleState::NewSignup, rules);
698        assert_eq!(lifecycle.transition_rules.len(), 1);
699    }
700
701    #[test]
702    fn test_persona_lifecycle_transition_to() {
703        let mut lifecycle =
704            PersonaLifecycle::new("user-123".to_string(), LifecycleState::NewSignup);
705        let now = Utc::now();
706        lifecycle.transition_to(LifecycleState::Active, now);
707
708        assert_eq!(lifecycle.current_state, LifecycleState::Active);
709        assert_eq!(lifecycle.state_history.len(), 2);
710    }
711
712    #[test]
713    fn test_persona_lifecycle_transition_to_same_state() {
714        let mut lifecycle =
715            PersonaLifecycle::new("user-123".to_string(), LifecycleState::NewSignup);
716        let now = Utc::now();
717        lifecycle.transition_to(LifecycleState::NewSignup, now);
718
719        // Should not add duplicate history entry
720        assert_eq!(lifecycle.state_history.len(), 1);
721    }
722
723    #[test]
724    fn test_persona_lifecycle_transition_if_elapsed() {
725        let rules = vec![TransitionRule {
726            to: LifecycleState::Active,
727            after_days: Some(7),
728            condition: None,
729            on_transition: None,
730        }];
731        let lifecycle =
732            PersonaLifecycle::with_rules("user-123".to_string(), LifecycleState::NewSignup, rules);
733
734        // Before 7 days - no transition
735        let future_time_5days = lifecycle.state_entered_at + Duration::days(5);
736        assert!(lifecycle.transition_if_elapsed(future_time_5days).is_none());
737
738        // After 7 days - should transition
739        let future_time_8days = lifecycle.state_entered_at + Duration::days(8);
740        let result = lifecycle.transition_if_elapsed(future_time_8days);
741        assert!(result.is_some());
742        assert_eq!(result.unwrap().0, LifecycleState::Active);
743    }
744
745    #[test]
746    fn test_persona_lifecycle_transition_if_elapsed_with_condition() {
747        let rules = vec![TransitionRule {
748            to: LifecycleState::Churned,
749            after_days: Some(30),
750            condition: Some("payment_failed_count > 2".to_string()),
751            on_transition: None,
752        }];
753        let mut lifecycle =
754            PersonaLifecycle::with_rules("user-123".to_string(), LifecycleState::ChurnRisk, rules);
755
756        let future_time = lifecycle.state_entered_at + Duration::days(35);
757
758        // Without metadata - condition not met
759        assert!(lifecycle.transition_if_elapsed(future_time).is_none());
760
761        // With metadata - condition met
762        lifecycle.set_metadata("payment_failed_count".to_string(), serde_json::json!(3));
763        let result = lifecycle.transition_if_elapsed(future_time);
764        assert!(result.is_some());
765    }
766
767    #[test]
768    fn test_persona_lifecycle_apply_lifecycle_effects() {
769        let lifecycle = PersonaLifecycle::new("user-123".to_string(), LifecycleState::NewSignup);
770        let effects = lifecycle.apply_lifecycle_effects();
771
772        assert_eq!(effects.get("account_age"), Some(&"0".to_string()));
773        assert_eq!(effects.get("order_count"), Some(&"0".to_string()));
774        assert_eq!(effects.get("loyalty_level"), Some(&"bronze".to_string()));
775    }
776
777    #[test]
778    fn test_persona_lifecycle_apply_lifecycle_effects_power_user() {
779        let lifecycle = PersonaLifecycle::new("user-123".to_string(), LifecycleState::PowerUser);
780        let effects = lifecycle.apply_lifecycle_effects();
781
782        assert_eq!(effects.get("loyalty_level"), Some(&"gold".to_string()));
783        assert_eq!(effects.get("engagement_level"), Some(&"high".to_string()));
784    }
785
786    #[test]
787    fn test_persona_lifecycle_current_state_duration() {
788        let lifecycle = PersonaLifecycle::new("user-123".to_string(), LifecycleState::NewSignup);
789        let future_time = lifecycle.state_entered_at + Duration::days(10);
790        let duration = lifecycle.current_state_duration(future_time);
791
792        assert_eq!(duration.num_days(), 10);
793    }
794
795    #[test]
796    fn test_persona_lifecycle_metadata() {
797        let mut lifecycle =
798            PersonaLifecycle::new("user-123".to_string(), LifecycleState::NewSignup);
799
800        lifecycle.set_metadata("order_count".to_string(), serde_json::json!(5));
801
802        let value = lifecycle.get_metadata("order_count");
803        assert!(value.is_some());
804        assert_eq!(value.unwrap().as_u64(), Some(5));
805    }
806
807    #[test]
808    fn test_persona_lifecycle_metadata_not_found() {
809        let lifecycle = PersonaLifecycle::new("user-123".to_string(), LifecycleState::NewSignup);
810        assert!(lifecycle.get_metadata("nonexistent").is_none());
811    }
812
813    #[test]
814    fn test_persona_lifecycle_from_preset() {
815        let lifecycle =
816            PersonaLifecycle::from_preset(LifecyclePreset::Subscription, "user-123".to_string());
817        assert_eq!(lifecycle.persona_id, "user-123");
818        assert!(!lifecycle.transition_rules.is_empty());
819    }
820
821    #[test]
822    fn test_persona_lifecycle_serialize() {
823        let lifecycle = PersonaLifecycle::new("user-123".to_string(), LifecycleState::Active);
824        let json = serde_json::to_string(&lifecycle).unwrap();
825        assert!(json.contains("user-123"));
826        assert!(json.contains("active"));
827    }
828
829    // =========================================================================
830    // LifecyclePreset tests
831    // =========================================================================
832
833    #[test]
834    fn test_lifecycle_preset_all() {
835        let presets = LifecyclePreset::all();
836        assert_eq!(presets.len(), 4);
837    }
838
839    #[test]
840    fn test_lifecycle_preset_name() {
841        assert_eq!(LifecyclePreset::Subscription.name(), "Subscription");
842        assert_eq!(LifecyclePreset::Loan.name(), "Loan");
843        assert_eq!(LifecyclePreset::OrderFulfillment.name(), "Order Fulfillment");
844        assert_eq!(LifecyclePreset::UserEngagement.name(), "User Engagement");
845    }
846
847    #[test]
848    fn test_lifecycle_preset_description() {
849        let desc = LifecyclePreset::Subscription.description();
850        assert!(desc.contains("NEW"));
851        assert!(desc.contains("CANCELED"));
852    }
853
854    #[test]
855    fn test_lifecycle_preset_serialize() {
856        let preset = LifecyclePreset::Loan;
857        let json = serde_json::to_string(&preset).unwrap();
858        assert_eq!(json, "\"loan\"");
859    }
860
861    #[test]
862    fn test_lifecycle_preset_deserialize() {
863        let json = "\"order_fulfillment\"";
864        let preset: LifecyclePreset = serde_json::from_str(json).unwrap();
865        assert_eq!(preset, LifecyclePreset::OrderFulfillment);
866    }
867
868    // =========================================================================
869    // ExtendedLifecycleState tests
870    // =========================================================================
871
872    #[test]
873    fn test_extended_lifecycle_state_serialize() {
874        let state = ExtendedLifecycleState::SubscriptionActive;
875        let json = serde_json::to_string(&state).unwrap();
876        assert_eq!(json, "\"subscription_active\"");
877    }
878
879    #[test]
880    fn test_extended_lifecycle_state_deserialize() {
881        let json = "\"loan_defaulted\"";
882        let state: ExtendedLifecycleState = serde_json::from_str(json).unwrap();
883        assert_eq!(state, ExtendedLifecycleState::LoanDefaulted);
884    }
885
886    #[test]
887    fn test_extended_lifecycle_state_eq() {
888        assert_eq!(ExtendedLifecycleState::OrderPending, ExtendedLifecycleState::OrderPending);
889        assert_ne!(ExtendedLifecycleState::OrderPending, ExtendedLifecycleState::OrderShipped);
890    }
891
892    // =========================================================================
893    // LifecycleScenarios tests
894    // =========================================================================
895
896    #[test]
897    fn test_new_signup_scenario() {
898        let lifecycle = LifecycleScenarios::new_signup_scenario("user-1".to_string());
899        assert_eq!(lifecycle.current_state, LifecycleState::NewSignup);
900        assert_eq!(lifecycle.transition_rules.len(), 1);
901    }
902
903    #[test]
904    fn test_power_user_scenario() {
905        let lifecycle = LifecycleScenarios::power_user_scenario("user-2".to_string());
906        assert_eq!(lifecycle.current_state, LifecycleState::PowerUser);
907        assert_eq!(lifecycle.transition_rules.len(), 1);
908    }
909
910    #[test]
911    fn test_churn_risk_scenario() {
912        let lifecycle = LifecycleScenarios::churn_risk_scenario("user-3".to_string());
913        assert_eq!(lifecycle.current_state, LifecycleState::ChurnRisk);
914        assert_eq!(lifecycle.transition_rules.len(), 2);
915    }
916
917    #[test]
918    fn test_active_scenario() {
919        let lifecycle = LifecycleScenarios::active_scenario("user-4".to_string());
920        assert_eq!(lifecycle.current_state, LifecycleState::Active);
921        assert_eq!(lifecycle.transition_rules.len(), 2);
922    }
923
924    #[test]
925    fn test_subscription_preset() {
926        let lifecycle = LifecycleScenarios::subscription_preset("sub-1".to_string());
927        assert_eq!(lifecycle.persona_id, "sub-1");
928        assert!(!lifecycle.transition_rules.is_empty());
929    }
930
931    #[test]
932    fn test_loan_preset() {
933        let lifecycle = LifecycleScenarios::loan_preset("loan-1".to_string());
934        assert_eq!(lifecycle.persona_id, "loan-1");
935        assert!(!lifecycle.transition_rules.is_empty());
936    }
937
938    #[test]
939    fn test_order_fulfillment_preset() {
940        let lifecycle = LifecycleScenarios::order_fulfillment_preset("order-1".to_string());
941        assert_eq!(lifecycle.persona_id, "order-1");
942        assert!(!lifecycle.transition_rules.is_empty());
943    }
944
945    #[test]
946    fn test_user_engagement_preset() {
947        let lifecycle = LifecycleScenarios::user_engagement_preset("engage-1".to_string());
948        assert_eq!(lifecycle.persona_id, "engage-1");
949        assert!(!lifecycle.transition_rules.is_empty());
950    }
951
952    // =========================================================================
953    // Condition evaluation tests
954    // =========================================================================
955
956    #[test]
957    fn test_evaluate_condition_greater_than() {
958        let mut lifecycle =
959            PersonaLifecycle::new("user-123".to_string(), LifecycleState::NewSignup);
960        lifecycle.set_metadata("count".to_string(), serde_json::json!(10));
961
962        // Directly test the condition evaluation via the rules mechanism
963        let rules = vec![TransitionRule {
964            to: LifecycleState::Active,
965            after_days: Some(0),
966            condition: Some("count > 5".to_string()),
967            on_transition: None,
968        }];
969        let mut test_lifecycle =
970            PersonaLifecycle::with_rules("test".to_string(), LifecycleState::NewSignup, rules);
971        test_lifecycle.set_metadata("count".to_string(), serde_json::json!(10));
972
973        let result = test_lifecycle.transition_if_elapsed(Utc::now());
974        assert!(result.is_some());
975    }
976
977    #[test]
978    fn test_evaluate_condition_less_than() {
979        let rules = vec![TransitionRule {
980            to: LifecycleState::ChurnRisk,
981            after_days: Some(0),
982            condition: Some("score < 50".to_string()),
983            on_transition: None,
984        }];
985        let mut lifecycle =
986            PersonaLifecycle::with_rules("test".to_string(), LifecycleState::Active, rules);
987        lifecycle.set_metadata("score".to_string(), serde_json::json!(30));
988
989        let result = lifecycle.transition_if_elapsed(Utc::now());
990        assert!(result.is_some());
991    }
992
993    #[test]
994    fn test_evaluate_condition_equals() {
995        let rules = vec![TransitionRule {
996            to: LifecycleState::Active,
997            after_days: Some(0),
998            condition: Some("status == 1".to_string()),
999            on_transition: None,
1000        }];
1001        let mut lifecycle =
1002            PersonaLifecycle::with_rules("test".to_string(), LifecycleState::NewSignup, rules);
1003        lifecycle.set_metadata("status".to_string(), serde_json::json!(1));
1004
1005        let result = lifecycle.transition_if_elapsed(Utc::now());
1006        assert!(result.is_some());
1007    }
1008
1009    #[test]
1010    fn test_evaluate_condition_not_equals() {
1011        let rules = vec![TransitionRule {
1012            to: LifecycleState::Active,
1013            after_days: Some(0),
1014            condition: Some("level != 0".to_string()),
1015            on_transition: None,
1016        }];
1017        let mut lifecycle =
1018            PersonaLifecycle::with_rules("test".to_string(), LifecycleState::NewSignup, rules);
1019        lifecycle.set_metadata("level".to_string(), serde_json::json!(5));
1020
1021        let result = lifecycle.transition_if_elapsed(Utc::now());
1022        assert!(result.is_some());
1023    }
1024
1025    #[test]
1026    fn test_evaluate_condition_missing_metadata() {
1027        let rules = vec![TransitionRule {
1028            to: LifecycleState::Active,
1029            after_days: Some(0),
1030            condition: Some("missing > 0".to_string()),
1031            on_transition: None,
1032        }];
1033        let lifecycle =
1034            PersonaLifecycle::with_rules("test".to_string(), LifecycleState::NewSignup, rules);
1035
1036        // Should not transition - condition evaluates to false when metadata is missing
1037        let result = lifecycle.transition_if_elapsed(Utc::now());
1038        assert!(result.is_none());
1039    }
1040
1041    #[test]
1042    fn test_evaluate_condition_invalid_format() {
1043        let rules = vec![TransitionRule {
1044            to: LifecycleState::Active,
1045            after_days: Some(0),
1046            condition: Some("invalid_format".to_string()),
1047            on_transition: None,
1048        }];
1049        let lifecycle =
1050            PersonaLifecycle::with_rules("test".to_string(), LifecycleState::NewSignup, rules);
1051
1052        // Should not transition - invalid condition format
1053        let result = lifecycle.transition_if_elapsed(Utc::now());
1054        assert!(result.is_none());
1055    }
1056}