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 json::{self, FieldsAsExt as _},
20 number::{self, approx_eq_dec, FromDecimal as _, IsZero, RoundDecimal},
21 schema::{self, Integrity},
22 warning::{self, GatherWarnings as _, IntoCaveat as _},
23 FromSchema, SaturatingAdd as _, Verdict,
24};
25
26pub trait Cost: Copy {
28 fn cost(&self, money: Money) -> Money;
30}
31
32impl Cost for () {
33 fn cost(&self, money: Money) -> Money {
34 money
35 }
36}
37
38#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
40pub enum Warning {
41 ExclusiveVatGreaterThanInclusive,
43
44 InvalidType { type_found: json::ValueKind },
46
47 MissingExclVatField,
49
50 Number(number::Warning),
52}
53
54impl Warning {
55 fn invalid_type(elem: &json::Element<'_>) -> Self {
56 Self::InvalidType {
57 type_found: elem.value().kind(),
58 }
59 }
60}
61
62impl fmt::Display for Warning {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 match self {
65 Self::ExclusiveVatGreaterThanInclusive => write!(
66 f,
67 "The `excl_vat` field is greater than the `incl_vat` field"
68 ),
69 Self::InvalidType { type_found } => {
70 write!(
71 f,
72 "The value should be a `Price {{ excl_vat, incl_vat }}` object but is `{type_found}`"
73 )
74 }
75 Self::MissingExclVatField => write!(f, "The `excl_vat` field is required."),
76 Self::Number(kind) => fmt::Display::fmt(kind, f),
77 }
78 }
79}
80
81impl crate::Warning for Warning {
82 fn id(&self) -> warning::Id {
83 match self {
84 Self::ExclusiveVatGreaterThanInclusive => {
85 warning::Id::from_static("exclusive_vat_greater_than_inclusive")
86 }
87 Self::InvalidType { type_found } => {
88 warning::Id::from_string(format!("invalid_type({type_found})"))
89 }
90 Self::MissingExclVatField => warning::Id::from_static("missing_excl_vat_field"),
91 Self::Number(kind) => kind.id(),
92 }
93 }
94}
95
96from_warning_all!(number::Warning => Warning::Number);
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)]
100#[cfg_attr(test, derive(serde::Deserialize))]
101pub struct Price {
102 pub excl_vat: Money,
104
105 #[cfg_attr(test, serde(default))]
112 pub incl_vat: Option<Money>,
113}
114
115impl RoundDecimal for Price {
116 fn round_to_ocpi_scale(self) -> Self {
117 let Self { excl_vat, incl_vat } = self;
118 Self {
119 excl_vat: excl_vat.round_to_ocpi_scale(),
120 incl_vat: incl_vat.round_to_ocpi_scale(),
121 }
122 }
123}
124
125impl fmt::Display for Price {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 if let Some(incl_vat) = self.incl_vat {
128 if f.alternate() {
129 write!(f, "{{ -vat: {:#}, +vat: {:#} }}", self.excl_vat, incl_vat)
130 } else {
131 write!(f, "{{ -vat: {}, +vat: {} }}", self.excl_vat, incl_vat)
132 }
133 } else {
134 fmt::Display::fmt(&self.excl_vat, f)
135 }
136 }
137}
138
139impl json::FromJson<'_> for Price {
140 type Warning = Warning;
141
142 fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::Warning> {
143 let mut warnings = warning::Set::new();
144 let value = elem.as_value();
145
146 let Some(fields) = value.as_object_fields() else {
147 return warnings.bail(elem, Warning::invalid_type(elem));
148 };
149
150 let Some(excl_vat) = fields.find_field("excl_vat") else {
151 return warnings.bail(elem, Warning::MissingExclVatField);
152 };
153
154 let excl_vat = Money::from_json(excl_vat.element())?.gather_warnings_into(&mut warnings);
155
156 let incl_vat = fields
157 .find_field("incl_vat")
158 .map(|f| Money::from_json(f.element()))
159 .transpose()?
160 .gather_warnings_into(&mut warnings);
161
162 if let Some(incl_vat) = incl_vat {
163 if excl_vat > incl_vat {
164 warnings.insert(elem, Warning::ExclusiveVatGreaterThanInclusive);
165 }
166 }
167
168 Ok(Self { excl_vat, incl_vat }.into_caveat(warnings))
169 }
170}
171
172impl<'buf> FromSchema<'buf, schema::v221::Price<'buf>> for Price {
173 type Warning = Warning;
174
175 fn from_schema(source: &schema::v221::Price<'buf>) -> Verdict<Self, Self::Warning> {
176 let mut warnings = warning::Set::new();
177
178 let (elem, excl_vat, incl_vat) = match source {
181 schema::v221::Price::Number(number) => {
182 let excl_vat = Money::from_schema(number)?.gather_warnings_into(&mut warnings);
183 let price = Self {
184 excl_vat,
185 incl_vat: None,
186 };
187 return Ok(price.into_caveat(warnings));
188 }
189 schema::v221::Price::Object {
190 elem,
191 excl_vat,
192 incl_vat,
193 } => (elem, excl_vat, incl_vat),
194 };
195
196 let Integrity::Ok(excl_vat) = excl_vat else {
200 return warnings.bail(elem, Warning::MissingExclVatField);
201 };
202 let excl_vat = Money::from_schema(excl_vat)?.gather_warnings_into(&mut warnings);
203
204 let incl_vat = match incl_vat {
208 Integrity::Ok(Some(number)) => {
209 Some(Money::from_schema(number)?.gather_warnings_into(&mut warnings))
210 }
211 Integrity::Ok(None) | Integrity::Missing | Integrity::Err => None,
212 };
213
214 if let Some(incl_vat) = incl_vat {
215 if excl_vat > incl_vat {
216 warnings.insert(elem, Warning::ExclusiveVatGreaterThanInclusive);
217 }
218 }
219
220 Ok(Self { excl_vat, incl_vat }.into_caveat(warnings))
221 }
222}
223
224impl IsZero for Price {
225 fn is_zero(&self) -> bool {
226 self.excl_vat.is_zero() && self.incl_vat.is_none_or(|v| v.is_zero())
227 }
228}
229
230impl Price {
231 pub fn zero() -> Self {
232 Self {
233 excl_vat: Money::zero(),
234 incl_vat: Some(Money::zero()),
235 }
236 }
237
238 #[must_use]
240 pub fn rescale(self) -> Self {
241 Self {
242 excl_vat: self.excl_vat.rescale(),
243 incl_vat: self.incl_vat.map(Money::rescale),
244 }
245 }
246
247 #[must_use]
249 pub(crate) fn saturating_add(self, rhs: Self) -> Self {
250 let incl_vat = self
251 .incl_vat
252 .zip(rhs.incl_vat)
253 .map(|(lhs, rhs)| lhs.saturating_add(rhs));
254
255 Self {
256 excl_vat: self.excl_vat.saturating_add(rhs.excl_vat),
257 incl_vat,
258 }
259 }
260
261 #[must_use]
262 pub fn round_dp(self, digits: u32) -> Self {
263 Self {
264 excl_vat: self.excl_vat.round_dp(digits),
265 incl_vat: self.incl_vat.map(|v| v.round_dp(digits)),
266 }
267 }
268
269 pub fn display_currency(&self, currency: currency::Code) -> DisplayPriceCurrency<'_> {
271 DisplayPriceCurrency {
272 currency,
273 price: self,
274 }
275 }
276}
277
278pub(crate) struct PriceOrNumber(Price);
283
284impl PriceOrNumber {
285 pub(crate) fn into_inner(self) -> Price {
286 self.0
287 }
288}
289
290impl json::FromJson<'_> for PriceOrNumber {
291 type Warning = Warning;
292
293 fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::Warning> {
294 let mut warnings = warning::Set::new();
295 let value = elem.as_value();
296
297 if value.kind() == json::ValueKind::Number {
298 warnings.insert(elem, Warning::invalid_type(elem));
299
300 let excl_vat = Money::from_json(elem)?.gather_warnings_into(&mut warnings);
301 let price = Price {
302 excl_vat,
303 incl_vat: None,
304 };
305 return Ok(Self(price).into_caveat(warnings));
306 }
307
308 let price = Price::from_json(elem).gather_warnings_into(&mut warnings)?;
309 Ok(Self(price).into_caveat(warnings))
310 }
311}
312
313pub struct DisplayPriceCurrency<'a> {
318 currency: currency::Code,
319 price: &'a Price,
320}
321
322impl fmt::Display for DisplayPriceCurrency<'_> {
323 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324 if let Some(incl_vat) = self.price.incl_vat {
325 write!(
326 f,
327 "{{ -vat: {:#}, +vat: {:#} }}",
328 self.price.excl_vat, incl_vat
329 )
330 } else {
331 fmt::Display::fmt(&self.price.excl_vat.display_currency(self.currency), f)
332 }
333 }
334}
335
336impl Default for Price {
337 fn default() -> Self {
338 Self::zero()
339 }
340}
341
342#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
344#[cfg_attr(test, derive(serde::Deserialize))]
345pub struct Money(Decimal);
346
347impl_dec_newtype!(Money, "ยค");
348
349impl IsZero for Money {
350 fn is_zero(&self) -> bool {
351 const TOLERANCE: Decimal = dec!(0.01);
352
353 approx_eq_dec(&self.0, &Decimal::ZERO, TOLERANCE)
354 }
355}
356
357impl Money {
358 #[must_use]
359 pub(crate) const fn zero() -> Self {
360 Self(Decimal::ZERO)
361 }
362
363 #[must_use]
365 pub fn apply_vat(self, vat: Vat) -> Self {
366 const ONE: Decimal = dec!(1);
367
368 let x = vat.as_unit_interval().saturating_add(ONE);
369 Self(self.0.saturating_mul(x))
370 }
371
372 pub fn display_currency(&self, currency: currency::Code) -> DisplayCurrency<'_> {
374 DisplayCurrency {
375 currency,
376 money: self,
377 }
378 }
379}
380
381pub struct DisplayCurrency<'a> {
386 currency: currency::Code,
387 money: &'a Money,
388}
389
390impl fmt::Display for DisplayCurrency<'_> {
391 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
392 write!(f, "{}{:#}", self.currency.into_symbol(), self.money)
393 }
394}
395
396#[derive(Debug, PartialEq, Eq, Clone, Copy)]
398pub struct Vat(Decimal);
399
400impl_dec_newtype!(Vat, "%");
401
402impl Vat {
403 #[expect(clippy::missing_panics_doc, reason = "The divisor is non-zero")]
404 pub fn as_unit_interval(self) -> Decimal {
405 const PERCENT: Decimal = dec!(100);
406
407 self.0.checked_div(PERCENT).expect("divisor is non-zero")
408 }
409}
410
411#[derive(Clone, Copy, Debug)]
413pub(crate) enum VatOrigin {
414 Unknown,
418
419 NotProvided,
423
424 Provided(Vat),
426}
427
428impl json::FromJson<'_> for VatOrigin {
429 type Warning = number::Warning;
430
431 fn from_json(elem: &'_ json::Element<'_>) -> Verdict<Self, Self::Warning> {
432 let vat = Decimal::from_json(elem)?;
433 Ok(vat.map(|d| Self::Provided(Vat::from_decimal(d))))
434 }
435}
436
437impl<'buf> FromSchema<'buf, schema::Number<'buf>> for VatOrigin {
438 type Warning = number::Warning;
439
440 fn from_schema(source: &schema::Number<'buf>) -> Verdict<Self, Self::Warning> {
441 let vat = Decimal::from_schema(source)?;
442 Ok(vat.map(|d| Self::Provided(Vat::from_decimal(d))))
443 }
444}