1use chrono::NaiveDate;
10use rust_decimal::Decimal;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
18#[serde(rename_all = "snake_case")]
19pub enum CustomerValueSegment {
20 Enterprise,
22 #[default]
24 MidMarket,
25 Smb,
27 Consumer,
29}
30
31impl CustomerValueSegment {
32 pub fn customer_share(&self) -> f64 {
34 match self {
35 Self::Enterprise => 0.05,
36 Self::MidMarket => 0.20,
37 Self::Smb => 0.50,
38 Self::Consumer => 0.25,
39 }
40 }
41
42 pub fn revenue_share(&self) -> f64 {
44 match self {
45 Self::Enterprise => 0.40,
46 Self::MidMarket => 0.35,
47 Self::Smb => 0.20,
48 Self::Consumer => 0.05,
49 }
50 }
51
52 pub fn order_value_range(&self) -> (Decimal, Decimal) {
54 match self {
55 Self::Enterprise => (Decimal::from(50000), Decimal::from(5000000)),
56 Self::MidMarket => (Decimal::from(5000), Decimal::from(50000)),
57 Self::Smb => (Decimal::from(500), Decimal::from(5000)),
58 Self::Consumer => (Decimal::from(50), Decimal::from(500)),
59 }
60 }
61
62 pub fn code(&self) -> &'static str {
64 match self {
65 Self::Enterprise => "ENT",
66 Self::MidMarket => "MID",
67 Self::Smb => "SMB",
68 Self::Consumer => "CON",
69 }
70 }
71
72 pub fn service_level(&self) -> &'static str {
74 match self {
75 Self::Enterprise => "dedicated_team",
76 Self::MidMarket => "named_account_manager",
77 Self::Smb => "shared_support",
78 Self::Consumer => "self_service",
79 }
80 }
81
82 pub fn typical_payment_terms_days(&self) -> u16 {
84 match self {
85 Self::Enterprise => 60,
86 Self::MidMarket => 45,
87 Self::Smb => 30,
88 Self::Consumer => 0, }
90 }
91
92 pub fn importance_score(&self) -> f64 {
94 match self {
95 Self::Enterprise => 1.0,
96 Self::MidMarket => 0.7,
97 Self::Smb => 0.4,
98 Self::Consumer => 0.2,
99 }
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
105#[serde(rename_all = "snake_case")]
106pub enum RiskTrigger {
107 DecliningOrderFrequency,
109 DecliningOrderValue,
111 PaymentIssues,
113 Complaints,
115 ReducedEngagement,
117 CompetitorMention,
119 ContractExpiring,
121 ContactDeparture,
123 BudgetCuts,
125 Restructuring,
127 Other(String),
129}
130
131impl RiskTrigger {
132 pub fn severity(&self) -> f64 {
134 match self {
135 Self::DecliningOrderFrequency => 0.6,
136 Self::DecliningOrderValue => 0.5,
137 Self::PaymentIssues => 0.8,
138 Self::Complaints => 0.7,
139 Self::ReducedEngagement => 0.4,
140 Self::CompetitorMention => 0.9,
141 Self::ContractExpiring => 0.5,
142 Self::ContactDeparture => 0.6,
143 Self::BudgetCuts => 0.7,
144 Self::Restructuring => 0.5,
145 Self::Other(_) => 0.5,
146 }
147 }
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
152#[serde(rename_all = "snake_case")]
153pub enum ChurnReason {
154 Price,
156 Competitor,
158 ServiceQuality,
160 ProductFit,
162 BusinessClosed,
164 BudgetConstraints,
166 Consolidation,
168 Acquisition,
170 ProjectCompleted,
172 Unknown,
174 Other(String),
176}
177
178#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
180#[serde(rename_all = "snake_case")]
181pub enum CustomerLifecycleStage {
182 Prospect {
184 conversion_probability: f64,
186 source: Option<String>,
188 first_contact_date: NaiveDate,
190 },
191 New {
193 first_order_date: NaiveDate,
195 onboarding_complete: bool,
197 },
198 Growth {
200 since: NaiveDate,
202 growth_rate: f64,
204 },
205 Mature {
207 stable_since: NaiveDate,
209 #[serde(with = "rust_decimal::serde::str")]
211 avg_annual_spend: Decimal,
212 },
213 AtRisk {
215 triggers: Vec<RiskTrigger>,
217 flagged_date: NaiveDate,
219 churn_probability: f64,
221 },
222 Churned {
224 last_activity: NaiveDate,
226 win_back_probability: f64,
228 reason: Option<ChurnReason>,
230 },
231 WonBack {
233 churned_date: NaiveDate,
235 won_back_date: NaiveDate,
237 },
238}
239
240impl CustomerLifecycleStage {
241 pub fn is_active(&self) -> bool {
243 !matches!(self, Self::Prospect { .. } | Self::Churned { .. })
244 }
245
246 pub fn is_good_standing(&self) -> bool {
248 matches!(
249 self,
250 Self::New { .. } | Self::Growth { .. } | Self::Mature { .. } | Self::WonBack { .. }
251 )
252 }
253
254 pub fn stage_name(&self) -> &'static str {
256 match self {
257 Self::Prospect { .. } => "prospect",
258 Self::New { .. } => "new",
259 Self::Growth { .. } => "growth",
260 Self::Mature { .. } => "mature",
261 Self::AtRisk { .. } => "at_risk",
262 Self::Churned { .. } => "churned",
263 Self::WonBack { .. } => "won_back",
264 }
265 }
266
267 pub fn retention_priority(&self) -> u8 {
269 match self {
270 Self::AtRisk { .. } => 1,
271 Self::Growth { .. } => 2,
272 Self::Mature { .. } => 3,
273 Self::New { .. } => 4,
274 Self::WonBack { .. } => 5,
275 Self::Churned { .. } => 6,
276 Self::Prospect { .. } => 7,
277 }
278 }
279}
280
281impl Default for CustomerLifecycleStage {
282 fn default() -> Self {
283 Self::Mature {
284 stable_since: NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
285 avg_annual_spend: Decimal::from(50000),
286 }
287 }
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct CustomerNetworkPosition {
293 pub customer_id: String,
295 pub referred_by: Option<String>,
297 pub referrals_made: Vec<String>,
299 pub parent_customer: Option<String>,
301 pub child_customers: Vec<String>,
303 pub billing_consolidation: bool,
305 pub industry_cluster_id: Option<String>,
307 pub region: Option<String>,
309 pub network_join_date: Option<NaiveDate>,
311}
312
313impl CustomerNetworkPosition {
314 pub fn new(customer_id: impl Into<String>) -> Self {
316 Self {
317 customer_id: customer_id.into(),
318 referred_by: None,
319 referrals_made: Vec::new(),
320 parent_customer: None,
321 child_customers: Vec::new(),
322 billing_consolidation: false,
323 industry_cluster_id: None,
324 region: None,
325 network_join_date: None,
326 }
327 }
328
329 pub fn with_referral(mut self, referrer_id: impl Into<String>) -> Self {
331 self.referred_by = Some(referrer_id.into());
332 self
333 }
334
335 pub fn with_parent(mut self, parent_id: impl Into<String>) -> Self {
337 self.parent_customer = Some(parent_id.into());
338 self
339 }
340
341 pub fn add_referral(&mut self, referred_id: impl Into<String>) {
343 self.referrals_made.push(referred_id.into());
344 }
345
346 pub fn add_child(&mut self, child_id: impl Into<String>) {
348 self.child_customers.push(child_id.into());
349 }
350
351 pub fn network_influence(&self) -> usize {
353 self.referrals_made.len() + self.child_customers.len()
354 }
355
356 pub fn is_root(&self) -> bool {
358 self.parent_customer.is_none()
359 }
360
361 pub fn was_referred(&self) -> bool {
363 self.referred_by.is_some()
364 }
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct CustomerEngagement {
370 pub total_orders: u32,
372 pub orders_last_12_months: u32,
374 #[serde(with = "rust_decimal::serde::str")]
376 pub lifetime_revenue: Decimal,
377 #[serde(with = "rust_decimal::serde::str")]
379 pub revenue_last_12_months: Decimal,
380 #[serde(with = "rust_decimal::serde::str")]
382 pub average_order_value: Decimal,
383 pub days_since_last_order: u32,
385 pub last_order_date: Option<NaiveDate>,
387 pub first_order_date: Option<NaiveDate>,
389 pub products_purchased: u32,
391 pub support_tickets: u32,
393 pub nps_score: Option<i8>,
395}
396
397impl Default for CustomerEngagement {
398 fn default() -> Self {
399 Self {
400 total_orders: 0,
401 orders_last_12_months: 0,
402 lifetime_revenue: Decimal::ZERO,
403 revenue_last_12_months: Decimal::ZERO,
404 average_order_value: Decimal::ZERO,
405 days_since_last_order: 0,
406 last_order_date: None,
407 first_order_date: None,
408 products_purchased: 0,
409 support_tickets: 0,
410 nps_score: None,
411 }
412 }
413}
414
415impl CustomerEngagement {
416 pub fn record_order(&mut self, amount: Decimal, order_date: NaiveDate, product_count: u32) {
418 self.total_orders += 1;
419 self.lifetime_revenue += amount;
420 self.products_purchased += product_count;
421
422 if self.total_orders > 0 {
424 self.average_order_value = self.lifetime_revenue / Decimal::from(self.total_orders);
425 }
426
427 if self.first_order_date.is_none() {
429 self.first_order_date = Some(order_date);
430 }
431 self.last_order_date = Some(order_date);
432 self.days_since_last_order = 0;
433 }
434
435 pub fn update_days_since_last_order(&mut self, current_date: NaiveDate) {
437 if let Some(last_order) = self.last_order_date {
438 self.days_since_last_order = (current_date - last_order).num_days().max(0) as u32;
439 }
440 }
441
442 pub fn health_score(&self) -> f64 {
444 let mut score = 0.0;
445
446 let order_freq_score = if self.orders_last_12_months > 0 {
448 (self.orders_last_12_months as f64 / 12.0).min(1.0)
449 } else {
450 0.0
451 };
452 score += 0.30 * order_freq_score;
453
454 let recency_score = if self.days_since_last_order == 0 {
456 1.0
457 } else {
458 (1.0 - (self.days_since_last_order as f64 / 365.0)).max(0.0)
459 };
460 score += 0.30 * recency_score;
461
462 let value_score = if self.average_order_value > Decimal::ZERO {
464 let aov_f64 = self
465 .average_order_value
466 .to_string()
467 .parse::<f64>()
468 .unwrap_or(0.0);
469 (aov_f64 / 10000.0).min(1.0) } else {
471 0.0
472 };
473 score += 0.25 * value_score;
474
475 if let Some(nps) = self.nps_score {
477 let nps_normalized = ((nps as i32 + 100) as f64 / 200.0).clamp(0.0, 1.0);
479 score += 0.15 * nps_normalized;
480 } else {
481 score += 0.15 * 0.5; }
483
484 score.clamp(0.0, 1.0)
485 }
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize)]
490pub struct SegmentedCustomer {
491 pub customer_id: String,
493 pub name: String,
495 pub segment: CustomerValueSegment,
497 pub lifecycle_stage: CustomerLifecycleStage,
499 pub network_position: CustomerNetworkPosition,
501 pub engagement: CustomerEngagement,
503 pub segment_assigned_date: NaiveDate,
505 pub previous_segment: Option<CustomerValueSegment>,
507 pub segment_change_date: Option<NaiveDate>,
509 pub industry: Option<String>,
511 #[serde(with = "rust_decimal::serde::str")]
513 pub annual_contract_value: Decimal,
514 pub churn_risk_score: f64,
516 pub upsell_potential: f64,
518 pub account_manager: Option<String>,
520}
521
522impl SegmentedCustomer {
523 pub fn new(
525 customer_id: impl Into<String>,
526 name: impl Into<String>,
527 segment: CustomerValueSegment,
528 assignment_date: NaiveDate,
529 ) -> Self {
530 let customer_id = customer_id.into();
531 Self {
532 customer_id: customer_id.clone(),
533 name: name.into(),
534 segment,
535 lifecycle_stage: CustomerLifecycleStage::default(),
536 network_position: CustomerNetworkPosition::new(customer_id),
537 engagement: CustomerEngagement::default(),
538 segment_assigned_date: assignment_date,
539 previous_segment: None,
540 segment_change_date: None,
541 industry: None,
542 annual_contract_value: Decimal::ZERO,
543 churn_risk_score: 0.0,
544 upsell_potential: 0.5,
545 account_manager: None,
546 }
547 }
548
549 pub fn with_lifecycle_stage(mut self, stage: CustomerLifecycleStage) -> Self {
551 self.lifecycle_stage = stage;
552 self
553 }
554
555 pub fn with_industry(mut self, industry: impl Into<String>) -> Self {
557 self.industry = Some(industry.into());
558 self
559 }
560
561 pub fn with_annual_contract_value(mut self, value: Decimal) -> Self {
563 self.annual_contract_value = value;
564 self
565 }
566
567 pub fn change_segment(&mut self, new_segment: CustomerValueSegment, change_date: NaiveDate) {
569 if self.segment != new_segment {
570 self.previous_segment = Some(self.segment);
571 self.segment = new_segment;
572 self.segment_change_date = Some(change_date);
573 }
574 }
575
576 pub fn calculate_churn_risk(&mut self) {
578 let mut risk = 0.0;
579
580 match &self.lifecycle_stage {
582 CustomerLifecycleStage::AtRisk {
583 churn_probability, ..
584 } => {
585 risk += 0.4 * churn_probability;
586 }
587 CustomerLifecycleStage::New { .. } => risk += 0.15,
588 CustomerLifecycleStage::WonBack { .. } => risk += 0.25,
589 CustomerLifecycleStage::Growth { .. } => risk += 0.05,
590 CustomerLifecycleStage::Mature { .. } => risk += 0.10,
591 _ => {}
592 }
593
594 let health = self.engagement.health_score();
596 risk += 0.4 * (1.0 - health);
597
598 let recency_risk = (self.engagement.days_since_last_order as f64 / 180.0).min(1.0);
600 risk += 0.2 * recency_risk;
601
602 self.churn_risk_score = risk.clamp(0.0, 1.0);
603 }
604
605 pub fn estimated_lifetime_value(&self) -> Decimal {
607 let expected_years = match self.segment {
609 CustomerValueSegment::Enterprise => Decimal::from(8),
610 CustomerValueSegment::MidMarket => Decimal::from(5),
611 CustomerValueSegment::Smb => Decimal::from(3),
612 CustomerValueSegment::Consumer => Decimal::from(2),
613 };
614 let retention_factor =
615 Decimal::from_f64_retain(1.0 - self.churn_risk_score).unwrap_or(Decimal::ONE);
616 self.annual_contract_value * expected_years * retention_factor
617 }
618
619 pub fn is_high_value(&self) -> bool {
621 matches!(
622 self.segment,
623 CustomerValueSegment::Enterprise | CustomerValueSegment::MidMarket
624 )
625 }
626}
627
628#[derive(Debug, Clone, Default, Serialize, Deserialize)]
630pub struct SegmentedCustomerPool {
631 pub customers: Vec<SegmentedCustomer>,
633 #[serde(skip)]
635 segment_index: HashMap<CustomerValueSegment, Vec<usize>>,
636 #[serde(skip)]
638 lifecycle_index: HashMap<String, Vec<usize>>,
639 pub statistics: SegmentStatistics,
641}
642
643#[derive(Debug, Clone, Default, Serialize, Deserialize)]
645pub struct SegmentStatistics {
646 pub customers_by_segment: HashMap<String, usize>,
648 pub revenue_by_segment: HashMap<String, Decimal>,
650 #[serde(with = "rust_decimal::serde::str")]
652 pub total_revenue: Decimal,
653 pub avg_churn_risk: f64,
655 pub referral_rate: f64,
657 pub at_risk_count: usize,
659}
660
661impl SegmentedCustomerPool {
662 pub fn new() -> Self {
664 Self {
665 customers: Vec::new(),
666 segment_index: HashMap::new(),
667 lifecycle_index: HashMap::new(),
668 statistics: SegmentStatistics::default(),
669 }
670 }
671
672 pub fn add_customer(&mut self, customer: SegmentedCustomer) {
674 let idx = self.customers.len();
675 let segment = customer.segment;
676 let stage_name = customer.lifecycle_stage.stage_name().to_string();
677
678 self.customers.push(customer);
679
680 self.segment_index.entry(segment).or_default().push(idx);
681 self.lifecycle_index
682 .entry(stage_name)
683 .or_default()
684 .push(idx);
685 }
686
687 pub fn by_segment(&self, segment: CustomerValueSegment) -> Vec<&SegmentedCustomer> {
689 self.segment_index
690 .get(&segment)
691 .map(|indices| indices.iter().map(|&idx| &self.customers[idx]).collect())
692 .unwrap_or_default()
693 }
694
695 pub fn by_lifecycle_stage(&self, stage_name: &str) -> Vec<&SegmentedCustomer> {
697 self.lifecycle_index
698 .get(stage_name)
699 .map(|indices| indices.iter().map(|&idx| &self.customers[idx]).collect())
700 .unwrap_or_default()
701 }
702
703 pub fn at_risk_customers(&self) -> Vec<&SegmentedCustomer> {
705 self.customers
706 .iter()
707 .filter(|c| matches!(c.lifecycle_stage, CustomerLifecycleStage::AtRisk { .. }))
708 .collect()
709 }
710
711 pub fn high_value_customers(&self) -> Vec<&SegmentedCustomer> {
713 self.customers
714 .iter()
715 .filter(|c| c.is_high_value())
716 .collect()
717 }
718
719 pub fn rebuild_indexes(&mut self) {
721 self.segment_index.clear();
722 self.lifecycle_index.clear();
723
724 for (idx, customer) in self.customers.iter().enumerate() {
725 self.segment_index
726 .entry(customer.segment)
727 .or_default()
728 .push(idx);
729 self.lifecycle_index
730 .entry(customer.lifecycle_stage.stage_name().to_string())
731 .or_default()
732 .push(idx);
733 }
734 }
735
736 pub fn calculate_statistics(&mut self) {
738 let mut customers_by_segment: HashMap<String, usize> = HashMap::new();
739 let mut revenue_by_segment: HashMap<String, Decimal> = HashMap::new();
740 let mut total_revenue = Decimal::ZERO;
741 let mut total_churn_risk = 0.0;
742 let mut referral_count = 0usize;
743 let mut at_risk_count = 0usize;
744
745 for customer in &self.customers {
746 let segment_name = format!("{:?}", customer.segment);
747
748 *customers_by_segment
749 .entry(segment_name.clone())
750 .or_insert(0) += 1;
751 *revenue_by_segment
752 .entry(segment_name)
753 .or_insert(Decimal::ZERO) += customer.annual_contract_value;
754
755 total_revenue += customer.annual_contract_value;
756 total_churn_risk += customer.churn_risk_score;
757
758 if customer.network_position.was_referred() {
759 referral_count += 1;
760 }
761
762 if matches!(
763 customer.lifecycle_stage,
764 CustomerLifecycleStage::AtRisk { .. }
765 ) {
766 at_risk_count += 1;
767 }
768 }
769
770 let avg_churn_risk = if self.customers.is_empty() {
771 0.0
772 } else {
773 total_churn_risk / self.customers.len() as f64
774 };
775
776 let referral_rate = if self.customers.is_empty() {
777 0.0
778 } else {
779 referral_count as f64 / self.customers.len() as f64
780 };
781
782 self.statistics = SegmentStatistics {
783 customers_by_segment,
784 revenue_by_segment,
785 total_revenue,
786 avg_churn_risk,
787 referral_rate,
788 at_risk_count,
789 };
790 }
791
792 pub fn check_segment_distribution(&self) -> Vec<String> {
794 let mut issues = Vec::new();
795 let total = self.customers.len() as f64;
796
797 if total == 0.0 {
798 return issues;
799 }
800
801 for segment in [
802 CustomerValueSegment::Enterprise,
803 CustomerValueSegment::MidMarket,
804 CustomerValueSegment::Smb,
805 CustomerValueSegment::Consumer,
806 ] {
807 let expected = segment.customer_share();
808 let actual = self
809 .segment_index
810 .get(&segment)
811 .map(|v| v.len())
812 .unwrap_or(0) as f64
813 / total;
814
815 if (actual - expected).abs() > expected * 0.2 {
817 issues.push(format!(
818 "Segment {:?} distribution {:.1}% deviates from expected {:.1}%",
819 segment,
820 actual * 100.0,
821 expected * 100.0
822 ));
823 }
824 }
825
826 issues
827 }
828}
829
830#[cfg(test)]
831mod tests {
832 use super::*;
833
834 #[test]
835 fn test_customer_value_segment() {
836 let total_customer_share: f64 = [
838 CustomerValueSegment::Enterprise,
839 CustomerValueSegment::MidMarket,
840 CustomerValueSegment::Smb,
841 CustomerValueSegment::Consumer,
842 ]
843 .iter()
844 .map(|s| s.customer_share())
845 .sum();
846
847 assert!((total_customer_share - 1.0).abs() < 0.01);
848
849 let total_revenue_share: f64 = [
850 CustomerValueSegment::Enterprise,
851 CustomerValueSegment::MidMarket,
852 CustomerValueSegment::Smb,
853 CustomerValueSegment::Consumer,
854 ]
855 .iter()
856 .map(|s| s.revenue_share())
857 .sum();
858
859 assert!((total_revenue_share - 1.0).abs() < 0.01);
860 }
861
862 #[test]
863 fn test_lifecycle_stage() {
864 let stage = CustomerLifecycleStage::Growth {
865 since: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
866 growth_rate: 0.15,
867 };
868
869 assert!(stage.is_active());
870 assert!(stage.is_good_standing());
871 assert_eq!(stage.stage_name(), "growth");
872 }
873
874 #[test]
875 fn test_at_risk_lifecycle() {
876 let stage = CustomerLifecycleStage::AtRisk {
877 triggers: vec![
878 RiskTrigger::DecliningOrderFrequency,
879 RiskTrigger::Complaints,
880 ],
881 flagged_date: NaiveDate::from_ymd_opt(2024, 6, 1).unwrap(),
882 churn_probability: 0.6,
883 };
884
885 assert!(stage.is_active());
886 assert!(!stage.is_good_standing());
887 assert_eq!(stage.retention_priority(), 1);
888 }
889
890 #[test]
891 fn test_customer_network_position() {
892 let mut pos = CustomerNetworkPosition::new("C-001")
893 .with_referral("C-000")
894 .with_parent("C-PARENT");
895
896 pos.add_referral("C-002");
897 pos.add_referral("C-003");
898 pos.add_child("C-SUB-001");
899
900 assert!(pos.was_referred());
901 assert!(!pos.is_root());
902 assert_eq!(pos.network_influence(), 3);
903 }
904
905 #[test]
906 fn test_customer_engagement() {
907 let mut engagement = CustomerEngagement::default();
908 engagement.record_order(
909 Decimal::from(5000),
910 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
911 3,
912 );
913 engagement.record_order(
914 Decimal::from(7500),
915 NaiveDate::from_ymd_opt(2024, 3, 1).unwrap(),
916 5,
917 );
918
919 assert_eq!(engagement.total_orders, 2);
920 assert_eq!(engagement.lifetime_revenue, Decimal::from(12500));
921 assert_eq!(engagement.products_purchased, 8);
922 assert!(engagement.average_order_value > Decimal::ZERO);
923 }
924
925 #[test]
926 fn test_segmented_customer() {
927 let customer = SegmentedCustomer::new(
928 "C-001",
929 "Acme Corp",
930 CustomerValueSegment::Enterprise,
931 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
932 )
933 .with_annual_contract_value(Decimal::from(500000))
934 .with_industry("Technology");
935
936 assert!(customer.is_high_value());
937 assert_eq!(customer.segment.code(), "ENT");
938 assert!(customer.estimated_lifetime_value() > Decimal::ZERO);
939 }
940
941 #[test]
942 fn test_segment_change() {
943 let mut customer = SegmentedCustomer::new(
944 "C-001",
945 "Growing Inc",
946 CustomerValueSegment::Smb,
947 NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(),
948 );
949
950 customer.change_segment(
951 CustomerValueSegment::MidMarket,
952 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
953 );
954
955 assert_eq!(customer.segment, CustomerValueSegment::MidMarket);
956 assert_eq!(customer.previous_segment, Some(CustomerValueSegment::Smb));
957 assert!(customer.segment_change_date.is_some());
958 }
959
960 #[test]
961 fn test_segmented_customer_pool() {
962 let mut pool = SegmentedCustomerPool::new();
963
964 pool.add_customer(SegmentedCustomer::new(
965 "C-001",
966 "Enterprise Corp",
967 CustomerValueSegment::Enterprise,
968 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
969 ));
970
971 pool.add_customer(SegmentedCustomer::new(
972 "C-002",
973 "SMB Inc",
974 CustomerValueSegment::Smb,
975 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
976 ));
977
978 assert_eq!(pool.customers.len(), 2);
979 assert_eq!(pool.by_segment(CustomerValueSegment::Enterprise).len(), 1);
980 assert_eq!(pool.high_value_customers().len(), 1);
981 }
982
983 #[test]
984 fn test_churn_risk_calculation() {
985 let mut customer = SegmentedCustomer::new(
986 "C-001",
987 "At Risk Corp",
988 CustomerValueSegment::MidMarket,
989 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
990 )
991 .with_lifecycle_stage(CustomerLifecycleStage::AtRisk {
992 triggers: vec![RiskTrigger::DecliningOrderFrequency],
993 flagged_date: NaiveDate::from_ymd_opt(2024, 6, 1).unwrap(),
994 churn_probability: 0.7,
995 });
996
997 customer.engagement.days_since_last_order = 90;
998 customer.calculate_churn_risk();
999
1000 assert!(customer.churn_risk_score > 0.3);
1001 }
1002}