ocpi-tariffs 0.49.1

OCPI tariff calculations
Documentation
#![allow(clippy::indexing_slicing, reason = "tests are allowed to panic")]
#![allow(
    clippy::unwrap_in_result,
    reason = "A test can unwrap wherever it wants"
)]
#![allow(
    clippy::panic_in_result_fn,
    reason = "A test can unwrap wherever it wants"
)]
use std::assert_matches;

use super::{find_or_infer, Source};
use crate::{cdr, test, timezone::Warning, warning::test::VerdictTestExt as _, Verdict, Version};

#[test]
fn should_find_timezone() {
    const JSON: &str = r#"{
    "country_code": "NL",
    "cdr_location": {
        "time_zone": "Europe/Amsterdam"
    }
}"#;

    test::setup();
    let timezone = parse_expect_v221_and_time_zone_field(JSON)
        .unwrap()
        .unwrap();

    assert_matches!(timezone, Source::Found(chrono_tz::Tz::Europe__Amsterdam));
}

#[test]
fn should_find_timezone_but_warn_about_use_of_location_for_v221_cdr() {
    const JSON: &str = r#"{
    "country_code": "NL",
    "location": {
        "time_zone": "Europe/Amsterdam"
    }
}"#;

    test::setup();
    // If you parse a CDR that has a `location` field as v221 you will get multiple warnings.
    let cdr = cdr::build(crate::json::parse_object(JSON).unwrap(), Version::V221);

    // The schema validation will complain about the unexpected `location` field. It records the
    // unexpected field but does not descend into it, so its child `$.location.time_zone` is not
    // reported separately.
    test::assert_unexpected_fields(&cdr, &["$.location"]);

    let (timezone_source, warnings) = find_or_infer(&cdr).unwrap().into_parts();
    let warnings = warnings.into_path_as_str_map();
    let warnings = &warnings["$"];

    assert_matches!(
        timezone_source,
        Source::Found(chrono_tz::Tz::Europe__Amsterdam)
    );
    // And the `find_or_infer` fn will warn about a v221 CDR having a `location` field.
    assert_matches!(warnings.as_slice(), [Warning::V221CdrHasLocationField]);
}

#[test]
fn should_find_timezone_without_cdr_country() {
    const JSON: &str = r#"{
    "cdr_location": {
        "time_zone": "Europe/Amsterdam"
    }
}"#;

    test::setup();
    let timezone = parse_expect_v221_and_time_zone_field(JSON)
        .unwrap()
        .unwrap();

    assert_matches!(timezone, Source::Found(chrono_tz::Tz::Europe__Amsterdam));
}

#[test]
fn should_infer_timezone_and_warn_about_invalid_type() {
    const JSON: &str = r#"{
    "country_code": "NL",
    "cdr_location": {
        "time_zone": null,
        "country": "BEL"
    }
}"#;

    test::setup();
    let (timezone, warnings) = parse_expect_v221_and_time_zone_field(JSON)
        .unwrap()
        .into_parts();
    let warnings = warnings.into_path_as_str_map();
    let warnings = &warnings["$.cdr_location.time_zone"];

    assert_matches!(timezone, Source::Inferred(chrono_tz::Tz::Europe__Brussels));
    assert_matches!(warnings.as_slice(), [Warning::InvalidTimezoneType]);
}

#[test]
fn should_find_timezone_and_warn_about_invalid_type() {
    const JSON: &str = r#"{
    "country_code": "NL",
    "cdr_location": {
        "time_zone": "Europe/Hamsterdam",
        "country": "BEL"
    }
}"#;

    test::setup();
    let (timezone, warnings) = parse_expect_v221_and_time_zone_field(JSON)
        .unwrap()
        .into_parts();

    assert_matches!(timezone, Source::Inferred(chrono_tz::Tz::Europe__Brussels));
    let warnings = warnings.into_path_as_str_map();
    let warnings = &warnings["$.cdr_location.time_zone"];
    assert_matches!(warnings.as_slice(), [Warning::InvalidTimezone]);
}

#[test]
fn should_find_timezone_and_warn_about_escape_codes_and_invalid_type() {
    const JSON: &str = r#"{
    "country_code": "NL",
    "cdr_location": {
        "time_zone": "Europe\/Hamsterdam",
        "country": "BEL"
    }
}"#;

    test::setup();
    let (timezone, warnings) = parse_expect_v221_and_time_zone_field(JSON)
        .unwrap()
        .into_parts();

    assert_matches!(timezone, Source::Inferred(chrono_tz::Tz::Europe__Brussels));

    let groups = warnings.into_path_as_str_map();
    let warnings = &groups["$.cdr_location.time_zone"];
    assert_matches!(
        warnings.as_slice(),
        [Warning::ContainsEscapeCodes, Warning::InvalidTimezone]
    );
}

