ocpi_tariffs/
money.rs

1use std::fmt;
2
3use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5
6use crate::{
7    json::{self, FieldsAsExt as _},
8    number,
9    warning::{self, IntoCaveat, IntoWarning as _},
10    Caveat, HoursDecimal, Kwh, Number, Verdict,
11};
12
13pub mod price {
14    //! The [Warning](crate::Warning) infra,tructure for the [Price](super::Price) type.
15
16    use std::{borrow::Cow, fmt};
17
18    use crate::{from_warning_set_to, number, warning};
19
20    #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
21    pub enum WarningKind {
22        /// The `excl_vat` field is greater than the `incl_vat` field.
23        ExclusiveVatGreaterThanInclusive,
24
25        /// The JSON value given is not an object.
26        InvalidType,
27
28        /// The `excl_vat` field is required.
29        MissingExclVatField,
30
31        /// Both the `excl_vat` and `incl_vat` fields should be valid numbers.
32        Number(number::WarningKind),
33    }
34
35    impl fmt::Display for WarningKind {
36        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37            match self {
38                WarningKind::ExclusiveVatGreaterThanInclusive => write!(
39                    f,
40                    "The `excl_vat` field is greater than the `incl_vat` field"
41                ),
42                WarningKind::InvalidType => write!(f, "The value should be a number."),
43                WarningKind::MissingExclVatField => write!(f, "The `excl_vat` field is required."),
44                WarningKind::Number(kind) => fmt::Display::fmt(kind, f),
45            }
46        }
47    }
48
49    impl warning::Kind for WarningKind {
50        fn id(&self) -> Cow<'static, str> {
51            match self {
52                WarningKind::ExclusiveVatGreaterThanInclusive => {
53                    "exclusive_vat_greater_than_inclusive".into()
54                }
55                WarningKind::InvalidType => "invalid_type".into(),
56                WarningKind::MissingExclVatField => "missing_excl_vat_field".into(),
57                WarningKind::Number(kind) => format!("number.{}", kind.id()).into(),
58            }
59        }
60    }
61
62    impl From<number::WarningKind> for WarningKind {
63        fn from(warn_kind: number::WarningKind) -> Self {
64            Self::Number(warn_kind)
65        }
66    }
67
68    from_warning_set_to!(number::WarningKind => WarningKind);
69}
70
71/// A price consisting of a value including VAT, and a value excluding VAT.
72#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Deserialize, Serialize)]
73pub struct Price {
74    /// The price excluding VAT.
75    pub excl_vat: Money,
76
77    /// The price including VAT.
78    ///
79    /// If no vat is applicable this value will be equal to the `excl_vat`.
80    ///
81    /// If no vat could be determined this value will be `None`.
82    /// The v211 tariffs can't determine VAT.
83    #[serde(default)]
84    pub incl_vat: Option<Money>,
85}
86
87impl json::FromJson for Price {
88    type WarningKind = price::WarningKind;
89
90    fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
91        let mut warnings = warning::Set::new();
92        let value = elem.as_value();
93
94        let Some(fields) = value.as_object_fields() else {
95            warnings.push(price::WarningKind::InvalidType.into_warning(elem));
96            return Err(warnings);
97        };
98
99        let Some(excl_vat) = fields.find_field("excl_vat") else {
100            warnings.push(price::WarningKind::MissingExclVatField.into_warning(elem));
101            return Err(warnings);
102        };
103
104        let excl_vat = Money::from_json(excl_vat.element())?.gather_warnings_into(&mut warnings);
105
106        let incl_vat = fields
107            .find_field("incl_vat")
108            .map(|f| Money::from_json(f.element()))
109            .transpose()?
110            .map(|v| v.gather_warnings_into(&mut warnings));
111
112        if let Some(incl_vat) = incl_vat {
113            if excl_vat > incl_vat {
114                warnings
115                    .push(price::WarningKind::ExclusiveVatGreaterThanInclusive.into_warning(elem));
116            }
117        }
118
119        Ok(Self { excl_vat, incl_vat }.into_caveat(warnings))
120    }
121}
122
123impl IntoCaveat for Price {
124    fn into_caveat<K: warning::Kind>(self, warnings: warning::Set<K>) -> Caveat<Self, K> {
125        Caveat::new(self, warnings)
126    }
127}
128
129impl Price {
130    pub fn zero() -> Self {
131        Self {
132            excl_vat: Money::zero(),
133            incl_vat: Some(Money::zero()),
134        }
135    }
136
137    pub fn is_zero(&self) -> bool {
138        self.excl_vat.is_zero() && self.incl_vat.is_none_or(|v| v.is_zero())
139    }
140
141    /// Round this number to the OCPI specified amount of decimals.
142    #[must_use]
143    pub fn rescale(self) -> Self {
144        Self {
145            excl_vat: self.excl_vat.rescale(),
146            incl_vat: self.incl_vat.map(Money::rescale),
147        }
148    }
149
150    /// Saturating addition.
151    #[must_use]
152    pub fn saturating_add(self, rhs: Self) -> Self {
153        let incl_vat = self
154            .incl_vat
155            .zip(rhs.incl_vat)
156            .map(|(lhs, rhs)| lhs.saturating_add(rhs));
157
158        Self {
159            excl_vat: self.excl_vat.saturating_add(rhs.excl_vat),
160            incl_vat,
161        }
162    }
163
164    #[must_use]
165    pub fn round_dp(self, digits: u32) -> Self {
166        Self {
167            excl_vat: self.excl_vat.round_dp(digits),
168            incl_vat: self.incl_vat.map(|v| v.round_dp(digits)),
169        }
170    }
171}
172
173impl Default for Price {
174    fn default() -> Self {
175        Self::zero()
176    }
177}
178
179/// A monetary amount, the currency is dependant on the specified tariff.
180#[derive(Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
181#[serde(transparent)]
182pub struct Money(Number);
183
184impl json::FromJson for Money {
185    type WarningKind = number::WarningKind;
186
187    fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
188        Number::from_json(elem).map(|v| v.map(Self))
189    }
190}
191
192impl Money {
193    pub fn zero() -> Self {
194        Self(Number::default())
195    }
196
197    pub fn is_zero(&self) -> bool {
198        self.0.is_zero()
199    }
200
201    /// Round this number to the OCPI specified amount of decimals.
202    #[must_use]
203    pub fn rescale(self) -> Self {
204        Self(self.0.rescale())
205    }
206
207    #[must_use]
208    pub fn round_dp(self, digits: u32) -> Self {
209        Self(self.0.round_dp(digits))
210    }
211
212    /// Saturating addition
213    #[must_use]
214    pub fn saturating_add(self, other: Self) -> Self {
215        Self(self.0.saturating_add(other.0))
216    }
217
218    /// Apply a VAT percentage to this monetary amount.
219    #[must_use]
220    pub fn apply_vat(self, vat: Vat) -> Self {
221        Self(self.0.saturating_mul(vat.as_fraction()))
222    }
223
224    /// Cost of a certain amount of [`Kwh`] with this price.
225    #[must_use]
226    pub fn kwh_cost(self, kwh: Kwh) -> Self {
227        Self(self.0.saturating_mul(kwh.into()))
228    }
229
230    /// Cost of a certain amount of [`HoursDecimal`] with this price.
231    #[must_use]
232    pub fn time_cost(self, hours: HoursDecimal) -> Self {
233        Self(self.0.saturating_mul(hours.as_num_hours_number()))
234    }
235}
236
237impl From<Money> for Decimal {
238    fn from(value: Money) -> Self {
239        value.0.into()
240    }
241}
242
243impl From<Money> for Number {
244    fn from(value: Money) -> Self {
245        value.0
246    }
247}
248
249impl From<Decimal> for Money {
250    fn from(value: Decimal) -> Self {
251        Self(value.into())
252    }
253}
254
255impl fmt::Display for Money {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        write!(f, "{:.4}", self.0)
258    }
259}
260
261/// A VAT percentage.
262#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
263#[serde(transparent)]
264pub struct Vat(Number);
265
266impl From<Vat> for Decimal {
267    fn from(value: Vat) -> Self {
268        value.0.into()
269    }
270}
271
272impl Vat {
273    #[expect(clippy::missing_panics_doc, reason = "The divisor is non-zero")]
274    pub fn as_fraction(self) -> Number {
275        self.0
276            .checked_div(100.into())
277            .expect("divisor is non-zero")
278            .saturating_add(1.into())
279    }
280}
281
282impl fmt::Display for Vat {
283    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284        self.0.fmt(f)
285    }
286}
287
288#[cfg(test)]
289mod test_price {
290    use assert_matches::assert_matches;
291    use rust_decimal::Decimal;
292    use rust_decimal_macros::dec;
293
294    use crate::{
295        json::{self, FromJson as _},
296        Warning,
297    };
298
299    use super::{price::WarningKind, Price};
300
301    #[test]
302    fn should_create_from_json_with_only_excl_vat_field() {
303        const JSON: &str = r#"{
304            "excl_vat": 10.2
305        }"#;
306
307        let elem = json::parse(JSON).unwrap();
308        let price = Price::from_json(&elem).unwrap().unwrap();
309
310        assert!(price.incl_vat.is_none());
311        assert_eq!(Decimal::from(price.excl_vat), dec!(10.2));
312    }
313
314    #[test]
315    fn should_create_from_json_with_excl_and_incl_vat_fields() {
316        const JSON: &str = r#"{
317            "excl_vat": 10.2,
318            "incl_vat": 12.3
319        }"#;
320
321        let elem = json::parse(JSON).unwrap();
322        let price = Price::from_json(&elem).unwrap().unwrap();
323
324        assert_eq!(Decimal::from(price.incl_vat.unwrap()), dec!(12.3));
325        assert_eq!(Decimal::from(price.excl_vat), dec!(10.2));
326    }
327
328    #[test]
329    fn should_fail_to_create_from_non_object_json() {
330        const JSON: &str = "12.3";
331
332        let elem = json::parse(JSON).unwrap();
333        let warnings = Price::from_json(&elem).unwrap_err();
334
335        assert_matches!(
336            *warnings,
337            [Warning {
338                kind: WarningKind::InvalidType,
339                ..
340            }]
341        );
342    }
343
344    #[test]
345    fn should_fail_to_create_from_json_as_excl_vat_is_required() {
346        const JSON: &str = r#"{
347            "incl_vat": 12.3
348        }"#;
349
350        let elem = json::parse(JSON).unwrap();
351        let warnings = Price::from_json(&elem).unwrap_err();
352
353        assert_matches!(
354            *warnings,
355            [Warning {
356                kind: WarningKind::MissingExclVatField,
357                ..
358            }]
359        );
360    }
361
362    #[test]
363    fn should_create_from_json_and_warn_about_excl_vat_greater_than_incl_vat() {
364        const JSON: &str = r#"{
365            "excl_vat": 12.3,
366            "incl_vat": 10.2
367        }"#;
368
369        let elem = json::parse(JSON).unwrap();
370        let (_price, warnings) = Price::from_json(&elem).unwrap().into_parts();
371
372        assert_matches!(
373            *warnings,
374            [Warning {
375                kind: WarningKind::ExclusiveVatGreaterThanInclusive,
376                ..
377            }]
378        );
379    }
380}