bo4e_core/bo/
external_costs.rs

1//! External costs (Fremdkosten) business object.
2
3use serde::{Deserialize, Serialize};
4
5use crate::com::{Amount, ExternalCostBlock, TimePeriod};
6use crate::enums::Division;
7use crate::traits::{Bo4eMeta, Bo4eObject};
8
9/// External/third-party costs.
10///
11/// Represents costs from external parties like network operators,
12/// metering service providers, etc.
13///
14/// German: Fremdkosten
15///
16/// # Example
17///
18/// ```rust
19/// use bo4e_core::bo::ExternalCosts;
20/// use bo4e_core::com::Amount;
21/// use bo4e_core::enums::Division;
22///
23/// let external_costs = ExternalCosts {
24///     designation: Some("Netzkosten 2024".to_string()),
25///     division: Some(Division::Electricity),
26///     total_amount: Some(Amount::eur(350.00)),
27///     ..Default::default()
28/// };
29/// ```
30#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
31#[serde(rename_all = "camelCase")]
32pub struct ExternalCosts {
33    /// BO4E metadata
34    #[serde(flatten)]
35    pub meta: Bo4eMeta,
36
37    /// Name/designation of the external costs (Bezeichnung)
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub designation: Option<String>,
40
41    /// Description (Beschreibung)
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub description: Option<String>,
44
45    /// Energy division (Sparte)
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub division: Option<Division>,
48
49    /// Period the costs apply to (Abrechnungszeitraum)
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub period: Option<TimePeriod>,
52
53    /// Total amount (Gesamtbetrag)
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub total_amount: Option<Amount>,
56
57    /// External cost blocks (Fremdkostenbloecke)
58    #[serde(default, skip_serializing_if = "Vec::is_empty")]
59    pub cost_blocks: Vec<ExternalCostBlock>,
60
61    /// External provider/party
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub external_party: Option<Box<super::BusinessPartner>>,
64
65    /// Related market location (Marktlokation)
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub market_location: Option<Box<super::MarketLocation>>,
68}
69
70impl Bo4eObject for ExternalCosts {
71    fn type_name_german() -> &'static str {
72        "Fremdkosten"
73    }
74
75    fn type_name_english() -> &'static str {
76        "ExternalCosts"
77    }
78
79    fn meta(&self) -> &Bo4eMeta {
80        &self.meta
81    }
82
83    fn meta_mut(&mut self) -> &mut Bo4eMeta {
84        &mut self.meta
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::com::ExternalCostBlock;
92
93    #[test]
94    fn test_external_costs_creation() {
95        let external_costs = ExternalCosts {
96            designation: Some("Netzkosten 2024".to_string()),
97            division: Some(Division::Electricity),
98            total_amount: Some(Amount::eur(350.00)),
99            ..Default::default()
100        };
101
102        assert_eq!(external_costs.total_amount, Some(Amount::eur(350.00)));
103    }
104
105    #[test]
106    fn test_external_costs_with_blocks() {
107        let external_costs = ExternalCosts {
108            designation: Some("Network Costs".to_string()),
109            cost_blocks: vec![
110                ExternalCostBlock {
111                    designation: Some("Network usage".to_string()),
112                    total_amount: Some(Amount::eur(200.0)),
113                    ..Default::default()
114                },
115                ExternalCostBlock {
116                    designation: Some("Metering".to_string()),
117                    total_amount: Some(Amount::eur(50.0)),
118                    ..Default::default()
119                },
120            ],
121            total_amount: Some(Amount::eur(250.0)),
122            ..Default::default()
123        };
124
125        assert_eq!(external_costs.cost_blocks.len(), 2);
126    }
127
128    #[test]
129    fn test_serialize() {
130        let external_costs = ExternalCosts {
131            meta: Bo4eMeta::with_type("Fremdkosten"),
132            designation: Some("Test External Costs".to_string()),
133            total_amount: Some(Amount::eur(100.0)),
134            ..Default::default()
135        };
136
137        let json = serde_json::to_string(&external_costs).unwrap();
138        assert!(json.contains(r#""designation":"Test External Costs""#));
139        assert!(json.contains(r#""_typ":"Fremdkosten""#));
140    }
141
142    #[test]
143    fn test_roundtrip() {
144        let external_costs = ExternalCosts {
145            meta: Bo4eMeta::with_type("Fremdkosten"),
146            designation: Some("Gas Network Costs".to_string()),
147            description: Some("External gas network costs".to_string()),
148            division: Some(Division::Gas),
149            total_amount: Some(Amount::eur(180.0)),
150            ..Default::default()
151        };
152
153        let json = serde_json::to_string(&external_costs).unwrap();
154        let parsed: ExternalCosts = serde_json::from_str(&json).unwrap();
155        assert_eq!(external_costs, parsed);
156    }
157
158    #[test]
159    fn test_bo4e_object_impl() {
160        assert_eq!(ExternalCosts::type_name_german(), "Fremdkosten");
161        assert_eq!(ExternalCosts::type_name_english(), "ExternalCosts");
162    }
163}