bo4e_core/bo/
offer.rs

1//! Offer (Angebot) business object.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::com::{OfferVariant, TimePeriod};
7use crate::enums::{Division, OfferStatus};
8use crate::traits::{Bo4eMeta, Bo4eObject};
9
10/// An offer/quote for energy supply or services.
11///
12/// German: Angebot
13///
14/// # Example
15///
16/// ```rust
17/// use bo4e_core::bo::Offer;
18/// use bo4e_core::enums::{Division, OfferStatus};
19///
20/// let offer = Offer {
21///     offer_number: Some("A-2024-001".to_string()),
22///     status: Some(OfferStatus::Binding),
23///     division: Some(Division::Electricity),
24///     ..Default::default()
25/// };
26/// ```
27#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
28#[serde(rename_all = "camelCase")]
29pub struct Offer {
30    /// BO4E metadata
31    #[serde(flatten)]
32    pub meta: Bo4eMeta,
33
34    /// Offer number (Angebotsnummer)
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub offer_number: Option<String>,
37
38    /// Description (Beschreibung)
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub description: Option<String>,
41
42    /// Status of offer (Angebotsstatus)
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub status: Option<OfferStatus>,
45
46    /// Energy division (Sparte)
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub division: Option<Division>,
49
50    /// Date the offer was created (Angebotsdatum)
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub offer_date: Option<DateTime<Utc>>,
53
54    /// Date until which the offer is valid (Gueltig bis)
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub valid_until: Option<DateTime<Utc>>,
57
58    /// Delivery period (Lieferzeitraum)
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub delivery_period: Option<TimePeriod>,
61
62    /// Offer variants (Angebotsvarianten)
63    #[serde(default, skip_serializing_if = "Vec::is_empty")]
64    pub variants: Vec<OfferVariant>,
65
66    /// Reference to the bidder/supplier (Anbieter)
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub bidder: Option<Box<super::BusinessPartner>>,
69
70    /// Reference to the customer (Kunde)
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub customer: Option<Box<super::BusinessPartner>>,
73
74    /// Reference to associated tender (Ausschreibung)
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub tender_id: Option<String>,
77}
78
79impl Bo4eObject for Offer {
80    fn type_name_german() -> &'static str {
81        "Angebot"
82    }
83
84    fn type_name_english() -> &'static str {
85        "Offer"
86    }
87
88    fn meta(&self) -> &Bo4eMeta {
89        &self.meta
90    }
91
92    fn meta_mut(&mut self) -> &mut Bo4eMeta {
93        &mut self.meta
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn test_offer_creation() {
103        let offer = Offer {
104            offer_number: Some("A-2024-001".to_string()),
105            status: Some(OfferStatus::Binding),
106            division: Some(Division::Electricity),
107            ..Default::default()
108        };
109
110        assert_eq!(offer.status, Some(OfferStatus::Binding));
111    }
112
113    #[test]
114    fn test_offer_with_variants() {
115        let variant = OfferVariant {
116            offer_status: Some(OfferStatus::NonBinding),
117            parts_count: Some(3),
118            ..Default::default()
119        };
120
121        let offer = Offer {
122            offer_number: Some("A-2024-001".to_string()),
123            variants: vec![variant],
124            ..Default::default()
125        };
126
127        assert_eq!(offer.variants.len(), 1);
128    }
129
130    #[test]
131    fn test_serialize() {
132        let offer = Offer {
133            meta: Bo4eMeta::with_type("Angebot"),
134            offer_number: Some("A-123".to_string()),
135            status: Some(OfferStatus::Binding),
136            ..Default::default()
137        };
138
139        let json = serde_json::to_string(&offer).unwrap();
140        assert!(json.contains(r#""offerNumber":"A-123""#));
141    }
142
143    #[test]
144    fn test_roundtrip() {
145        let offer = Offer {
146            meta: Bo4eMeta::with_type("Angebot"),
147            offer_number: Some("A-123".to_string()),
148            description: Some("Test offer".to_string()),
149            status: Some(OfferStatus::Binding),
150            division: Some(Division::Electricity),
151            ..Default::default()
152        };
153
154        let json = serde_json::to_string(&offer).unwrap();
155        let parsed: Offer = serde_json::from_str(&json).unwrap();
156        assert_eq!(offer, parsed);
157    }
158
159    #[test]
160    fn test_bo4e_object_impl() {
161        assert_eq!(Offer::type_name_german(), "Angebot");
162        assert_eq!(Offer::type_name_english(), "Offer");
163    }
164}