use chrono::{DateTime, TimeDelta, Utc};
use rust_decimal::Decimal;
use crate::{
define_enum_from_json, expect_array_or_bail, expect_object_or_bail,
json::{self, FieldsAsExt as _, FromJson as _},
number::FromDecimal as _,
parse_nullable_or_bail,
price::{v221, PeriodRange, Warning},
required_field_or_bail,
warning::{self, GatherWarnings as _, IntoCaveat as _},
Enum, IntoEnum, Kwh, Money, Price, ToDuration, Verdict,
};
#[derive(Clone, Debug)]
pub(crate) struct Cdr {
start_date_time: DateTime<Utc>,
stop_date_time: DateTime<Utc>,
charging_periods: Vec<ChargingPeriod>,
total_cost: Decimal,
total_energy: Decimal,
total_time: TimeDelta,
total_parking_time: Option<TimeDelta>,
}
pub struct WithTariffs<'buf> {
cdr: Cdr,
tariffs: Vec<json::Element<'buf>>,
}
#[derive(Debug, Clone)]
struct Dimension {
dimension_type: DimensionType,
volume: Decimal,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum DimensionType {
Energy,
Flat,
MaxCurrent,
MinCurrent,
ParkingTime,
Time,
}
impl IntoEnum for DimensionType {
fn enum_from_str(s: &str) -> Enum<DimensionType> {
let dt = if s.eq_ignore_ascii_case("energy") {
Self::Energy
} else if s.eq_ignore_ascii_case("flat") {
Self::Flat
} else if s.eq_ignore_ascii_case("max_current") {
Self::MaxCurrent
} else if s.eq_ignore_ascii_case("min_current") {
Self::MinCurrent
} else if s.eq_ignore_ascii_case("parking_time") {
Self::ParkingTime
} else if s.eq_ignore_ascii_case("time") {
Self::Time
} else {
return Enum::Unknown(s.to_string());
};
Enum::Known(dt)
}
}
define_enum_from_json!(DimensionType, display_name: "dimension type", warning_id: "dimension_type");
#[derive(Clone, Debug)]
struct ChargingPeriod {
start_date_time: DateTime<Utc>,
dimensions: Vec<Dimension>,
}
impl From<Cdr> for v221::Cdr {
fn from(cdr: Cdr) -> Self {
let Cdr {
start_date_time,
stop_date_time,
charging_periods,
total_cost,
total_energy,
total_time,
total_parking_time,
} = cdr;
Self {
end_date_time: stop_date_time,
start_date_time,
charging_periods: charging_periods
.into_iter()
.map(ChargingPeriod::into)
.collect(),
totals: v221::cdr::Totals {
cost: Price {
excl_vat: Money::from_decimal(total_cost),
incl_vat: None,
},
energy: Kwh::from_decimal(total_energy),
energy_cost: None,
time: total_time,
time_cost: None,
fixed_cost: None,
parking_time: total_parking_time,
parking_cost: None,
reservation_cost: None,
},
}
}
}
impl<'a> From<WithTariffs<'a>> for v221::cdr::WithTariffs<'a> {
fn from(value: WithTariffs<'a>) -> Self {
let WithTariffs { cdr, tariffs } = value;
Self {
cdr: cdr.into(),
tariffs,
}
}
}
impl From<ChargingPeriod> for v221::cdr::ChargingPeriod {
fn from(period: ChargingPeriod) -> Self {
let ChargingPeriod {
start_date_time,
dimensions,
} = period;
let dimensions = dimensions
.into_iter()
.filter_map(|d| {
let Dimension {
dimension_type,
volume,
} = d;
if let DimensionType::Flat = dimension_type {
return None;
}
let dimension_type = match dimension_type {
DimensionType::Energy => v221::cdr::DimensionType::Energy,
DimensionType::MaxCurrent => v221::cdr::DimensionType::MaxCurrent,
DimensionType::MinCurrent => v221::cdr::DimensionType::MinCurrent,
DimensionType::ParkingTime => v221::cdr::DimensionType::ParkingTime,
DimensionType::Time => v221::cdr::DimensionType::Time,
DimensionType::Flat => return None,
};
Some(v221::cdr::Dimension {
dimension_type,
volume,
})
})
.collect();
Self {
start_date_time,
dimensions,
}
}
}
impl<'buf> json::FromJson<'buf> for Cdr {
type Warning = Warning;
fn from_json(elem: &json::Element<'buf>) -> Verdict<Self, Self::Warning> {
let warnings = warning::Set::<Warning>::new();
let fields = expect_object_or_bail!(elem, warnings);
let fields = fields.as_raw_map();
cdr_from_fields(elem, &fields)
}
}
impl<'buf> json::FromJson<'buf> for WithTariffs<'buf> {
type Warning = Warning;
fn from_json(elem: &json::Element<'buf>) -> Verdict<Self, Self::Warning> {
let warnings = warning::Set::<Warning>::new();
let fields = expect_object_or_bail!(elem, warnings);
let fields = fields.as_raw_map();
let (cdr, warnings) = cdr_from_fields(elem, &fields)?.into_parts();
let tariffs_elem = fields.get("tariffs");
let tariffs = if let Some(elem) = tariffs_elem {
let tariffs = expect_array_or_bail!(elem, warnings);
tariffs.to_vec()
} else {
Vec::default()
};
let cdr = WithTariffs { cdr, tariffs };
Ok(cdr.into_caveat(warnings))
}
}
fn cdr_from_fields<'buf>(
elem: &json::Element<'buf>,
fields: &json::RawRefMap<'_, 'buf>,
) -> Verdict<Cdr, Warning> {
let mut warnings = warning::Set::<Warning>::new();
let start_date_time_elem = required_field_or_bail!(elem, fields, "start_date_time", warnings);
let stop_date_time_elem = required_field_or_bail!(elem, fields, "stop_date_time", warnings);
let charging_periods_elem = required_field_or_bail!(elem, fields, "charging_periods", warnings);
let total_cost_elem = required_field_or_bail!(elem, fields, "total_cost", warnings);
let total_energy_elem = required_field_or_bail!(elem, fields, "total_energy", warnings);
let total_time_elem = required_field_or_bail!(elem, fields, "total_time", warnings);
let start_date_time =
DateTime::from_json(start_date_time_elem)?.gather_warnings_into(&mut warnings);
let stop_date_time =
DateTime::from_json(stop_date_time_elem)?.gather_warnings_into(&mut warnings);
let charging_periods = expect_array_or_bail!(charging_periods_elem, warnings)
.iter()
.map(ChargingPeriod::from_json)
.collect::<Result<Vec<_>, _>>()?;
let mut charging_periods = charging_periods.gather_warnings_into(&mut warnings);
if charging_periods.is_empty() {
return warnings.bail(Warning::NoPeriods, charging_periods_elem);
}
let total_cost = Decimal::from_json(total_cost_elem)?.gather_warnings_into(&mut warnings);
let total_energy = Decimal::from_json(total_energy_elem)?.gather_warnings_into(&mut warnings);
let total_time = Decimal::from_json(total_time_elem)?.gather_warnings_into(&mut warnings);
let total_parking_time =
parse_nullable_or_bail!(fields, "total_parking_time", Decimal, warnings);
let cdr_range = start_date_time..stop_date_time;
charging_periods.sort_unstable_by_key(|p| p.start_date_time);
match charging_periods.as_slice() {
[] => (),
[period] => {
if !cdr_range.contains(&period.start_date_time) {
warnings.insert(
Warning::PeriodsOutsideStartEndDateTime {
cdr_range,
period_range: PeriodRange::Single(period.start_date_time),
},
elem,
);
}
}
[period_earliest, .., period_latest] => {
let period_range = period_earliest.start_date_time..period_latest.start_date_time;
if !(cdr_range.contains(&period_range.start) && cdr_range.contains(&period_range.end)) {
warnings.insert(
Warning::PeriodsOutsideStartEndDateTime {
cdr_range,
period_range: PeriodRange::Many(period_range),
},
elem,
);
}
}
}
let cdr = Cdr {
start_date_time,
stop_date_time,
charging_periods,
total_cost,
total_energy,
total_time: total_time.to_duration(),
total_parking_time: total_parking_time.as_ref().map(ToDuration::to_duration),
};
Ok(cdr.into_caveat(warnings))
}
impl json::FromJson<'_> for ChargingPeriod {
type Warning = Warning;
fn from_json(elem: &'_ json::Element<'_>) -> Verdict<Self, Self::Warning> {
let mut warnings = warning::Set::<Warning>::new();
let fields = expect_object_or_bail!(elem, warnings);
let fields = fields.as_raw_map();
let start_date_time_elem =
required_field_or_bail!(elem, fields, "start_date_time", warnings);
let start_date_time =
DateTime::from_json(start_date_time_elem)?.gather_warnings_into(&mut warnings);
let dimensions_elem = required_field_or_bail!(elem, fields, "dimensions", warnings);
let dimensions = expect_array_or_bail!(dimensions_elem, warnings)
.iter()
.map(Option::<Dimension>::from_json)
.collect::<Result<Vec<_>, _>>()?;
let dimensions = dimensions
.gather_warnings_into(&mut warnings)
.into_iter()
.flatten()
.collect();
let elem = Self {
start_date_time,
dimensions,
};
Ok(elem.into_caveat(warnings))
}
}
impl json::FromJson<'_> for Option<Dimension> {
type Warning = Warning;
fn from_json(elem: &'_ json::Element<'_>) -> Verdict<Self, Self::Warning> {
let mut warnings = warning::Set::<Warning>::new();
let fields = expect_object_or_bail!(elem, warnings);
let fields = fields.as_raw_map();
let volume_elem = required_field_or_bail!(elem, fields, "volume", warnings);
let volume = Decimal::from_json(volume_elem)?.gather_warnings_into(&mut warnings);
let type_elem = required_field_or_bail!(elem, fields, "type", warnings);
let dimension_type =
Enum::<DimensionType>::from_json(type_elem)?.gather_warnings_into(&mut warnings);
let dimension_type = match dimension_type {
Enum::Known(v) => v,
Enum::Unknown(s) => {
warnings.insert(
Warning::field_invalid_value(s,
"A CDR DimensionType should be one of `ENERGY`, `MAX_CURRENT`, `MIN_CURRENT`, `MAX_POWER`, `MIN_POWER`, `PARKING_TIME`, or `TIME`"
),
type_elem
);
return Ok(None.into_caveat(warnings));
}
};
let dimension = Dimension {
dimension_type,
volume,
};
Ok(Some(dimension).into_caveat(warnings))
}
}