ocpi_tariffs/
money.rs

1//! Various monetary types.
2use std::{borrow::Cow, fmt};
3
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    from_warning_set_to, into_caveat,
9    json::{self, FieldsAsExt as _},
10    number,
11    warning::{self, GatherWarnings as _, IntoCaveat},
12    Number, Verdict,
13};
14
15/// A item that has a cost.
16pub trait Cost: Copy {
17    /// The cost of this dimension at a certain price.
18    fn cost(&self, price: Money) -> Money;
19}
20
21impl Cost for () {
22    fn cost(&self, price: Money) -> Money {
23        price
24    }
25}
26
27/// The warnings that can happen when parsing or linting a monetary value.
28#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
29pub enum WarningKind {
30    /// The `excl_vat` field is greater than the `incl_vat` field.
31    ExclusiveVatGreaterThanInclusive,
32
33    /// The JSON value given is not an object.
34    InvalidType,
35
36    /// The `excl_vat` field is required.
37    MissingExclVatField,
38
39    /// Both the `excl_vat` and `incl_vat` fields should be valid numbers.
40    Number(number::WarningKind),
41}
42
43impl fmt::Display for WarningKind {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            WarningKind::ExclusiveVatGreaterThanInclusive => write!(
47                f,
48                "The `excl_vat` field is greater than the `incl_vat` field"
49            ),
50            WarningKind::InvalidType => write!(f, "The value should be a number."),
51            WarningKind::MissingExclVatField => write!(f, "The `excl_vat` field is required."),
52            WarningKind::Number(kind) => fmt::Display::fmt(kind, f),
53        }
54    }
55}
56
57impl warning::Kind for WarningKind {
58    fn id(&self) -> Cow<'static, str> {
59        match self {
60            WarningKind::ExclusiveVatGreaterThanInclusive => {
61                "exclusive_vat_greater_than_inclusive".into()
62            }
63            WarningKind::InvalidType => "invalid_type".into(),
64            WarningKind::MissingExclVatField => "missing_excl_vat_field".into(),
65            WarningKind::Number(kind) => format!("number.{}", kind.id()).into(),
66        }
67    }
68}
69
70impl From<number::WarningKind> for WarningKind {
71    fn from(warn_kind: number::WarningKind) -> Self {
72        Self::Number(warn_kind)
73    }
74}
75
76from_warning_set_to!(number::WarningKind => WarningKind);
77
78/// A price consisting of a value including VAT, and a value excluding VAT.
79#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Deserialize, Serialize)]
80pub struct Price {
81    /// The price excluding VAT.
82    pub excl_vat: Money,
83
84    /// The price including VAT.
85    ///
86    /// If no vat is applicable this value will be equal to the `excl_vat`.
87    ///
88    /// If no vat could be determined this value will be `None`.
89    /// The v211 tariffs can't determine VAT.
90    #[serde(default)]
91    pub incl_vat: Option<Money>,
92}
93
94impl fmt::Display for Price {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        write!(f, "{{ excl_vat: {}, incl_vat: ", self.excl_vat)?;
97
98        if let Some(incl_vat) = self.incl_vat {
99            write!(f, "{incl_vat} }}")
100        } else {
101            f.write_str("None }")
102        }
103    }
104}
105
106impl json::FromJson<'_, '_> for Price {
107    type WarningKind = WarningKind;
108
109    fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
110        let mut warnings = warning::Set::new();
111        let value = elem.as_value();
112
113        let Some(fields) = value.as_object_fields() else {
114            warnings.with_elem(WarningKind::InvalidType, elem);
115            return Err(warnings);
116        };
117
118        let Some(excl_vat) = fields.find_field("excl_vat") else {
119            warnings.with_elem(WarningKind::MissingExclVatField, elem);
120            return Err(warnings);
121        };
122
123        let excl_vat = Money::from_json(excl_vat.element())?.gather_warnings_into(&mut warnings);
124
125        let incl_vat = fields
126            .find_field("incl_vat")
127            .map(|f| Money::from_json(f.element()))
128            .transpose()?
129            .gather_warnings_into(&mut warnings);
130
131        if let Some(incl_vat) = incl_vat {
132            if excl_vat > incl_vat {
133                warnings.with_elem(WarningKind::ExclusiveVatGreaterThanInclusive, elem);
134            }
135        }
136
137        Ok(Self { excl_vat, incl_vat }.into_caveat(warnings))
138    }
139}
140
141into_caveat!(Price);
142
143impl Price {
144    pub fn zero() -> Self {
145        Self {
146            excl_vat: Money::zero(),
147            incl_vat: Some(Money::zero()),
148        }
149    }
150
151    pub fn is_zero(&self) -> bool {
152        self.excl_vat.is_zero() && self.incl_vat.is_none_or(|v| v.is_zero())
153    }
154
155    /// Round this number to the OCPI specified amount of decimals.
156    #[must_use]
157    pub fn rescale(self) -> Self {
158        Self {
159            excl_vat: self.excl_vat.rescale(),
160            incl_vat: self.incl_vat.map(Money::rescale),
161        }
162    }
163
164    /// Saturating addition.
165    #[must_use]
166    pub fn saturating_add(self, rhs: Self) -> Self {
167        let incl_vat = self
168            .incl_vat
169            .zip(rhs.incl_vat)
170            .map(|(lhs, rhs)| lhs.saturating_add(rhs));
171
172        Self {
173            excl_vat: self.excl_vat.saturating_add(rhs.excl_vat),
174            incl_vat,
175        }
176    }
177
178    #[must_use]
179    pub fn round_dp(self, digits: u32) -> Self {
180        Self {
181            excl_vat: self.excl_vat.round_dp(digits),
182            incl_vat: self.incl_vat.map(|v| v.round_dp(digits)),
183        }
184    }
185}
186
187impl Default for Price {
188    fn default() -> Self {
189        Self::zero()
190    }
191}
192
193/// A monetary amount, the currency is dependant on the specified tariff.
194#[derive(Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
195#[serde(transparent)]
196pub struct Money(Number);
197
198impl json::FromJson<'_, '_> for Money {
199    type WarningKind = number::WarningKind;
200
201    fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
202        Number::from_json(elem).map(|v| v.map(Self))
203    }
204}
205
206impl Money {
207    pub(crate) fn from_number(number: Number) -> Self {
208        Self(number)
209    }
210
211    pub fn zero() -> Self {
212        Self(Number::default())
213    }
214
215    pub fn is_zero(&self) -> bool {
216        self.0.is_zero()
217    }
218
219    /// Round this number to the OCPI specified amount of decimals.
220    #[must_use]
221    pub fn rescale(self) -> Self {
222        Self(self.0.rescale())
223    }
224
225    #[must_use]
226    pub fn round_dp(self, digits: u32) -> Self {
227        Self(self.0.round_dp(digits))
228    }
229
230    /// Saturating addition
231    #[must_use]
232    pub fn saturating_add(self, other: Self) -> Self {
233        Self(self.0.saturating_add(other.0))
234    }
235
236    /// Apply a VAT percentage to this monetary amount.
237    #[must_use]
238    pub fn apply_vat(self, vat: Vat) -> Self {
239        Self(self.0.saturating_mul(vat.as_fraction()))
240    }
241}
242
243impl From<Money> for Decimal {
244    fn from(value: Money) -> Self {
245        value.0.into()
246    }
247}
248
249impl From<Money> for Number {
250    fn from(value: Money) -> Self {
251        value.0
252    }
253}
254
255impl From<Decimal> for Money {
256    fn from(value: Decimal) -> Self {
257        Self(value.into())
258    }
259}
260
261impl fmt::Display for Money {
262    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263        write!(f, "{:.4}", self.0)
264    }
265}
266
267/// A VAT percentage.
268#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
269#[serde(transparent)]
270pub struct Vat(Number);
271
272impl From<Vat> for Decimal {
273    fn from(value: Vat) -> Self {
274        value.0.into()
275    }
276}
277
278impl Vat {
279    #[expect(clippy::missing_panics_doc, reason = "The divisor is non-zero")]
280    pub fn as_fraction(self) -> Number {
281        self.0
282            .checked_div(100.into())
283            .expect("divisor is non-zero")
284            .saturating_add(1.into())
285    }
286}
287
288impl fmt::Display for Vat {
289    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290        self.0.fmt(f)
291    }
292}
293
294/// A VAT percentage with embedded information about whether it's applicable, inapplicable or unknown.
295#[derive(Clone, Copy, Debug)]
296pub enum VatApplicable {
297    /// The VAT percentage is not known.
298    ///
299    /// All `incl_vat` fields should be `None` in the final calculation.
300    Unknown,
301
302    /// The VAT is known but not applicable.
303    ///
304    /// The total `incl_vat` should be equal to `excl_vat`.
305    Inapplicable,
306
307    /// The VAT us know nadn applicable.
308    Applicable(Vat),
309}
310
311impl Serialize for VatApplicable {
312    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
313    where
314        S: serde::Serializer,
315    {
316        let vat = match self {
317            Self::Unknown | Self::Inapplicable => None,
318            Self::Applicable(vat) => Some(vat),
319        };
320
321        vat.serialize(serializer)
322    }
323}
324
325impl<'de> Deserialize<'de> for VatApplicable {
326    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
327    where
328        D: serde::Deserializer<'de>,
329    {
330        let vat = <Option<Vat>>::deserialize(deserializer)?;
331
332        let vat = if let Some(vat) = vat {
333            Self::Applicable(vat)
334        } else {
335            Self::Inapplicable
336        };
337
338        Ok(vat)
339    }
340}
341
342#[cfg(test)]
343mod test {
344    use rust_decimal::Decimal;
345    use rust_decimal_macros::dec;
346
347    use crate::test::{AsDecimal, DecimalPartialEq};
348
349    use super::{Money, Price};
350
351    impl AsDecimal for Money {
352        fn as_dec(&self) -> &Decimal {
353            self.0.as_dec()
354        }
355    }
356
357    impl DecimalPartialEq for Money {
358        fn eq_dec(&self, other: &Self) -> bool {
359            /// Use a tolerance of 1 cent when comparing money amounts.
360            /// E.g. 35.01 == 35.02
361            const EQ_TOLERANCE: Decimal = dec!(0.01);
362            /// Decimal precision used when comparing the outcomes of the calculation with the CDR.
363            const EQ_PRECISION: u32 = 2;
364
365            let a = self.0.round_dp(EQ_PRECISION);
366            let b = other.0.rescale().round_dp(EQ_PRECISION);
367            let diff = a.as_dec() - b.as_dec();
368            diff.abs() <= EQ_TOLERANCE
369        }
370    }
371
372    impl DecimalPartialEq for Price {
373        fn eq_dec(&self, other: &Self) -> bool {
374            let incl_eq = match (self.incl_vat, other.incl_vat) {
375                (Some(a), Some(b)) => a.eq_dec(&b),
376                (None, None) => true,
377                _ => return false,
378            };
379
380            incl_eq && self.excl_vat.eq_dec(&other.excl_vat)
381        }
382    }
383}
384
385#[cfg(test)]
386mod test_price {
387    use assert_matches::assert_matches;
388    use rust_decimal::Decimal;
389    use rust_decimal_macros::dec;
390
391    use crate::json::{self, FromJson as _};
392
393    use super::{Price, WarningKind};
394
395    #[test]
396    fn should_create_from_json_with_only_excl_vat_field() {
397        const JSON: &str = r#"{
398            "excl_vat": 10.2
399        }"#;
400
401        let elem = json::parse(JSON).unwrap();
402        let price = Price::from_json(&elem).unwrap().unwrap();
403
404        assert!(price.incl_vat.is_none());
405        assert_eq!(Decimal::from(price.excl_vat), dec!(10.2));
406    }
407
408    #[test]
409    fn should_create_from_json_with_excl_and_incl_vat_fields() {
410        const JSON: &str = r#"{
411            "excl_vat": 10.2,
412            "incl_vat": 12.3
413        }"#;
414
415        let elem = json::parse(JSON).unwrap();
416        let price = Price::from_json(&elem).unwrap().unwrap();
417
418        assert_eq!(Decimal::from(price.incl_vat.unwrap()), dec!(12.3));
419        assert_eq!(Decimal::from(price.excl_vat), dec!(10.2));
420    }
421
422    #[test]
423    fn should_fail_to_create_from_non_object_json() {
424        const JSON: &str = "12.3";
425
426        let elem = json::parse(JSON).unwrap();
427        let warnings = Price::from_json(&elem).unwrap_err().into_kind_vec();
428
429        assert_matches!(*warnings, [WarningKind::InvalidType]);
430    }
431
432    #[test]
433    fn should_fail_to_create_from_json_as_excl_vat_is_required() {
434        const JSON: &str = r#"{
435            "incl_vat": 12.3
436        }"#;
437
438        let elem = json::parse(JSON).unwrap();
439        let warnings = Price::from_json(&elem).unwrap_err().into_kind_vec();
440
441        assert_matches!(*warnings, [WarningKind::MissingExclVatField]);
442    }
443
444    #[test]
445    fn should_create_from_json_and_warn_about_excl_vat_greater_than_incl_vat() {
446        const JSON: &str = r#"{
447            "excl_vat": 12.3,
448            "incl_vat": 10.2
449        }"#;
450
451        let elem = json::parse(JSON).unwrap();
452        let (_price, warnings) = Price::from_json(&elem).unwrap().into_parts();
453        let warnings = warnings.into_kind_vec();
454
455        assert_matches!(*warnings, [WarningKind::ExclusiveVatGreaterThanInclusive]);
456    }
457}