ocpi-tariffs 0.49.0

OCPI tariff calculations
Documentation
use super::{Warning, CDR, DAY_OF_WEEK_VALUES, TARIFF};
use crate::json;
use crate::schema;

#[test]
fn tariff_schema_fields_are_sorted() {
    let doc = json::parse("{}".into()).unwrap();
    drop(schema::walk(&doc, &TARIFF).into_warnings());
}

#[test]
fn cdr_schema_fields_are_sorted() {
    let doc = json::parse("{}".into()).unwrap();
    drop(schema::walk(&doc, &CDR).into_warnings());
}

#[test]
fn minimal_valid_tariff_is_clean() {
    // `elements` is `+`, so a valid Tariff carries at least one element.
    let src = r#"{
        "currency": "EUR",
        "elements": [
            {"price_components": [{"price": 0.25, "step_size": 1, "type": "ENERGY"}]}
        ],
        "id": "T1",
        "last_updated": "2015-06-29T20:39:09Z"
    }"#;
    let doc = json::parse(src.into()).unwrap();
    let warnings = schema::walk(&doc, &TARIFF).into_warnings();
    assert!(warnings.is_empty());
}

#[test]
fn tariff_missing_currency_reported() {
    let src = r#"{
        "elements": [
            {"price_components": [{"price": 0.25, "step_size": 1, "type": "ENERGY"}]}
        ],
        "id": "T1",
        "last_updated": "2015-06-29T20:39:09Z"
    }"#;
    let doc = json::parse(src.into()).unwrap();
    let warnings = schema::walk(&doc, &TARIFF).into_warnings();
    assert_eq!(warnings.len_warnings(), 1);
    assert_eq!(
        warnings.path_map()["$"],
        vec![&Warning::MissingField { name: "currency" }]
    );
}

#[test]
fn tariff_221_field_rejected() {
    // "country_code" is a 2.2.1 field; 2.1.1 schema should flag it.
    let src = r#"{
        "country_code": "NL",
        "currency": "EUR",
        "elements": [
            {"price_components": [{"price": 0.25, "step_size": 1, "type": "ENERGY"}]}
        ],
        "id": "T1",
        "last_updated": "2015-06-29T20:39:09Z"
    }"#;
    let doc = json::parse(src.into()).unwrap();
    let warnings = schema::walk(&doc, &TARIFF).into_warnings();
    assert_eq!(warnings.len_warnings(), 1);
    assert_eq!(
        warnings.path_map()["$.country_code"],
        vec![&Warning::UnexpectedField]
    );
}

#[test]
fn minimal_valid_cdr_is_clean() {
    // `charging_periods` is `+`, and each period's `dimensions` is `+`.
    let src = r#"{
        "auth_id": "TOK001",
        "auth_method": "AUTH_REQUEST",
        "charging_periods": [
            {
                "start_date_time": "2024-01-01T09:00:00Z",
                "dimensions": [{"type": "ENERGY", "volume": 10.0}]
            }
        ],
        "currency": "EUR",
        "id": "CDR001",
        "last_updated": "2024-01-01T10:00:00Z",
        "location": {
            "address": "Main St 1",
            "city": "Amsterdam",
            "coordinates": {"latitude": "52.370121", "longitude": "4.899975"},
            "country": "NLD",
            "id": "LOC1",
            "last_updated": "2024-01-01T00:00:00Z",
            "postal_code": "1234AB",
            "type": "ON_STREET"
        },
        "start_date_time": "2024-01-01T09:00:00Z",
        "stop_date_time": "2024-01-01T10:00:00Z",
        "total_cost": 5.0,
        "total_energy": 10.0,
        "total_time": 1.0
    }"#;
    let doc = json::parse(src.into()).unwrap();
    let warnings = schema::walk(&doc, &CDR).into_warnings();
    assert!(warnings.is_empty());
}

