ocpi_tariffs/
money.rs

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