ocpi-tariffs 0.43.0

OCPI tariff calculations
Documentation
use assert_matches::assert_matches;
use chrono::TimeDelta;

use crate::{
    assert_approx_eq,
    generate::{self, PartialCdr},
    tariff,
    warning::test::VerdictTestExt as _,
    Kwh, Money, Price,
};

use super::test;

const DATE: &str = "2025-11-10";
const TARIFF_JSON: &str = r#"{
    "country_code": "DE",
    "party_id": "ALL",
    "id": "1",
    "currency": "EUR",
    "type": "REGULAR",
    "elements": [
        {
            "price_components": [{
                  "type": "ENERGY",
                  "price": 0.50,
                  "vat": 20.0,
                  "step_size": 1
            }]
        }
    ],
    "last_updated": "2018-12-05T12:01:09Z"
}
"#;

fn generate_config() -> generate::Config {
    generate::Config {
        timezone: chrono_tz::Europe::Amsterdam,
        start_date_time: test::datetime_utc(DATE, "15:02:12"),
        end_date_time: test::datetime_utc(DATE, "15:12:12"),
        max_power_supply_kw: 12.into(),
        requested_kwh: 80.into(),
        max_current_supply_amp: 2.into(),
    }
}

#[track_caller]
fn generate(tariff_json: &str) -> generate::Caveat<generate::Report> {
    let tariff = tariff::parse(tariff_json).unwrap().unwrap_certain();
    generate::cdr_from_tariff(&tariff, generate_config()).unwrap()
}

#[test]
fn should_warn_duration_below_min() {
    let tariff = tariff::parse(TARIFF_JSON).unwrap().unwrap_certain();
    let config = generate::Config {
        timezone: chrono_tz::Europe::Amsterdam,
        start_date_time: test::datetime_utc(DATE, "15:02:12"),
        end_date_time: test::datetime_utc(DATE, "15:03:12"),
        max_power_supply_kw: 12.into(),
        requested_kwh: 80.into(),
        max_current_supply_amp: 2.into(),
    };
    let failure = generate::cdr_from_tariff(&tariff, config).unwrap_only_error();
    assert_matches!(
        failure.into_warning(),
        generate::Warning::DurationBelowMinimum
    );
}

#[test]
fn should_warn_end_before_start() {
    let tariff = tariff::parse(TARIFF_JSON).unwrap().unwrap_certain();
    let config = generate::Config {
        timezone: chrono_tz::Europe::Amsterdam,
        start_date_time: test::datetime_utc(DATE, "15:12:12"),
        end_date_time: test::datetime_utc(DATE, "15:02:12"),
        max_power_supply_kw: 12.into(),
        requested_kwh: 80.into(),
        max_current_supply_amp: 2.into(),
    };
    let failure = generate::cdr_from_tariff(&tariff, config).unwrap_only_error();
    assert_matches!(
        failure.into_warning(),
        generate::Warning::StartDateTimeIsAfterEndDateTime
    );
}

#[test]
fn should_generate_energy_for_ten_minutes() {
    let report = generate(TARIFF_JSON);
    let (report, warnings) = report.into_parts();
    assert!(warnings.is_empty(), "{:#?}", warnings.path_id_map());

    let PartialCdr {
        cpo_country_code: _,
        party_id: _,
        start_date_time: _,
        end_date_time: _,
        cpo_currency_code: _,
        total_energy,
        total_charging_duration,
        total_parking_duration,
        total_cost,
        total_energy_cost,
        total_fixed_cost,
        total_parking_duration_cost,
        total_charging_duration_cost,
        charging_periods: _,
    } = report.partial_cdr;

    assert_approx_eq!(
        total_cost,
        Some(Price {
            excl_vat: Money::from(1),
            incl_vat: Some(Money::from(1.2))
        })
    );
    assert_eq!(
        total_charging_duration,
        Some(TimeDelta::minutes(10)),
        "The charging session is 10 min and is stopped before the battery is fully charged."
    );
    assert_eq!(
        total_parking_duration, None,
        "There is no parking time since the battery never fully charged."
    );
    assert_approx_eq!(total_energy, Some(Kwh::from(2)));
    assert_approx_eq!(
        total_energy_cost,
        Some(Price {
            excl_vat: Money::from(1),
            incl_vat: Some(Money::from(1.2))
        }),
        "The cost per KwH is 50 cents and the VAT is 20%."
    );
    assert_eq!(total_fixed_cost, None, "There are no fixed costs.");
    assert_eq!(
        total_parking_duration_cost, None,
        "There is no parking cost as there is no parking time."
    );
    assert_eq!(
        total_charging_duration_cost, None,
        "There are no time costs defined in the tariff."
    );
}