Skip to main content

bo4e_core/com/
contract_part.rs

1//! Contract part (Vertragsteil) component.
2
3use serde::{Deserialize, Serialize};
4
5use crate::traits::{Bo4eMeta, Bo4eObject};
6
7/// Part of a contract linking a service to a location.
8///
9/// Used to represent a contractual service in relation to a location
10/// (market or metering location). Contracts for multiple locations are
11/// modeled with multiple contract parts.
12///
13/// German: Vertragsteil
14///
15/// # Example
16///
17/// ```rust
18/// use bo4e_core::com::ContractPart;
19///
20/// let part = ContractPart {
21///     location_id: Some("DE0001234567890123456789012345678".to_string()),
22///     contract_part_start: Some("2024-01-01T00:00:00+01:00".to_string()),
23///     contract_part_end: Some("2024-12-31T23:59:59+01:00".to_string()),
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 = "Vertragsteil"))]
30#[serde(rename_all = "camelCase")]
31pub struct ContractPart {
32    /// BO4E metadata
33    #[serde(flatten)]
34    pub meta: Bo4eMeta,
35
36    /// Start of the contract part validity (inclusive) (Vertragsteilbeginn)
37    #[serde(skip_serializing_if = "Option::is_none")]
38    #[cfg_attr(feature = "json-schema", schemars(rename = "vertragsteilbeginn"))]
39    pub contract_part_start: Option<String>,
40
41    /// End of the contract part validity (exclusive) (Vertragsteilende)
42    #[serde(skip_serializing_if = "Option::is_none")]
43    #[cfg_attr(feature = "json-schema", schemars(rename = "vertragsteilende"))]
44    pub contract_part_end: Option<String>,
45
46    /// Identifier for the market or metering location belonging to this contract part (Lokation)
47    #[serde(skip_serializing_if = "Option::is_none")]
48    #[cfg_attr(feature = "json-schema", schemars(rename = "lokation"))]
49    pub location_id: Option<String>,
50
51    // Note: The following fields would typically reference Menge COM type.
52    // Using simplified f64 values for now.
53    /// Contractually fixed consumption quantity (Vertraglich fixierte Menge)
54    #[serde(skip_serializing_if = "Option::is_none")]
55    #[cfg_attr(feature = "json-schema", schemars(rename = "vertraglichFixierteMenge"))]
56    pub fixed_quantity_value: Option<f64>,
57
58    /// Minimum consumption quantity (inclusive) (Minimale Abnahmemenge)
59    #[serde(skip_serializing_if = "Option::is_none")]
60    #[cfg_attr(feature = "json-schema", schemars(rename = "minimaleAbnahmemenge"))]
61    pub minimum_quantity_value: Option<f64>,
62
63    /// Maximum consumption quantity (exclusive) (Maximale Abnahmemenge)
64    #[serde(skip_serializing_if = "Option::is_none")]
65    #[cfg_attr(feature = "json-schema", schemars(rename = "maximaleAbnahmemenge"))]
66    pub maximum_quantity_value: Option<f64>,
67}
68
69impl Bo4eObject for ContractPart {
70    fn type_name_german() -> &'static str {
71        "Vertragsteil"
72    }
73
74    fn type_name_english() -> &'static str {
75        "ContractPart"
76    }
77
78    fn meta(&self) -> &Bo4eMeta {
79        &self.meta
80    }
81
82    fn meta_mut(&mut self) -> &mut Bo4eMeta {
83        &mut self.meta
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn test_contract_part_default() {
93        let part = ContractPart::default();
94        assert!(part.location_id.is_none());
95        assert!(part.contract_part_start.is_none());
96    }
97
98    #[test]
99    fn test_contract_part_serialize() {
100        let part = ContractPart {
101            location_id: Some("DE00012345678901234567890123456".to_string()),
102            contract_part_start: Some("2024-01-01T00:00:00+01:00".to_string()),
103            contract_part_end: Some("2024-12-31T23:59:59+01:00".to_string()),
104            fixed_quantity_value: Some(50000.0),
105            ..Default::default()
106        };
107
108        let json = serde_json::to_string(&part).unwrap();
109        assert!(json.contains(r#""locationId":"DE00012345678901234567890123456""#));
110        assert!(json.contains(r#""contractPartStart":"#));
111        assert!(json.contains(r#""fixedQuantityValue":50000"#));
112    }
113
114    #[test]
115    fn test_contract_part_roundtrip() {
116        let part = ContractPart {
117            meta: Bo4eMeta::with_type("Vertragsteil"),
118            contract_part_start: Some("2024-06-01T00:00:00+02:00".to_string()),
119            contract_part_end: Some("2025-05-31T23:59:59+02:00".to_string()),
120            location_id: Some("MALO-12345".to_string()),
121            fixed_quantity_value: Some(100000.0),
122            minimum_quantity_value: Some(80000.0),
123            maximum_quantity_value: Some(120000.0),
124        };
125
126        let json = serde_json::to_string(&part).unwrap();
127        let parsed: ContractPart = serde_json::from_str(&json).unwrap();
128        assert_eq!(part, parsed);
129    }
130
131    #[test]
132    fn test_bo4e_object_impl() {
133        assert_eq!(ContractPart::type_name_german(), "Vertragsteil");
134        assert_eq!(ContractPart::type_name_english(), "ContractPart");
135    }
136}