1#[cfg(test)]
4mod test;
5
6#[cfg(test)]
7mod test_from_schema;
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 number::{self, approx_eq_dec, FromDecimal as _, IsZero, RoundDecimal},
17 schema,
18 warning::{self, GatherWarnings as _, IntoCaveat as _},
19 FromSchema, 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 Number(number::Warning),
42
43 Rejected,
47}
48
49impl fmt::Display for Warning {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 match self {
52 Self::ExclusiveVatGreaterThanInclusive => write!(
53 f,
54 "The `excl_vat` field is greater than the `incl_vat` field"
55 ),
56 Self::Number(kind) => fmt::Display::fmt(kind, f),
57 Self::Rejected => f.write_str(
58 "The schema IR for a `Price` was rejected; see the schema validation warnings.",
59 ),
60 }
61 }
62}
63
64impl crate::Warning for Warning {
65 fn id(&self) -> warning::Id {
66 match self {
67 Self::ExclusiveVatGreaterThanInclusive => {
68 warning::Id::from_static("exclusive_vat_greater_than_inclusive")
69 }
70 Self::Number(kind) => kind.id(),
71 Self::Rejected => warning::Id::from_static("rejected"),
72 }
73 }
74
75 fn is_rejected(&self) -> bool {
76 matches!(self, Self::Rejected)
77 }
78}
79
80impl From<warning::Rejected> for Warning {
81 fn from(_: warning::Rejected) -> Self {
82 Self::Rejected
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<'buf> FromSchema<'buf, schema::v221::Price<'buf>> for Price {
130 type Warning = Warning;
131
132 fn from_schema(source: &schema::v221::Price<'buf>) -> Verdict<Self, Self::Warning> {
133 let mut warnings = warning::Set::new();
134
135 let (elem, excl_vat, incl_vat) = match source {
138 schema::v221::Price::Number(number) => {
139 let excl_vat = Money::from_schema(number)?.gather_warnings_into(&mut warnings);
140 let price = Self {
141 excl_vat,
142 incl_vat: None,
143 };
144 return Ok(price.into_caveat(warnings));
145 }
146 schema::v221::Price::Object {
147 elem,
148 excl_vat,
149 incl_vat,
150 } => (elem, excl_vat, incl_vat),
151 };
152
153 let excl_vat = warnings.ok_or_bail(excl_vat)?;
157 let excl_vat = Money::from_schema(excl_vat)?.gather_warnings_into(&mut warnings);
158
159 let incl_vat = incl_vat
163 .map_some(Money::from_schema)
164 .transpose()?
165 .gather_warnings_into(&mut warnings);
166
167 if let Some(incl_vat) = incl_vat {
168 if excl_vat > incl_vat {
169 warnings.insert(elem, Warning::ExclusiveVatGreaterThanInclusive);
170 }
171 }
172
173 Ok(Self { excl_vat, incl_vat }.into_caveat(warnings))
174 }
175}
176
177impl IsZero for Price {
178 fn is_zero(&self) -> bool {
179 self.excl_vat.is_zero() && self.incl_vat.is_none_or(|v| v.is_zero())
180 }
181}
182
183impl Price {
184 pub fn zero() -> Self {
186 Self {
187 excl_vat: Money::zero(),
188 incl_vat: Some(Money::zero()),
189 }
190 }
191
192 #[must_use]
194 pub fn rescale(self) -> Self {
195 Self {
196 excl_vat: self.excl_vat.rescale(),
197 incl_vat: self.incl_vat.map(Money::rescale),
198 }
199 }
200
201 #[must_use]
203 pub(crate) fn saturating_add(self, rhs: Self) -> Self {
204 let incl_vat = self
205 .incl_vat
206 .zip(rhs.incl_vat)
207 .map(|(lhs, rhs)| lhs.saturating_add(rhs));
208
209 Self {
210 excl_vat: self.excl_vat.saturating_add(rhs.excl_vat),
211 incl_vat,
212 }
213 }
214
215 #[must_use]
216 pub fn round_dp(self, digits: u32) -> Self {
218 Self {
219 excl_vat: self.excl_vat.round_dp(digits),
220 incl_vat: self.incl_vat.map(|v| v.round_dp(digits)),
221 }
222 }
223
224 pub fn display_currency(&self, currency: currency::Code) -> DisplayPriceCurrency<'_> {
226 DisplayPriceCurrency {
227 currency,
228 price: self,
229 }
230 }
231}
232
233pub struct DisplayPriceCurrency<'a> {
238 currency: currency::Code,
239 price: &'a Price,
240}
241
242impl fmt::Display for DisplayPriceCurrency<'_> {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 if let Some(incl_vat) = self.price.incl_vat {
245 write!(
246 f,
247 "{{ -vat: {:#}, +vat: {:#} }}",
248 self.price.excl_vat, incl_vat
249 )
250 } else {
251 fmt::Display::fmt(&self.price.excl_vat.display_currency(self.currency), f)
252 }
253 }
254}
255
256impl Default for Price {
257 fn default() -> Self {
258 Self::zero()
259 }
260}
261
262#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
264#[cfg_attr(test, derive(serde::Deserialize))]
265pub struct Money(Decimal);
266
267impl_dec_newtype!(Money, "ยค");
268
269impl IsZero for Money {
270 fn is_zero(&self) -> bool {
271 const TOLERANCE: Decimal = dec!(0.01);
272
273 approx_eq_dec(&self.0, &Decimal::ZERO, TOLERANCE)
274 }
275}
276
277impl Money {
278 #[must_use]
279 pub(crate) const fn zero() -> Self {
280 Self(Decimal::ZERO)
281 }
282
283 #[must_use]
285 pub fn apply_vat(self, vat: Vat) -> Self {
286 const ONE: Decimal = dec!(1);
287
288 let x = vat.as_unit_interval().saturating_add(ONE);
289 Self(self.0.saturating_mul(x))
290 }
291
292 pub fn display_currency(&self, currency: currency::Code) -> DisplayCurrency<'_> {
294 DisplayCurrency {
295 currency,
296 money: self,
297 }
298 }
299}
300
301pub struct DisplayCurrency<'a> {
306 currency: currency::Code,
307 money: &'a Money,
308}
309
310impl fmt::Display for DisplayCurrency<'_> {
311 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312 write!(f, "{}{:#}", self.currency.into_symbol(), self.money)
313 }
314}
315
316#[derive(Debug, PartialEq, Eq, Clone, Copy)]
318pub struct Vat(Decimal);
319
320impl_dec_newtype!(Vat, "%");
321
322impl Vat {
323 #[expect(clippy::missing_panics_doc, reason = "The divisor is non-zero")]
324 pub fn as_unit_interval(self) -> Decimal {
328 const PERCENT: Decimal = dec!(100);
329
330 self.0.checked_div(PERCENT).expect("divisor is non-zero")
331 }
332}
333
334#[derive(Clone, Copy, Debug)]
336pub(crate) enum VatOrigin {
337 Unknown,
341
342 NotProvided,
346
347 Provided(Vat),
349}
350
351impl<'buf> FromSchema<'buf, schema::Number<'buf>> for VatOrigin {
352 type Warning = number::Warning;
353
354 fn from_schema(source: &schema::Number<'buf>) -> Verdict<Self, Self::Warning> {
355 let vat = Decimal::from_schema(source)?;
356 Ok(vat.map(|d| Self::Provided(Vat::from_decimal(d))))
357 }
358}