ocpi_tariffs/
money.rs

1//! Various monetary types.
2use std::{borrow::Cow, fmt};
3
4use rust_decimal::Decimal;
5use rust_decimal_macros::dec;
6
7use crate::{
8    from_warning_all, impl_dec_newtype, into_caveat,
9    json::{self, FieldsAsExt as _},
10    number::{self, FromDecimal as _},
11    warning::{self, GatherWarnings as _, IntoCaveat},
12    SaturatingAdd as _, Verdict,
13};
14
15/// An item that has a cost.
16pub trait Cost: Copy {
17    /// The cost of this dimension at a certain price.
18    fn cost(&self, money: Money) -> Money;
19}
20
21impl Cost for () {
22    fn cost(&self, money: Money) -> Money {
23        money
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) => kind.id(),
66        }
67    }
68}
69
70from_warning_all!(number::WarningKind => WarningKind::Number);
71
72/// A price consisting of a value including VAT, and a value excluding VAT.
73#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)]
74#[cfg_attr(test, derive(serde::Deserialize))]
75pub struct Price {
76    /// The price excluding VAT.
77    pub excl_vat: Money,
78
79    /// The price including VAT.
80    ///
81    /// If no vat is applicable this value will be equal to the `excl_vat`.
82    ///
83    /// If no vat could be determined this value will be `None`.
84    /// The v211 tariffs can't determine VAT.
85    #[cfg_attr(test, serde(default))]
86    pub incl_vat: Option<Money>,
87}
88
89impl fmt::Display for Price {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        if let Some(incl_vat) = self.incl_vat {
92            write!(f, "{{ -vat: {}, +vat: {} }}", self.excl_vat, incl_vat)
93        } else {
94            fmt::Display::fmt(&self.excl_vat, f)
95        }
96    }
97}
98
99impl json::FromJson<'_, '_> for Price {
100    type WarningKind = WarningKind;
101
102    fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
103        let mut warnings = warning::Set::new();
104        let value = elem.as_value();
105
106        let Some(fields) = value.as_object_fields() else {
107            return warnings.bail(WarningKind::InvalidType, elem);
108        };
109
110        let Some(excl_vat) = fields.find_field("excl_vat") else {
111            return warnings.bail(WarningKind::MissingExclVatField, elem);
112        };
113
114        let excl_vat = Money::from_json(excl_vat.element())?.gather_warnings_into(&mut warnings);
115
116        let incl_vat = fields
117            .find_field("incl_vat")
118            .map(|f| Money::from_json(f.element()))
119            .transpose()?
120            .gather_warnings_into(&mut warnings);
121
122        if let Some(incl_vat) = incl_vat {
123            if excl_vat > incl_vat {
124                warnings.with_elem(WarningKind::ExclusiveVatGreaterThanInclusive, elem);
125            }
126        }
127
128        Ok(Self { excl_vat, incl_vat }.into_caveat(warnings))
129    }
130}
131
132into_caveat!(Price);
133
134impl Price {
135    pub fn zero() -> Self {
136        Self {
137            excl_vat: Money::zero(),
138            incl_vat: Some(Money::zero()),
139        }
140    }
141
142    pub fn is_zero(&self) -> bool {
143        self.excl_vat.is_zero() && self.incl_vat.is_none_or(|v| v.is_zero())
144    }
145
146    /// Round this number to the OCPI specified amount of decimals.
147    #[must_use]
148    pub fn rescale(self) -> Self {
149        Self {
150            excl_vat: self.excl_vat.rescale(),
151            incl_vat: self.incl_vat.map(Money::rescale),
152        }
153    }
154
155    /// Saturating addition.
156    #[must_use]
157    pub(crate) fn saturating_add(self, rhs: Self) -> Self {
158        let incl_vat = self
159            .incl_vat
160            .zip(rhs.incl_vat)
161            .map(|(lhs, rhs)| lhs.saturating_add(rhs));
162
163        Self {
164            excl_vat: self.excl_vat.saturating_add(rhs.excl_vat),
165            incl_vat,
166        }
167    }
168
169    #[must_use]
170    pub fn round_dp(self, digits: u32) -> Self {
171        Self {
172            excl_vat: self.excl_vat.round_dp(digits),
173            incl_vat: self.incl_vat.map(|v| v.round_dp(digits)),
174        }
175    }
176}
177
178impl Default for Price {
179    fn default() -> Self {
180        Self::zero()
181    }
182}
183
184/// A monetary amount, the currency is dependent on the specified tariff.
185#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
186#[cfg_attr(test, derive(serde::Deserialize))]
187pub struct Money(Decimal);
188
189impl_dec_newtype!(Money, "ยค");
190
191impl Money {
192    /// Apply a VAT percentage to this monetary amount.
193    #[must_use]
194    pub fn apply_vat(self, vat: Vat) -> Self {
195        const ONE: Decimal = dec!(1);
196
197        let x = vat.as_unit_interval().saturating_add(ONE);
198        Self(self.0.saturating_mul(x))
199    }
200}
201
202/// A VAT percentage.
203#[derive(Debug, PartialEq, Eq, Clone, Copy)]
204pub struct Vat(Decimal);
205
206impl_dec_newtype!(Vat, "%");
207
208impl Vat {
209    #[expect(clippy::missing_panics_doc, reason = "The divisor is non-zero")]
210    pub fn as_unit_interval(self) -> Decimal {
211        const PERCENT: Decimal = dec!(100);
212
213        self.0.checked_div(PERCENT).expect("divisor is non-zero")
214    }
215}
216
217/// A VAT percentage with embedded information about whether it's applicable, inapplicable or unknown.
218#[derive(Clone, Copy, Debug)]
219pub enum VatApplicable {
220    /// The VAT percentage is not known.
221    ///
222    /// All `incl_vat` fields should be `None` in the final calculation.
223    Unknown,
224
225    /// The VAT is known but not applicable.
226    ///
227    /// The total `incl_vat` should be equal to `excl_vat`.
228    Inapplicable,
229
230    /// The VAT us known and applicable.
231    Applicable(Vat),
232}
233
234impl json::FromJson<'_, '_> for VatApplicable {
235    type WarningKind = number::WarningKind;
236
237    fn from_json(elem: &'_ json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
238        let vat = Decimal::from_json(elem)?;
239        Ok(vat.map(|d| Self::Applicable(Vat::from_decimal(d))))
240    }
241}
242
243#[cfg(test)]
244mod test {
245    use crate::test::ApproxEq;
246
247    use super::Price;
248
249    impl ApproxEq for Price {
250        fn approx_eq(&self, other: &Self) -> bool {
251            let incl_eq = match (self.incl_vat, other.incl_vat) {
252                (Some(a), Some(b)) => a.approx_eq(&b),
253                (None, None) => true,
254                _ => return false,
255            };
256
257            incl_eq && self.excl_vat.approx_eq(&other.excl_vat)
258        }
259    }
260}
261
262#[cfg(test)]
263mod test_price {
264    use assert_matches::assert_matches;
265    use rust_decimal::Decimal;
266    use rust_decimal_macros::dec;
267
268    use crate::json::{self, FromJson as _};
269
270    use super::{Price, WarningKind};
271
272    #[test]
273    fn should_create_from_json_with_only_excl_vat_field() {
274        const JSON: &str = r#"{
275            "excl_vat": 10.2
276        }"#;
277
278        let elem = json::parse(JSON).unwrap();
279        let price = Price::from_json(&elem).unwrap().unwrap();
280
281        assert!(price.incl_vat.is_none());
282        assert_eq!(Decimal::from(price.excl_vat), dec!(10.2));
283    }
284
285    #[test]
286    fn should_create_from_json_with_excl_and_incl_vat_fields() {
287        const JSON: &str = r#"{
288            "excl_vat": 10.2,
289            "incl_vat": 12.3
290        }"#;
291
292        let elem = json::parse(JSON).unwrap();
293        let price = Price::from_json(&elem).unwrap().unwrap();
294
295        assert_eq!(Decimal::from(price.incl_vat.unwrap()), dec!(12.3));
296        assert_eq!(Decimal::from(price.excl_vat), dec!(10.2));
297    }
298
299    #[test]
300    fn should_fail_to_create_from_non_object_json() {
301        const JSON: &str = "12.3";
302
303        let elem = json::parse(JSON).unwrap();
304        let warnings = Price::from_json(&elem).unwrap_err().into_kind_vec();
305
306        assert_matches!(*warnings, [WarningKind::InvalidType]);
307    }
308
309    #[test]
310    fn should_fail_to_create_from_json_as_excl_vat_is_required() {
311        const JSON: &str = r#"{
312            "incl_vat": 12.3
313        }"#;
314
315        let elem = json::parse(JSON).unwrap();
316        let warnings = Price::from_json(&elem).unwrap_err().into_kind_vec();
317
318        assert_matches!(*warnings, [WarningKind::MissingExclVatField]);
319    }
320
321    #[test]
322    fn should_create_from_json_and_warn_about_excl_vat_greater_than_incl_vat() {
323        const JSON: &str = r#"{
324            "excl_vat": 12.3,
325            "incl_vat": 10.2
326        }"#;
327
328        let elem = json::parse(JSON).unwrap();
329        let (_price, warnings) = Price::from_json(&elem).unwrap().into_parts();
330        let warnings = warnings.into_kind_vec();
331
332        assert_matches!(*warnings, [WarningKind::ExclusiveVatGreaterThanInclusive]);
333    }
334}