bo4e_core/bo/
market_location.rs

1//! Market location (Marktlokation) business object.
2//!
3//! Represents the point of energy delivery or receipt in the energy market.
4
5use serde::{Deserialize, Serialize};
6
7use crate::com::Address;
8use crate::enums::{CustomerType, Division, EnergyDirection};
9use crate::traits::{Bo4eMeta, Bo4eObject};
10
11/// A market location (MaLo) - the point of energy delivery/receipt.
12///
13/// German: Marktlokation
14///
15/// A market location is the central business object representing
16/// a point in the energy market where energy is delivered or received.
17/// It has an 11-digit identifier.
18///
19/// # Example
20///
21/// ```rust
22/// use bo4e_core::bo::MarketLocation;
23/// use bo4e_core::enums::{Division, EnergyDirection};
24///
25/// let malo = MarketLocation {
26///     market_location_id: Some("12345678901".to_string()),
27///     division: Some(Division::Electricity),
28///     energy_direction: Some(EnergyDirection::FeedOut),
29///     ..Default::default()
30/// };
31/// ```
32#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct MarketLocation {
35    /// BO4E metadata
36    #[serde(flatten)]
37    pub meta: Bo4eMeta,
38
39    /// Market location ID - 11 digits (Marktlokations-ID)
40    #[serde(skip_serializing_if = "Option::is_none", alias = "marktlokationsId")]
41    pub market_location_id: Option<String>,
42
43    /// Energy division (Sparte)
44    #[serde(skip_serializing_if = "Option::is_none", alias = "sparte")]
45    pub division: Option<Division>,
46
47    /// Energy direction (Energierichtung)
48    #[serde(skip_serializing_if = "Option::is_none", alias = "energierichtung")]
49    pub energy_direction: Option<EnergyDirection>,
50
51    /// Customer type (Kundentyp)
52    #[serde(skip_serializing_if = "Option::is_none", alias = "kundentyp")]
53    pub customer_type: Option<CustomerType>,
54
55    /// Location address (Adresse)
56    #[serde(skip_serializing_if = "Option::is_none", alias = "adresse")]
57    pub address: Option<Address>,
58
59    /// Supply start date (Lieferbeginn)
60    #[serde(skip_serializing_if = "Option::is_none", alias = "lieferbeginn")]
61    pub supply_start: Option<chrono::DateTime<chrono::Utc>>,
62
63    /// Supply end date (Lieferende)
64    #[serde(skip_serializing_if = "Option::is_none", alias = "lieferende")]
65    pub supply_end: Option<chrono::DateTime<chrono::Utc>>,
66
67    /// Annual consumption in kWh (Jahresverbrauchsprognose)
68    #[serde(
69        skip_serializing_if = "Option::is_none",
70        alias = "jahresverbrauchsprognose"
71    )]
72    pub annual_consumption: Option<f64>,
73
74    /// Network operator code (Netzbetreiber-Codenummer)
75    #[serde(
76        skip_serializing_if = "Option::is_none",
77        alias = "netzbetreiberCodenummer"
78    )]
79    pub network_operator_code: Option<String>,
80
81    /// Basic supplier code (Grundversorger-Codenummer)
82    #[serde(
83        skip_serializing_if = "Option::is_none",
84        alias = "grundversorgerCodenummer"
85    )]
86    pub basic_supplier_code: Option<String>,
87
88    /// Metering point operator code (Messstellenbetreiber-Codenummer)
89    #[serde(
90        skip_serializing_if = "Option::is_none",
91        alias = "messstellenbetreiberCodenummer"
92    )]
93    pub metering_operator_code: Option<String>,
94
95    /// Transmission system operator code (Übertragungsnetzbetreiber-Codenummer)
96    #[serde(
97        skip_serializing_if = "Option::is_none",
98        alias = "uebertragungsnetzbetreiberCodenummer"
99    )]
100    pub transmission_operator_code: Option<String>,
101
102    /// Grid connection level (Netzebene)
103    #[serde(skip_serializing_if = "Option::is_none", alias = "netzebene")]
104    pub grid_level: Option<String>,
105
106    /// Network area (Netzgebiet)
107    #[serde(skip_serializing_if = "Option::is_none", alias = "netzgebiet")]
108    pub network_area: Option<String>,
109
110    /// Billing balance area (Bilanzierungsgebiet)
111    #[serde(skip_serializing_if = "Option::is_none", alias = "bilanzierungsgebiet")]
112    pub balancing_area: Option<String>,
113
114    /// Associated metering location IDs
115    #[serde(
116        default,
117        skip_serializing_if = "Vec::is_empty",
118        alias = "messlokationsIds"
119    )]
120    pub metering_location_ids: Vec<String>,
121
122    /// Is Controllable Resource (Steuerbare Ressource)
123    #[serde(
124        skip_serializing_if = "Option::is_none",
125        alias = "istSteuerbareRessource"
126    )]
127    pub is_controllable_resource: Option<bool>,
128}
129
130impl Bo4eObject for MarketLocation {
131    fn type_name_german() -> &'static str {
132        "Marktlokation"
133    }
134
135    fn type_name_english() -> &'static str {
136        "MarketLocation"
137    }
138
139    fn meta(&self) -> &Bo4eMeta {
140        &self.meta
141    }
142
143    fn meta_mut(&mut self) -> &mut Bo4eMeta {
144        &mut self.meta
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_malo_id_format() {
154        // MaLo IDs are 11 digits
155        let malo = MarketLocation {
156            market_location_id: Some("12345678901".to_string()),
157            division: Some(Division::Electricity),
158            energy_direction: Some(EnergyDirection::FeedOut),
159            ..Default::default()
160        };
161
162        assert_eq!(malo.market_location_id.as_ref().unwrap().len(), 11);
163    }
164
165    #[test]
166    fn test_serialize() {
167        let malo = MarketLocation {
168            meta: Bo4eMeta::with_type("Marktlokation"),
169            market_location_id: Some("12345678901".to_string()),
170            ..Default::default()
171        };
172
173        let json = serde_json::to_string(&malo).unwrap();
174        assert!(json.contains(r#""marketLocationId":"12345678901""#));
175    }
176
177    #[test]
178    fn test_roundtrip() {
179        let malo = MarketLocation {
180            meta: Bo4eMeta::with_type("Marktlokation"),
181            market_location_id: Some("12345678901".to_string()),
182            division: Some(Division::Electricity),
183            energy_direction: Some(EnergyDirection::FeedOut),
184            annual_consumption: Some(3500.0),
185            metering_location_ids: vec!["DE00012345".to_string()],
186            ..Default::default()
187        };
188
189        let json = serde_json::to_string(&malo).unwrap();
190        let parsed: MarketLocation = serde_json::from_str(&json).unwrap();
191        assert_eq!(malo, parsed);
192    }
193
194    #[test]
195    fn test_bo4e_object_impl() {
196        assert_eq!(MarketLocation::type_name_german(), "Marktlokation");
197        assert_eq!(MarketLocation::type_name_english(), "MarketLocation");
198    }
199}