#[test]
fn cdr_stop_date_time_not_end_date_time() {
    // 2.1.1 uses `stop_date_time`; using `end_date_time` (2.2.1) should
    // produce an unexpected field and a missing required field.
    let src = r#"{
        "auth_id": "TOK001",
        "auth_method": "AUTH_REQUEST",
        "charging_periods": [
            {
                "start_date_time": "2024-01-01T09:00:00Z",
                "dimensions": [{"type": "ENERGY", "volume": 10.0}]
            }
        ],
        "currency": "EUR",
        "end_date_time": "2024-01-01T10:00:00Z",
        "id": "CDR001",
        "last_updated": "2024-01-01T10:00:00Z",
        "location": {
            "address": "Main St 1",
            "city": "Amsterdam",
            "coordinates": {"latitude": "52.370121", "longitude": "4.899975"},
            "country": "NLD",
            "id": "LOC1",
            "last_updated": "2024-01-01T00:00:00Z",
            "postal_code": "1234AB",
            "type": "ON_STREET"
        },
        "start_date_time": "2024-01-01T09:00:00Z",
        "total_cost": 5.0,
        "total_energy": 10.0,
        "total_time": 1.0
    }"#;
    let doc = json::parse(src.into()).unwrap();
    let warnings = schema::walk(&doc, &CDR).into_warnings();
    let by_path = warnings.path_map();
    assert_eq!(warnings.len_warnings(), 2);
    assert_eq!(by_path["$.end_date_time"], vec![&Warning::UnexpectedField]);
    assert_eq!(
        by_path["$"],
        vec![&Warning::MissingField {
            name: "stop_date_time"
        }]
    );
}

#[test]
fn cdr_empty_charging_periods_reported() {
    // `charging_periods` is `+`; an empty array is a cardinality violation even
    // though the field itself is present.
    let src = r#"{
        "auth_id": "TOK001",
        "auth_method": "AUTH_REQUEST",
        "charging_periods": [],
        "currency": "EUR",
        "id": "CDR001",
        "last_updated": "2024-01-01T10:00:00Z",
        "location": {
            "address": "Main St 1",
            "city": "Amsterdam",
            "coordinates": {"latitude": "52.370121", "longitude": "4.899975"},
            "country": "NLD",
            "id": "LOC1",
            "last_updated": "2024-01-01T00:00:00Z",
            "postal_code": "1234AB",
            "type": "ON_STREET"
        },
        "start_date_time": "2024-01-01T09:00:00Z",
        "stop_date_time": "2024-01-01T10:00:00Z",
        "total_cost": 5.0,
        "total_energy": 10.0,
        "total_time": 1.0
    }"#;
    let doc = json::parse(src.into()).unwrap();
    let warnings = schema::walk(&doc, &CDR).into_warnings();
    assert_eq!(warnings.len_warnings(), 1);
    assert_eq!(
        warnings.path_map()["$.charging_periods"],
        vec![&Warning::Cardinality {
            expected: schema::Cardinality::OneOrMore,
            len: 0,
        }]
    );
}

#[test]
fn tariff_currency_wrong_kind_reported() {
    // `currency` is a string; a numeric value is a scalar type mismatch.
    let src = r#"{
        "currency": 978,
        "elements": [
            {"price_components": [{"price": 0.25, "step_size": 1, "type": "ENERGY"}]}
        ],
        "id": "T1",
        "last_updated": "2015-06-29T20:39:09Z"
    }"#;
    let doc = json::parse(src.into()).unwrap();
    let warnings = schema::walk(&doc, &TARIFF).into_warnings();
    assert_eq!(warnings.len_warnings(), 1);
    assert_eq!(
        warnings.path_map()["$.currency"],
        vec![&Warning::TypeMismatch {
            expected: json::ValueKind::String,
            actual: json::ValueKind::Number,
        }]
    );
}

