use rust_decimal::Decimal;
use crate::datetime::DvgwPeriod;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct Party {
pub role: String,
pub id: String,
pub agency: Option<String>,
}
impl Party {
#[must_use]
pub fn is_dvgw_coded(&self) -> bool {
self.agency.as_deref() == Some(crate::document::DVGW_AGENCY_CODE)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct Reference {
pub qualifier: String,
pub value: String,
}
pub mod rff {
pub const PRUEFIDENTIFIKATOR: &str = "Z13";
pub const CLEARINGNUMMER: &str = "ANX";
pub const ORIGINAL_NOMINIERUNG: &str = "AGO";
}
pub mod nad {
pub const ABSENDER: &str = "MS";
pub const EMPFAENGER: &str = "MR";
pub const ZUSAETZLICHER_BKV: &str = "ZSY";
pub const BILANZKREIS_INTERN: &str = "ZEU";
pub const BILANZKREIS_EXTERN: &str = "ZES";
pub const NETZKONTO: &str = "ZSZ";
pub const NETZBETREIBER: &str = "ZSO";
pub const NETZKONTO_ZO_T3: &str = "ZSH";
pub const VORGELAGERTER_NETZBETREIBER: &str = "ZET";
pub const VIRTUELLER_HANDELSPUNKT: &str = "VHP";
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct Quantity {
pub qualifier: String,
pub value: Option<Decimal>,
pub raw_value: String,
pub unit: Option<String>,
pub period: Option<DvgwPeriod>,
pub status: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct LocationGroup {
pub qualifier: String,
pub code: Option<String>,
pub agency: Option<String>,
pub quantities: Vec<Quantity>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct ItemDescription {
pub characteristic: Option<String>,
pub code: Option<String>,
}
pub mod imd {
pub const NOMINIERT: &str = "17G";
pub const GEGENSEITE: &str = "18G";
pub const GEMATCHT: &str = "16G";
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct LineItem {
pub number: Option<String>,
pub item_type: Option<String>,
pub descriptions: Vec<ItemDescription>,
pub locations: Vec<LocationGroup>,
pub parties: Vec<Party>,
}
impl Quantity {
#[must_use]
pub fn energy_kwh(&self) -> Option<Decimal> {
if self.unit.as_deref() != Some("KW1") {
return None;
}
let value = self.value?;
let period = self.period?;
let seconds = Decimal::from(period.duration().whole_seconds());
if seconds <= Decimal::ZERO {
return None;
}
Some(value * seconds / Decimal::from(3600))
}
}
impl LineItem {
#[must_use]
pub fn party(&self, role: &str) -> Option<&Party> {
self.parties.iter().find(|p| p.role == role)
}
pub fn quantities(&self) -> impl Iterator<Item = &Quantity> {
self.locations.iter().flat_map(|l| l.quantities.iter())
}
#[must_use]
pub fn description_code(&self) -> Option<&str> {
self.descriptions.iter().find_map(|d| d.code.as_deref())
}
}
pub type EnergyByQualifier = std::collections::BTreeMap<String, Decimal>;
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::prelude::FromPrimitive;
fn qty(value: i64) -> Quantity {
Quantity {
qualifier: "Z03".into(),
value: Decimal::from_i64(value),
raw_value: value.to_string(),
unit: Some("KW1".into()),
period: None,
status: Vec::new(),
}
}
#[test]
fn a_position_flattens_quantities_across_its_location_groups() {
let item = LineItem {
number: Some("1".into()),
item_type: Some("Z01".into()),
descriptions: Vec::new(),
locations: vec![
LocationGroup {
qualifier: "Z99".into(),
code: None,
agency: None,
quantities: vec![qty(100), qty(200)],
},
LocationGroup {
qualifier: "Z19".into(),
code: Some("ABCD1234".into()),
agency: Some("332".into()),
quantities: vec![qty(300)],
},
],
parties: vec![Party {
role: nad::BILANZKREIS_INTERN.into(),
id: "THE0BFH000000001".into(),
agency: Some("332".into()),
}],
};
assert_eq!(item.quantities().count(), 3, "the time series must survive");
assert_eq!(
item.party(nad::BILANZKREIS_INTERN).unwrap().id,
"THE0BFH000000001"
);
assert!(item.party(nad::BILANZKREIS_EXTERN).is_none());
assert!(item.parties[0].is_dvgw_coded());
}
}