#[test]
fn should_find_timezone_and_warn_about_escape_codes() {
    const JSON: &str = r#"{
    "country_code": "NL",
    "cdr_location": {
        "time_zone": "Europe\/Amsterdam",
        "country": "BEL"
    }
}"#;

    test::setup();
    let (timezone, warnings) = parse_expect_v221_and_time_zone_field(JSON)
        .unwrap()
        .into_parts();

    assert_matches!(timezone, Source::Found(chrono_tz::Tz::Europe__Amsterdam));

    let groups = warnings.into_path_as_str_map();
    let warnings = &groups["$.cdr_location.time_zone"];
    assert_matches!(warnings.as_slice(), [Warning::ContainsEscapeCodes]);
}

#[test]
fn should_infer_timezone_from_location_country() {
    const JSON: &str = r#"{
    "country_code": "NL",
    "cdr_location": {
        "country": "BEL"
    }
}"#;

    test::setup();
    let timezone = parse_expect_v221(JSON).unwrap().unwrap();

    assert_matches!(timezone, Source::Inferred(chrono_tz::Tz::Europe__Brussels));
}

#[test]
fn should_find_timezone_but_report_alpha2_location_country_code() {
    const JSON: &str = r#"{
    "country_code": "NL",
    "cdr_location": {
        "country": "BE"
    }
}"#;

    test::setup();
    let (timezone, warnings) = parse_expect_v221(JSON).unwrap().into_parts();

    assert_matches!(timezone, Source::Inferred(chrono_tz::Tz::Europe__Brussels));

    let groups = warnings.into_path_as_str_map();
    let warnings = &groups["$.cdr_location.country"];
    assert_matches!(
        warnings.as_slice(),
        [Warning::LocationCountryShouldBeAlpha3,]
    );
}

#[test]
fn should_not_find_timezone_due_to_no_location() {
    const JSON: &str = r#"{ "country_code": "BE" }"#;

    test::setup();
    let error = parse_expect_v221(JSON).unwrap_only_error();

    assert_eq!(error.element().path.as_str(), "$");
    assert_matches!(error.warning(), Warning::NoLocation);
}

#[test]
fn should_not_find_timezone_due_to_no_country() {
    // The `$.country_code` field is not used at all when inferring the timezone.
    const JSON: &str = r#"{
    "country_code": "BE",
    "cdr_location": {}
}"#;

    test::setup();
    let error = parse_expect_v221(JSON).unwrap_only_error();

    assert_eq!(error.element().path.as_str(), "$.cdr_location");
    assert_matches!(error.warning(), Warning::NoLocationCountry);
}

#[test]
fn should_not_find_timezone_due_to_country_having_many_timezones() {
    const JSON: &str = r#"{
    "country_code": "BE",
    "cdr_location": {
        "country": "CHN"
    }
}"#;

    test::setup();
    let error = parse_expect_v221(JSON).unwrap_only_error();

    assert_eq!(error.element().path.as_str(), "$.cdr_location.country");
    assert_matches!(error.warning(), Warning::CantInferTimezoneFromCountry("CN"));
}

#[test]
fn should_fail_due_to_json_not_being_object() {
    const JSON: &str = r#"["not_a_cdr"]"#;

    test::setup();

    // A CDR that is not a JSON object is now rejected up front by `json::parse_object`
    // rather than later during timezone inference.
    let error = crate::json::parse_object(JSON).unwrap_err();

    assert_matches!(error, crate::json::ParseError::ShouldBeAnObject);
}

/// Parse CDR and infer the timezone.
#[track_caller]
fn parse_expect_v221(json: &str) -> Verdict<Source, Warning> {
    let json = crate::json::parse_object(json).unwrap();
    let (cdr, mut warnings) = cdr::build(json, Version::V221).into_parts();
    // The CDR JSON's used in this test suite are minimal to test specific issues without noise.
    // We don't care if a field is required by the spec and missing from test CDR.
    warnings.remove_missing_fields();

    assert!(
        warnings.is_empty(),
        "The CDR has warnings;\n{:?}",
        warnings.path_id_map()
    );

    find_or_infer(&cdr)
}

/// Parse CDR and infer the timezone and assert that the `$.cdr_location.time_zone` fields.
#[track_caller]
fn parse_expect_v221_and_time_zone_field(json: &str) -> Verdict<Source, Warning> {
    let cdr = cdr::build(crate::json::parse_object(json).unwrap(), Version::V221);
    test::assert_unexpected_fields(&cdr, &["$.cdr_location.time_zone"]);

    find_or_infer(&cdr)
}