use bon::Builder;
use serde::{Deserialize, Serialize};
use crate::types::validate_fields;
use crate::types::{
CiString, CountryCode, Currency, DateTime, DisplayText, Extensions, PartyId, PartyRef, Url, Validate,
Validator, ViolationCode,
};
use super::locations::EnergyMix;
use super::types::Price;
pub use crate::v2_3_0::tariffs::{
DayOfWeek, PriceComponent, ReservationRestrictionType, TariffDimensionType, TariffElement,
TariffRestrictions, TariffType,
};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[builder(on(_, into))]
pub struct Tariff {
pub country_code: CountryCode,
pub party_id: PartyId,
pub id: CiString<36>,
pub currency: Currency,
#[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
pub tariff_type: Option<TariffType>,
#[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>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_price: Option<Price>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_price: Option<Price>,
pub elements: Vec<TariffElement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start_date_time: Option<DateTime>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end_date_time: Option<DateTime>,
#[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 owner_party(&self) -> PartyRef {
PartyRef { country_code: self.country_code.clone(), party_id: self.party_id.clone() }
}
#[must_use]
pub fn is_active_at(&self, instant: DateTime) -> bool {
self.start_date_time.is_none_or(|s| instant >= s) && self.end_date_time.is_none_or(|e| instant < e)
}
#[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, country_code, party_id, id, currency, tariff_type as "type", tariff_alt_text,
tariff_alt_url, min_price, max_price, elements, start_date_time, end_date_time,
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",
);
}
if let (Some(start), Some(end)) = (self.start_date_time, self.end_date_time)
&& end <= start
{
v.report_at(
"end_date_time",
ViolationCode::Inconsistent,
"a tariff's validity window must be non-empty",
);
}
if let (Some(min), Some(max)) = (self.min_price.as_ref(), self.max_price.as_ref())
&& max.excl_vat < min.excl_vat
{
v.report_at(
"max_price",
ViolationCode::Inconsistent,
"max_price.excl_vat is below min_price.excl_vat",
);
}
for (i, element) in self.elements.iter().enumerate() {
let Some(restrictions) = element.restrictions.as_ref() else { continue };
if restrictions.reservation.is_none() {
continue;
}
for (j, pc) in element.price_components.iter().enumerate() {
if !matches!(pc.component_type, TariffDimensionType::Flat | TariffDimensionType::Time) {
v.enter("elements");
v.enter(&i.to_string());
v.enter("price_components");
v.enter(&j.to_string());
v.report_at(
"type",
ViolationCode::Inconsistent,
format!(
"a reservation Tariff Element can only have FLAT and TIME dimensions, \
not {}",
pc.component_type
),
);
v.leave();
v.leave();
v.leave();
v.leave();
}
}
}
}
}