1#[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
22pub trait Cost: Copy {
24 fn cost(&self, money: Money) -> Money;
26}
27
28impl Cost for () {
29 fn cost(&self, money: Money) -> Money {
30 money
31 }
32}
33
34#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
36pub enum Warning {
37 ExclusiveVatGreaterThanInclusive,
39
40 InvalidType { type_found: json::ValueKind },
42
43 MissingExclVatField,
45
46 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#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)]
96#[cfg_attr(test, derive(serde::Deserialize))]
97pub struct Price {
98 pub excl_vat: Money,
100
101 #[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 #[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 #[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 pub fn display_currency(&self, currency: currency::Code) -> DisplayPriceCurrency<'_> {
215 DisplayPriceCurrency {
216 currency,
217 price: self,
218 }
219 }
220}
221
222pub(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
257pub 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#[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 #[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 pub fn display_currency(&self, currency: currency::Code) -> DisplayCurrency<'_> {
318 DisplayCurrency {
319 currency,
320 money: self,
321 }
322 }
323}
324
325pub 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#[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#[derive(Clone, Copy, Debug)]
357pub(crate) enum VatOrigin {
358 Unknown,
362
363 NotProvided,
367
368 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}