1use std::{borrow::Cow, fmt};
3
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6
7use crate::{
8 from_warning_set_to, into_caveat,
9 json::{self, FieldsAsExt as _},
10 number,
11 warning::{self, GatherWarnings as _, IntoCaveat},
12 Number, Verdict,
13};
14
15pub trait Cost: Copy {
17 fn cost(&self, price: Money) -> Money;
19}
20
21impl Cost for () {
22 fn cost(&self, price: Money) -> Money {
23 price
24 }
25}
26
27#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
29pub enum WarningKind {
30 ExclusiveVatGreaterThanInclusive,
32
33 InvalidType,
35
36 MissingExclVatField,
38
39 Number(number::WarningKind),
41}
42
43impl fmt::Display for WarningKind {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 match self {
46 WarningKind::ExclusiveVatGreaterThanInclusive => write!(
47 f,
48 "The `excl_vat` field is greater than the `incl_vat` field"
49 ),
50 WarningKind::InvalidType => write!(f, "The value should be a number."),
51 WarningKind::MissingExclVatField => write!(f, "The `excl_vat` field is required."),
52 WarningKind::Number(kind) => fmt::Display::fmt(kind, f),
53 }
54 }
55}
56
57impl warning::Kind for WarningKind {
58 fn id(&self) -> Cow<'static, str> {
59 match self {
60 WarningKind::ExclusiveVatGreaterThanInclusive => {
61 "exclusive_vat_greater_than_inclusive".into()
62 }
63 WarningKind::InvalidType => "invalid_type".into(),
64 WarningKind::MissingExclVatField => "missing_excl_vat_field".into(),
65 WarningKind::Number(kind) => format!("number.{}", kind.id()).into(),
66 }
67 }
68}
69
70impl From<number::WarningKind> for WarningKind {
71 fn from(warn_kind: number::WarningKind) -> Self {
72 Self::Number(warn_kind)
73 }
74}
75
76from_warning_set_to!(number::WarningKind => WarningKind);
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Deserialize, Serialize)]
80pub struct Price {
81 pub excl_vat: Money,
83
84 #[serde(default)]
91 pub incl_vat: Option<Money>,
92}
93
94impl fmt::Display for Price {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 write!(f, "{{ excl_vat: {}, incl_vat: ", self.excl_vat)?;
97
98 if let Some(incl_vat) = self.incl_vat {
99 write!(f, "{incl_vat} }}")
100 } else {
101 f.write_str("None }")
102 }
103 }
104}
105
106impl json::FromJson<'_, '_> for Price {
107 type WarningKind = WarningKind;
108
109 fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
110 let mut warnings = warning::Set::new();
111 let value = elem.as_value();
112
113 let Some(fields) = value.as_object_fields() else {
114 warnings.with_elem(WarningKind::InvalidType, elem);
115 return Err(warnings);
116 };
117
118 let Some(excl_vat) = fields.find_field("excl_vat") else {
119 warnings.with_elem(WarningKind::MissingExclVatField, elem);
120 return Err(warnings);
121 };
122
123 let excl_vat = Money::from_json(excl_vat.element())?.gather_warnings_into(&mut warnings);
124
125 let incl_vat = fields
126 .find_field("incl_vat")
127 .map(|f| Money::from_json(f.element()))
128 .transpose()?
129 .gather_warnings_into(&mut warnings);
130
131 if let Some(incl_vat) = incl_vat {
132 if excl_vat > incl_vat {
133 warnings.with_elem(WarningKind::ExclusiveVatGreaterThanInclusive, elem);
134 }
135 }
136
137 Ok(Self { excl_vat, incl_vat }.into_caveat(warnings))
138 }
139}
140
141into_caveat!(Price);
142
143impl Price {
144 pub fn zero() -> Self {
145 Self {
146 excl_vat: Money::zero(),
147 incl_vat: Some(Money::zero()),
148 }
149 }
150
151 pub fn is_zero(&self) -> bool {
152 self.excl_vat.is_zero() && self.incl_vat.is_none_or(|v| v.is_zero())
153 }
154
155 #[must_use]
157 pub fn rescale(self) -> Self {
158 Self {
159 excl_vat: self.excl_vat.rescale(),
160 incl_vat: self.incl_vat.map(Money::rescale),
161 }
162 }
163
164 #[must_use]
166 pub fn saturating_add(self, rhs: Self) -> Self {
167 let incl_vat = self
168 .incl_vat
169 .zip(rhs.incl_vat)
170 .map(|(lhs, rhs)| lhs.saturating_add(rhs));
171
172 Self {
173 excl_vat: self.excl_vat.saturating_add(rhs.excl_vat),
174 incl_vat,
175 }
176 }
177
178 #[must_use]
179 pub fn round_dp(self, digits: u32) -> Self {
180 Self {
181 excl_vat: self.excl_vat.round_dp(digits),
182 incl_vat: self.incl_vat.map(|v| v.round_dp(digits)),
183 }
184 }
185}
186
187impl Default for Price {
188 fn default() -> Self {
189 Self::zero()
190 }
191}
192
193#[derive(Debug, Default, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
195#[serde(transparent)]
196pub struct Money(Number);
197
198impl json::FromJson<'_, '_> for Money {
199 type WarningKind = number::WarningKind;
200
201 fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
202 Number::from_json(elem).map(|v| v.map(Self))
203 }
204}
205
206impl Money {
207 pub(crate) fn from_number(number: Number) -> Self {
208 Self(number)
209 }
210
211 pub fn zero() -> Self {
212 Self(Number::default())
213 }
214
215 pub fn is_zero(&self) -> bool {
216 self.0.is_zero()
217 }
218
219 #[must_use]
221 pub fn rescale(self) -> Self {
222 Self(self.0.rescale())
223 }
224
225 #[must_use]
226 pub fn round_dp(self, digits: u32) -> Self {
227 Self(self.0.round_dp(digits))
228 }
229
230 #[must_use]
232 pub fn saturating_add(self, other: Self) -> Self {
233 Self(self.0.saturating_add(other.0))
234 }
235
236 #[must_use]
238 pub fn apply_vat(self, vat: Vat) -> Self {
239 Self(self.0.saturating_mul(vat.as_fraction()))
240 }
241}
242
243impl From<Money> for Decimal {
244 fn from(value: Money) -> Self {
245 value.0.into()
246 }
247}
248
249impl From<Money> for Number {
250 fn from(value: Money) -> Self {
251 value.0
252 }
253}
254
255impl From<Decimal> for Money {
256 fn from(value: Decimal) -> Self {
257 Self(value.into())
258 }
259}
260
261impl fmt::Display for Money {
262 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
263 write!(f, "{:.4}", self.0)
264 }
265}
266
267#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
269#[serde(transparent)]
270pub struct Vat(Number);
271
272impl From<Vat> for Decimal {
273 fn from(value: Vat) -> Self {
274 value.0.into()
275 }
276}
277
278impl Vat {
279 #[expect(clippy::missing_panics_doc, reason = "The divisor is non-zero")]
280 pub fn as_fraction(self) -> Number {
281 self.0
282 .checked_div(100.into())
283 .expect("divisor is non-zero")
284 .saturating_add(1.into())
285 }
286}
287
288impl fmt::Display for Vat {
289 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290 self.0.fmt(f)
291 }
292}
293
294#[derive(Clone, Copy, Debug)]
296pub enum VatApplicable {
297 Unknown,
301
302 Inapplicable,
306
307 Applicable(Vat),
309}
310
311impl Serialize for VatApplicable {
312 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
313 where
314 S: serde::Serializer,
315 {
316 let vat = match self {
317 Self::Unknown | Self::Inapplicable => None,
318 Self::Applicable(vat) => Some(vat),
319 };
320
321 vat.serialize(serializer)
322 }
323}
324
325impl<'de> Deserialize<'de> for VatApplicable {
326 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
327 where
328 D: serde::Deserializer<'de>,
329 {
330 let vat = <Option<Vat>>::deserialize(deserializer)?;
331
332 let vat = if let Some(vat) = vat {
333 Self::Applicable(vat)
334 } else {
335 Self::Inapplicable
336 };
337
338 Ok(vat)
339 }
340}
341
342#[cfg(test)]
343mod test {
344 use rust_decimal::Decimal;
345 use rust_decimal_macros::dec;
346
347 use crate::test::{AsDecimal, DecimalPartialEq};
348
349 use super::{Money, Price};
350
351 impl AsDecimal for Money {
352 fn as_dec(&self) -> &Decimal {
353 self.0.as_dec()
354 }
355 }
356
357 impl DecimalPartialEq for Money {
358 fn eq_dec(&self, other: &Self) -> bool {
359 const EQ_TOLERANCE: Decimal = dec!(0.01);
362 const EQ_PRECISION: u32 = 2;
364
365 let a = self.0.round_dp(EQ_PRECISION);
366 let b = other.0.rescale().round_dp(EQ_PRECISION);
367 let diff = a.as_dec() - b.as_dec();
368 diff.abs() <= EQ_TOLERANCE
369 }
370 }
371
372 impl DecimalPartialEq for Price {
373 fn eq_dec(&self, other: &Self) -> bool {
374 let incl_eq = match (self.incl_vat, other.incl_vat) {
375 (Some(a), Some(b)) => a.eq_dec(&b),
376 (None, None) => true,
377 _ => return false,
378 };
379
380 incl_eq && self.excl_vat.eq_dec(&other.excl_vat)
381 }
382 }
383}
384
385#[cfg(test)]
386mod test_price {
387 use assert_matches::assert_matches;
388 use rust_decimal::Decimal;
389 use rust_decimal_macros::dec;
390
391 use crate::json::{self, FromJson as _};
392
393 use super::{Price, WarningKind};
394
395 #[test]
396 fn should_create_from_json_with_only_excl_vat_field() {
397 const JSON: &str = r#"{
398 "excl_vat": 10.2
399 }"#;
400
401 let elem = json::parse(JSON).unwrap();
402 let price = Price::from_json(&elem).unwrap().unwrap();
403
404 assert!(price.incl_vat.is_none());
405 assert_eq!(Decimal::from(price.excl_vat), dec!(10.2));
406 }
407
408 #[test]
409 fn should_create_from_json_with_excl_and_incl_vat_fields() {
410 const JSON: &str = r#"{
411 "excl_vat": 10.2,
412 "incl_vat": 12.3
413 }"#;
414
415 let elem = json::parse(JSON).unwrap();
416 let price = Price::from_json(&elem).unwrap().unwrap();
417
418 assert_eq!(Decimal::from(price.incl_vat.unwrap()), dec!(12.3));
419 assert_eq!(Decimal::from(price.excl_vat), dec!(10.2));
420 }
421
422 #[test]
423 fn should_fail_to_create_from_non_object_json() {
424 const JSON: &str = "12.3";
425
426 let elem = json::parse(JSON).unwrap();
427 let warnings = Price::from_json(&elem).unwrap_err().into_kind_vec();
428
429 assert_matches!(*warnings, [WarningKind::InvalidType]);
430 }
431
432 #[test]
433 fn should_fail_to_create_from_json_as_excl_vat_is_required() {
434 const JSON: &str = r#"{
435 "incl_vat": 12.3
436 }"#;
437
438 let elem = json::parse(JSON).unwrap();
439 let warnings = Price::from_json(&elem).unwrap_err().into_kind_vec();
440
441 assert_matches!(*warnings, [WarningKind::MissingExclVatField]);
442 }
443
444 #[test]
445 fn should_create_from_json_and_warn_about_excl_vat_greater_than_incl_vat() {
446 const JSON: &str = r#"{
447 "excl_vat": 12.3,
448 "incl_vat": 10.2
449 }"#;
450
451 let elem = json::parse(JSON).unwrap();
452 let (_price, warnings) = Price::from_json(&elem).unwrap().into_parts();
453 let warnings = warnings.into_kind_vec();
454
455 assert_matches!(*warnings, [WarningKind::ExclusiveVatGreaterThanInclusive]);
456 }
457}