Skip to main content

ocpi_tariffs/
money.rs

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