#[test]
fn cdr_numeric_latitude_reported() {
    // OCPI latitude/longitude are decimal-degree STRINGS; a JSON number is wrong.
    let src = r#"{
        "auth_id": "TOK001",
        "auth_method": "AUTH_REQUEST",
        "charging_periods": [
            {
                "start_date_time": "2024-01-01T09:00:00Z",
                "dimensions": [{"type": "ENERGY", "volume": 10.0}]
            }
        ],
        "currency": "EUR",
        "id": "CDR001",
        "last_updated": "2024-01-01T10:00:00Z",
        "location": {
            "address": "Main St 1",
            "city": "Amsterdam",
            "coordinates": {"latitude": 52.370121, "longitude": "4.899975"},
            "country": "NLD",
            "id": "LOC1",
            "last_updated": "2024-01-01T00:00:00Z",
            "postal_code": "1234AB",
            "type": "ON_STREET"
        },
        "start_date_time": "2024-01-01T09:00:00Z",
        "stop_date_time": "2024-01-01T10:00:00Z",
        "total_cost": 5.0,
        "total_energy": 10.0,
        "total_time": 1.0
    }"#;
    let doc = json::parse(src.into()).unwrap();
    let warnings = schema::walk(&doc, &CDR).into_warnings();
    assert_eq!(warnings.len_warnings(), 1);
    assert_eq!(
        warnings.path_map()["$.location.coordinates.latitude"],
        vec![&Warning::TypeMismatch {
            expected: json::ValueKind::String,
            actual: json::ValueKind::Number,
        }]
    );
}

#[test]
fn day_of_week_non_string_element_reported() {
    // `day_of_week` is an array of `DayOfWeek` enum strings; a numeric element
    // is a type mismatch.
    let src = r#"{
        "currency": "EUR",
        "elements": [
            {
                "price_components": [{"price": 0.25, "step_size": 1, "type": "ENERGY"}],
                "restrictions": {"day_of_week": ["MONDAY", 5]}
            }
        ],
        "id": "T1",
        "last_updated": "2015-06-29T20:39:09Z"
    }"#;
    let doc = json::parse(src.into()).unwrap();
    let warnings = schema::walk(&doc, &TARIFF).into_warnings();
    assert_eq!(warnings.len_warnings(), 1);
    assert_eq!(
        warnings.path_map()["$.elements[0].restrictions.day_of_week[1]"],
        vec![&Warning::TypeMismatch {
            expected: json::ValueKind::String,
            actual: json::ValueKind::Number,
        }]
    );
}

#[test]
fn day_of_week_unknown_value_reported() {
    // "FUNDAY" is a string but not a valid `DayOfWeek` variant.
    let src = r#"{
        "currency": "EUR",
        "elements": [
            {
                "price_components": [{"price": 0.25, "step_size": 1, "type": "ENERGY"}],
                "restrictions": {"day_of_week": ["MONDAY", "FUNDAY"]}
            }
        ],
        "id": "T1",
        "last_updated": "2015-06-29T20:39:09Z"
    }"#;
    let doc = json::parse(src.into()).unwrap();
    let warnings = schema::walk(&doc, &TARIFF).into_warnings();
    assert_eq!(warnings.len_warnings(), 1);
    assert_eq!(
        warnings.path_map()["$.elements[0].restrictions.day_of_week[1]"],
        vec![&Warning::FieldInvalidValue {
            expected: DAY_OF_WEEK_VALUES,
            actual: "FUNDAY".to_owned(),
        }]
    );
}

#[test]
fn price_component_type_lowercase_is_clean() {
    // The enum value check is case-insensitive: "energy" matches "ENERGY".
    let src = r#"{
        "currency": "EUR",
        "elements": [
            {"price_components": [{"price": 0.25, "step_size": 1, "type": "energy"}]}
        ],
        "id": "T1",
        "last_updated": "2015-06-29T20:39:09Z"
    }"#;
    let doc = json::parse(src.into()).unwrap();
    let warnings = schema::walk(&doc, &TARIFF).into_warnings();
    assert!(warnings.is_empty());
}