ocpi-tariffs 0.49.0

OCPI tariff calculations
Documentation
#![allow(clippy::missing_panics_doc, reason = "tests are allowed to panic")]
#![allow(clippy::panic, reason = "tests are allowed to panic")]

//! Tests for `min_current`/`max_current` and power tariff restrictions.
//!
//! Each restriction is checked against the correct bound of the period's
//! observed range:
//!
//!   - `min_current`: element applies when `current_max >= threshold`
//!     (the peak reached the threshold at some point in the period)
//!   - `max_current`: element applies when `current_min < threshold`
//!     (the trough stayed below the cap at some point in the period)
//!   - `min_power` / `max_power`: same logic, using `power_max` / `power_min`

use rust_decimal_macros::dec;

use crate::{
    cdr, json, price,
    price::test::UnwrapReport as _,
    tariff,
    test::{self},
    Version,
};

// ---------------------------------------------------------------------------
// CDR / tariff helpers
// ---------------------------------------------------------------------------

/// CDR with a single period that carries explicit `MIN_CURRENT` and `MAX_CURRENT`
/// dimensions alongside an ENERGY dimension, so the Consumed struct gets both
/// `current_min` and `current_max` populated.
fn cdr_with_current(
    energy_kwh: f64,
    min_current_amp: f64,
    max_current_amp: f64,
    total_cost_excl: f64,
    total_cost_incl: f64,
    total_energy_cost_excl: f64,
    total_energy_cost_incl: f64,
) -> String {
    serde_json::json!({
        "country_code": "NL",
        "party_id": "TDR",
        "start_date_time": "2022-01-13T10:00:00Z",
        "end_date_time": "2022-01-13T11:00:00Z",
        "currency": "EUR",
        "tariffs": [],
        "cdr_location": { "country": "NLD" },
        "charging_periods": [{
            "start_date_time": "2022-01-13T10:00:00Z",
            "dimensions": [
                { "type": "ENERGY",      "volume": energy_kwh      },
                { "type": "MIN_CURRENT", "volume": min_current_amp },
                { "type": "MAX_CURRENT", "volume": max_current_amp }
            ]
        }],
        "total_cost":        { "excl_vat": total_cost_excl,        "incl_vat": total_cost_incl        },
        "total_energy_cost": { "excl_vat": total_energy_cost_excl, "incl_vat": total_energy_cost_incl },
        "total_time":   1.0,
        "total_energy": energy_kwh,
        "last_updated": "2022-01-13T00:00:00Z"
    })
    .to_string()
}

/// CDR with a single period that carries explicit `MIN_POWER` and `MAX_POWER`
/// dimensions alongside ENERGY.
fn cdr_with_power(
    energy_kwh: f64,
    min_power_kw: f64,
    max_power_kw: f64,
    total_cost_excl: f64,
    total_cost_incl: f64,
    total_energy_cost_excl: f64,
    total_energy_cost_incl: f64,
) -> String {
    serde_json::json!({
        "country_code": "NL",
        "party_id": "TDR",
        "start_date_time": "2022-01-13T10:00:00Z",
        "end_date_time": "2022-01-13T11:00:00Z",
        "currency": "EUR",
        "tariffs": [],
        "cdr_location": { "country": "NLD" },
        "charging_periods": [{
            "start_date_time": "2022-01-13T10:00:00Z",
            "dimensions": [
                { "type": "ENERGY",    "volume": energy_kwh  },
                { "type": "MIN_POWER", "volume": min_power_kw },
                { "type": "MAX_POWER", "volume": max_power_kw }
            ]
        }],
        "total_cost":        { "excl_vat": total_cost_excl,        "incl_vat": total_cost_incl        },
        "total_energy_cost": { "excl_vat": total_energy_cost_excl, "incl_vat": total_energy_cost_incl },
        "total_time":   1.0,
        "total_energy": energy_kwh,
        "last_updated": "2022-01-13T00:00:00Z"
    })
    .to_string()
}

/// Tariff with two elements:
///  - element 0: `min_current` restriction + HIGH price (0.80 EUR/kWh)
///  - element 1: default (no restriction) + LOW price (0.20 EUR/kWh)
fn tariff_min_current(threshold_amp: f64) -> String {
    serde_json::json!({
        "id": "T_MIN_CURRENT",
        "country_code": "NL",
        "party_id": "TDR",
        "currency": "EUR",
        "elements": [
            {
                "restrictions": { "min_current": threshold_amp },
                "price_components": [{ "type": "ENERGY", "price": 0.80, "vat": 10.0, "step_size": 1 }]
            },
            {
                "price_components": [{ "type": "ENERGY", "price": 0.20, "vat": 10.0, "step_size": 1 }]
            }
        ],
        "last_updated": "2022-01-01T00:00:00Z"
    })
    .to_string()
}

