Skip to main content

datasynth_core/models/sourcing/
rfx.rs

1//! RFx (Request for Information/Proposal/Quotation) 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 RFx event.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
12#[serde(rename_all = "snake_case")]
13pub enum RfxType {
14    /// Request for Information (discovery)
15    Rfi,
16    /// Request for Proposal (complex requirements)
17    #[default]
18    Rfp,
19    /// Request for Quotation (price-focused)
20    Rfq,
21}
22
23/// Status of an RFx event.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
25#[serde(rename_all = "snake_case")]
26pub enum RfxStatus {
27    /// RFx created, not yet published
28    #[default]
29    Draft,
30    /// Published and open for responses
31    Published,
32    /// Response period ended, under evaluation
33    Closed,
34    /// Evaluation complete, winner selected
35    Awarded,
36    /// RFx cancelled
37    Cancelled,
38    /// No suitable bids received
39    NoAward,
40}
41
42/// Scoring method for bid evaluation.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
44#[serde(rename_all = "snake_case")]
45pub enum ScoringMethod {
46    /// Lowest price wins
47    LowestPrice,
48    /// Best value (weighted price + quality)
49    #[default]
50    BestValue,
51    /// Quality-based selection (price secondary)
52    QualityBased,
53}
54
55/// Evaluation criterion for an RFx.
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct RfxEvaluationCriterion {
58    /// Criterion name
59    pub name: String,
60    /// Weight (0.0 to 1.0, all criteria sum to 1.0)
61    pub weight: f64,
62    /// Description of what is being evaluated
63    pub description: String,
64}
65
66/// Line item within an RFx.
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct RfxLineItem {
69    /// Item number
70    pub item_number: u16,
71    /// Material/service description
72    pub description: String,
73    /// Material ID (if applicable)
74    pub material_id: Option<String>,
75    /// Required quantity
76    pub quantity: Decimal,
77    /// Unit of measure
78    pub uom: String,
79    /// Target unit price (budget)
80    pub target_price: Option<Decimal>,
81}
82
83/// An RFx (Request for x) event.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct RfxEvent {
86    /// Unique RFx identifier
87    pub rfx_id: String,
88    /// RFx type (RFI/RFP/RFQ)
89    pub rfx_type: RfxType,
90    /// Company code
91    pub company_code: String,
92    /// Title
93    pub title: String,
94    /// Description
95    pub description: String,
96    /// Current status
97    pub status: RfxStatus,
98    /// Sourcing project ID
99    pub sourcing_project_id: String,
100    /// Spend category
101    pub category_id: String,
102    /// Scoring method
103    pub scoring_method: ScoringMethod,
104    /// Evaluation criteria
105    pub criteria: Vec<RfxEvaluationCriterion>,
106    /// Line items
107    pub line_items: Vec<RfxLineItem>,
108    /// Invited vendor IDs
109    pub invited_vendors: Vec<String>,
110    /// Publication date
111    pub publish_date: NaiveDate,
112    /// Response deadline
113    pub response_deadline: NaiveDate,
114    /// Number of bids received
115    pub bid_count: u32,
116    /// Owner (sourcing manager)
117    pub owner_id: String,
118    /// Awarded vendor ID
119    pub awarded_vendor_id: Option<String>,
120    /// Awarded bid ID
121    pub awarded_bid_id: Option<String>,
122}
123
124impl ToNodeProperties for RfxEvent {
125    fn node_type_name(&self) -> &'static str {
126        "rfx_event"
127    }
128    fn node_type_code(&self) -> u16 {
129        321
130    }
131    fn to_node_properties(&self) -> HashMap<String, GraphPropertyValue> {
132        let mut p = HashMap::new();
133        p.insert(
134            "rfxId".into(),
135            GraphPropertyValue::String(self.rfx_id.clone()),
136        );
137        p.insert(
138            "rfxType".into(),
139            GraphPropertyValue::String(format!("{:?}", self.rfx_type)),
140        );
141        p.insert(
142            "entityCode".into(),
143            GraphPropertyValue::String(self.company_code.clone()),
144        );
145        p.insert(
146            "title".into(),
147            GraphPropertyValue::String(self.title.clone()),
148        );
149        p.insert(
150            "status".into(),
151            GraphPropertyValue::String(format!("{:?}", self.status)),
152        );
153        p.insert(
154            "scoringMethod".into(),
155            GraphPropertyValue::String(format!("{:?}", self.scoring_method)),
156        );
157        p.insert(
158            "publishDate".into(),
159            GraphPropertyValue::Date(self.publish_date),
160        );
161        p.insert(
162            "responseDeadline".into(),
163            GraphPropertyValue::Date(self.response_deadline),
164        );
165        p.insert(
166            "bidCount".into(),
167            GraphPropertyValue::Int(self.bid_count as i64),
168        );
169        p.insert(
170            "invitedVendorCount".into(),
171            GraphPropertyValue::Int(self.invited_vendors.len() as i64),
172        );
173        p.insert(
174            "isAwarded".into(),
175            GraphPropertyValue::Bool(matches!(self.status, RfxStatus::Awarded)),
176        );
177        p
178    }
179}