ocpi-tariffs 0.49.0

OCPI tariff calculations
Documentation
//! Tests for the `v2.2.1` CDR intermediate-representation (IR) builder.

use super::{build_cdr, Integrity, Warning};
use crate::{json, warning};
use std::assert_matches;

/// Return true if any warning in the set satisfies `pred`.
fn any_warning(warnings: &warning::Set<Warning>, pred: impl Fn(&Warning) -> bool) -> bool {
    warnings
        .iter()
        .any(|group| group.to_parts().1.into_iter().any(&pred))
}

/// A CDR exercising the modeled IR fields. It omits some schema-required but unmodeled
/// fields (`cdr_token`, `cdr_location`, `id`, ...), so the build reports missing-field
/// warnings; the tests assert on the IR fields, not on a clean warning set.
const CDR: &str = r#"{
    "currency": "EUR",
    "start_date_time": "2024-01-01T00:00:00Z",
    "end_date_time": "2024-01-01T01:00:00Z",
    "charging_periods": [
        {
            "start_date_time": "2024-01-01T00:00:00Z",
            "dimensions": [{"type": "ENERGY", "volume": 10.0}]
        }
    ],
    "total_cost": {"excl_vat": 5.0, "incl_vat": 6.05},
    "total_energy": 10.0,
    "total_time": 1.0,
    "tariffs": [
        {
            "country_code": "NL",
            "party_id": "CPO",
            "currency": "EUR",
            "id": "T1",
            "last_updated": "2024-01-01T00:00:00Z",
            "elements": [
                {"price_components": [{"price": 0.5, "step_size": 1, "type": "ENERGY"}]}
            ]
        }
    ]
}"#;

#[test]
fn cdr_builds_its_modeled_fields() {
    let doc = json::parse(CDR.into()).unwrap();
    let (cdr, _warnings) = build_cdr(&doc).into_parts();

    let Integrity::Ok(currency) = &cdr.currency else {
        panic!("currency should be built: {:?}", cdr.currency);
    };
    assert_eq!(
        currency.element().to_raw_str().unwrap().as_unescaped_str(),
        "EUR"
    );

    // A `Price` total with both legs present.
    let Integrity::Ok(total_cost) = &cdr.total_cost else {
        panic!("total_cost should be built: {:?}", cdr.total_cost);
    };
    assert_matches!(total_cost.excl_vat, Integrity::Ok(_));
    assert_matches!(total_cost.incl_vat, Integrity::Ok(_));
    assert_matches!(cdr.total_energy, Integrity::Ok(_));

    // Charging periods -> dimensions.
    let Integrity::Ok(periods) = &cdr.charging_periods else {
        panic!(
            "charging_periods should be built: {:?}",
            cdr.charging_periods
        );
    };
    assert_eq!(periods.len(), 1);
    let Integrity::Ok(period) = &periods[0] else {
        panic!("the charging period should be built");
    };
    let Integrity::Ok(dimensions) = &period.dimensions else {
        panic!("dimensions should be built");
    };
    assert_eq!(dimensions.len(), 1);
    let Integrity::Ok(dimension) = &dimensions[0] else {
        panic!("the dimension should be built");
    };
    let Integrity::Ok(dimension_type) = &dimension.dimension_type else {
        panic!("the dimension type should be built");
    };
    assert_eq!(dimension_type.canonical(), "ENERGY");
    assert_matches!(dimension.volume, Integrity::Ok(_));

    // The embedded tariff is built via the same machinery as a top-level tariff.
    // `tariffs` is optional, so a present array is `Ok(Some(..))`.
    let Integrity::Ok(Some(tariffs)) = &cdr.tariffs else {
        panic!("tariffs should be built: {:?}", cdr.tariffs);
    };
    assert_eq!(tariffs.len(), 1);
    let Integrity::Ok(tariff) = &tariffs[0] else {
        panic!("the embedded tariff should be built");
    };
    assert_matches!(tariff.currency, Integrity::Ok(_));
    assert_matches!(tariff.elements, Integrity::Ok(_));
}

#[test]
fn unknown_cdr_dimension_type_is_err() {
    let src = CDR.replace(
        r#""type": "ENERGY", "volume""#,
        r#""type": "FOO", "volume""#,
    );
    let doc = json::parse(src.as_str().into()).unwrap();
    let (cdr, warnings) = build_cdr(&doc).into_parts();

    let Integrity::Ok(periods) = &cdr.charging_periods else {
        panic!("charging_periods should be built");
    };
    let Integrity::Ok(period) = &periods[0] else {
        panic!("the charging period should be built");
    };
    let Integrity::Ok(dimensions) = &period.dimensions else {
        panic!("dimensions should be built");
    };
    let Integrity::Ok(dimension) = &dimensions[0] else {
        panic!("the dimension should be built");
    };
    assert_matches!(dimension.dimension_type, Integrity::Err);
    assert!(any_warning(&warnings, |w| matches!(
        w,
        Warning::FieldInvalidValue { .. }
    )));
}

#[test]
fn building_is_total_on_degenerate_input() {
    for src in ["{}", "[]", "\"not an object\""] {
        let doc = json::parse(src.into()).unwrap();
        let (cdr, _warnings) = build_cdr(&doc).into_parts();
        // Required fields default to `Missing`.
        assert_matches!(cdr.currency, Integrity::Missing);
        assert_matches!(cdr.charging_periods, Integrity::Missing);
        assert_matches!(cdr.total_cost, Integrity::Missing);
        // `tariffs` is optional: an empty object yields `Ok(None)`, while a non-object
        // root falls back to the `Missing` default.
        assert_matches!(cdr.tariffs, Integrity::Ok(None) | Integrity::Missing);
    }
}