Skip to main content

datasynth_core/models/sourcing/
contract.rs

1//! Procurement contract models.
2
3use chrono::NaiveDate;
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8use super::super::graph_properties::{GraphPropertyValue, ToNodeProperties};
9
10/// Type of procurement contract.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
12#[serde(rename_all = "snake_case")]
13pub enum ContractType {
14    /// Fixed price contract
15    #[default]
16    FixedPrice,
17    /// Blanket/framework agreement with quantity commitments
18    Blanket,
19    /// Time and materials contract
20    TimeAndMaterials,
21    /// Cost-plus contract
22    CostPlus,
23    /// Service level agreement
24    ServiceAgreement,
25}
26
27/// Status of a procurement contract.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
29#[serde(rename_all = "snake_case")]
30pub enum ContractStatus {
31    /// Contract drafted
32    #[default]
33    Draft,
34    /// Pending approval
35    PendingApproval,
36    /// Active and in force
37    Active,
38    /// Suspended (temporarily inactive)
39    Suspended,
40    /// Expired
41    Expired,
42    /// Terminated early
43    Terminated,
44    /// Renewed (new contract created)
45    Renewed,
46}
47
48/// Contract terms and conditions.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ContractTerms {
51    /// Payment terms (e.g., "NET30", "2/10 NET30")
52    pub payment_terms: String,
53    /// Delivery terms (incoterms)
54    pub delivery_terms: Option<String>,
55    /// Warranty period in months
56    pub warranty_months: Option<u32>,
57    /// Early termination penalty percentage
58    pub early_termination_penalty_pct: Option<f64>,
59    /// Auto-renewal enabled
60    pub auto_renewal: bool,
61    /// Notice period for termination (days)
62    pub termination_notice_days: u32,
63    /// Price adjustment clause enabled
64    pub price_adjustment_clause: bool,
65    /// Maximum annual price increase percentage
66    pub max_annual_price_increase_pct: Option<f64>,
67}
68
69impl Default for ContractTerms {
70    fn default() -> Self {
71        Self {
72            payment_terms: "NET30".to_string(),
73            delivery_terms: None,
74            warranty_months: None,
75            early_termination_penalty_pct: None,
76            auto_renewal: false,
77            termination_notice_days: 90,
78            price_adjustment_clause: false,
79            max_annual_price_increase_pct: None,
80        }
81    }
82}
83
84/// Service level agreement within a contract.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct ContractSla {
87    /// SLA metric name (e.g., "on_time_delivery", "defect_rate")
88    pub metric_name: String,
89    /// Target value
90    pub target_value: f64,
91    /// Minimum acceptable value
92    pub minimum_value: f64,
93    /// Penalty for breach (percentage of contract value)
94    pub breach_penalty_pct: f64,
95    /// Measurement frequency (monthly, quarterly, etc.)
96    pub measurement_frequency: String,
97}
98
99/// Line item within a contract.
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct ContractLineItem {
102    /// Line number
103    pub line_number: u16,
104    /// Material/service ID
105    pub material_id: Option<String>,
106    /// Description
107    pub description: String,
108    /// Contracted unit price
109    #[serde(with = "rust_decimal::serde::str")]
110    pub unit_price: Decimal,
111    /// Unit of measure
112    pub uom: String,
113    /// Minimum order quantity
114    pub min_quantity: Option<Decimal>,
115    /// Maximum/committed quantity
116    pub max_quantity: Option<Decimal>,
117    /// Quantity released (ordered) so far
118    #[serde(default)]
119    pub quantity_released: Decimal,
120    /// Value released so far
121    #[serde(default, with = "rust_decimal::serde::str")]
122    pub value_released: Decimal,
123}
124
125/// A procurement contract.
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct ProcurementContract {
128    /// Unique contract identifier
129    pub contract_id: String,
130    /// Company code
131    pub company_code: String,
132    /// Contract type
133    pub contract_type: ContractType,
134    /// Current status
135    pub status: ContractStatus,
136    /// Vendor ID
137    pub vendor_id: String,
138    /// Contract title
139    pub title: String,
140    /// Sourcing project ID (origin)
141    pub sourcing_project_id: Option<String>,
142    /// Winning bid ID (origin)
143    pub bid_id: Option<String>,
144    /// Start date
145    pub start_date: NaiveDate,
146    /// End date
147    pub end_date: NaiveDate,
148    /// Total contract value
149    #[serde(with = "rust_decimal::serde::str")]
150    pub total_value: Decimal,
151    /// Value consumed so far
152    #[serde(with = "rust_decimal::serde::str")]
153    pub consumed_value: Decimal,
154    /// Contract terms
155    pub terms: ContractTerms,
156    /// SLAs
157    pub slas: Vec<ContractSla>,
158    /// Line items
159    pub line_items: Vec<ContractLineItem>,
160    /// Spend category
161    pub category_id: String,
162    /// Contract owner
163    pub owner_id: String,
164    /// Amendment count
165    pub amendment_count: u32,
166    /// Previous contract ID (if renewal)
167    pub previous_contract_id: Option<String>,
168}
169
170impl ToNodeProperties for ProcurementContract {
171    fn node_type_name(&self) -> &'static str {
172        "procurement_contract"
173    }
174    fn node_type_code(&self) -> u16 {
175        324
176    }
177    fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
178        let mut p = HashMap::new();
179        p.insert(
180            "contractId".into(),
181            GraphPropertyValue::String(self.contract_id.clone()),
182        );
183        p.insert(
184            "entityCode".into(),
185            GraphPropertyValue::String(self.company_code.clone()),
186        );
187        p.insert(
188            "contractType".into(),
189            GraphPropertyValue::String(format!("{:?}", self.contract_type)),
190        );
191        p.insert(
192            "status".into(),
193            GraphPropertyValue::String(format!("{:?}", self.status)),
194        );
195        p.insert(
196            "vendorId".into(),
197            GraphPropertyValue::String(self.vendor_id.clone()),
198        );
199        p.insert(
200            "title".into(),
201            GraphPropertyValue::String(self.title.clone()),
202        );
203        p.insert(
204            "startDate".into(),
205            GraphPropertyValue::Date(self.start_date),
206        );
207        p.insert("endDate".into(), GraphPropertyValue::Date(self.end_date));
208        p.insert(
209            "totalValue".into(),
210            GraphPropertyValue::Decimal(self.total_value),
211        );
212        p.insert(
213            "consumedValue".into(),
214            GraphPropertyValue::Decimal(self.consumed_value),
215        );
216        p.insert(
217            "lineItemCount".into(),
218            GraphPropertyValue::Int(self.line_items.len() as i64),
219        );
220        p.insert(
221            "amendmentCount".into(),
222            GraphPropertyValue::Int(self.amendment_count as i64),
223        );
224        p.insert(
225            "isActive".into(),
226            GraphPropertyValue::Bool(matches!(self.status, ContractStatus::Active)),
227        );
228        p
229    }
230}