ocpi-tariffs 0.52.0

OCPI tariff calculations
Documentation
//! Tests for lowering a `schema::v221::Price` into a `Price` via `FromSchema`.
//!
//! The schema encodes both price shapes OCPI has used - the 2.2.1 object and the 2.1.1 bare
//! number - so the lowering never has to decide between them. Structural problems (a missing
//! `excl_vat`, a wrong-kind value) are reported by the schema walk, and the lowering only
//! signals that it could not build a value. Each test drives a real `v2.2.1` tariff through
//! `build_tariff` and lowers its `min_price` field.

#![allow(
    clippy::indexing_slicing,
    reason = "unwraps and indexing are allowed anywhere in tests"
)]

use std::assert_matches;

use rust_decimal::Decimal;
use rust_decimal_macros::dec;

use super::{Price, Warning};
use crate::{
    json,
    schema::{self, Integrity},
    warning::{self, test::VerdictTestExt as _},
    FromSchema as _, Verdict,
};

/// A minimal, valid `v2.2.1` tariff. `{PRICE}` is replaced with the value under test.
const TARIFF: &str = r#"{
    "country_code": "NL",
    "party_id": "ENE",
    "currency": "EUR",
    "id": "ID",
    "last_updated": "2024-01-01T00:00:00Z",
    "min_price": {PRICE},
    "elements": [
        {"price_components": [{"price": 0.25, "step_size": 1, "type": "ENERGY", "vat": 21}]}
    ]
}"#;

/// Build a tariff carrying `min_price` and lower it, returning the schema walk's warnings
/// alongside so a test can assert on what the walk reported.
#[track_caller]
fn min_price(price: &str) -> (Verdict<Price, Warning>, warning::Set<schema::Warning>) {
    let src = TARIFF.replace("{PRICE}", price);
    let doc = json::parse(src.as_str().into()).unwrap();
    let (tariff, schema_warnings) = schema::v221::build_tariff(&doc).into_parts();

    let Integrity::Ok(Some(price)) = &tariff.min_price else {
        panic!("min_price should be built: {:?}", tariff.min_price);
    };

    (Price::from_schema(price), schema_warnings)
}

#[test]
fn object_with_only_excl_vat_lowers() {
    const PRICE: &str = r#"{ "excl_vat": 10.2 }"#;

    let (verdict, _schema) = min_price(PRICE);
    let price = verdict.unwrap().unwrap();

    assert!(price.incl_vat.is_none());
    assert_eq!(Decimal::from(price.excl_vat), dec!(10.2));
}

#[test]
fn object_with_excl_and_incl_vat_lowers() {
    const PRICE: &str = r#"{ "excl_vat": 10.2, "incl_vat": 12.3 }"#;

    let (verdict, _schema) = min_price(PRICE);
    let price = verdict.unwrap().unwrap();

    assert_eq!(Decimal::from(price.incl_vat.unwrap()), dec!(12.3));
    assert_eq!(Decimal::from(price.excl_vat), dec!(10.2));
}

#[test]
fn bare_number_lowers_to_excl_vat() {
    // The 2.1.1 shape. The schema accepts it and flags the shape; the lowering reads it as
    // `excl_vat` with no VAT information.
    const PRICE: &str = "12.3";

    let (verdict, schema_warnings) = min_price(PRICE);
    let price = verdict.unwrap().unwrap();

    assert!(price.incl_vat.is_none());
    assert_eq!(Decimal::from(price.excl_vat), dec!(12.3));

    let schema_warnings = schema_warnings.path_map();
    assert_matches!(
        *schema_warnings["$.min_price"],
        [schema::Warning::TypeMismatch { .. }]
    );
}

#[test]
fn object_without_excl_vat_is_rejected() {
    const PRICE: &str = r#"{ "incl_vat": 12.3 }"#;

    let (verdict, schema_warnings) = min_price(PRICE);
    let error = verdict.unwrap_only_error();

    // The lowering cannot build a price, but it does not restate why: the schema walk
    // already located the missing field.
    assert_matches!(error.into_warning(), Warning::Rejected);

    let schema_warnings = schema_warnings.path_map();
    assert_matches!(
        *schema_warnings["$.min_price"],
        [schema::Warning::MissingField { name: "excl_vat" }]
    );
}

#[test]
fn excl_vat_greater_than_incl_vat_warns() {
    const PRICE: &str = r#"{ "excl_vat": 12.3, "incl_vat": 10.2 }"#;

    let (verdict, _schema) = min_price(PRICE);
    let (_price, warnings) = verdict.unwrap().into_parts();

    let warnings = warnings.path_map();
    assert_matches!(
        *warnings["$.min_price"],
        [Warning::ExclusiveVatGreaterThanInclusive]
    );
}