1#[cfg(test)]
4mod test;
5
6#[cfg(test)]
7mod test_from_schema;
8
9#[cfg(test)]
10mod test_price;
11
12use std::fmt;
13
14use rust_decimal::Decimal;
15use rust_decimal_macros::dec;
16
17use crate::{
18 currency, from_warning_all, impl_dec_newtype,
19 number::{self, approx_eq_dec, FromDecimal as _, IsZero, RoundDecimal},
20 schema,
21 warning::{self, GatherWarnings as _, IntoCaveat as _},
22 FromSchema, SaturatingAdd as _, Verdict,
23};
24
25pub trait Cost: Copy {
27 fn cost(&self, money: Money) -> Money;
29}
30
31impl Cost for () {
32 fn cost(&self, money: Money) -> Money {
33 money
34 }
35}
36
37#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
39pub enum Warning {
40 ExclusiveVatGreaterThanInclusive,
42
43 Number(number::Warning),
45
46 Rejected,
50}
51
52impl fmt::Display for Warning {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 match self {
55 Self::ExclusiveVatGreaterThanInclusive => write!(
56 f,
57 "The `excl_vat` field is greater than the `incl_vat` field"
58 ),
59 Self::Number(kind) => fmt::Display::fmt(kind, f),
60 Self::Rejected => f.write_str(
61 "The schema IR for a `Price` was rejected; see the schema validation warnings.",
62 ),
63 }
64 }
65}
66
67impl crate::Warning for Warning {
68 fn id(&self) -> warning::Id {
69 match self {
70 Self::ExclusiveVatGreaterThanInclusive => {
71 warning::Id::from_static("exclusive_vat_greater_than_inclusive")
72 }
73 Self::Number(kind) => kind.id(),
74 Self::Rejected => warning::Id::from_static("rejected"),
75 }
76 }
77
78 fn is_rejected(&self) -> bool {
79 matches!(self, Self::Rejected)
80 }
81}
82
83impl From<warning::Rejected> for Warning {
84 fn from(_: warning::Rejected) -> Self {
85 Self::Rejected
86 }
87}
88
89from_warning_all!(number::Warning => Warning::Number);
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)]
93#[cfg_attr(test, derive(serde::Deserialize))]
94pub struct Price {
95 pub excl_vat: Money,
97
98 #[cfg_attr(test, serde(default))]
105 pub incl_vat: Option<Money>,
106}
107
108impl RoundDecimal for Price {
109 fn round_to_ocpi_scale(self) -> Self {
110 let Self { excl_vat, incl_vat } = self;
111 Self {
112 excl_vat: excl_vat.round_to_ocpi_scale(),
113 incl_vat: incl_vat.round_to_ocpi_scale(),
114 }
115 }
116}
117
118impl fmt::Display for Price {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 if let Some(incl_vat) = self.incl_vat {
121 if f.alternate() {
122 write!(f, "{{ -vat: {:#}, +vat: {:#} }}", self.excl_vat, incl_vat)
123 } else {
124 write!(f, "{{ -vat: {}, +vat: {} }}", self.excl_vat, incl_vat)
125 }
126 } else {
127 fmt::Display::fmt(&self.excl_vat, f)
128 }
129 }
130}
131
132impl<'buf> FromSchema<'buf, schema::v221::Price<'buf>> for Price {
133 type Warning = Warning;
134
135 fn from_schema(source: &schema::v221::Price<'buf>) -> Verdict<Self, Self::Warning> {
136 let mut warnings = warning::Set::new();
137
138 let (elem, excl_vat, incl_vat) = match source {
141 schema::v221::Price::Number(number) => {
142 let excl_vat = Money::from_schema(number)?.gather_warnings_into(&mut warnings);
143 let price = Self {
144 excl_vat,
145 incl_vat: None,
146 };
147 return Ok(price.into_caveat(warnings));
148 }
149 schema::v221::Price::Object {
150 elem,
151 excl_vat,
152 incl_vat,
153 } => (elem, excl_vat, incl_vat),
154 };
155
156 let excl_vat = warnings.ok_or_bail(excl_vat)?;
160 let excl_vat = Money::from_schema(excl_vat)?.gather_warnings_into(&mut warnings);
161
162 let incl_vat = incl_vat
166 .map_some(Money::from_schema)
167 .transpose()?
168 .gather_warnings_into(&mut warnings);
169
170 if let Some(incl_vat) = incl_vat {
171 if excl_vat > incl_vat {
172 warnings.insert(elem, Warning::ExclusiveVatGreaterThanInclusive);
173 }
174 }
175
176 Ok(Self { excl_vat, incl_vat }.into_caveat(warnings))
177 }
178}
179
180impl IsZero for Price {
181 fn is_zero(&self) -> bool {
182 self.excl_vat.is_zero() && self.incl_vat.is_none_or(|v| v.is_zero())
183 }
184}
185
186impl Price {
187 pub fn zero() -> Self {
188 Self {
189 excl_vat: Money::zero(),
190 incl_vat: Some(Money::zero()),
191 }
192 }
193
194 #[must_use]
196 pub fn rescale(self) -> Self {
197 Self {
198 excl_vat: self.excl_vat.rescale(),
199 incl_vat: self.incl_vat.map(Money::rescale),
200 }
201 }
202
203 #[must_use]
205 pub(crate) fn saturating_add(self, rhs: Self) -> Self {
206 let incl_vat = self
207 .incl_vat
208 .zip(rhs.incl_vat)
209 .map(|(lhs, rhs)| lhs.saturating_add(rhs));
210
211 Self {
212 excl_vat: self.excl_vat.saturating_add(rhs.excl_vat),
213 incl_vat,
214 }
215 }
216
217 #[must_use]
218 pub fn round_dp(self, digits: u32) -> Self {
219 Self {
220 excl_vat: self.excl_vat.round_dp(digits),
221 incl_vat: self.incl_vat.map(|v| v.round_dp(digits)),
222 }
223 }
224
225 pub fn display_currency(&self, currency: currency::Code) -> DisplayPriceCurrency<'_> {
227 DisplayPriceCurrency {
228 currency,
229 price: self,
230 }
231 }
232}
233
234pub struct DisplayPriceCurrency<'a> {
239 currency: currency::Code,
240 price: &'a Price,
241}
242
243impl fmt::Display for DisplayPriceCurrency<'_> {
244 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245 if let Some(incl_vat) = self.price.incl_vat {
246 write!(
247 f,
248 "{{ -vat: {:#}, +vat: {:#} }}",
249 self.price.excl_vat, incl_vat
250 )
251 } else {
252 fmt::Display::fmt(&self.price.excl_vat.display_currency(self.currency), f)
253 }
254 }
255}
256
257impl Default for Price {
258 fn default() -> Self {
259 Self::zero()
260 }
261}
262
263#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
265#[cfg_attr(test, derive(serde::Deserialize))]
266pub struct Money(Decimal);
267
268impl_dec_newtype!(Money, "ยค");
269
270impl IsZero for Money {
271 fn is_zero(&self) -> bool {
272 const TOLERANCE: Decimal = dec!(0.01);
273
274 approx_eq_dec(&self.0, &Decimal::ZERO, TOLERANCE)
275 }
276}
277
278impl Money {
279 #[must_use]
280 pub(crate) const fn zero() -> Self {
281 Self(Decimal::ZERO)
282 }
283
284 #[must_use]
286 pub fn apply_vat(self, vat: Vat) -> Self {
287 const ONE: Decimal = dec!(1);
288
289 let x = vat.as_unit_interval().saturating_add(ONE);
290 Self(self.0.saturating_mul(x))
291 }
292
293 pub fn display_currency(&self, currency: currency::Code) -> DisplayCurrency<'_> {
295 DisplayCurrency {
296 currency,
297 money: self,
298 }
299 }
300}
301
302pub struct DisplayCurrency<'a> {
307 currency: currency::Code,
308 money: &'a Money,
309}
310
311impl fmt::Display for DisplayCurrency<'_> {
312 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313 write!(f, "{}{:#}", self.currency.into_symbol(), self.money)
314 }
315}
316
317#[derive(Debug, PartialEq, Eq, Clone, Copy)]
319pub struct Vat(Decimal);
320
321impl_dec_newtype!(Vat, "%");
322
323impl Vat {
324 #[expect(clippy::missing_panics_doc, reason = "The divisor is non-zero")]
325 pub fn as_unit_interval(self) -> Decimal {
326 const PERCENT: Decimal = dec!(100);
327
328 self.0.checked_div(PERCENT).expect("divisor is non-zero")
329 }
330}
331
332#[derive(Clone, Copy, Debug)]
334pub(crate) enum VatOrigin {
335 Unknown,
339
340 NotProvided,
344
345 Provided(Vat),
347}
348
349impl<'buf> FromSchema<'buf, schema::Number<'buf>> for VatOrigin {
350 type Warning = number::Warning;
351
352 fn from_schema(source: &schema::Number<'buf>) -> Verdict<Self, Self::Warning> {
353 let vat = Decimal::from_schema(source)?;
354 Ok(vat.map(|d| Self::Provided(Vat::from_decimal(d))))
355 }
356}