1use bon::Builder;
11use serde::{Deserialize, Serialize};
12
13use crate::types::validate_fields;
14use crate::types::{
15 Currency, DateTime, DisplayText, Extensions, LocalDate, LocalTime, Number, OcpiString, Url, Validate,
16 Validator, ViolationCode,
17};
18
19use super::locations::EnergyMix;
20
21pub use crate::v2_3_0::tariffs::{DayOfWeek, TariffDimensionType};
23
24#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
28#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
29#[builder(on(_, into))]
30pub struct Tariff {
31 pub id: OcpiString<36>,
33 pub currency: Currency,
35 #[serde(default, skip_serializing_if = "Vec::is_empty")]
37 #[builder(default)]
38 pub tariff_alt_text: Vec<DisplayText>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub tariff_alt_url: Option<Url>,
42 pub elements: Vec<TariffElement>,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub energy_mix: Option<EnergyMix>,
47 pub last_updated: DateTime,
49 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
51 #[builder(default)]
52 pub extensions: Extensions,
53}
54
55impl Tariff {
56 #[must_use]
59 pub fn is_free_of_charge(&self) -> bool {
60 match self.elements.as_slice() {
61 [element] if element.restrictions.is_none() => match element.price_components.as_slice() {
62 [pc] => pc.component_type == TariffDimensionType::Flat && pc.price.is_zero(),
63 _ => false,
64 },
65 _ => false,
66 }
67 }
68}
69
70impl Validate for Tariff {
71 fn validate_in(&self, v: &mut Validator) {
72 validate_fields!(
73 self,
74 v,
75 id,
76 currency,
77 tariff_alt_text,
78 tariff_alt_url,
79 elements,
80 energy_mix,
81 last_updated,
82 );
83 if self.elements.is_empty() {
84 v.report_at(
85 "elements",
86 ViolationCode::EmptyRequiredList,
87 "a Tariff has cardinality `+` elements: at least one is required",
88 );
89 }
90 }
91}
92
93#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
97#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
98#[builder(on(_, into))]
99pub struct TariffElement {
100 pub price_components: Vec<PriceComponent>,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub restrictions: Option<TariffRestrictions>,
105 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
107 #[builder(default)]
108 pub extensions: Extensions,
109}
110
111impl Validate for TariffElement {
112 fn validate_in(&self, v: &mut Validator) {
113 validate_fields!(self, v, price_components, restrictions);
114 if self.price_components.is_empty() {
115 v.report_at(
116 "price_components",
117 ViolationCode::EmptyRequiredList,
118 "a TariffElement has cardinality `+` price_components",
119 );
120 }
121 }
122}
123
124#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
131#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
132pub struct PriceComponent {
133 #[serde(rename = "type")]
135 pub component_type: TariffDimensionType,
136 pub price: Number,
138 pub step_size: u32,
140 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
142 pub extensions: Extensions,
143}
144
145impl PriceComponent {
146 #[must_use]
148 pub fn new(component_type: TariffDimensionType, price: Number) -> Self {
149 Self { component_type, price, step_size: 1, extensions: Extensions::new() }
150 }
151}
152
153impl Validate for PriceComponent {
154 fn validate_in(&self, v: &mut Validator) {
155 validate_fields!(self, v, component_type as "type", price);
156 if self.step_size == 0 && self.component_type.step_size_unit().is_some() {
157 v.report_at(
158 "step_size",
159 ViolationCode::OutOfRange,
160 "a step_size of 0 would bill nothing for a dimension that has a unit",
161 );
162 }
163 }
164}
165
166#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Builder)]
173#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
174#[builder(on(_, into))]
175pub struct TariffRestrictions {
176 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub start_time: Option<LocalTime>,
179 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub end_time: Option<LocalTime>,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub start_date: Option<LocalDate>,
185 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub end_date: Option<LocalDate>,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub min_kwh: Option<Number>,
191 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub max_kwh: Option<Number>,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub min_power: Option<Number>,
197 #[serde(default, skip_serializing_if = "Option::is_none")]
199 pub max_power: Option<Number>,
200 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub min_duration: Option<u64>,
203 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub max_duration: Option<u64>,
206 #[serde(default, skip_serializing_if = "Vec::is_empty")]
208 #[builder(default)]
209 pub day_of_week: Vec<DayOfWeek>,
210 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
212 #[builder(default)]
213 pub extensions: Extensions,
214}
215
216impl Validate for TariffRestrictions {
217 fn validate_in(&self, v: &mut Validator) {
218 validate_fields!(
219 self,
220 v,
221 start_time,
222 end_time,
223 start_date,
224 end_date,
225 min_kwh,
226 max_kwh,
227 min_power,
228 max_power,
229 day_of_week,
230 );
231 for (lo_name, lo, hi_name, hi) in [
232 ("min_kwh", self.min_kwh, "max_kwh", self.max_kwh),
233 ("min_power", self.min_power, "max_power", self.max_power),
234 ] {
235 if let (Some(lo_v), Some(hi_v)) = (lo, hi)
236 && hi_v <= lo_v
237 {
238 v.report_at(
239 hi_name,
240 ViolationCode::Inconsistent,
241 format!("{hi_name} is not above {lo_name}, so this element can never apply"),
242 );
243 }
244 }
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn a_2_1_1_price_component_cannot_express_vat() {
254 let json = r#"{"type":"ENERGY","price":0.25,"step_size":1}"#;
255 let component: PriceComponent = serde_json::from_str(json).unwrap();
256 assert_eq!(serde_json::to_string(&component).unwrap(), json);
257 let with_vat: PriceComponent =
259 serde_json::from_str(r#"{"type":"ENERGY","price":0.25,"step_size":1,"vat":10}"#).unwrap();
260 assert_eq!(with_vat.extensions.get::<u32>("vat").unwrap(), Some(10));
261 }
262
263 #[test]
264 fn a_free_of_charge_tariff_has_the_usual_shape() {
265 let tariff = Tariff::builder()
266 .id("15")
267 .currency("EUR")
268 .elements(vec![
269 TariffElement::builder()
270 .price_components(vec![PriceComponent::new(TariffDimensionType::Flat, Number::ZERO)])
271 .build(),
272 ])
273 .last_updated("2015-06-29T20:39:09Z".parse::<DateTime>().unwrap())
274 .build();
275 assert!(tariff.is_free_of_charge());
276 assert!(tariff.validate().is_ok());
277 }
278}