use bon::Builder;
use serde::{Deserialize, Serialize};
use crate::types::validate_fields;
use crate::types::{
Currency, DateTime, DisplayText, Extensions, LocalDate, LocalTime, Number, OcpiString, Url, Validate,
Validator, ViolationCode,
};
use super::locations::EnergyMix;
pub use crate::v2_3_0::tariffs::{DayOfWeek, TariffDimensionType};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[builder(on(_, into))]
pub struct Tariff {
pub id: OcpiString<36>,
pub currency: Currency,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[builder(default)]
pub tariff_alt_text: Vec<DisplayText>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tariff_alt_url: Option<Url>,
pub elements: Vec<TariffElement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub energy_mix: Option<EnergyMix>,
pub last_updated: DateTime,
#[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
#[builder(default)]
pub extensions: Extensions,
}
impl Tariff {
#[must_use]
pub fn is_free_of_charge(&self) -> bool {
match self.elements.as_slice() {
[element] if element.restrictions.is_none() => match element.price_components.as_slice() {
[pc] => pc.component_type == TariffDimensionType::Flat && pc.price.is_zero(),
_ => false,
},
_ => false,
}
}
}
impl Validate for Tariff {
fn validate_in(&self, v: &mut Validator) {
validate_fields!(
self,
v,
id,
currency,
tariff_alt_text,
tariff_alt_url,
elements,
energy_mix,
last_updated,
);
if self.elements.is_empty() {
v.report_at(
"elements",
ViolationCode::EmptyRequiredList,
"a Tariff has cardinality `+` elements: at least one is required",
);
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[builder(on(_, into))]
pub struct TariffElement {
pub price_components: Vec<PriceComponent>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub restrictions: Option<TariffRestrictions>,
#[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
#[builder(default)]
pub extensions: Extensions,
}
impl Validate for TariffElement {
fn validate_in(&self, v: &mut Validator) {
validate_fields!(self, v, price_components, restrictions);
if self.price_components.is_empty() {
v.report_at(
"price_components",
ViolationCode::EmptyRequiredList,
"a TariffElement has cardinality `+` price_components",
);
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct PriceComponent {
#[serde(rename = "type")]
pub component_type: TariffDimensionType,
pub price: Number,
pub step_size: u32,
#[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
pub extensions: Extensions,
}
impl PriceComponent {
#[must_use]
pub fn new(component_type: TariffDimensionType, price: Number) -> Self {
Self { component_type, price, step_size: 1, extensions: Extensions::new() }
}
}
impl Validate for PriceComponent {
fn validate_in(&self, v: &mut Validator) {
validate_fields!(self, v, component_type as "type", price);
if self.step_size == 0 && self.component_type.step_size_unit().is_some() {
v.report_at(
"step_size",
ViolationCode::OutOfRange,
"a step_size of 0 would bill nothing for a dimension that has a unit",
);
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Builder)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[builder(on(_, into))]
pub struct TariffRestrictions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start_time: Option<LocalTime>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end_time: Option<LocalTime>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start_date: Option<LocalDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end_date: Option<LocalDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_kwh: Option<Number>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_kwh: Option<Number>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_power: Option<Number>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_power: Option<Number>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_duration: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_duration: Option<u64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[builder(default)]
pub day_of_week: Vec<DayOfWeek>,
#[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
#[builder(default)]
pub extensions: Extensions,
}
impl Validate for TariffRestrictions {
fn validate_in(&self, v: &mut Validator) {
validate_fields!(
self,
v,
start_time,
end_time,
start_date,
end_date,
min_kwh,
max_kwh,
min_power,
max_power,
day_of_week,
);
for (lo_name, lo, hi_name, hi) in [
("min_kwh", self.min_kwh, "max_kwh", self.max_kwh),
("min_power", self.min_power, "max_power", self.max_power),
] {
if let (Some(lo_v), Some(hi_v)) = (lo, hi)
&& hi_v <= lo_v
{
v.report_at(
hi_name,
ViolationCode::Inconsistent,
format!("{hi_name} is not above {lo_name}, so this element can never apply"),
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_2_1_1_price_component_cannot_express_vat() {
let json = r#"{"type":"ENERGY","price":0.25,"step_size":1}"#;
let component: PriceComponent = serde_json::from_str(json).unwrap();
assert_eq!(serde_json::to_string(&component).unwrap(), json);
let with_vat: PriceComponent =
serde_json::from_str(r#"{"type":"ENERGY","price":0.25,"step_size":1,"vat":10}"#).unwrap();
assert_eq!(with_vat.extensions.get::<u32>("vat").unwrap(), Some(10));
}
#[test]
fn a_free_of_charge_tariff_has_the_usual_shape() {
let tariff = Tariff::builder()
.id("15")
.currency("EUR")
.elements(vec![
TariffElement::builder()
.price_components(vec![PriceComponent::new(TariffDimensionType::Flat, Number::ZERO)])
.build(),
])
.last_updated("2015-06-29T20:39:09Z".parse::<DateTime>().unwrap())
.build();
assert!(tariff.is_free_of_charge());
assert!(tariff.validate().is_ok());
}
}