1use bon::Builder;
18use serde::{Deserialize, Serialize};
19
20use crate::types::validate_fields;
21use crate::types::{
22 CiString, CountryCode, Currency, DateTime, DisplayText, Extensions, PartyId, PartyRef, Url, Validate,
23 Validator, ViolationCode,
24};
25
26use super::locations::EnergyMix;
27use super::types::Price;
28
29pub use crate::v2_3_0::tariffs::{
31 DayOfWeek, PriceComponent, ReservationRestrictionType, TariffDimensionType, TariffElement,
32 TariffRestrictions, TariffType,
33};
34
35#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
39#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
40#[builder(on(_, into))]
41pub struct Tariff {
42 pub country_code: CountryCode,
44 pub party_id: PartyId,
46 pub id: CiString<36>,
48 pub currency: Currency,
50 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
52 pub tariff_type: Option<TariffType>,
53 #[serde(default, skip_serializing_if = "Vec::is_empty")]
55 #[builder(default)]
56 pub tariff_alt_text: Vec<DisplayText>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub tariff_alt_url: Option<Url>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub min_price: Option<Price>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub max_price: Option<Price>,
66 pub elements: Vec<TariffElement>,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub start_date_time: Option<DateTime>,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub end_date_time: Option<DateTime>,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub energy_mix: Option<EnergyMix>,
77 pub last_updated: DateTime,
79 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
81 #[builder(default)]
82 pub extensions: Extensions,
83}
84
85impl Tariff {
86 #[must_use]
88 pub fn owner_party(&self) -> PartyRef {
89 PartyRef { country_code: self.country_code.clone(), party_id: self.party_id.clone() }
90 }
91
92 #[must_use]
94 pub fn is_active_at(&self, instant: DateTime) -> bool {
95 self.start_date_time.is_none_or(|s| instant >= s) && self.end_date_time.is_none_or(|e| instant < e)
96 }
97
98 #[must_use]
100 pub fn is_free_of_charge(&self) -> bool {
101 match self.elements.as_slice() {
102 [element] if element.restrictions.is_none() => match element.price_components.as_slice() {
103 [pc] => pc.component_type == TariffDimensionType::Flat && pc.price.is_zero(),
104 _ => false,
105 },
106 _ => false,
107 }
108 }
109}
110
111impl Validate for Tariff {
112 fn validate_in(&self, v: &mut Validator) {
113 validate_fields!(
114 self, v, country_code, party_id, id, currency, tariff_type as "type", tariff_alt_text,
115 tariff_alt_url, min_price, max_price, elements, start_date_time, end_date_time,
116 energy_mix, last_updated,
117 );
118 if self.elements.is_empty() {
119 v.report_at(
120 "elements",
121 ViolationCode::EmptyRequiredList,
122 "a Tariff has cardinality `+` elements: at least one is required",
123 );
124 }
125 if let (Some(start), Some(end)) = (self.start_date_time, self.end_date_time)
126 && end <= start
127 {
128 v.report_at(
129 "end_date_time",
130 ViolationCode::Inconsistent,
131 "a tariff's validity window must be non-empty",
132 );
133 }
134 if let (Some(min), Some(max)) = (self.min_price.as_ref(), self.max_price.as_ref())
135 && max.excl_vat < min.excl_vat
136 {
137 v.report_at(
138 "max_price",
139 ViolationCode::Inconsistent,
140 "max_price.excl_vat is below min_price.excl_vat",
141 );
142 }
143 for (i, element) in self.elements.iter().enumerate() {
144 let Some(restrictions) = element.restrictions.as_ref() else { continue };
145 if restrictions.reservation.is_none() {
146 continue;
147 }
148 for (j, pc) in element.price_components.iter().enumerate() {
149 if !matches!(pc.component_type, TariffDimensionType::Flat | TariffDimensionType::Time) {
150 v.enter("elements");
151 v.enter(&i.to_string());
152 v.enter("price_components");
153 v.enter(&j.to_string());
154 v.report_at(
155 "type",
156 ViolationCode::Inconsistent,
157 format!(
158 "a reservation Tariff Element can only have FLAT and TIME dimensions, \
159 not {}",
160 pc.component_type
161 ),
162 );
163 v.leave();
164 v.leave();
165 v.leave();
166 v.leave();
167 }
168 }
169 }
170 }
171}