1use chrono::{DateTime, Duration, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum LifecycleState {
15 NewSignup,
17 Active,
19 PowerUser,
21 ChurnRisk,
23 Churned,
25 UpgradePending,
27 PaymentFailed,
29}
30
31impl LifecycleState {
32 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 pub fn is_terminal(&self) -> bool {
47 matches!(self, LifecycleState::Churned)
48 }
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct TransitionRule {
54 pub to: LifecycleState,
56 pub after_days: Option<u64>,
58 pub condition: Option<String>,
60 pub on_transition: Option<String>,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct PersonaLifecycle {
70 pub persona_id: String,
72 pub current_state: LifecycleState,
74 pub state_history: Vec<(DateTime<Utc>, LifecycleState)>,
76 pub transition_rules: Vec<TransitionRule>,
78 pub state_entered_at: DateTime<Utc>,
80 #[serde(default)]
82 pub metadata: HashMap<String, serde_json::Value>,
83}
84
85impl PersonaLifecycle {
86 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 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 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 if let Some(after_days) = rule.after_days {
122 if elapsed_days >= after_days {
123 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 fn evaluate_condition(&self, condition: &str) -> bool {
141 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 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 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 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 pub fn current_state_duration(&self, current_time: DateTime<Utc>) -> Duration {
233 current_time - self.state_entered_at
234 }
235
236 pub fn set_metadata(&mut self, key: String, value: serde_json::Value) {
238 self.metadata.insert(key, value);
239 }
240
241 pub fn get_metadata(&self, key: &str) -> Option<&serde_json::Value> {
243 self.metadata.get(key)
244 }
245
246 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
265#[serde(rename_all = "snake_case")]
266pub enum LifecyclePreset {
267 Subscription,
269 Loan,
271 OrderFulfillment,
273 UserEngagement,
275}
276
277impl LifecyclePreset {
278 pub fn all() -> Vec<Self> {
280 vec![
281 LifecyclePreset::Subscription,
282 LifecyclePreset::Loan,
283 LifecyclePreset::OrderFulfillment,
284 LifecyclePreset::UserEngagement,
285 ]
286 }
287
288 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
314#[serde(rename_all = "snake_case")]
315pub enum ExtendedLifecycleState {
316 #[serde(rename = "new_signup")]
319 NewSignup,
320 Active,
322 #[serde(rename = "power_user")]
324 PowerUser,
325 #[serde(rename = "churn_risk")]
327 ChurnRisk,
328 Churned,
330 #[serde(rename = "upgrade_pending")]
332 UpgradePending,
333 #[serde(rename = "payment_failed")]
335 PaymentFailed,
336
337 #[serde(rename = "subscription_new")]
340 SubscriptionNew,
341 #[serde(rename = "subscription_active")]
343 SubscriptionActive,
344 #[serde(rename = "subscription_past_due")]
346 SubscriptionPastDue,
347 #[serde(rename = "subscription_canceled")]
349 SubscriptionCanceled,
350
351 #[serde(rename = "loan_application")]
354 LoanApplication,
355 #[serde(rename = "loan_approved")]
357 LoanApproved,
358 #[serde(rename = "loan_active")]
360 LoanActive,
361 #[serde(rename = "loan_past_due")]
363 LoanPastDue,
364 #[serde(rename = "loan_defaulted")]
366 LoanDefaulted,
367
368 #[serde(rename = "order_pending")]
371 OrderPending,
372 #[serde(rename = "order_processing")]
374 OrderProcessing,
375 #[serde(rename = "order_shipped")]
377 OrderShipped,
378 #[serde(rename = "order_delivered")]
380 OrderDelivered,
381 #[serde(rename = "order_completed")]
383 OrderCompleted,
384}
385
386pub struct LifecycleScenarios;
388
389impl LifecycleScenarios {
390 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 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 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 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 pub fn subscription_preset(persona_id: String) -> PersonaLifecycle {
458 let rules = vec![
461 TransitionRule {
462 to: LifecycleState::Active,
463 after_days: Some(0), condition: None,
465 on_transition: None,
466 },
467 TransitionRule {
468 to: LifecycleState::PaymentFailed,
469 after_days: Some(30), condition: Some("payment_failed_count > 0".to_string()),
471 on_transition: None,
472 },
473 TransitionRule {
474 to: LifecycleState::Churned,
475 after_days: Some(60), 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 pub fn loan_preset(persona_id: String) -> PersonaLifecycle {
488 let rules = vec![
489 TransitionRule {
490 to: LifecycleState::Active, after_days: Some(7), condition: Some("credit_score > 650".to_string()),
493 on_transition: None,
494 },
495 TransitionRule {
496 to: LifecycleState::PaymentFailed, after_days: Some(90), condition: Some("payment_failed_count > 0".to_string()),
499 on_transition: None,
500 },
501 TransitionRule {
502 to: LifecycleState::Churned, after_days: Some(120), 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 pub fn order_fulfillment_preset(persona_id: String) -> PersonaLifecycle {
516 let rules = vec![
517 TransitionRule {
518 to: LifecycleState::Active, after_days: Some(0), condition: None,
521 on_transition: None,
522 },
523 TransitionRule {
524 to: LifecycleState::PowerUser, after_days: Some(1), condition: Some("inventory_available == true".to_string()),
527 on_transition: None,
528 },
529 TransitionRule {
530 to: LifecycleState::UpgradePending, after_days: Some(3), condition: None,
533 on_transition: None,
534 },
535 TransitionRule {
536 to: LifecycleState::Churned, after_days: Some(7), condition: None,
539 on_transition: None,
540 },
541 ];
542
543 PersonaLifecycle::with_rules(persona_id, LifecycleState::NewSignup, rules)
544 }
545
546 pub fn user_engagement_preset(persona_id: String) -> PersonaLifecycle {
551 let rules = vec![
552 TransitionRule {
553 to: LifecycleState::Active, after_days: Some(7), condition: Some("login_count >= 3".to_string()),
556 on_transition: None,
557 },
558 TransitionRule {
559 to: LifecycleState::ChurnRisk, after_days: Some(90), condition: Some("last_login_days_ago > 30".to_string()),
562 on_transition: None,
563 },
564 TransitionRule {
565 to: LifecycleState::Churned, after_days: Some(60), 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 #[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 #[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 #[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 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 let future_time_5days = lifecycle.state_entered_at + Duration::days(5);
736 assert!(lifecycle.transition_if_elapsed(future_time_5days).is_none());
737
738 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 assert!(lifecycle.transition_if_elapsed(future_time).is_none());
760
761 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 #[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 #[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 #[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 #[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 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 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 let result = lifecycle.transition_if_elapsed(Utc::now());
1054 assert!(result.is_none());
1055 }
1056}