Skip to main content

bo4e_core/bo/
costs.rs

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