Skip to main content

datasynth_core/models/
customer_segment.rs

1//! Customer segmentation and lifecycle models.
2//!
3//! Provides comprehensive customer relationship modeling including:
4//! - Value-based segmentation (Enterprise, Mid-Market, SMB, Consumer)
5//! - Customer lifecycle stages with transition tracking
6//! - Network position for referrals and corporate hierarchies
7//! - Revenue attribution and churn prediction
8
9use chrono::NaiveDate;
10use rust_decimal::Decimal;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13
14/// Customer value segment classification.
15///
16/// Based on research showing typical B2B/B2C customer distributions.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
18#[serde(rename_all = "snake_case")]
19pub enum CustomerValueSegment {
20    /// Enterprise customers (~5% of customers, ~40% of revenue)
21    Enterprise,
22    /// Mid-market customers (~20% of customers, ~35% of revenue)
23    #[default]
24    MidMarket,
25    /// Small/medium business (~50% of customers, ~20% of revenue)
26    Smb,
27    /// Consumer/individual (~25% of customers, ~5% of revenue)
28    Consumer,
29}
30
31impl CustomerValueSegment {
32    /// Get the typical customer share percentage.
33    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    /// Get the typical revenue share percentage.
43    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    /// Get the typical order value range.
53    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    /// Get the segment code.
63    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    /// Get the typical service level.
73    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    /// Get the typical payment terms in days.
83    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, // Immediate
89        }
90    }
91
92    /// Get the strategic importance score.
93    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/// Risk triggers for at-risk customers.
104#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
105#[serde(rename_all = "snake_case")]
106pub enum RiskTrigger {
107    /// Declining order frequency
108    DecliningOrderFrequency,
109    /// Declining order value
110    DecliningOrderValue,
111    /// Payment issues
112    PaymentIssues,
113    /// Complaints or support tickets
114    Complaints,
115    /// Reduced engagement
116    ReducedEngagement,
117    /// Competitor mention
118    CompetitorMention,
119    /// Contract expiring soon
120    ContractExpiring,
121    /// Key contact departure
122    ContactDeparture,
123    /// Budget cuts announced
124    BudgetCuts,
125    /// Organizational restructuring
126    Restructuring,
127    /// Custom trigger
128    Other(String),
129}
130
131impl RiskTrigger {
132    /// Get the severity score (0.0 to 1.0).
133    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/// Churn reason for lost customers.
151#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
152#[serde(rename_all = "snake_case")]
153pub enum ChurnReason {
154    /// Price was too high
155    Price,
156    /// Switched to competitor
157    Competitor,
158    /// Poor service quality
159    ServiceQuality,
160    /// Product didn't meet needs
161    ProductFit,
162    /// Business closure
163    BusinessClosed,
164    /// Budget constraints
165    BudgetConstraints,
166    /// Internal consolidation
167    Consolidation,
168    /// Acquisition by another company
169    Acquisition,
170    /// Natural end of need
171    ProjectCompleted,
172    /// Unknown reason
173    Unknown,
174    /// Other reason
175    Other(String),
176}
177
178/// Customer lifecycle stage with metadata.
179#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
180#[serde(rename_all = "snake_case")]
181pub enum CustomerLifecycleStage {
182    /// Potential customer, not yet converted
183    Prospect {
184        /// Probability of conversion (0.0 to 1.0)
185        conversion_probability: f64,
186        /// Lead source
187        source: Option<String>,
188        /// First contact date
189        first_contact_date: NaiveDate,
190    },
191    /// Newly acquired customer
192    New {
193        /// Date of first order
194        first_order_date: NaiveDate,
195        /// Onboarding completed
196        onboarding_complete: bool,
197    },
198    /// Growing customer (increasing spend)
199    Growth {
200        /// Start of growth phase
201        since: NaiveDate,
202        /// Year-over-year growth rate
203        growth_rate: f64,
204    },
205    /// Mature customer (stable relationship)
206    Mature {
207        /// Date when stable state achieved
208        stable_since: NaiveDate,
209        /// Average annual spend
210        #[serde(with = "rust_decimal::serde::str")]
211        avg_annual_spend: Decimal,
212    },
213    /// Customer showing churn signals
214    AtRisk {
215        /// Risk triggers detected
216        triggers: Vec<RiskTrigger>,
217        /// Date when flagged at-risk
218        flagged_date: NaiveDate,
219        /// Estimated churn probability
220        churn_probability: f64,
221    },
222    /// Customer has churned
223    Churned {
224        /// Date of last activity
225        last_activity: NaiveDate,
226        /// Probability of win-back
227        win_back_probability: f64,
228        /// Reason for churn
229        reason: Option<ChurnReason>,
230    },
231    /// Won-back customer
232    WonBack {
233        /// Original churn date
234        churned_date: NaiveDate,
235        /// Win-back date
236        won_back_date: NaiveDate,
237    },
238}
239
240impl CustomerLifecycleStage {
241    /// Check if the customer is active.
242    pub fn is_active(&self) -> bool {
243        !matches!(self, Self::Prospect { .. } | Self::Churned { .. })
244    }
245
246    /// Check if the customer is in good standing.
247    pub fn is_good_standing(&self) -> bool {
248        matches!(
249            self,
250            Self::New { .. } | Self::Growth { .. } | Self::Mature { .. } | Self::WonBack { .. }
251        )
252    }
253
254    /// Get the stage name.
255    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    /// Get the retention priority (1 = highest).
268    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/// Customer network position for referrals and hierarchies.
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct CustomerNetworkPosition {
293    /// Customer ID
294    pub customer_id: String,
295    /// Customer who referred this customer
296    pub referred_by: Option<String>,
297    /// Customers this customer referred
298    pub referrals_made: Vec<String>,
299    /// Parent customer in corporate hierarchy
300    pub parent_customer: Option<String>,
301    /// Child customers in corporate hierarchy
302    pub child_customers: Vec<String>,
303    /// Whether billing is consolidated to parent
304    pub billing_consolidation: bool,
305    /// Industry cluster for similar customer analysis
306    pub industry_cluster_id: Option<String>,
307    /// Geographic region
308    pub region: Option<String>,
309    /// Date joined the network
310    pub network_join_date: Option<NaiveDate>,
311}
312
313impl CustomerNetworkPosition {
314    /// Create a new network position.
315    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    /// Set referral source.
330    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    /// Set parent in corporate hierarchy.
336    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    /// Add a referral made.
342    pub fn add_referral(&mut self, referred_id: impl Into<String>) {
343        self.referrals_made.push(referred_id.into());
344    }
345
346    /// Add a child customer.
347    pub fn add_child(&mut self, child_id: impl Into<String>) {
348        self.child_customers.push(child_id.into());
349    }
350
351    /// Get total network influence (referrals + children).
352    pub fn network_influence(&self) -> usize {
353        self.referrals_made.len() + self.child_customers.len()
354    }
355
356    /// Check if this is a root customer (no parent).
357    pub fn is_root(&self) -> bool {
358        self.parent_customer.is_none()
359    }
360
361    /// Check if this customer was referred.
362    pub fn was_referred(&self) -> bool {
363        self.referred_by.is_some()
364    }
365}
366
367/// Customer engagement metrics.
368#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct CustomerEngagement {
370    /// Total orders placed
371    pub total_orders: u32,
372    /// Orders in the last 12 months
373    pub orders_last_12_months: u32,
374    /// Total revenue (lifetime)
375    #[serde(with = "rust_decimal::serde::str")]
376    pub lifetime_revenue: Decimal,
377    /// Revenue in the last 12 months
378    #[serde(with = "rust_decimal::serde::str")]
379    pub revenue_last_12_months: Decimal,
380    /// Average order value
381    #[serde(with = "rust_decimal::serde::str")]
382    pub average_order_value: Decimal,
383    /// Days since last order
384    pub days_since_last_order: u32,
385    /// Last order date
386    pub last_order_date: Option<NaiveDate>,
387    /// First order date
388    pub first_order_date: Option<NaiveDate>,
389    /// Number of products purchased
390    pub products_purchased: u32,
391    /// Support tickets created
392    pub support_tickets: u32,
393    /// Net promoter score (if available)
394    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    /// Record an order.
417    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        // Update average order value
423        if self.total_orders > 0 {
424            self.average_order_value = self.lifetime_revenue / Decimal::from(self.total_orders);
425        }
426
427        // Update first/last order dates
428        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    /// Update days since last order (call periodically).
436    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    /// Calculate customer health score (0.0 to 1.0).
443    pub fn health_score(&self) -> f64 {
444        let mut score = 0.0;
445
446        // Order frequency component (30%)
447        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        // Recency component (30%)
455        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        // Value component (25%)
463        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) // Normalize to $10k
470        } else {
471            0.0
472        };
473        score += 0.25 * value_score;
474
475        // NPS component (15%)
476        if let Some(nps) = self.nps_score {
477            // Cast to i32 first to avoid overflow (NPS is -100 to +100, i8 range is -128 to +127)
478            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; // Neutral if no NPS
482        }
483
484        score.clamp(0.0, 1.0)
485    }
486}
487
488/// Segmented customer record with full metadata.
489#[derive(Debug, Clone, Serialize, Deserialize)]
490pub struct SegmentedCustomer {
491    /// Customer ID
492    pub customer_id: String,
493    /// Customer name
494    pub name: String,
495    /// Value segment
496    pub segment: CustomerValueSegment,
497    /// Current lifecycle stage
498    pub lifecycle_stage: CustomerLifecycleStage,
499    /// Network position
500    pub network_position: CustomerNetworkPosition,
501    /// Engagement metrics
502    pub engagement: CustomerEngagement,
503    /// Segment assignment date
504    pub segment_assigned_date: NaiveDate,
505    /// Previous segment (if changed)
506    pub previous_segment: Option<CustomerValueSegment>,
507    /// Segment change date
508    pub segment_change_date: Option<NaiveDate>,
509    /// Industry
510    pub industry: Option<String>,
511    /// Annual contract value
512    #[serde(with = "rust_decimal::serde::str")]
513    pub annual_contract_value: Decimal,
514    /// Churn risk score (0.0 to 1.0)
515    pub churn_risk_score: f64,
516    /// Upsell potential score (0.0 to 1.0)
517    pub upsell_potential: f64,
518    /// Account manager
519    pub account_manager: Option<String>,
520}
521
522impl SegmentedCustomer {
523    /// Create a new segmented customer.
524    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    /// Set lifecycle stage.
550    pub fn with_lifecycle_stage(mut self, stage: CustomerLifecycleStage) -> Self {
551        self.lifecycle_stage = stage;
552        self
553    }
554
555    /// Set industry.
556    pub fn with_industry(mut self, industry: impl Into<String>) -> Self {
557        self.industry = Some(industry.into());
558        self
559    }
560
561    /// Set annual contract value.
562    pub fn with_annual_contract_value(mut self, value: Decimal) -> Self {
563        self.annual_contract_value = value;
564        self
565    }
566
567    /// Change segment (with history tracking).
568    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    /// Update churn risk score based on engagement and lifecycle.
577    pub fn calculate_churn_risk(&mut self) {
578        let mut risk = 0.0;
579
580        // Lifecycle stage risk
581        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        // Engagement health
595        let health = self.engagement.health_score();
596        risk += 0.4 * (1.0 - health);
597
598        // Days since last order
599        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    /// Get customer lifetime value estimate.
606    pub fn estimated_lifetime_value(&self) -> Decimal {
607        // Simple CLV: ACV * expected relationship duration
608        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    /// Check if customer is high value.
620    pub fn is_high_value(&self) -> bool {
621        matches!(
622            self.segment,
623            CustomerValueSegment::Enterprise | CustomerValueSegment::MidMarket
624        )
625    }
626}
627
628/// Pool of segmented customers.
629#[derive(Debug, Clone, Default, Serialize, Deserialize)]
630pub struct SegmentedCustomerPool {
631    /// All segmented customers
632    pub customers: Vec<SegmentedCustomer>,
633    /// Index by segment
634    #[serde(skip)]
635    segment_index: HashMap<CustomerValueSegment, Vec<usize>>,
636    /// Index by lifecycle stage name
637    #[serde(skip)]
638    lifecycle_index: HashMap<String, Vec<usize>>,
639    /// Pool statistics
640    pub statistics: SegmentStatistics,
641}
642
643/// Statistics for the segmented customer pool.
644#[derive(Debug, Clone, Default, Serialize, Deserialize)]
645pub struct SegmentStatistics {
646    /// Total customers by segment
647    pub customers_by_segment: HashMap<String, usize>,
648    /// Revenue by segment
649    pub revenue_by_segment: HashMap<String, Decimal>,
650    /// Total revenue
651    #[serde(with = "rust_decimal::serde::str")]
652    pub total_revenue: Decimal,
653    /// Average churn risk
654    pub avg_churn_risk: f64,
655    /// Referral rate
656    pub referral_rate: f64,
657    /// Customers at risk
658    pub at_risk_count: usize,
659}
660
661impl SegmentedCustomerPool {
662    /// Create a new empty pool.
663    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    /// Add a customer to the pool.
673    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    /// Get customers by segment.
688    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    /// Get customers by lifecycle stage.
696    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    /// Get at-risk customers.
704    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    /// Get high-value customers.
712    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    /// Rebuild indexes (call after deserialization).
720    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    /// Calculate pool statistics.
737    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    /// Check segment distribution against targets.
793    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            // Allow 20% deviation
816            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        // Verify shares sum to 1.0
837        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}