1#[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
21pub trait Cost: Copy {
23 fn cost(&self, money: Money) -> Money;
25}
26
27impl Cost for () {
28 fn cost(&self, money: Money) -> Money {
29 money
30 }
31}
32
33#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
35pub enum Warning {
36 ExclusiveVatGreaterThanInclusive,
38
39 InvalidType { type_found: json::ValueKind },
41
42 MissingExclVatField,
44
45 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#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)]
90#[cfg_attr(test, derive(serde::Deserialize))]
91pub struct Price {
92 pub excl_vat: Money,
94
95 #[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 #[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 #[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 pub fn display_currency(&self, currency: currency::Code) -> DisplayPriceCurrency<'_> {
209 DisplayPriceCurrency {
210 currency,
211 price: self,
212 }
213 }
214}
215
216pub 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#[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 #[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 pub fn display_currency(&self, currency: currency::Code) -> DisplayCurrency<'_> {
264 DisplayCurrency {
265 currency,
266 money: self,
267 }
268 }
269}
270
271pub 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#[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#[derive(Clone, Copy, Debug)]
303pub enum VatApplicable {
304 Unknown,
308
309 Inapplicable,
313
314 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}