ocpi-tariffs 0.51.0

OCPI tariff calculations
Documentation
//! Dutch (`nl-NL`) renderings of `explain`.
//!
//! These mirror a representative subset of the English tests in [`super::test`], checking that the
//! Dutch arm of the renderer produces the expected vocabulary, decimal-comma number formatting and
//! word order. The semantic decisions are shared with English (and covered there), so these focus
//! on the language surface.

use crate::{json, tariff, Language, Version};

/// Parse a `v2.2.1` tariff JSON, explain it in Dutch, and return the rendered Markdown.
fn explain_nl(json: &str) -> String {
    crate::test::setup();

    let json = json::parse_object(json).unwrap();
    let tariff = tariff::build(json, Version::V221).ignore_warnings();

    let rendered = tariff::explain(&tariff, Language::NlNl)
        .expect("the tariff should be explainable")
        .ignore_warnings();

    tracing::info!("\n{rendered}\n");

    rendered
}

/// Amounts use a decimal comma and the currency symbol is spaced from the amount.
#[test]
fn dutch_energy_rate_uses_decimal_comma() {
    let rendered = explain_nl(
        r#"{
            "id": "energy", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "price_components": [{ "type": "ENERGY", "price": 0.30, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert_eq!(rendered, "**Energietarief:** € 0,30 per kWh.");
}

/// Two charging-time tiers read as a bulleted list with a "remaining charging time" catch-all.
#[test]
fn dutch_tiered_charging_time() {
    let rendered = explain_nl(
        r#"{
            "id": "tiered", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "max_duration": 10800 },
                    "price_components": [{ "type": "TIME", "price": 0.50, "step_size": 1 }]
                },
                {
                    "price_components": [{ "type": "TIME", "price": 0.75, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("- Het eerste 3 uur: € 0,50 per uur"),
        "{rendered}"
    );
    assert!(
        rendered.contains("- De resterende laadtijd: € 0,75 per uur"),
        "{rendered}"
    );
}

/// Energy tiers are bounded by consumed kWh.
#[test]
fn dutch_energy_tiers_by_kwh() {
    let rendered = explain_nl(
        r#"{
            "id": "energy-tiers", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "max_kwh": 20 },
                    "price_components": [{ "type": "ENERGY", "price": 0.30, "step_size": 1 }]
                },
                {
                    "price_components": [{ "type": "ENERGY", "price": 0.45, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("- Het eerste 20 kWh: € 0,30 per kWh"),
        "{rendered}"
    );
    assert!(
        rendered.contains("- De resterende energie: € 0,45 per kWh"),
        "{rendered}"
    );
}

/// A zero rate is narrated as "gratis".
#[test]
fn dutch_free_then_paid_idle_time() {
    let rendered = explain_nl(
        r#"{
            "id": "free-idle", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "max_duration": 14400 },
                    "price_components": [{ "type": "PARKING_TIME", "price": 0, "step_size": 60 }]
                },
                {
                    "price_components": [{ "type": "PARKING_TIME", "price": 3.00, "step_size": 60 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("**Parkeertijd (aangesloten maar niet aan het laden):**"),
        "{rendered}"
    );
    assert!(
        rendered.contains("- Het eerste 4 uur: gratis"),
        "{rendered}"
    );
    assert!(
        rendered.contains("- De resterende parkeertijd: € 3,00 per uur"),
        "{rendered}"
    );
    assert!(
        rendered.contains("_Berekend in stappen van 1 minuut, naar boven afgerond._"),
        "{rendered}"
    );
}

/// A window whose end precedes its start wraps past midnight.
#[test]
fn dutch_wrapping_night_window() {
    let rendered = explain_nl(
        r#"{
            "id": "night", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "start_time": "23:00", "end_time": "07:00" },
                    "price_components": [{ "type": "ENERGY", "price": 0.20, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert_eq!(
        rendered,
        "**Energietarief:** tussen 23:00 en 07:00 de volgende dag, € 0,20 per kWh."
    );
}

/// A weekend-restricted flat fee lists the days, lowercased as Dutch weekdays are.
#[test]
fn dutch_weekday_restricted_flat_fee() {
    let rendered = explain_nl(
        r#"{
            "id": "weekend", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "day_of_week": ["SATURDAY", "SUNDAY"] },
                    "price_components": [{ "type": "FLAT", "price": 1.50, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert_eq!(
        rendered,
        "**Vast tarief:** op zaterdag, zondag, € 1,50 per sessie."
    );
}

/// A power-gated tier followed by a catch-all reads as "Anders", not a "remaining" phrase.
#[test]
fn dutch_power_gated_tier_reads_as_otherwise() {
    let rendered = explain_nl(
        r#"{
            "id": "fast", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "min_power": 50 },
                    "price_components": [{ "type": "ENERGY", "price": 0.60, "step_size": 1 }]
                },
                {
                    "price_components": [{ "type": "ENERGY", "price": 0.40, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("- Bij laden op 50 kW of meer: € 0,60 per kWh"),
        "{rendered}"
    );
    assert!(rendered.contains("- Anders: € 0,40 per kWh"), "{rendered}");
    assert!(!rendered.contains("resterende"), "{rendered}");
}

/// A per-tier billing step is noted inline in Dutch.
#[test]
fn dutch_mixed_step_size_per_tier() {
    let rendered = explain_nl(
        r#"{
            "id": "mixed-step", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "max_duration": 10800 },
                    "price_components": [{ "type": "TIME", "price": 0.50, "step_size": 1 }]
                },
                {
                    "price_components": [{ "type": "TIME", "price": 0.75, "step_size": 300 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains(
            "- De resterende laadtijd: € 0,75 per uur (berekend in stappen van 5 minuten, naar \
             boven afgerond)"
        ),
        "{rendered}"
    );
}

/// VAT and the overall price bounds are narrated in Dutch, with VAT-inclusive amounts.
#[test]
fn dutch_vat_and_price_bounds() {
    let rendered = explain_nl(
        r#"{
            "id": "bounds", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "min_price": { "excl_vat": 1.00, "incl_vat": 1.21 },
            "max_price": { "excl_vat": 50.00, "incl_vat": 60.50 },
            "elements": [
                {
                    "price_components": [{ "type": "ENERGY", "price": 0.25, "vat": 21.0, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("€ 0,25 per kWh (excl. 21 btw)"),
        "{rendered}"
    );
    assert!(
        rendered.contains("Een sessie kost altijd minstens € 1,00 (€ 1,21 incl. btw)."),
        "{rendered}"
    );
    assert!(
        rendered.contains("Een sessie kost nooit meer dan € 50,00 (€ 60,50 incl. btw)."),
        "{rendered}"
    );
}

/// The tariff's own validity window is narrated in Dutch.
#[test]
fn dutch_validity_window() {
    let rendered = explain_nl(
        r#"{
            "id": "seasonal", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "start_date_time": "2024-01-01T00:00:00Z",
            "end_date_time": "2024-12-31T23:00:00Z",
            "elements": [
                {
                    "price_components": [{ "type": "ENERGY", "price": 0.40, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains(
            "Dit tarief is alleen geldig van 2024-01-01 00:00 tot 2024-12-31 23:00 (UTC)."
        ),
        "{rendered}"
    );
}

/// A duration is humanized with Dutch units; note "uur" does not inflect for number.
#[test]
fn dutch_duration_humanized_to_hours_and_minutes() {
    let rendered = explain_nl(
        r#"{
            "id": "ninety", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "max_duration": 5400 },
                    "price_components": [{ "type": "TIME", "price": 0.10, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert!(
        rendered.contains("het eerste 1 uur 30 minuten, € 0,10 per uur"),
        "{rendered}"
    );
}

/// A reservation-only element leaves nothing to charge; the fallback is explained in Dutch.
#[test]
fn dutch_reservation_only_fallback() {
    let rendered = explain_nl(
        r#"{
            "id": "reservation", "country_code": "NL", "party_id": "TST", "currency": "EUR",
            "elements": [
                {
                    "restrictions": { "reservation": "RESERVATION" },
                    "price_components": [{ "type": "TIME", "price": 5.00, "step_size": 1 }]
                }
            ]
        }"#,
    );

    assert_eq!(
        rendered,
        "Dit tarief brengt nooit kosten in rekening voor een gewone laadsessie: elk element geldt \
         alleen voor reserveringssessies."
    );
}