use crate::{json, tariff, Version};
const TARIFF_JSON: &str = include_str!("../tariff.json");
fn explain_v221(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)
.expect("the tariff should be explainable")
.ignore_warnings();
tracing::info!("\n{rendered}\n");
rendered
}
#[test]
fn explain_sample_tariff() {
crate::test::setup();
let json = json::parse_object(TARIFF_JSON).unwrap();
let version = tariff::infer_version(json);
let tariff = tariff::build_versioned(version.unwrap_certain()).ignore_warnings();
let rendered = tariff::explain(&tariff)
.expect("the sample tariff should be explainable")
.ignore_warnings();
tracing::info!("\n{rendered}\n");
assert!(rendered.contains("per kWh"), "{rendered}");
assert!(rendered.contains("**Charging time:**"), "{rendered}");
assert!(
rendered.contains("**Idle time (connected but not charging):**"),
"{rendered}"
);
assert!(!rendered.contains("illed in"), "{rendered}");
}
#[test]
fn explain_tiered_charging_time() {
let rendered = explain_v221(
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("- For the first 3 hours: €0.50 per hour"),
"{rendered}"
);
assert!(
rendered.contains("- For the remaining charging time: €0.75 per hour"),
"{rendered}"
);
}
#[test]
fn explain_energy_tiers_by_kwh() {
let rendered = explain_v221(
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("- For the first 20 kWh: €0.30 per kWh"),
"{rendered}"
);
assert!(
rendered.contains("- For the remaining energy: €0.45 per kWh"),
"{rendered}"
);
}
#[test]
fn explain_free_then_paid_idle_time() {
let rendered = explain_v221(
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("- For the first 4 hours: free"),
"{rendered}"
);
assert!(
rendered.contains("- For the remaining idle time: €3.00 per hour"),
"{rendered}"
);
}
#[test]
fn explain_wrapping_night_window() {
let rendered = explain_v221(
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!(
rendered.contains("between 23:00 and 07:00 the next day, €0.20 per kWh"),
"{rendered}"
);
}
#[test]
fn explain_free_tier_has_no_step_note() {
let rendered = explain_v221(
r#"{
"id": "free-step", "country_code": "NL", "party_id": "TST", "currency": "EUR",
"elements": [
{
"restrictions": { "max_duration": 3600 },
"price_components": [{ "type": "PARKING_TIME", "price": 0, "step_size": 60 }]
},
{
"price_components": [{ "type": "PARKING_TIME", "price": 5.00, "step_size": 300 }]
}
]
}"#,
);
assert!(
rendered.contains("- For the first 1 hour: free"),
"{rendered}"
);
assert!(!rendered.contains("free (billed in"), "{rendered}");
assert!(
rendered.contains("(billed in 300-second steps, rounded up)"),
"{rendered}"
);
}
#[test]
fn explain_equal_start_end_time_is_never() {
let rendered = explain_v221(
r#"{
"id": "never", "country_code": "NL", "party_id": "TST", "currency": "EUR",
"elements": [
{
"restrictions": { "start_time": "08:00", "end_time": "08:00" },
"price_components": [{ "type": "ENERGY", "price": 0.50, "step_size": 1 }]
}
]
}"#,
);
assert!(
rendered.contains("never (its window 08:00 to 08:00 is empty)"),
"{rendered}"
);
assert!(!rendered.contains("between 08:00 and 08:00"), "{rendered}");
}
#[test]
fn explain_weekday_restricted_flat_fee() {
let rendered = explain_v221(
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!(
rendered.contains("**Flat fee:** on Saturday, Sunday, €1.50 per session"),
"{rendered}"
);
}
#[test]
fn explain_small_rate_is_not_shown_as_zero() {
let rendered = explain_v221(
r#"{
"id": "tiny", "country_code": "NL", "party_id": "TST", "currency": "EUR",
"elements": [
{
"price_components": [{ "type": "ENERGY", "price": 0.004, "step_size": 1 }]
}
]
}"#,
);
assert!(
rendered.contains("**Energy:** €0.004 per kWh"),
"{rendered}"
);
assert!(!rendered.contains("€0.00 per kWh"), "{rendered}");
}
#[test]
fn explain_multiple_flat_tiers() {
let rendered = explain_v221(
r#"{
"id": "flat-tiers", "country_code": "NL", "party_id": "TST", "currency": "EUR",
"elements": [
{
"restrictions": { "end_time": "22:00" },
"price_components": [{ "type": "FLAT", "price": 1.00, "step_size": 1 }]
},
{
"price_components": [{ "type": "FLAT", "price": 2.50, "step_size": 1 }]
}
]
}"#,
);
assert!(
rendered.contains("- Before 22:00: €1.00 per session"),
"{rendered}"
);
assert!(
rendered.contains("- Otherwise: €2.50 per session"),
"{rendered}"
);
}
#[test]
fn explain_flat_fee_gated_by_duration() {
let rendered = explain_v221(
r#"{
"id": "flat-dur", "country_code": "NL", "party_id": "TST", "currency": "EUR",
"elements": [
{
"restrictions": { "max_duration": 3600 },
"price_components": [{ "type": "FLAT", "price": 1.00, "step_size": 1 }]
}
]
}"#,
);
assert!(
rendered.contains("**Flat fee:** for the first 1 hour, €1.00 per session"),
"{rendered}"
);
}
#[test]
fn explain_vat_and_price_bounds() {
let rendered = explain_v221(
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 VAT)"),
"{rendered}"
);
assert!(
rendered.contains("at least €1.00 (€1.21 incl. VAT)"),
"{rendered}"
);
assert!(
rendered.contains("never costs more than €50.00 (€60.50 incl. VAT)"),
"{rendered}"
);
}
#[test]
fn explain_validity_window() {
let rendered = explain_v221(
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("only valid from 2024-01-01 00:00 until 2024-12-31 23:00 (UTC)"),
"{rendered}"
);
}
#[test]
fn explain_reservation_only_element_is_excluded() {
let rendered = explain_v221(
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!(
rendered.contains("every element applies only to reservation sessions"),
"{rendered}"
);
assert!(!rendered.contains("€5.00"), "{rendered}");
}
#[test]
fn explain_fallback_no_price_components() {
let rendered = explain_v221(
r#"{
"id": "empty", "country_code": "NL", "party_id": "TST", "currency": "EUR",
"elements": [
{ "price_components": [] }
]
}"#,
);
assert!(
rendered.contains("none of its applicable elements define a price component"),
"{rendered}"
);
}
#[test]
fn explain_fallback_free_flat_only() {
let rendered = explain_v221(
r#"{
"id": "gratis", "country_code": "NL", "party_id": "TST", "currency": "EUR",
"elements": [
{ "price_components": [{ "type": "FLAT", "price": 0, "step_size": 1 }] }
]
}"#,
);
assert!(
rendered.contains("is free: its only charge is a flat fee of zero"),
"{rendered}"
);
}
#[test]
fn explain_unreachable_tier_after_catch_all() {
let rendered = explain_v221(
r#"{
"id": "unreachable", "country_code": "NL", "party_id": "TST", "currency": "EUR",
"elements": [
{
"price_components": [{ "type": "TIME", "price": 0.50, "step_size": 1 }]
},
{
"restrictions": { "max_duration": 10800 },
"price_components": [{ "type": "TIME", "price": 0.75, "step_size": 1 }]
}
]
}"#,
);
assert!(
rendered.contains("Any later tiers never apply"),
"{rendered}"
);
assert!(!rendered.contains("€0.75"), "{rendered}");
}
#[test]
fn explain_mixed_step_size_per_tier() {
let rendered = explain_v221(
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("1-second"), "{rendered}");
assert!(
rendered.contains("(billed in 300-second steps, rounded up)"),
"{rendered}"
);
assert!(!rendered.contains("_Billed in"), "{rendered}");
}
#[test]
fn explain_power_gated_tier_reads_as_otherwise() {
let rendered = explain_v221(
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("- While charging at 50 kW or more: €0.60 per kWh"),
"{rendered}"
);
assert!(
rendered.contains("- Otherwise: €0.40 per kWh"),
"{rendered}"
);
assert!(!rendered.contains("for the remaining energy"), "{rendered}");
}
#[test]
fn explain_duplicate_component_in_element_is_ignored() {
let rendered = explain_v221(
r#"{
"id": "dup", "country_code": "NL", "party_id": "TST", "currency": "EUR",
"elements": [
{
"price_components": [
{ "type": "ENERGY", "price": 0.30, "step_size": 1 },
{ "type": "ENERGY", "price": 0.99, "step_size": 1 }
]
}
]
}"#,
);
assert!(rendered.contains("**Energy:** €0.30 per kWh"), "{rendered}");
assert!(!rendered.contains("0.99"), "{rendered}");
}
#[test]
fn explain_non_euro_currency_symbol() {
let rendered = explain_v221(
r#"{
"id": "usd", "country_code": "US", "party_id": "TST", "currency": "USD",
"elements": [
{
"price_components": [{ "type": "ENERGY", "price": 0.35, "step_size": 1 }]
}
]
}"#,
);
assert!(rendered.contains("$0.35 per kWh"), "{rendered}");
}
#[test]
fn explain_banded_duration_window() {
let rendered = explain_v221(
r#"{
"id": "banded", "country_code": "NL", "party_id": "TST", "currency": "EUR",
"elements": [
{
"restrictions": { "min_duration": 3600, "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("- Between 1 hour and 3 hours into the session: €0.50 per hour"),
"{rendered}"
);
assert!(
rendered.contains("- For the remaining charging time: €0.75 per hour"),
"{rendered}"
);
}
#[test]
fn explain_idle_charge_starts_after_delay() {
let rendered = explain_v221(
r#"{
"id": "delayed-idle", "country_code": "NL", "party_id": "TST", "currency": "EUR",
"elements": [
{
"restrictions": { "min_duration": 14400 },
"price_components": [{ "type": "PARKING_TIME", "price": 2.00, "step_size": 60 }]
}
]
}"#,
);
assert!(
rendered.contains(
"**Idle time (connected but not charging):** after the first 4 hours, €2.00 per hour"
),
"{rendered}"
);
}
#[test]
fn explain_stacked_qualifiers() {
let rendered = explain_v221(
r#"{
"id": "stacked", "country_code": "NL", "party_id": "TST", "currency": "EUR",
"elements": [
{
"restrictions": {
"start_time": "07:00",
"end_time": "19:00",
"day_of_week": ["MONDAY", "FRIDAY"],
"start_date": "2024-06-01"
},
"price_components": [{ "type": "ENERGY", "price": 0.50, "step_size": 1 }]
}
]
}"#,
);
assert!(
rendered.contains(
"**Energy:** between 07:00 and 19:00, on Monday, Friday, from 2024-06-01 onwards, €0.50 per kWh"
),
"{rendered}"
);
}
#[test]
fn explain_bound_and_qualifier_together() {
let rendered = explain_v221(
r#"{
"id": "bound-and-qual", "country_code": "NL", "party_id": "TST", "currency": "EUR",
"elements": [
{
"restrictions": { "max_duration": 10800, "day_of_week": ["SATURDAY", "SUNDAY"] },
"price_components": [{ "type": "TIME", "price": 0.50, "step_size": 1 }]
},
{
"price_components": [{ "type": "TIME", "price": 0.75, "step_size": 1 }]
}
]
}"#,
);
assert!(
rendered.contains("- On Saturday, Sunday, for the first 3 hours: €0.50 per hour"),
"{rendered}"
);
}
#[test]
fn explain_duration_humanized_to_hours_and_minutes() {
let rendered = explain_v221(
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("for the first 1 hour 30 minutes, €0.10 per hour"),
"{rendered}"
);
}
#[test]
fn explain_time_tier_surfaces_energy_threshold() {
let rendered = explain_v221(
r#"{
"id": "kwh-gate", "country_code": "NL", "party_id": "TST", "currency": "EUR",
"elements": [
{
"restrictions": { "min_kwh": 5 },
"price_components": [{ "type": "TIME", "price": 0.30, "step_size": 60 }]
},
{
"price_components": [{ "type": "TIME", "price": 0, "step_size": 1 }]
}
]
}"#,
);
assert!(
rendered.contains("- After the first 5 kWh: €0.30 per hour"),
"{rendered}"
);
}
#[test]
fn explain_energy_tier_surfaces_duration_bound() {
let rendered = explain_v221(
r#"{
"id": "dur-gate", "country_code": "NL", "party_id": "TST", "currency": "EUR",
"elements": [
{
"restrictions": { "max_duration": 3600 },
"price_components": [{ "type": "ENERGY", "price": 0.20, "step_size": 1 }]
},
{
"price_components": [{ "type": "ENERGY", "price": 0.30, "step_size": 1 }]
}
]
}"#,
);
assert!(
rendered.contains("- For the first 1 hour: €0.20 per kWh"),
"{rendered}"
);
assert!(
rendered.contains("- Otherwise: €0.30 per kWh"),
"{rendered}"
);
}