Skip to main content

bo4e_core/com/
offer_part.rs

1//! Offer part (Angebotsteil) component.
2
3use serde::{Deserialize, Serialize};
4
5use crate::traits::{Bo4eMeta, Bo4eObject};
6
7/// Part of an offer variant.
8///
9/// Aggregates offer positions. Offer parts are typically created for a market location
10/// or delivery address. Contains the quantities and total costs of all offer positions.
11/// A variant consists of at least one offer part.
12///
13/// German: Angebotsteil
14///
15/// # Example
16///
17/// ```rust
18/// use bo4e_core::com::OfferPart;
19///
20/// let part = OfferPart {
21///     request_sub_reference: Some("Lot 1".to_string()),
22///     ..Default::default()
23/// };
24/// ```
25#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
26#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
27#[cfg_attr(feature = "json-schema", schemars(rename = "Angebotsteil"))]
28#[serde(rename_all = "camelCase")]
29pub struct OfferPart {
30    /// BO4E metadata
31    #[serde(flatten)]
32    pub meta: Bo4eMeta,
33
34    /// Sub-reference identifying a sub-chapter of a request, e.g., tender lot (AnfrageSubreferenz)
35    #[serde(skip_serializing_if = "Option::is_none")]
36    #[cfg_attr(feature = "json-schema", schemars(rename = "anfrageSubreferenz"))]
37    pub request_sub_reference: Option<String>,
38
39    // Note: The following fields would typically reference other COM types
40    // (Angebotsposition, Marktlokation, Menge, Betrag, Zeitraum) which will be added later.
41    // For now, we use simplified representations.
42    /// Number of positions in this offer part
43    #[serde(skip_serializing_if = "Option::is_none")]
44    #[cfg_attr(feature = "json-schema", schemars(rename = "anzahlPositionen"))]
45    pub position_count: Option<i32>,
46
47    /// Total quantity value for this offer part (simplified - Gesamtmengeangebotsteil)
48    #[serde(skip_serializing_if = "Option::is_none")]
49    #[cfg_attr(feature = "json-schema", schemars(rename = "gesamtmengeAngebotsteil"))]
50    pub total_quantity_value: Option<f64>,
51
52    /// Total cost value for this offer part (simplified - Gesamtkostenangebotsteil)
53    #[serde(skip_serializing_if = "Option::is_none")]
54    #[cfg_attr(feature = "json-schema", schemars(rename = "gesamtkostenAngebotsteil"))]
55    pub total_cost_value: Option<f64>,
56
57    /// Delivery period start (simplified - Lieferzeitraum)
58    #[serde(skip_serializing_if = "Option::is_none")]
59    #[cfg_attr(feature = "json-schema", schemars(rename = "lieferzeitraumBeginn"))]
60    pub delivery_period_start: Option<String>,
61
62    /// Delivery period end (simplified - Lieferzeitraum)
63    #[serde(skip_serializing_if = "Option::is_none")]
64    #[cfg_attr(feature = "json-schema", schemars(rename = "lieferzeitraumEnde"))]
65    pub delivery_period_end: Option<String>,
66}
67
68impl Bo4eObject for OfferPart {
69    fn type_name_german() -> &'static str {
70        "Angebotsteil"
71    }
72
73    fn type_name_english() -> &'static str {
74        "OfferPart"
75    }
76
77    fn meta(&self) -> &Bo4eMeta {
78        &self.meta
79    }
80
81    fn meta_mut(&mut self) -> &mut Bo4eMeta {
82        &mut self.meta
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn test_offer_part_default() {
92        let part = OfferPart::default();
93        assert!(part.request_sub_reference.is_none());
94        assert!(part.total_cost_value.is_none());
95    }
96
97    #[test]
98    fn test_offer_part_serialize() {
99        let part = OfferPart {
100            request_sub_reference: Some("LOT-001".to_string()),
101            position_count: Some(5),
102            total_quantity_value: Some(50000.0),
103            total_cost_value: Some(12500.0),
104            ..Default::default()
105        };
106
107        let json = serde_json::to_string(&part).unwrap();
108        assert!(json.contains(r#""requestSubReference":"LOT-001""#));
109        assert!(json.contains(r#""positionCount":5"#));
110    }
111
112    #[test]
113    fn test_offer_part_roundtrip() {
114        let part = OfferPart {
115            meta: Bo4eMeta::with_type("Angebotsteil"),
116            request_sub_reference: Some("LOT-002".to_string()),
117            position_count: Some(3),
118            total_quantity_value: Some(30000.0),
119            total_cost_value: Some(7500.0),
120            delivery_period_start: Some("2024-01-01T00:00:00+01:00".to_string()),
121            delivery_period_end: Some("2024-12-31T23:59:59+01:00".to_string()),
122        };
123
124        let json = serde_json::to_string(&part).unwrap();
125        let parsed: OfferPart = serde_json::from_str(&json).unwrap();
126        assert_eq!(part, parsed);
127    }
128
129    #[test]
130    fn test_bo4e_object_impl() {
131        assert_eq!(OfferPart::type_name_german(), "Angebotsteil");
132        assert_eq!(OfferPart::type_name_english(), "OfferPart");
133    }
134}