Skip to main content

bo4e_core/com/
offer_position.rs

1//! Offer position (Angebotsposition) component.
2
3use serde::{Deserialize, Serialize};
4
5use crate::traits::{Bo4eMeta, Bo4eObject};
6
7/// Position within an offer part.
8///
9/// Offers the individual components being offered. Example:
10/// - Position quantity: 4000 kWh
11/// - Position price: 24.56 ct/kWh
12/// - Position cost: 982.40 EUR
13///
14/// German: Angebotsposition
15///
16/// # Example
17///
18/// ```rust
19/// use bo4e_core::com::OfferPosition;
20///
21/// let position = OfferPosition {
22///     position_description: Some("Electricity supply".to_string()),
23///     ..Default::default()
24/// };
25/// ```
26#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
27#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
28#[cfg_attr(feature = "json-schema", schemars(rename = "Angebotsposition"))]
29#[serde(rename_all = "camelCase")]
30pub struct OfferPosition {
31    /// BO4E metadata
32    #[serde(flatten)]
33    pub meta: Bo4eMeta,
34
35    /// Description of the offer position (Positionsbezeichnung)
36    #[serde(skip_serializing_if = "Option::is_none")]
37    #[cfg_attr(feature = "json-schema", schemars(rename = "positionsbezeichnung"))]
38    pub position_description: Option<String>,
39
40    // Note: The following fields would typically reference other COM types
41    // (Preis, Menge, Betrag) which will be added in a later epic.
42    // For now, we use simplified representations.
43    /// Position price value (simplified - Positionspreis)
44    #[serde(skip_serializing_if = "Option::is_none")]
45    #[cfg_attr(feature = "json-schema", schemars(rename = "positionspreis"))]
46    pub position_price_value: Option<f64>,
47
48    /// Position quantity value (simplified - Positionsmenge)
49    #[serde(skip_serializing_if = "Option::is_none")]
50    #[cfg_attr(feature = "json-schema", schemars(rename = "positionsmenge"))]
51    pub position_quantity_value: Option<f64>,
52
53    /// Position cost value (simplified - Positionskosten)
54    #[serde(skip_serializing_if = "Option::is_none")]
55    #[cfg_attr(feature = "json-schema", schemars(rename = "positionskosten"))]
56    pub position_cost_value: Option<f64>,
57}
58
59impl Bo4eObject for OfferPosition {
60    fn type_name_german() -> &'static str {
61        "Angebotsposition"
62    }
63
64    fn type_name_english() -> &'static str {
65        "OfferPosition"
66    }
67
68    fn meta(&self) -> &Bo4eMeta {
69        &self.meta
70    }
71
72    fn meta_mut(&mut self) -> &mut Bo4eMeta {
73        &mut self.meta
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn test_offer_position_default() {
83        let pos = OfferPosition::default();
84        assert!(pos.position_description.is_none());
85        assert!(pos.position_price_value.is_none());
86    }
87
88    #[test]
89    fn test_offer_position_serialize() {
90        let pos = OfferPosition {
91            position_description: Some("Electricity delivery".to_string()),
92            position_price_value: Some(24.56),
93            position_quantity_value: Some(4000.0),
94            position_cost_value: Some(982.40),
95            ..Default::default()
96        };
97
98        let json = serde_json::to_string(&pos).unwrap();
99        assert!(json.contains(r#""positionDescription":"Electricity delivery""#));
100        assert!(json.contains("24.56"));
101        assert!(json.contains("4000"));
102    }
103
104    #[test]
105    fn test_offer_position_roundtrip() {
106        let pos = OfferPosition {
107            meta: Bo4eMeta::with_type("Angebotsposition"),
108            position_description: Some("Gas supply".to_string()),
109            position_price_value: Some(0.08),
110            position_quantity_value: Some(10000.0),
111            position_cost_value: Some(800.0),
112        };
113
114        let json = serde_json::to_string(&pos).unwrap();
115        let parsed: OfferPosition = serde_json::from_str(&json).unwrap();
116        assert_eq!(pos, parsed);
117    }
118
119    #[test]
120    fn test_bo4e_object_impl() {
121        assert_eq!(OfferPosition::type_name_german(), "Angebotsposition");
122        assert_eq!(OfferPosition::type_name_english(), "OfferPosition");
123    }
124}