Skip to main content

bo4e_core/bo/
tender.rs

1//! Tender (Ausschreibung) business object.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::com::TimePeriod;
7use crate::enums::{Division, TenderStatus, TenderType};
8use crate::traits::{Bo4eMeta, Bo4eObject};
9
10/// A tender/RFP (Request for Proposal) for energy supply.
11///
12/// German: Ausschreibung
13///
14/// # Example
15///
16/// ```rust
17/// use bo4e_core::bo::Tender;
18/// use bo4e_core::enums::{Division, TenderStatus, TenderType};
19///
20/// let tender = Tender {
21///     tender_number: Some("T-2024-001".to_string()),
22///     tender_type: Some(TenderType::PublicLaw),
23///     status: Some(TenderStatus::Phase2),
24///     division: Some(Division::Electricity),
25///     ..Default::default()
26/// };
27/// ```
28#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
29#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
30#[cfg_attr(feature = "json-schema", schemars(rename = "Ausschreibung"))]
31#[serde(rename_all = "camelCase")]
32pub struct Tender {
33    /// BO4E metadata
34    #[serde(flatten)]
35    pub meta: Bo4eMeta,
36
37    /// Tender number (Ausschreibungsnummer)
38    #[serde(skip_serializing_if = "Option::is_none")]
39    #[cfg_attr(feature = "json-schema", schemars(rename = "ausschreibungsnummer"))]
40    pub tender_number: Option<String>,
41
42    /// Description (Beschreibung)
43    #[serde(skip_serializing_if = "Option::is_none")]
44    #[cfg_attr(feature = "json-schema", schemars(rename = "beschreibung"))]
45    pub description: Option<String>,
46
47    /// Type of tender (Ausschreibungstyp)
48    #[serde(skip_serializing_if = "Option::is_none")]
49    #[cfg_attr(feature = "json-schema", schemars(rename = "ausschreibungstyp"))]
50    pub tender_type: Option<TenderType>,
51
52    /// Status/phase of tender (Ausschreibungsstatus)
53    #[serde(skip_serializing_if = "Option::is_none")]
54    #[cfg_attr(feature = "json-schema", schemars(rename = "ausschreibungsstatus"))]
55    pub status: Option<TenderStatus>,
56
57    /// Energy division (Sparte)
58    #[serde(skip_serializing_if = "Option::is_none")]
59    #[cfg_attr(feature = "json-schema", schemars(rename = "sparte"))]
60    pub division: Option<Division>,
61
62    /// Publication date (Veroeffentlichungsdatum)
63    #[serde(skip_serializing_if = "Option::is_none")]
64    #[cfg_attr(feature = "json-schema", schemars(rename = "veroeffentlichungsdatum"))]
65    pub publication_date: Option<DateTime<Utc>>,
66
67    /// Submission deadline (Abgabefrist)
68    #[serde(skip_serializing_if = "Option::is_none")]
69    #[cfg_attr(feature = "json-schema", schemars(rename = "abgabefrist"))]
70    pub submission_deadline: Option<DateTime<Utc>>,
71
72    /// Delivery period (Lieferzeitraum)
73    #[serde(skip_serializing_if = "Option::is_none")]
74    #[cfg_attr(feature = "json-schema", schemars(rename = "lieferzeitraum"))]
75    pub delivery_period: Option<TimePeriod>,
76
77    /// Tendering party (Ausschreibender)
78    #[serde(skip_serializing_if = "Option::is_none")]
79    #[cfg_attr(feature = "json-schema", schemars(rename = "ausschreibender"))]
80    pub tendering_party: Option<Box<super::BusinessPartner>>,
81
82    /// Estimated annual consumption in kWh (Jahresverbrauch)
83    #[serde(skip_serializing_if = "Option::is_none")]
84    #[cfg_attr(feature = "json-schema", schemars(rename = "jahresverbrauch"))]
85    pub estimated_annual_consumption: Option<f64>,
86
87    /// Number of delivery points (Anzahl Lieferstellen)
88    #[serde(skip_serializing_if = "Option::is_none")]
89    #[cfg_attr(feature = "json-schema", schemars(rename = "anzahlLieferstellen"))]
90    pub number_of_delivery_points: Option<i32>,
91}
92
93impl Bo4eObject for Tender {
94    fn type_name_german() -> &'static str {
95        "Ausschreibung"
96    }
97
98    fn type_name_english() -> &'static str {
99        "Tender"
100    }
101
102    fn meta(&self) -> &Bo4eMeta {
103        &self.meta
104    }
105
106    fn meta_mut(&mut self) -> &mut Bo4eMeta {
107        &mut self.meta
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_tender_creation() {
117        let tender = Tender {
118            tender_number: Some("T-2024-001".to_string()),
119            tender_type: Some(TenderType::PublicLaw),
120            status: Some(TenderStatus::Phase2),
121            division: Some(Division::Electricity),
122            ..Default::default()
123        };
124
125        assert_eq!(tender.status, Some(TenderStatus::Phase2));
126    }
127
128    #[test]
129    fn test_europe_wide_tender() {
130        let tender = Tender {
131            tender_number: Some("T-EU-2024-001".to_string()),
132            tender_type: Some(TenderType::EuropeWide),
133            status: Some(TenderStatus::Phase1),
134            estimated_annual_consumption: Some(10_000_000.0),
135            number_of_delivery_points: Some(50),
136            ..Default::default()
137        };
138
139        assert_eq!(tender.tender_type, Some(TenderType::EuropeWide));
140        assert_eq!(tender.estimated_annual_consumption, Some(10_000_000.0));
141    }
142
143    #[test]
144    fn test_serialize() {
145        let tender = Tender {
146            meta: Bo4eMeta::with_type("Ausschreibung"),
147            tender_number: Some("T-123".to_string()),
148            tender_type: Some(TenderType::PublicLaw),
149            ..Default::default()
150        };
151
152        let json = serde_json::to_string(&tender).unwrap();
153        assert!(json.contains(r#""tenderNumber":"T-123""#));
154    }
155
156    #[test]
157    fn test_roundtrip() {
158        let tender = Tender {
159            meta: Bo4eMeta::with_type("Ausschreibung"),
160            tender_number: Some("T-123".to_string()),
161            description: Some("Test tender".to_string()),
162            tender_type: Some(TenderType::PublicLaw),
163            status: Some(TenderStatus::Phase2),
164            division: Some(Division::Electricity),
165            estimated_annual_consumption: Some(1_000_000.0),
166            number_of_delivery_points: Some(10),
167            ..Default::default()
168        };
169
170        let json = serde_json::to_string(&tender).unwrap();
171        let parsed: Tender = serde_json::from_str(&json).unwrap();
172        assert_eq!(tender, parsed);
173    }
174
175    #[test]
176    fn test_bo4e_object_impl() {
177        assert_eq!(Tender::type_name_german(), "Ausschreibung");
178        assert_eq!(Tender::type_name_english(), "Tender");
179    }
180}