Skip to main content

datasynth_core/models/
vendor_network.rs

1//! Multi-tier vendor network models for supply chain simulation.
2//!
3//! Provides comprehensive vendor relationship modeling including:
4//! - Supply chain tiers (Tier 1, 2, 3 suppliers)
5//! - Strategic importance and spend classification
6//! - Vendor clustering for realistic behavior patterns
7//! - Lifecycle stages and dependency tracking
8//! - Quality scoring and payment history
9
10use chrono::NaiveDate;
11use rust_decimal::Decimal;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14
15/// Type of vendor relationship in the supply chain.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
17#[serde(rename_all = "snake_case")]
18pub enum VendorRelationshipType {
19    /// Direct supplier of goods or materials
20    #[default]
21    DirectSupplier,
22    /// Provider of services
23    ServiceProvider,
24    /// Contract worker or firm
25    Contractor,
26    /// Product distributor
27    Distributor,
28    /// Manufacturer of finished goods
29    Manufacturer,
30    /// Supplier of raw materials
31    RawMaterialSupplier,
32    /// Original equipment manufacturer partner
33    OemPartner,
34    /// Affiliated company
35    Affiliate,
36    /// Joint venture partner
37    JointVenturePartner,
38    /// Subcontractor
39    Subcontractor,
40}
41
42impl VendorRelationshipType {
43    /// Get the relationship type code.
44    pub fn code(&self) -> &'static str {
45        match self {
46            Self::DirectSupplier => "DS",
47            Self::ServiceProvider => "SP",
48            Self::Contractor => "CT",
49            Self::Distributor => "DI",
50            Self::Manufacturer => "MF",
51            Self::RawMaterialSupplier => "RM",
52            Self::OemPartner => "OE",
53            Self::Affiliate => "AF",
54            Self::JointVenturePartner => "JV",
55            Self::Subcontractor => "SC",
56        }
57    }
58
59    /// Check if this is a strategic relationship type.
60    pub fn is_strategic(&self) -> bool {
61        matches!(
62            self,
63            Self::OemPartner | Self::JointVenturePartner | Self::Affiliate | Self::Manufacturer
64        )
65    }
66}
67
68/// Supply chain tier classification.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
70#[serde(rename_all = "snake_case")]
71pub enum SupplyChainTier {
72    /// Direct supplier to the company (full visibility)
73    #[default]
74    Tier1,
75    /// Supplier to Tier 1 (partial visibility)
76    Tier2,
77    /// Supplier to Tier 2 (minimal visibility)
78    Tier3,
79}
80
81impl SupplyChainTier {
82    /// Get the tier number.
83    pub fn tier_number(&self) -> u8 {
84        match self {
85            Self::Tier1 => 1,
86            Self::Tier2 => 2,
87            Self::Tier3 => 3,
88        }
89    }
90
91    /// Get visibility level (0.0 to 1.0).
92    pub fn visibility(&self) -> f64 {
93        match self {
94            Self::Tier1 => 1.0,
95            Self::Tier2 => 0.5,
96            Self::Tier3 => 0.2,
97        }
98    }
99
100    /// Get the child tier (supplier to this tier).
101    pub fn child_tier(&self) -> Option<Self> {
102        match self {
103            Self::Tier1 => Some(Self::Tier2),
104            Self::Tier2 => Some(Self::Tier3),
105            Self::Tier3 => None,
106        }
107    }
108
109    /// Get the parent tier (customer of this tier).
110    pub fn parent_tier(&self) -> Option<Self> {
111        match self {
112            Self::Tier1 => None,
113            Self::Tier2 => Some(Self::Tier1),
114            Self::Tier3 => Some(Self::Tier2),
115        }
116    }
117}
118
119/// Strategic importance level of a vendor.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
121#[serde(rename_all = "snake_case")]
122pub enum StrategicLevel {
123    /// Critical to operations, single-source dependency
124    Critical,
125    /// Important strategic partner
126    Important,
127    /// Standard operational supplier
128    #[default]
129    Standard,
130    /// Transactional, easily replaceable
131    Transactional,
132}
133
134impl StrategicLevel {
135    /// Get the importance score (0.0 to 1.0).
136    pub fn importance_score(&self) -> f64 {
137        match self {
138            Self::Critical => 1.0,
139            Self::Important => 0.75,
140            Self::Standard => 0.5,
141            Self::Transactional => 0.25,
142        }
143    }
144
145    /// Get typical procurement oversight level.
146    pub fn oversight_level(&self) -> &'static str {
147        match self {
148            Self::Critical => "executive",
149            Self::Important => "senior_management",
150            Self::Standard => "procurement_team",
151            Self::Transactional => "automated",
152        }
153    }
154}
155
156/// Spend tier based on annual procurement volume.
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
158#[serde(rename_all = "snake_case")]
159pub enum SpendTier {
160    /// Highest spend tier (top 5% by spend)
161    Platinum,
162    /// High spend tier (next 15% by spend)
163    Gold,
164    /// Medium spend tier (next 30% by spend)
165    #[default]
166    Silver,
167    /// Lower spend tier (bottom 50% by spend)
168    Bronze,
169}
170
171impl SpendTier {
172    /// Get the minimum spend percentage for this tier.
173    pub fn min_spend_percentile(&self) -> f64 {
174        match self {
175            Self::Platinum => 0.95,
176            Self::Gold => 0.80,
177            Self::Silver => 0.50,
178            Self::Bronze => 0.0,
179        }
180    }
181
182    /// Get the discount eligibility multiplier.
183    pub fn discount_multiplier(&self) -> f64 {
184        match self {
185            Self::Platinum => 1.15,
186            Self::Gold => 1.10,
187            Self::Silver => 1.05,
188            Self::Bronze => 1.0,
189        }
190    }
191
192    /// Get the payment priority level.
193    pub fn payment_priority(&self) -> u8 {
194        match self {
195            Self::Platinum => 1,
196            Self::Gold => 2,
197            Self::Silver => 3,
198            Self::Bronze => 4,
199        }
200    }
201}
202
203/// Vendor cluster for behavioral grouping.
204///
205/// Based on research showing vendors typically cluster into 4 groups.
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
207#[serde(rename_all = "snake_case")]
208pub enum VendorCluster {
209    /// Reliable strategic partners (~20% of vendors)
210    ReliableStrategic,
211    /// Standard operational vendors (~50% of vendors)
212    #[default]
213    StandardOperational,
214    /// Transactional vendors (~25% of vendors)
215    Transactional,
216    /// Problematic vendors requiring monitoring (~5% of vendors)
217    Problematic,
218}
219
220impl VendorCluster {
221    /// Get the typical distribution percentage for this cluster.
222    pub fn typical_distribution(&self) -> f64 {
223        match self {
224            Self::ReliableStrategic => 0.20,
225            Self::StandardOperational => 0.50,
226            Self::Transactional => 0.25,
227            Self::Problematic => 0.05,
228        }
229    }
230
231    /// Get the on-time delivery probability.
232    pub fn on_time_delivery_probability(&self) -> f64 {
233        match self {
234            Self::ReliableStrategic => 0.98,
235            Self::StandardOperational => 0.92,
236            Self::Transactional => 0.85,
237            Self::Problematic => 0.70,
238        }
239    }
240
241    /// Get the quality issue probability.
242    pub fn quality_issue_probability(&self) -> f64 {
243        match self {
244            Self::ReliableStrategic => 0.01,
245            Self::StandardOperational => 0.03,
246            Self::Transactional => 0.07,
247            Self::Problematic => 0.15,
248        }
249    }
250
251    /// Get the invoice accuracy probability.
252    pub fn invoice_accuracy_probability(&self) -> f64 {
253        match self {
254            Self::ReliableStrategic => 0.99,
255            Self::StandardOperational => 0.95,
256            Self::Transactional => 0.90,
257            Self::Problematic => 0.80,
258        }
259    }
260}
261
262/// Reason for vendor decline.
263#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
264#[serde(rename_all = "snake_case")]
265pub enum DeclineReason {
266    /// Quality degradation
267    QualityIssues,
268    /// Price increases
269    PriceIssues,
270    /// Delivery problems
271    DeliveryIssues,
272    /// Financial instability
273    FinancialConcerns,
274    /// Strategic shift to alternatives
275    StrategicShift,
276    /// Regulatory or compliance issues
277    ComplianceIssues,
278    /// Other reasons
279    Other(String),
280}
281
282/// Reason for vendor termination.
283#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
284#[serde(rename_all = "snake_case")]
285pub enum TerminationReason {
286    /// Contract expiration not renewed
287    ContractExpired,
288    /// Vendor breach of contract
289    Breach,
290    /// Vendor bankruptcy
291    Bankruptcy,
292    /// Mutual agreement
293    MutualAgreement,
294    /// Compliance violation
295    ComplianceViolation,
296    /// Performance issues
297    PerformanceIssues,
298    /// Strategic consolidation
299    Consolidation,
300    /// Vendor acquisition by another company
301    Acquisition,
302    /// Other reasons
303    Other(String),
304}
305
306/// Vendor lifecycle stage tracking.
307#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
308#[serde(rename_all = "snake_case")]
309pub enum VendorLifecycleStage {
310    /// Initial onboarding phase
311    Onboarding {
312        started: NaiveDate,
313        expected_completion: NaiveDate,
314    },
315    /// Ramp-up period (increasing volume)
316    RampUp {
317        started: NaiveDate,
318        target_volume_percent: u8,
319    },
320    /// Steady state operations
321    SteadyState { since: NaiveDate },
322    /// Declining relationship
323    Decline {
324        started: NaiveDate,
325        reason: DeclineReason,
326    },
327    /// Terminated relationship
328    Terminated {
329        date: NaiveDate,
330        reason: TerminationReason,
331    },
332}
333
334impl VendorLifecycleStage {
335    /// Check if the vendor is active.
336    pub fn is_active(&self) -> bool {
337        !matches!(self, Self::Terminated { .. })
338    }
339
340    /// Check if the vendor is in good standing.
341    pub fn is_good_standing(&self) -> bool {
342        matches!(
343            self,
344            Self::Onboarding { .. } | Self::RampUp { .. } | Self::SteadyState { .. }
345        )
346    }
347
348    /// Get the stage name.
349    pub fn stage_name(&self) -> &'static str {
350        match self {
351            Self::Onboarding { .. } => "onboarding",
352            Self::RampUp { .. } => "ramp_up",
353            Self::SteadyState { .. } => "steady_state",
354            Self::Decline { .. } => "decline",
355            Self::Terminated { .. } => "terminated",
356        }
357    }
358}
359
360impl Default for VendorLifecycleStage {
361    fn default() -> Self {
362        Self::SteadyState {
363            since: NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
364        }
365    }
366}
367
368/// Payment history summary for a vendor.
369#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct PaymentHistory {
371    /// Total number of invoices paid
372    pub total_invoices: u32,
373    /// Number of invoices paid on time
374    pub on_time_payments: u32,
375    /// Number of early payments (discount taken)
376    pub early_payments: u32,
377    /// Number of late payments
378    pub late_payments: u32,
379    /// Total payment amount
380    #[serde(with = "rust_decimal::serde::str")]
381    pub total_amount: Decimal,
382    /// Average days to payment
383    pub average_days_to_pay: f64,
384    /// Last payment date
385    pub last_payment_date: Option<NaiveDate>,
386    /// Total discounts captured
387    #[serde(with = "rust_decimal::serde::str")]
388    pub total_discounts: Decimal,
389}
390
391impl Default for PaymentHistory {
392    fn default() -> Self {
393        Self {
394            total_invoices: 0,
395            on_time_payments: 0,
396            early_payments: 0,
397            late_payments: 0,
398            total_amount: Decimal::ZERO,
399            average_days_to_pay: 30.0,
400            last_payment_date: None,
401            total_discounts: Decimal::ZERO,
402        }
403    }
404}
405
406impl PaymentHistory {
407    /// Calculate on-time payment rate.
408    pub fn on_time_rate(&self) -> f64 {
409        if self.total_invoices == 0 {
410            1.0
411        } else {
412            self.on_time_payments as f64 / self.total_invoices as f64
413        }
414    }
415
416    /// Calculate early payment rate.
417    pub fn early_payment_rate(&self) -> f64 {
418        if self.total_invoices == 0 {
419            0.0
420        } else {
421            self.early_payments as f64 / self.total_invoices as f64
422        }
423    }
424
425    /// Record a payment.
426    pub fn record_payment(
427        &mut self,
428        amount: Decimal,
429        payment_date: NaiveDate,
430        due_date: NaiveDate,
431        discount_taken: Decimal,
432    ) {
433        self.total_invoices += 1;
434        self.total_amount += amount;
435        self.last_payment_date = Some(payment_date);
436
437        if discount_taken > Decimal::ZERO {
438            self.early_payments += 1;
439            self.total_discounts += discount_taken;
440        } else if payment_date <= due_date {
441            self.on_time_payments += 1;
442        } else {
443            self.late_payments += 1;
444        }
445
446        // Update running average days to pay
447        let days = (payment_date - due_date).num_days() as f64;
448        let n = self.total_invoices as f64;
449        self.average_days_to_pay = ((self.average_days_to_pay * (n - 1.0)) + days) / n;
450    }
451}
452
453/// Vendor quality score based on multiple dimensions.
454#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct VendorQualityScore {
456    /// Delivery performance (0.0 - 1.0)
457    pub delivery_score: f64,
458    /// Quality of goods/services (0.0 - 1.0)
459    pub quality_score: f64,
460    /// Invoice accuracy (0.0 - 1.0)
461    pub invoice_accuracy_score: f64,
462    /// Responsiveness (0.0 - 1.0)
463    pub responsiveness_score: f64,
464    /// Last evaluation date
465    pub last_evaluation: NaiveDate,
466    /// Number of evaluations
467    pub evaluation_count: u32,
468}
469
470impl Default for VendorQualityScore {
471    fn default() -> Self {
472        Self {
473            delivery_score: 0.9,
474            quality_score: 0.9,
475            invoice_accuracy_score: 0.95,
476            responsiveness_score: 0.85,
477            last_evaluation: NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
478            evaluation_count: 0,
479        }
480    }
481}
482
483impl VendorQualityScore {
484    /// Calculate overall quality score (weighted average).
485    pub fn overall_score(&self) -> f64 {
486        const DELIVERY_WEIGHT: f64 = 0.30;
487        const QUALITY_WEIGHT: f64 = 0.35;
488        const INVOICE_WEIGHT: f64 = 0.20;
489        const RESPONSIVENESS_WEIGHT: f64 = 0.15;
490
491        self.delivery_score * DELIVERY_WEIGHT
492            + self.quality_score * QUALITY_WEIGHT
493            + self.invoice_accuracy_score * INVOICE_WEIGHT
494            + self.responsiveness_score * RESPONSIVENESS_WEIGHT
495    }
496
497    /// Get the quality rating grade.
498    pub fn grade(&self) -> &'static str {
499        let score = self.overall_score();
500        if score >= 0.95 {
501            "A+"
502        } else if score >= 0.90 {
503            "A"
504        } else if score >= 0.85 {
505            "B+"
506        } else if score >= 0.80 {
507            "B"
508        } else if score >= 0.70 {
509            "C"
510        } else if score >= 0.60 {
511            "D"
512        } else {
513            "F"
514        }
515    }
516
517    /// Update scores from an evaluation.
518    pub fn update(
519        &mut self,
520        delivery: f64,
521        quality: f64,
522        invoice_accuracy: f64,
523        responsiveness: f64,
524        eval_date: NaiveDate,
525    ) {
526        // Exponential moving average with alpha = 0.3
527        const ALPHA: f64 = 0.3;
528
529        self.delivery_score = ALPHA * delivery + (1.0 - ALPHA) * self.delivery_score;
530        self.quality_score = ALPHA * quality + (1.0 - ALPHA) * self.quality_score;
531        self.invoice_accuracy_score =
532            ALPHA * invoice_accuracy + (1.0 - ALPHA) * self.invoice_accuracy_score;
533        self.responsiveness_score =
534            ALPHA * responsiveness + (1.0 - ALPHA) * self.responsiveness_score;
535        self.last_evaluation = eval_date;
536        self.evaluation_count += 1;
537    }
538}
539
540/// Substitutability classification for single-source analysis.
541#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
542#[serde(rename_all = "snake_case")]
543pub enum Substitutability {
544    /// Easily replaceable (~60% of vendors)
545    #[default]
546    Easy,
547    /// Moderate effort to replace (~30% of vendors)
548    Moderate,
549    /// Difficult to replace (~10% of vendors)
550    Difficult,
551}
552
553impl Substitutability {
554    /// Get the typical distribution percentage.
555    pub fn typical_distribution(&self) -> f64 {
556        match self {
557            Self::Easy => 0.60,
558            Self::Moderate => 0.30,
559            Self::Difficult => 0.10,
560        }
561    }
562
563    /// Get the estimated replacement time in months.
564    pub fn replacement_time_months(&self) -> u8 {
565        match self {
566            Self::Easy => 1,
567            Self::Moderate => 3,
568            Self::Difficult => 6,
569        }
570    }
571
572    /// Get the risk factor for concentration analysis.
573    pub fn risk_factor(&self) -> f64 {
574        match self {
575            Self::Easy => 1.0,
576            Self::Moderate => 1.5,
577            Self::Difficult => 2.5,
578        }
579    }
580}
581
582/// Vendor dependency tracking for concentration analysis.
583#[derive(Debug, Clone, Serialize, Deserialize)]
584pub struct VendorDependency {
585    /// Vendor ID
586    pub vendor_id: String,
587    /// Is this a single-source vendor?
588    pub is_single_source: bool,
589    /// How easily can this vendor be replaced?
590    pub substitutability: Substitutability,
591    /// Concentration percentage (spend with this vendor / total category spend)
592    pub concentration_percent: f64,
593    /// Category of spend
594    pub spend_category: String,
595    /// Alternative vendors if available
596    pub alternatives: Vec<String>,
597    /// Last review date
598    pub last_review_date: Option<NaiveDate>,
599}
600
601impl VendorDependency {
602    /// Create a new vendor dependency record.
603    pub fn new(vendor_id: impl Into<String>, spend_category: impl Into<String>) -> Self {
604        Self {
605            vendor_id: vendor_id.into(),
606            is_single_source: false,
607            substitutability: Substitutability::default(),
608            concentration_percent: 0.0,
609            spend_category: spend_category.into(),
610            alternatives: Vec::new(),
611            last_review_date: None,
612        }
613    }
614
615    /// Calculate dependency risk score.
616    pub fn risk_score(&self) -> f64 {
617        let single_source_factor = if self.is_single_source { 2.0 } else { 1.0 };
618        let concentration_factor = self.concentration_percent;
619        let substitutability_factor = self.substitutability.risk_factor();
620
621        // Composite risk score (0.0 to ~5.0)
622        single_source_factor * concentration_factor * substitutability_factor
623    }
624
625    /// Check if this represents high risk.
626    pub fn is_high_risk(&self) -> bool {
627        self.is_single_source
628            && matches!(self.substitutability, Substitutability::Difficult)
629            && self.concentration_percent > 0.15
630    }
631}
632
633/// Vendor relationship in the supply chain network.
634#[derive(Debug, Clone, Serialize, Deserialize)]
635pub struct VendorRelationship {
636    /// Vendor ID
637    pub vendor_id: String,
638    /// Type of relationship
639    pub relationship_type: VendorRelationshipType,
640    /// Supply chain tier
641    pub tier: SupplyChainTier,
642    /// Strategic importance level
643    pub strategic_importance: StrategicLevel,
644    /// Spend tier classification
645    pub spend_tier: SpendTier,
646    /// Behavioral cluster
647    pub cluster: VendorCluster,
648    /// Relationship start date
649    pub start_date: NaiveDate,
650    /// Relationship end date (if terminated)
651    pub end_date: Option<NaiveDate>,
652    /// Current lifecycle stage
653    pub lifecycle_stage: VendorLifecycleStage,
654    /// Payment history summary
655    pub payment_history: PaymentHistory,
656    /// Quality score
657    pub quality_score: VendorQualityScore,
658    /// Parent vendor ID (for Tier 2/3)
659    pub parent_vendor: Option<String>,
660    /// Child vendor IDs (suppliers to this vendor)
661    pub child_vendors: Vec<String>,
662    /// Dependency analysis
663    pub dependency: Option<VendorDependency>,
664    /// Annual spend amount
665    #[serde(with = "rust_decimal::serde::str")]
666    pub annual_spend: Decimal,
667    /// Contract reference
668    pub contract_id: Option<String>,
669    /// Primary contact
670    pub primary_contact: Option<String>,
671    /// Notes
672    pub notes: Option<String>,
673}
674
675impl VendorRelationship {
676    /// Create a new vendor relationship.
677    pub fn new(
678        vendor_id: impl Into<String>,
679        relationship_type: VendorRelationshipType,
680        tier: SupplyChainTier,
681        start_date: NaiveDate,
682    ) -> Self {
683        Self {
684            vendor_id: vendor_id.into(),
685            relationship_type,
686            tier,
687            strategic_importance: StrategicLevel::default(),
688            spend_tier: SpendTier::default(),
689            cluster: VendorCluster::default(),
690            start_date,
691            end_date: None,
692            lifecycle_stage: VendorLifecycleStage::Onboarding {
693                started: start_date,
694                expected_completion: start_date + chrono::Duration::days(90),
695            },
696            payment_history: PaymentHistory::default(),
697            quality_score: VendorQualityScore::default(),
698            parent_vendor: None,
699            child_vendors: Vec::new(),
700            dependency: None,
701            annual_spend: Decimal::ZERO,
702            contract_id: None,
703            primary_contact: None,
704            notes: None,
705        }
706    }
707
708    /// Set strategic importance.
709    pub fn with_strategic_importance(mut self, level: StrategicLevel) -> Self {
710        self.strategic_importance = level;
711        self
712    }
713
714    /// Set spend tier.
715    pub fn with_spend_tier(mut self, tier: SpendTier) -> Self {
716        self.spend_tier = tier;
717        self
718    }
719
720    /// Set cluster.
721    pub fn with_cluster(mut self, cluster: VendorCluster) -> Self {
722        self.cluster = cluster;
723        self
724    }
725
726    /// Set parent vendor (for Tier 2/3).
727    pub fn with_parent(mut self, parent_id: impl Into<String>) -> Self {
728        self.parent_vendor = Some(parent_id.into());
729        self
730    }
731
732    /// Add a child vendor.
733    pub fn add_child(&mut self, child_id: impl Into<String>) {
734        self.child_vendors.push(child_id.into());
735    }
736
737    /// Set annual spend.
738    pub fn with_annual_spend(mut self, spend: Decimal) -> Self {
739        self.annual_spend = spend;
740        self
741    }
742
743    /// Check if relationship is active.
744    pub fn is_active(&self) -> bool {
745        self.end_date.is_none() && self.lifecycle_stage.is_active()
746    }
747
748    /// Calculate relationship age in days.
749    pub fn relationship_age_days(&self, as_of: NaiveDate) -> i64 {
750        (as_of - self.start_date).num_days()
751    }
752
753    /// Get composite relationship score.
754    pub fn relationship_score(&self) -> f64 {
755        let quality = self.quality_score.overall_score();
756        let payment = self.payment_history.on_time_rate();
757        let strategic = self.strategic_importance.importance_score();
758        let cluster_bonus = match self.cluster {
759            VendorCluster::ReliableStrategic => 0.1,
760            VendorCluster::StandardOperational => 0.0,
761            VendorCluster::Transactional => -0.05,
762            VendorCluster::Problematic => -0.15,
763        };
764
765        // Weighted composite score
766        (quality * 0.4 + payment * 0.3 + strategic * 0.3 + cluster_bonus).clamp(0.0, 1.0)
767    }
768}
769
770/// Multi-tier vendor network for a company.
771#[derive(Debug, Clone, Default, Serialize, Deserialize)]
772pub struct VendorNetwork {
773    /// Company code owning this network
774    pub company_code: String,
775    /// All vendor relationships
776    pub relationships: HashMap<String, VendorRelationship>,
777    /// Tier 1 vendor IDs
778    pub tier1_vendors: Vec<String>,
779    /// Tier 2 vendor IDs
780    pub tier2_vendors: Vec<String>,
781    /// Tier 3 vendor IDs
782    pub tier3_vendors: Vec<String>,
783    /// Network creation date
784    pub created_date: Option<NaiveDate>,
785    /// Network statistics
786    pub statistics: NetworkStatistics,
787}
788
789/// Statistics for the vendor network.
790#[derive(Debug, Clone, Default, Serialize, Deserialize)]
791pub struct NetworkStatistics {
792    /// Total vendor count
793    pub total_vendors: usize,
794    /// Active vendor count
795    pub active_vendors: usize,
796    /// Total annual spend
797    #[serde(with = "rust_decimal::serde::str")]
798    pub total_annual_spend: Decimal,
799    /// Average relationship age in days
800    pub avg_relationship_age_days: f64,
801    /// Concentration in top 5 vendors
802    pub top5_concentration: f64,
803    /// Single-source vendor count
804    pub single_source_count: usize,
805    /// Cluster distribution
806    pub cluster_distribution: HashMap<String, f64>,
807}
808
809impl VendorNetwork {
810    /// Create a new vendor network.
811    pub fn new(company_code: impl Into<String>) -> Self {
812        Self {
813            company_code: company_code.into(),
814            relationships: HashMap::new(),
815            tier1_vendors: Vec::new(),
816            tier2_vendors: Vec::new(),
817            tier3_vendors: Vec::new(),
818            created_date: None,
819            statistics: NetworkStatistics::default(),
820        }
821    }
822
823    /// Add a vendor relationship.
824    pub fn add_relationship(&mut self, relationship: VendorRelationship) {
825        let vendor_id = relationship.vendor_id.clone();
826        match relationship.tier {
827            SupplyChainTier::Tier1 => self.tier1_vendors.push(vendor_id.clone()),
828            SupplyChainTier::Tier2 => self.tier2_vendors.push(vendor_id.clone()),
829            SupplyChainTier::Tier3 => self.tier3_vendors.push(vendor_id.clone()),
830        }
831        self.relationships.insert(vendor_id, relationship);
832    }
833
834    /// Get a relationship by vendor ID.
835    pub fn get_relationship(&self, vendor_id: &str) -> Option<&VendorRelationship> {
836        self.relationships.get(vendor_id)
837    }
838
839    /// Get a mutable relationship by vendor ID.
840    pub fn get_relationship_mut(&mut self, vendor_id: &str) -> Option<&mut VendorRelationship> {
841        self.relationships.get_mut(vendor_id)
842    }
843
844    /// Get all vendors in a tier.
845    pub fn vendors_in_tier(&self, tier: SupplyChainTier) -> Vec<&VendorRelationship> {
846        let ids = match tier {
847            SupplyChainTier::Tier1 => &self.tier1_vendors,
848            SupplyChainTier::Tier2 => &self.tier2_vendors,
849            SupplyChainTier::Tier3 => &self.tier3_vendors,
850        };
851        ids.iter()
852            .filter_map(|id| self.relationships.get(id))
853            .collect()
854    }
855
856    /// Get child vendors (Tier N+1) of a given vendor.
857    pub fn get_children(&self, vendor_id: &str) -> Vec<&VendorRelationship> {
858        self.relationships
859            .get(vendor_id)
860            .map(|rel| {
861                rel.child_vendors
862                    .iter()
863                    .filter_map(|id| self.relationships.get(id))
864                    .collect()
865            })
866            .unwrap_or_default()
867    }
868
869    /// Get parent vendor (Tier N-1) of a given vendor.
870    pub fn get_parent(&self, vendor_id: &str) -> Option<&VendorRelationship> {
871        self.relationships
872            .get(vendor_id)
873            .and_then(|rel| rel.parent_vendor.as_ref())
874            .and_then(|parent_id| self.relationships.get(parent_id))
875    }
876
877    /// Calculate network statistics.
878    pub fn calculate_statistics(&mut self, as_of: NaiveDate) {
879        let active_count = self
880            .relationships
881            .values()
882            .filter(|r| r.is_active())
883            .count();
884
885        let total_spend: Decimal = self.relationships.values().map(|r| r.annual_spend).sum();
886
887        let avg_age = if self.relationships.is_empty() {
888            0.0
889        } else {
890            self.relationships
891                .values()
892                .map(|r| r.relationship_age_days(as_of) as f64)
893                .sum::<f64>()
894                / self.relationships.len() as f64
895        };
896
897        // Calculate top 5 concentration
898        let mut spends: Vec<Decimal> = self
899            .relationships
900            .values()
901            .map(|r| r.annual_spend)
902            .collect();
903        spends.sort_by(|a, b| b.cmp(a));
904        let top5_spend: Decimal = spends.iter().take(5).copied().sum();
905        let top5_conc = if total_spend > Decimal::ZERO {
906            (top5_spend / total_spend)
907                .to_string()
908                .parse::<f64>()
909                .unwrap_or(0.0)
910        } else {
911            0.0
912        };
913
914        // Count single-source vendors
915        let single_source = self
916            .relationships
917            .values()
918            .filter(|r| {
919                r.dependency
920                    .as_ref()
921                    .map(|d| d.is_single_source)
922                    .unwrap_or(false)
923            })
924            .count();
925
926        // Calculate cluster distribution
927        let mut cluster_counts: HashMap<String, usize> = HashMap::new();
928        for rel in self.relationships.values() {
929            *cluster_counts
930                .entry(format!("{:?}", rel.cluster))
931                .or_insert(0) += 1;
932        }
933        let cluster_distribution: HashMap<String, f64> = cluster_counts
934            .into_iter()
935            .map(|(k, v)| (k, v as f64 / self.relationships.len().max(1) as f64))
936            .collect();
937
938        self.statistics = NetworkStatistics {
939            total_vendors: self.relationships.len(),
940            active_vendors: active_count,
941            total_annual_spend: total_spend,
942            avg_relationship_age_days: avg_age,
943            top5_concentration: top5_conc,
944            single_source_count: single_source,
945            cluster_distribution,
946        };
947    }
948
949    /// Check concentration limits.
950    pub fn check_concentration_limits(&self, max_single_vendor: f64, max_top5: f64) -> Vec<String> {
951        let mut violations = Vec::new();
952
953        // Check individual vendor concentration
954        let total_spend: Decimal = self.relationships.values().map(|r| r.annual_spend).sum();
955        if total_spend > Decimal::ZERO {
956            for rel in self.relationships.values() {
957                let conc = (rel.annual_spend / total_spend)
958                    .to_string()
959                    .parse::<f64>()
960                    .unwrap_or(0.0);
961                if conc > max_single_vendor {
962                    violations.push(format!(
963                        "Vendor {} concentration {:.1}% exceeds limit {:.1}%",
964                        rel.vendor_id,
965                        conc * 100.0,
966                        max_single_vendor * 100.0
967                    ));
968                }
969            }
970        }
971
972        // Check top 5 concentration
973        if self.statistics.top5_concentration > max_top5 {
974            violations.push(format!(
975                "Top 5 vendor concentration {:.1}% exceeds limit {:.1}%",
976                self.statistics.top5_concentration * 100.0,
977                max_top5 * 100.0
978            ));
979        }
980
981        violations
982    }
983}
984
985#[cfg(test)]
986mod tests {
987    use super::*;
988
989    #[test]
990    fn test_supply_chain_tier() {
991        assert_eq!(SupplyChainTier::Tier1.tier_number(), 1);
992        assert_eq!(SupplyChainTier::Tier2.visibility(), 0.5);
993        assert_eq!(
994            SupplyChainTier::Tier1.child_tier(),
995            Some(SupplyChainTier::Tier2)
996        );
997        assert_eq!(SupplyChainTier::Tier3.child_tier(), None);
998    }
999
1000    #[test]
1001    fn test_vendor_cluster_distribution() {
1002        let total: f64 = [
1003            VendorCluster::ReliableStrategic,
1004            VendorCluster::StandardOperational,
1005            VendorCluster::Transactional,
1006            VendorCluster::Problematic,
1007        ]
1008        .iter()
1009        .map(|c| c.typical_distribution())
1010        .sum();
1011
1012        assert!((total - 1.0).abs() < 0.01);
1013    }
1014
1015    #[test]
1016    fn test_vendor_quality_score() {
1017        let mut score = VendorQualityScore::default();
1018        assert!(score.overall_score() > 0.8);
1019
1020        score.update(
1021            0.95,
1022            0.90,
1023            0.98,
1024            0.85,
1025            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1026        );
1027        assert_eq!(score.evaluation_count, 1);
1028        assert_eq!(score.grade(), "A");
1029    }
1030
1031    #[test]
1032    fn test_payment_history() {
1033        let mut history = PaymentHistory::default();
1034        history.record_payment(
1035            Decimal::from(1000),
1036            NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
1037            NaiveDate::from_ymd_opt(2024, 1, 20).unwrap(),
1038            Decimal::ZERO,
1039        );
1040
1041        assert_eq!(history.total_invoices, 1);
1042        assert_eq!(history.on_time_payments, 1);
1043        assert!((history.on_time_rate() - 1.0).abs() < 0.001);
1044    }
1045
1046    #[test]
1047    fn test_vendor_relationship() {
1048        let rel = VendorRelationship::new(
1049            "V-001",
1050            VendorRelationshipType::DirectSupplier,
1051            SupplyChainTier::Tier1,
1052            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1053        )
1054        .with_strategic_importance(StrategicLevel::Critical)
1055        .with_spend_tier(SpendTier::Platinum)
1056        .with_cluster(VendorCluster::ReliableStrategic)
1057        .with_annual_spend(Decimal::from(1000000));
1058
1059        assert!(rel.is_active());
1060        assert_eq!(rel.strategic_importance, StrategicLevel::Critical);
1061        assert!(rel.relationship_score() > 0.5);
1062    }
1063
1064    #[test]
1065    fn test_vendor_network() {
1066        let mut network = VendorNetwork::new("1000");
1067
1068        let rel1 = VendorRelationship::new(
1069            "V-001",
1070            VendorRelationshipType::DirectSupplier,
1071            SupplyChainTier::Tier1,
1072            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1073        )
1074        .with_annual_spend(Decimal::from(500000));
1075
1076        let rel2 = VendorRelationship::new(
1077            "V-002",
1078            VendorRelationshipType::RawMaterialSupplier,
1079            SupplyChainTier::Tier2,
1080            NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1081        )
1082        .with_parent("V-001")
1083        .with_annual_spend(Decimal::from(200000));
1084
1085        network.add_relationship(rel1);
1086        network.add_relationship(rel2);
1087
1088        assert_eq!(network.tier1_vendors.len(), 1);
1089        assert_eq!(network.tier2_vendors.len(), 1);
1090        assert!(network.get_relationship("V-001").is_some());
1091
1092        network.calculate_statistics(NaiveDate::from_ymd_opt(2024, 6, 1).unwrap());
1093        assert_eq!(network.statistics.total_vendors, 2);
1094        assert_eq!(network.statistics.active_vendors, 2);
1095    }
1096
1097    #[test]
1098    fn test_vendor_dependency() {
1099        let mut dep = VendorDependency::new("V-001", "Raw Materials");
1100        dep.is_single_source = true;
1101        dep.substitutability = Substitutability::Difficult;
1102        dep.concentration_percent = 0.25; // 25% concentration
1103
1104        assert!(dep.is_high_risk());
1105        // Risk score = 2.0 (single_source) * 0.25 (concentration) * 2.5 (difficult) = 1.25
1106        assert!(dep.risk_score() > 1.0);
1107    }
1108
1109    #[test]
1110    fn test_lifecycle_stage() {
1111        let stage = VendorLifecycleStage::SteadyState {
1112            since: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1113        };
1114        assert!(stage.is_active());
1115        assert!(stage.is_good_standing());
1116
1117        let terminated = VendorLifecycleStage::Terminated {
1118            date: NaiveDate::from_ymd_opt(2024, 6, 1).unwrap(),
1119            reason: TerminationReason::ContractExpired,
1120        };
1121        assert!(!terminated.is_active());
1122    }
1123}