/// Tariff with two elements:
///  - element 0: `max_current` restriction + LOW price (0.20 EUR/kWh)
///  - element 1: default (no restriction) + HIGH price (0.80 EUR/kWh)
fn tariff_max_current(threshold_amp: f64) -> String {
    serde_json::json!({
        "id": "T_MAX_CURRENT",
        "country_code": "NL",
        "party_id": "TDR",
        "currency": "EUR",
        "elements": [
            {
                "restrictions": { "max_current": threshold_amp },
                "price_components": [{ "type": "ENERGY", "price": 0.20, "vat": 10.0, "step_size": 1 }]
            },
            {
                "price_components": [{ "type": "ENERGY", "price": 0.80, "vat": 10.0, "step_size": 1 }]
            }
        ],
        "last_updated": "2022-01-01T00:00:00Z"
    })
    .to_string()
}

/// Tariff with two elements:
///  - element 0: `min_power` restriction + HIGH price (0.80 EUR/kWh)
///  - element 1: default (no restriction) + LOW price (0.20 EUR/kWh)
fn tariff_min_power(threshold_kw: f64) -> String {
    serde_json::json!({
        "id": "T_MIN_POWER",
        "country_code": "NL",
        "party_id": "TDR",
        "currency": "EUR",
        "elements": [
            {
                "restrictions": { "min_power": threshold_kw },
                "price_components": [{ "type": "ENERGY", "price": 0.80, "vat": 10.0, "step_size": 1 }]
            },
            {
                "price_components": [{ "type": "ENERGY", "price": 0.20, "vat": 10.0, "step_size": 1 }]
            }
        ],
        "last_updated": "2022-01-01T00:00:00Z"
    })
    .to_string()
}

/// Tariff with two elements:
///  - element 0: `max_power` restriction + LOW price (0.20 EUR/kWh)
///  - element 1: default (no restriction) + HIGH price (0.80 EUR/kWh)
fn tariff_max_power(threshold_kw: f64) -> String {
    serde_json::json!({
        "id": "T_MAX_POWER",
        "country_code": "NL",
        "party_id": "TDR",
        "currency": "EUR",
        "elements": [
            {
                "restrictions": { "max_power": threshold_kw },
                "price_components": [{ "type": "ENERGY", "price": 0.20, "vat": 10.0, "step_size": 1 }]
            },
            {
                "price_components": [{ "type": "ENERGY", "price": 0.80, "vat": 10.0, "step_size": 1 }]
            }
        ],
        "last_updated": "2022-01-01T00:00:00Z"
    })
    .to_string()
}

fn price_cdr(cdr_json: &str, tariff_json: &str) -> price::Report {
    let (cdr, _) = cdr::build(json::parse_object(cdr_json).unwrap(), Version::V221).into_parts();
    let (tariff, _) =
        tariff::build(json::parse_object(tariff_json).unwrap(), Version::V221).into_parts();
    let report = cdr::price(
        &cdr,
        price::TariffSource::Override(vec![tariff]),
        chrono_tz::Tz::UTC,
    )
    .unwrap_report(cdr.as_json_str());
    let (report, warnings) = report.into_parts();
    assert!(warnings.is_empty(), "{:#?}", warnings.path_id_map());
    report
}

// ---------------------------------------------------------------------------
// `min_current` tests
// ---------------------------------------------------------------------------

/// Period current range 10-20 Amps, threshold 15 Amps.
/// `current_max` (20) >= 15 → restricted element (0.80) applies.
#[test]
fn min_current_applies_when_peak_meets_threshold() {
    test::setup();
    // 10 kWh * 0.80 = 8.00 exclusive, 8.80 inclusive.
    let cdr_json = cdr_with_current(10.0, 10.0, 20.0, 8.00, 8.80, 8.00, 8.80);
    let report = price_cdr(&cdr_json, &tariff_min_current(15.0));
    let cost = report.total_cost.calculated.unwrap();
    assert_eq!(cost.excl_vat, dec!(8.00).into());
    assert_eq!(cost.incl_vat, Some(dec!(8.80).into()));
}

/// Period current range 10-14 A, threshold 15 Amps.
/// `current_max` (14) < 15 → restricted element does not apply; default (0.20) used.
#[test]
fn min_current_does_not_apply_when_peak_below_threshold() {
    test::setup();
    // 10 kWh * 0.20 = 2.00 exclusive, 2.20 inclusive.
    let cdr_json = cdr_with_current(10.0, 10.0, 14.0, 2.00, 2.20, 2.00, 2.20);
    let report = price_cdr(&cdr_json, &tariff_min_current(15.0));
    let cost = report.total_cost.calculated.unwrap();
    assert_eq!(cost.excl_vat, dec!(2.00).into());
    assert_eq!(cost.incl_vat, Some(dec!(2.20).into()));
}

