use chrono::{DateTime, TimeDelta, Utc};
use rust_decimal::Decimal;
use super::super::Consumed;
use crate::{
country, currency, expect_array_or_bail, expect_object_or_bail,
json::{self, FieldsAsExt as _, FromJson as _},
number::FromDecimal as _,
parse_nullable_or_bail, parse_required_or_bail,
price::{Period, PeriodRange, Warning},
required_field, required_field_or_bail, string,
warning::{self, GatherWarnings as _, IntoCaveat as _},
Ampere, Enum, IntoEnum, Kw, Kwh, ParseError, Price, ToDuration, Verdict,
};
#[derive(Debug)]
pub struct Cdr {
pub start_date_time: DateTime<Utc>,
pub end_date_time: DateTime<Utc>,
pub charging_periods: Vec<ChargingPeriod>,
pub totals: Totals,
}
#[derive(Debug)]
pub struct Totals {
pub cost: Price,
pub fixed_cost: Option<Price>,
pub energy: Kwh,
pub energy_cost: Option<Price>,
pub time: TimeDelta,
pub time_cost: Option<Price>,
pub parking_time: Option<TimeDelta>,
pub parking_cost: Option<Price>,
pub reservation_cost: Option<Price>,
}
#[derive(Debug, Clone)]
pub(crate) struct Dimension {
pub dimension_type: DimensionType,
pub volume: Decimal,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DimensionType {
Energy,
MaxCurrent,
MinCurrent,
MaxPower,
MinPower,
ParkingTime,
ReservationTime,
Time,
}
#[derive(Clone, Debug)]
pub struct ChargingPeriod {
pub start_date_time: DateTime<Utc>,
pub dimensions: Vec<Dimension>,
}
pub struct WithTariffs<'buf> {
pub cdr: Cdr,
pub tariffs: Vec<json::Element<'buf>>,
}
impl WithTariffs<'_> {
pub fn discard_tariffs(self) -> Cdr {
self.cdr
}
}
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 mut warnings = warning::Set::<Warning>::new();
let fields = expect_object_or_bail!(elem, warnings);
let fields = fields.as_raw_map();
let cdr = cdr_from_fields(elem, &fields)?.gather_warnings_into(&mut warnings);
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![]
};
Ok(WithTariffs { cdr, tariffs }.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();
{
if let Some(elem) = required_field!(elem, fields, "country_code", warnings) {
let code_set = country::CodeSet::from_json(elem).gather_warnings_into(&mut warnings)?;
if let country::CodeSet::Alpha3(_) = code_set {
warnings.insert(Warning::CountryShouldBeAlpha2, elem);
}
}
if let Some(elem) = required_field!(elem, fields, "currency", warnings) {
let _ignore_value = currency::Code::from_json(elem).gather_warnings_into(&mut warnings);
}
if let Some(elem) = required_field!(elem, fields, "party_id", warnings) {
let _ignore_value =
string::CiExactLen::<'_, 3>::from_json(elem).gather_warnings_into(&mut warnings);
}
}
let start_date_time =
parse_required_or_bail!(elem, fields, "start_date_time", DateTime<Utc>, warnings);
let end_date_time =
parse_required_or_bail!(elem, fields, "end_date_time", DateTime<Utc>, warnings);
let total_cost = parse_required_or_bail!(elem, fields, "total_cost", Price, warnings);
let total_energy = parse_required_or_bail!(elem, fields, "total_energy", Kwh, warnings);
let total_time = parse_required_or_bail!(elem, fields, "total_time", Decimal, warnings);
let charging_periods_elem = required_field_or_bail!(elem, fields, "charging_periods", 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_parking_time =
parse_nullable_or_bail!(fields, "total_parking_time", Decimal, warnings);
let total_fixed_cost = parse_nullable_or_bail!(fields, "total_fixed_cost", Price, warnings);
let total_energy_cost = parse_nullable_or_bail!(fields, "total_energy_cost", Price, warnings);
let total_time_cost = parse_nullable_or_bail!(fields, "total_time_cost", Price, warnings);
let total_parking_cost = parse_nullable_or_bail!(fields, "total_parking_cost", Price, warnings);
let total_reservation_cost =
parse_nullable_or_bail!(fields, "total_reservation_cost", Price, warnings);
let cdr_range = start_date_time..end_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,
end_date_time,
charging_periods,
totals: Totals {
cost: total_cost,
fixed_cost: total_fixed_cost,
energy: total_energy,
energy_cost: total_energy_cost,
time: total_time.to_duration(),
time_cost: total_time_cost,
parking_time: total_parking_time.map(|d| d.to_duration()),
parking_cost: total_parking_cost,
reservation_cost: total_reservation_cost,
},
};
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 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`, `RESERVATION_TIME` or `TIME`"
),
elem
);
return Ok(None.into_caveat(warnings));
}
};
let volume_elem = required_field_or_bail!(elem, fields, "volume", warnings);
let volume = Decimal::from_json(volume_elem)?.gather_warnings_into(&mut warnings);
let dimension = Dimension {
dimension_type,
volume,
};
Ok(Some(dimension).into_caveat(warnings))
}
}
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("max_current") {
Self::MaxCurrent
} else if s.eq_ignore_ascii_case("min_current") {
Self::MinCurrent
} else if s.eq_ignore_ascii_case("max_power") {
Self::MaxPower
} else if s.eq_ignore_ascii_case("min_power") {
Self::MinPower
} else if s.eq_ignore_ascii_case("parking_time") {
Self::ParkingTime
} else if s.eq_ignore_ascii_case("reservation_time") {
Self::ReservationTime
} else if s.eq_ignore_ascii_case("time") {
Self::Time
} else {
return Enum::Unknown(s.to_string());
};
Enum::Known(dt)
}
}
impl json::FromJson<'_> for Enum<DimensionType> {
type Warning = Warning;
fn from_json(elem: &'_ json::Element<'_>) -> Verdict<Self, Self::Warning> {
let mut warnings = warning::Set::new();
let Some(s) = elem.to_raw_str() else {
return warnings.bail(
Warning::FieldInvalidType {
expected_type: json::ValueKind::String,
},
elem,
);
};
let s = s.decode_escapes(elem).gather_warnings_into(&mut warnings);
let dt = DimensionType::enum_from_str(&s);
Ok(dt.into_caveat(warnings))
}
}
impl TryFrom<ChargingPeriod> for Period {
type Error = ParseError;
fn try_from(period: ChargingPeriod) -> Result<Self, Self::Error> {
let ChargingPeriod {
start_date_time,
dimensions,
} = period;
let mut consumed = Consumed {
current_max: None,
current_min: None,
duration_charging: None,
duration_parking: None,
energy: None,
power_max: None,
power_min: None,
};
for dimension in dimensions {
let Dimension {
dimension_type,
volume,
} = dimension;
match dimension_type {
DimensionType::MinCurrent => {
consumed.current_min = Some(Ampere::from_decimal(volume));
}
DimensionType::MaxCurrent => {
consumed.current_max = Some(Ampere::from_decimal(volume));
}
DimensionType::MaxPower => {
consumed.power_max = Some(Kw::from_decimal(volume));
}
DimensionType::MinPower => {
consumed.power_min = Some(Kw::from_decimal(volume));
}
DimensionType::Energy => {
consumed.energy = Some(Kwh::from_decimal(volume));
}
DimensionType::Time => {
consumed.duration_charging = Some(volume.to_duration());
}
DimensionType::ParkingTime => {
consumed.duration_parking = Some(volume.to_duration());
}
DimensionType::ReservationTime => {
}
}
}
Ok(Period {
start_date_time,
consumed,
})
}
}