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).expect("valid default date"),
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)]
831#[allow(clippy::unwrap_used)]
832mod tests {
833 use super::*;
834
835 #[test]
836 fn test_customer_value_segment() {
837 let total_customer_share: f64 = [
839 CustomerValueSegment::Enterprise,
840 CustomerValueSegment::MidMarket,
841 CustomerValueSegment::Smb,
842 CustomerValueSegment::Consumer,
843 ]
844 .iter()
845 .map(|s| s.customer_share())
846 .sum();
847
848 assert!((total_customer_share - 1.0).abs() < 0.01);
849
850 let total_revenue_share: f64 = [
851 CustomerValueSegment::Enterprise,
852 CustomerValueSegment::MidMarket,
853 CustomerValueSegment::Smb,
854 CustomerValueSegment::Consumer,
855 ]
856 .iter()
857 .map(|s| s.revenue_share())
858 .sum();
859
860 assert!((total_revenue_share - 1.0).abs() < 0.01);
861 }
862
863 #[test]
864 fn test_lifecycle_stage() {
865 let stage = CustomerLifecycleStage::Growth {
866 since: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
867 growth_rate: 0.15,
868 };
869
870 assert!(stage.is_active());
871 assert!(stage.is_good_standing());
872 assert_eq!(stage.stage_name(), "growth");
873 }
874
875 #[test]
876 fn test_at_risk_lifecycle() {
877 let stage = CustomerLifecycleStage::AtRisk {
878 triggers: vec![
879 RiskTrigger::DecliningOrderFrequency,
880 RiskTrigger::Complaints,
881 ],
882 flagged_date: NaiveDate::from_ymd_opt(2024, 6, 1).unwrap(),
883 churn_probability: 0.6,
884 };
885
886 assert!(stage.is_active());
887 assert!(!stage.is_good_standing());
888 assert_eq!(stage.retention_priority(), 1);
889 }
890
891 #[test]
892 fn test_customer_network_position() {
893 let mut pos = CustomerNetworkPosition::new("C-001")
894 .with_referral("C-000")
895 .with_parent("C-PARENT");
896
897 pos.add_referral("C-002");
898 pos.add_referral("C-003");
899 pos.add_child("C-SUB-001");
900
901 assert!(pos.was_referred());
902 assert!(!pos.is_root());
903 assert_eq!(pos.network_influence(), 3);
904 }
905
906 #[test]
907 fn test_customer_engagement() {
908 let mut engagement = CustomerEngagement::default();
909 engagement.record_order(
910 Decimal::from(5000),
911 NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
912 3,
913 );
914 engagement.record_order(
915 Decimal::from(7500),
916 NaiveDate::from_ymd_opt(2024, 3, 1).unwrap(),
917 5,
918 );
919
920 assert_eq!(engagement.total_orders, 2);
921 assert_eq!(engagement.lifetime_revenue, Decimal::from(12500));
922 assert_eq!(engagement.products_purchased, 8);
923 assert!(engagement.average_order_value > Decimal::ZERO);
924 }
925
926 #[test]
927 fn test_segmented_customer() {
928 let customer = SegmentedCustomer::new(
929 "C-001",
930 "Acme Corp",
931 CustomerValueSegment::Enterprise,
932 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
933 )
934 .with_annual_contract_value(Decimal::from(500000))
935 .with_industry("Technology");
936
937 assert!(customer.is_high_value());
938 assert_eq!(customer.segment.code(), "ENT");
939 assert!(customer.estimated_lifetime_value() > Decimal::ZERO);
940 }
941
942 #[test]
943 fn test_segment_change() {
944 let mut customer = SegmentedCustomer::new(
945 "C-001",
946 "Growing Inc",
947 CustomerValueSegment::Smb,
948 NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(),
949 );
950
951 customer.change_segment(
952 CustomerValueSegment::MidMarket,
953 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
954 );
955
956 assert_eq!(customer.segment, CustomerValueSegment::MidMarket);
957 assert_eq!(customer.previous_segment, Some(CustomerValueSegment::Smb));
958 assert!(customer.segment_change_date.is_some());
959 }
960
961 #[test]
962 fn test_segmented_customer_pool() {
963 let mut pool = SegmentedCustomerPool::new();
964
965 pool.add_customer(SegmentedCustomer::new(
966 "C-001",
967 "Enterprise Corp",
968 CustomerValueSegment::Enterprise,
969 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
970 ));
971
972 pool.add_customer(SegmentedCustomer::new(
973 "C-002",
974 "SMB Inc",
975 CustomerValueSegment::Smb,
976 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
977 ));
978
979 assert_eq!(pool.customers.len(), 2);
980 assert_eq!(pool.by_segment(CustomerValueSegment::Enterprise).len(), 1);
981 assert_eq!(pool.high_value_customers().len(), 1);
982 }
983
984 #[test]
985 fn test_churn_risk_calculation() {
986 let mut customer = SegmentedCustomer::new(
987 "C-001",
988 "At Risk Corp",
989 CustomerValueSegment::MidMarket,
990 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
991 )
992 .with_lifecycle_stage(CustomerLifecycleStage::AtRisk {
993 triggers: vec![RiskTrigger::DecliningOrderFrequency],
994 flagged_date: NaiveDate::from_ymd_opt(2024, 6, 1).unwrap(),
995 churn_probability: 0.7,
996 });
997
998 customer.engagement.days_since_last_order = 90;
999 customer.calculate_churn_risk();
1000
1001 assert!(customer.churn_risk_score > 0.3);
1002 }
1003}