#![allow(
clippy::indexing_slicing,
clippy::unwrap_in_result,
reason = "unwraps and indexing are allowed anywhere in tests"
)]
use std::assert_matches;
use chrono::TimeDelta;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use super::{Cdr, ChargingPeriod, Dimension, DimensionType};
use crate::{
json,
price::Warning,
schema::{v221, Integrity, Warning as SchemaWarning},
test::datetime_from_str,
warning, FromSchema as _,
};
const VALID: &str = r#"{
"country_code": "NL",
"party_id": "ENE",
"start_date_time": "2022-01-13T16:00:00Z",
"end_date_time": "2022-01-13T19:12:00Z",
"currency": "EUR",
"cdr_location": {"country": "NLD"},
"charging_periods": [
{
"start_date_time": "2022-01-13T16:00:00Z",
"dimensions": [{"type": "TIME", "volume": 2.5}]
}
],
"total_cost": {"excl_vat": 11.25, "incl_vat": 12.75},
"total_time": 3.2,
"total_energy": 0,
"last_updated": "2022-01-13T00:00:00Z"
}"#;
fn build_cdr<'buf>(doc: &json::Document<'buf>) -> v221::Cdr<'buf> {
v221::build_cdr(doc).ignore_warnings()
}
fn period<'a, 'buf>(cdr: &'a v221::Cdr<'buf>) -> &'a v221::ChargingPeriod<'buf> {
let Integrity::Ok(periods) = &cdr.charging_periods else {
panic!(
"charging_periods should be built: {:?}",
cdr.charging_periods
);
};
let Integrity::Ok(period) = &periods[0] else {
panic!("the charging period should be built");
};
period
}
fn dimension<'a, 'buf>(cdr: &'a v221::Cdr<'buf>) -> &'a v221::Dimension<'buf> {
let Integrity::Ok(dimensions) = &period(cdr).dimensions else {
panic!("dimensions should be built");
};
let Integrity::Ok(dimension) = &dimensions[0] else {
panic!("the dimension should be built");
};
dimension
}
fn all_warnings(warnings: &warning::Set<Warning>) -> Vec<&Warning> {
warnings.path_map().into_values().flatten().collect()
}
#[test]
fn dimension_lowers_from_schema() {
let doc = json::parse(VALID.into()).unwrap();
let cdr = build_cdr(&doc);
let (dim, warnings) = Option::<Dimension>::from_schema(dimension(&cdr))
.unwrap()
.into_parts();
let dim = dim.expect("a valid dimension should build");
assert_eq!(dim.dimension_type, DimensionType::Time);
assert_eq!(dim.volume, dec!(2.5));
assert!(all_warnings(&warnings).is_empty());
}
#[test]
fn unknown_dimension_type_drops_the_dimension() {
let src = VALID.replace(r#""type": "TIME""#, r#""type": "FOO""#);
let doc = json::parse(src.as_str().into()).unwrap();
let cdr = build_cdr(&doc);
let dim = Option::<Dimension>::from_schema(dimension(&cdr))
.unwrap()
.unwrap();
assert!(dim.is_none());
}
#[test]
fn missing_volume_is_rejected() {
let src = VALID.replace(r#", "volume": 2.5"#, "");
let doc = json::parse(src.as_str().into()).unwrap();
let cdr = build_cdr(&doc);
let err = Option::<Dimension>::from_schema(dimension(&cdr)).unwrap_err();
let error = err.unwrap();
assert_matches!(error.into_warning(), Warning::Rejected);
}
#[test]
fn charging_period_lowers_from_schema() {
let doc = json::parse(VALID.into()).unwrap();
let cdr = build_cdr(&doc);
let (charging_period, warnings) = ChargingPeriod::from_schema(period(&cdr))
.unwrap()
.into_parts();
assert_eq!(
charging_period.start_date_time,
datetime_from_str("2022-01-13T16:00:00Z")
);
assert_eq!(charging_period.dimensions.len(), 1);
assert_eq!(
charging_period.dimensions[0].dimension_type,
DimensionType::Time
);
assert!(all_warnings(&warnings).is_empty());
}
#[test]
fn unknown_dimension_type_drops_the_dimension_from_the_period() {
let src = VALID.replace(r#""type": "TIME""#, r#""type": "FOO""#);
let doc = json::parse(src.as_str().into()).unwrap();
let cdr = build_cdr(&doc);
let charging_period = ChargingPeriod::from_schema(period(&cdr)).unwrap().unwrap();
assert!(charging_period.dimensions.is_empty());
}
#[test]
fn unbuildable_dimension_entry_is_rejected() {
let src = VALID.replace(r#"{"type": "TIME", "volume": 2.5}"#, "5");
let doc = json::parse(src.as_str().into()).unwrap();
let cdr = build_cdr(&doc);
let err = ChargingPeriod::from_schema(period(&cdr)).unwrap_err();
let error = err.unwrap();
assert_matches!(error.into_warning(), Warning::Rejected);
}
#[test]
fn missing_dimensions_is_rejected() {
let src = VALID.replace(
r#""2022-01-13T16:00:00Z",
"dimensions": [{"type": "TIME", "volume": 2.5}]"#,
r#""2022-01-13T16:00:00Z""#,
);
let doc = json::parse(src.as_str().into()).unwrap();
let cdr = build_cdr(&doc);
let err = ChargingPeriod::from_schema(period(&cdr)).unwrap_err();
let error = err.unwrap();
assert_matches!(error.into_warning(), Warning::Rejected);
}
#[test]
fn missing_start_date_time_is_rejected() {
let src = VALID.replace(
r#"{
"start_date_time": "2022-01-13T16:00:00Z",
"dimensions""#,
r#"{
"dimensions""#,
);
let doc = json::parse(src.as_str().into()).unwrap();
let cdr = build_cdr(&doc);
let err = ChargingPeriod::from_schema(period(&cdr)).unwrap_err();
let error = err.unwrap();
assert_matches!(error.into_warning(), Warning::Rejected);
}
#[test]
fn unparsable_start_date_time_is_rejected() {
let src = VALID.replace(
r#""start_date_time": "2022-01-13T16:00:00Z",
"dimensions""#,
r#""start_date_time": "not a date",
"dimensions""#,
);
let doc = json::parse(src.as_str().into()).unwrap();
let cdr = build_cdr(&doc);
let err = ChargingPeriod::from_schema(period(&cdr)).unwrap_err();
let error = err.unwrap();
assert_matches!(error.into_warning(), Warning::DateTime(_));
}
#[test]
fn empty_dimensions_lowers_to_no_dimensions() {
let src = VALID.replace(r#"[{"type": "TIME", "volume": 2.5}]"#, "[]");
let doc = json::parse(src.as_str().into()).unwrap();
let (cdr, schema_warnings) = v221::build_cdr(&doc).into_parts();
let cardinality_warnings = schema_warnings
.path_map()
.into_values()
.flatten()
.filter(|warning| matches!(warning, SchemaWarning::Cardinality { .. }))
.count();
assert_eq!(cardinality_warnings, 1);
let charging_period = ChargingPeriod::from_schema(period(&cdr)).unwrap().unwrap();
assert!(charging_period.dimensions.is_empty());
}
#[test]
fn cdr_lowers_from_schema() {
let doc = json::parse(VALID.into()).unwrap();
let built = build_cdr(&doc);
let (cdr, warnings) = Cdr::from_schema(&built).unwrap().into_parts();
assert_eq!(
cdr.start_date_time,
datetime_from_str("2022-01-13T16:00:00Z")
);
assert_eq!(cdr.end_date_time, datetime_from_str("2022-01-13T19:12:00Z"));
assert_eq!(cdr.charging_periods.len(), 1);
assert_eq!(Decimal::from(cdr.totals.cost.excl_vat), dec!(11.25));
assert_eq!(
cdr.totals.cost.incl_vat.map(Decimal::from),
Some(dec!(12.75))
);
assert_eq!(Decimal::from(cdr.totals.energy), dec!(0));
assert_eq!(cdr.totals.duration_charging, TimeDelta::minutes(192));
assert!(cdr.totals.duration_idle.is_none());
assert!(all_warnings(&warnings).is_empty());
}
#[test]
fn charging_periods_are_sorted() {
let src = VALID.replace(
r#" {
"start_date_time": "2022-01-13T16:00:00Z",
"dimensions": [{"type": "TIME", "volume": 2.5}]
}"#,
r#" {
"start_date_time": "2022-01-13T18:00:00Z",
"dimensions": [{"type": "TIME", "volume": 1.0}]
},
{
"start_date_time": "2022-01-13T16:00:00Z",
"dimensions": [{"type": "TIME", "volume": 2.5}]
}"#,
);
let doc = json::parse(src.as_str().into()).unwrap();
let built = build_cdr(&doc);
let cdr = Cdr::from_schema(&built).unwrap().unwrap();
let starts: Vec<_> = cdr
.charging_periods
.iter()
.map(|period| period.start_date_time)
.collect();
assert_eq!(
starts,
vec![
datetime_from_str("2022-01-13T16:00:00Z"),
datetime_from_str("2022-01-13T18:00:00Z"),
]
);
}
#[test]
fn each_missing_required_field_is_rejected() {
for field in [
r#""start_date_time": "2022-01-13T16:00:00Z",
"#,
r#""end_date_time": "2022-01-13T19:12:00Z",
"#,
r#""total_cost": {"excl_vat": 11.25, "incl_vat": 12.75},
"#,
r#""total_time": 3.2,
"#,
r#""total_energy": 0,
"#,
] {
let src = VALID.replace(field, "");
let doc = json::parse(src.as_str().into()).unwrap();
let built = build_cdr(&doc);
let err = Cdr::from_schema(&built)
.err()
.unwrap_or_else(|| panic!("a CDR without `{field}` should be rejected"));
let error = err.unwrap();
assert_matches!(error.into_warning(), Warning::Rejected);
}
}
#[test]
fn missing_fields_that_should_not_halt_pricing() {
let src = VALID
.replace(r#""country_code": "NL","#, "")
.replace(r#""party_id": "ENE","#, "")
.replace(r#""currency": "EUR","#, "");
let doc = json::parse(src.as_str().into()).unwrap();
let built = build_cdr(&doc);
let cdr = Cdr::from_schema(&built).unwrap().unwrap();
assert_eq!(cdr.charging_periods.len(), 1);
}
#[test]
fn alpha3_country_code_is_flagged() {
let src = VALID.replace(r#""country_code": "NL""#, r#""country_code": "NLD""#);
let doc = json::parse(src.as_str().into()).unwrap();
let built = build_cdr(&doc);
let (_cdr, warnings) = Cdr::from_schema(&built).unwrap().into_parts();
assert_matches!(
all_warnings(&warnings).as_slice(),
[Warning::CountryShouldBeAlpha2]
);
}
#[test]
fn unbuildable_charging_period_entry_is_rejected() {
let src = VALID.replace(
r#"{
"start_date_time": "2022-01-13T16:00:00Z",
"dimensions": [{"type": "TIME", "volume": 2.5}]
}"#,
"5",
);
let doc = json::parse(src.as_str().into()).unwrap();
let built = build_cdr(&doc);
let err = Cdr::from_schema(&built).unwrap_err();
let error = err.unwrap();
assert_matches!(error.into_warning(), Warning::Rejected);
}
#[test]
fn empty_charging_periods_lowers_to_no_periods() {
let src = VALID.replace(
r#"[
{
"start_date_time": "2022-01-13T16:00:00Z",
"dimensions": [{"type": "TIME", "volume": 2.5}]
}
]"#,
"[]",
);
let doc = json::parse(src.as_str().into()).unwrap();
let built = build_cdr(&doc);
let cdr = Cdr::from_schema(&built).unwrap().unwrap();
assert!(cdr.charging_periods.is_empty());
}