// ---------------------------------------------------------------------------
// max_current tests
// ---------------------------------------------------------------------------

/// Period current range 10-20 A, threshold 15 Amps.
/// `current_min` (10) < 15 → restricted element (0.20) applies.
#[test]
fn max_current_applies_when_trough_below_threshold() {
    test::setup();
    // 10 kWh * 0.20 = 2.00 exclusive, 2.20 inclusive.
    let cdr_json = cdr_with_current(10.0, 10.0, 20.0, 2.00, 2.20, 2.00, 2.20);
    let report = price_cdr(&cdr_json, &tariff_max_current(15.0));
    let cost = report.total_cost.calculated.unwrap();
    assert_eq!(cost.excl_vat, dec!(2.00).into());
    assert_eq!(cost.incl_vat, Some(dec!(2.20).into()));
}

/// Period current range 16-20 A, threshold 15 Amps.
/// `current_min` (16) >= 15 → restricted element does not apply; default (0.80) used.
#[test]
fn max_current_does_not_apply_when_trough_meets_or_exceeds_threshold() {
    test::setup();
    // 10 kWh * 0.80 = 8.00 exclusive., 8.80 inclusive.
    let cdr_json = cdr_with_current(10.0, 16.0, 20.0, 8.00, 8.80, 8.00, 8.80);
    let report = price_cdr(&cdr_json, &tariff_max_current(15.0));
    let cost = report.total_cost.calculated.unwrap();
    assert_eq!(cost.excl_vat, dec!(8.00).into());
    assert_eq!(cost.incl_vat, Some(dec!(8.80).into()));
}

// ---------------------------------------------------------------------------
// `min_power` tests
// ---------------------------------------------------------------------------

/// Period power range 5-15 kW, threshold 10 kW.
/// `power_max` (15) >= 10 → restricted element (0.80) applies.
#[test]
fn min_power_applies_when_peak_meets_threshold() {
    test::setup();
    // 10 kWh * 0.80 = 8.00 exclusive, 8.80 inclusive.
    let cdr_json = cdr_with_power(10.0, 5.0, 15.0, 8.00, 8.80, 8.00, 8.80);
    let report = price_cdr(&cdr_json, &tariff_min_power(10.0));
    let cost = report.total_cost.calculated.unwrap();
    assert_eq!(cost.excl_vat, dec!(8.00).into());
    assert_eq!(cost.incl_vat, Some(dec!(8.80).into()));
}

/// Period power range 5-9 kW, threshold 10 kW.
/// `power_max` (9) < 10 → restricted element does not apply; default (0.20) used.
#[test]
fn min_power_does_not_apply_when_peak_below_threshold() {
    test::setup();
    // 10 kWh * 0.20 = 2.00 exclusive, 2.20 inclusive.
    let cdr_json = cdr_with_power(10.0, 5.0, 9.0, 2.00, 2.20, 2.00, 2.20);
    let report = price_cdr(&cdr_json, &tariff_min_power(10.0));
    let cost = report.total_cost.calculated.unwrap();
    assert_eq!(cost.excl_vat, dec!(2.00).into());
    assert_eq!(cost.incl_vat, Some(dec!(2.20).into()));
}

// ---------------------------------------------------------------------------
// max_power tests
// ---------------------------------------------------------------------------

/// Period power range 5-15 kW, threshold 10 kW.
/// `power_min` (5) < 10 → restricted element (0.20) applies.
#[test]
fn max_power_applies_when_trough_below_threshold() {
    test::setup();
    // 10 kWh * 0.20 = 2.00 exclusive, 2.20 inclusive.
    let cdr_json = cdr_with_power(10.0, 5.0, 15.0, 2.00, 2.20, 2.00, 2.20);
    let report = price_cdr(&cdr_json, &tariff_max_power(10.0));
    let cost = report.total_cost.calculated.unwrap();
    assert_eq!(cost.excl_vat, dec!(2.00).into());
    assert_eq!(cost.incl_vat, Some(dec!(2.20).into()));
}

/// Period power range 11-15 kW, threshold 10 kW.
/// `power_min` (11) >= 10 → restricted element does not apply; default (0.80) used.
#[test]
fn max_power_does_not_apply_when_trough_meets_or_exceeds_threshold() {
    test::setup();
    // 10 kWh * 0.80 = 8.00 exclusive, 8.80 inclusive.
    let cdr_json = cdr_with_power(10.0, 11.0, 15.0, 8.00, 8.80, 8.00, 8.80);
    let report = price_cdr(&cdr_json, &tariff_max_power(10.0));
    let cost = report.total_cost.calculated.unwrap();
    assert_eq!(cost.excl_vat, dec!(8.00).into());
    assert_eq!(cost.incl_vat, Some(dec!(8.80).into()));
}