Skip to main content

ocpi_tariffs/
money.rs

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