1use std::{borrow::Cow, fmt};
3
4use rust_decimal::Decimal;
5use rust_decimal_macros::dec;
6use serde::{Deserialize, Serialize};
7
8use crate::{
9 from_warning_set_to, impl_dec_newtype, into_caveat,
10 json::{self, FieldsAsExt as _},
11 number,
12 warning::{self, GatherWarnings as _, IntoCaveat},
13 SaturatingAdd as _, Verdict,
14};
15
16pub trait Cost: Copy {
18 fn cost(&self, money: Money) -> Money;
20}
21
22impl Cost for () {
23 fn cost(&self, money: Money) -> Money {
24 money
25 }
26}
27
28#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
30pub enum WarningKind {
31 ExclusiveVatGreaterThanInclusive,
33
34 InvalidType,
36
37 MissingExclVatField,
39
40 Number(number::WarningKind),
42}
43
44impl fmt::Display for WarningKind {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 match self {
47 WarningKind::ExclusiveVatGreaterThanInclusive => write!(
48 f,
49 "The `excl_vat` field is greater than the `incl_vat` field"
50 ),
51 WarningKind::InvalidType => write!(f, "The value should be a number."),
52 WarningKind::MissingExclVatField => write!(f, "The `excl_vat` field is required."),
53 WarningKind::Number(kind) => fmt::Display::fmt(kind, f),
54 }
55 }
56}
57
58impl warning::Kind for WarningKind {
59 fn id(&self) -> Cow<'static, str> {
60 match self {
61 WarningKind::ExclusiveVatGreaterThanInclusive => {
62 "exclusive_vat_greater_than_inclusive".into()
63 }
64 WarningKind::InvalidType => "invalid_type".into(),
65 WarningKind::MissingExclVatField => "missing_excl_vat_field".into(),
66 WarningKind::Number(kind) => format!("number.{}", kind.id()).into(),
67 }
68 }
69}
70
71impl From<number::WarningKind> for WarningKind {
72 fn from(warn_kind: number::WarningKind) -> Self {
73 Self::Number(warn_kind)
74 }
75}
76
77from_warning_set_to!(number::WarningKind => WarningKind);
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Deserialize, Serialize)]
81pub struct Price {
82 pub excl_vat: Money,
84
85 #[serde(default)]
92 pub incl_vat: Option<Money>,
93}
94
95impl fmt::Display for Price {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 write!(f, "{{ excl_vat: {}, incl_vat: ", self.excl_vat)?;
98
99 if let Some(incl_vat) = self.incl_vat {
100 write!(f, "{incl_vat} }}")
101 } else {
102 f.write_str("None }")
103 }
104 }
105}
106
107impl json::FromJson<'_, '_> for Price {
108 type WarningKind = WarningKind;
109
110 fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
111 let mut warnings = warning::Set::new();
112 let value = elem.as_value();
113
114 let Some(fields) = value.as_object_fields() else {
115 warnings.with_elem(WarningKind::InvalidType, elem);
116 return Err(warnings);
117 };
118
119 let Some(excl_vat) = fields.find_field("excl_vat") else {
120 warnings.with_elem(WarningKind::MissingExclVatField, elem);
121 return Err(warnings);
122 };
123
124 let excl_vat = Money::from_json(excl_vat.element())?.gather_warnings_into(&mut warnings);
125
126 let incl_vat = fields
127 .find_field("incl_vat")
128 .map(|f| Money::from_json(f.element()))
129 .transpose()?
130 .gather_warnings_into(&mut warnings);
131
132 if let Some(incl_vat) = incl_vat {
133 if excl_vat > incl_vat {
134 warnings.with_elem(WarningKind::ExclusiveVatGreaterThanInclusive, elem);
135 }
136 }
137
138 Ok(Self { excl_vat, incl_vat }.into_caveat(warnings))
139 }
140}
141
142into_caveat!(Price);
143
144impl Price {
145 pub fn zero() -> Self {
146 Self {
147 excl_vat: Money::zero(),
148 incl_vat: Some(Money::zero()),
149 }
150 }
151
152 pub fn is_zero(&self) -> bool {
153 self.excl_vat.is_zero() && self.incl_vat.is_none_or(|v| v.is_zero())
154 }
155
156 #[must_use]
158 pub fn rescale(self) -> Self {
159 Self {
160 excl_vat: self.excl_vat.rescale(),
161 incl_vat: self.incl_vat.map(Money::rescale),
162 }
163 }
164
165 #[must_use]
167 pub(crate) fn saturating_add(self, rhs: Self) -> Self {
168 let incl_vat = self
169 .incl_vat
170 .zip(rhs.incl_vat)
171 .map(|(lhs, rhs)| lhs.saturating_add(rhs));
172
173 Self {
174 excl_vat: self.excl_vat.saturating_add(rhs.excl_vat),
175 incl_vat,
176 }
177 }
178
179 #[must_use]
180 pub fn round_dp(self, digits: u32) -> Self {
181 Self {
182 excl_vat: self.excl_vat.round_dp(digits),
183 incl_vat: self.incl_vat.map(|v| v.round_dp(digits)),
184 }
185 }
186}
187
188impl Default for Price {
189 fn default() -> Self {
190 Self::zero()
191 }
192}
193
194#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
196pub struct Money(Decimal);
197
198impl_dec_newtype!(Money);
199
200impl Money {
201 #[must_use]
203 pub fn apply_vat(self, vat: Vat) -> Self {
204 const ONE: Decimal = dec!(1);
205
206 let x = vat.as_unit_interval().saturating_add(ONE);
207 Self(self.0.saturating_mul(x))
208 }
209}
210
211#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
213#[serde(transparent)]
214pub struct Vat(Decimal);
215
216impl From<Vat> for Decimal {
217 fn from(value: Vat) -> Self {
218 value.0
219 }
220}
221
222impl Vat {
223 #[expect(clippy::missing_panics_doc, reason = "The divisor is non-zero")]
224 pub fn as_unit_interval(self) -> Decimal {
225 const PERCENT: Decimal = dec!(100);
226
227 self.0.checked_div(PERCENT).expect("divisor is non-zero")
228 }
229}
230
231impl fmt::Display for Vat {
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 self.0.fmt(f)
234 }
235}
236
237#[derive(Clone, Copy, Debug)]
239pub enum VatApplicable {
240 Unknown,
244
245 Inapplicable,
249
250 Applicable(Vat),
252}
253
254impl Serialize for VatApplicable {
255 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
256 where
257 S: serde::Serializer,
258 {
259 let vat = match self {
260 Self::Unknown | Self::Inapplicable => None,
261 Self::Applicable(vat) => Some(vat),
262 };
263
264 vat.serialize(serializer)
265 }
266}
267
268impl<'de> Deserialize<'de> for VatApplicable {
269 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
270 where
271 D: serde::Deserializer<'de>,
272 {
273 let vat = <Option<Vat>>::deserialize(deserializer)?;
274
275 let vat = if let Some(vat) = vat {
276 Self::Applicable(vat)
277 } else {
278 Self::Inapplicable
279 };
280
281 Ok(vat)
282 }
283}
284
285#[cfg(test)]
286mod test {
287 use crate::test::ApproxEq;
288
289 use super::Price;
290
291 impl ApproxEq for Price {
292 fn approx_eq(&self, other: &Self) -> bool {
293 let incl_eq = match (self.incl_vat, other.incl_vat) {
294 (Some(a), Some(b)) => a.approx_eq(&b),
295 (None, None) => true,
296 _ => return false,
297 };
298
299 incl_eq && self.excl_vat.approx_eq(&other.excl_vat)
300 }
301 }
302}
303
304#[cfg(test)]
305mod test_price {
306 use assert_matches::assert_matches;
307 use rust_decimal::Decimal;
308 use rust_decimal_macros::dec;
309
310 use crate::json::{self, FromJson as _};
311
312 use super::{Price, WarningKind};
313
314 #[test]
315 fn should_create_from_json_with_only_excl_vat_field() {
316 const JSON: &str = r#"{
317 "excl_vat": 10.2
318 }"#;
319
320 let elem = json::parse(JSON).unwrap();
321 let price = Price::from_json(&elem).unwrap().unwrap();
322
323 assert!(price.incl_vat.is_none());
324 assert_eq!(Decimal::from(price.excl_vat), dec!(10.2));
325 }
326
327 #[test]
328 fn should_create_from_json_with_excl_and_incl_vat_fields() {
329 const JSON: &str = r#"{
330 "excl_vat": 10.2,
331 "incl_vat": 12.3
332 }"#;
333
334 let elem = json::parse(JSON).unwrap();
335 let price = Price::from_json(&elem).unwrap().unwrap();
336
337 assert_eq!(Decimal::from(price.incl_vat.unwrap()), dec!(12.3));
338 assert_eq!(Decimal::from(price.excl_vat), dec!(10.2));
339 }
340
341 #[test]
342 fn should_fail_to_create_from_non_object_json() {
343 const JSON: &str = "12.3";
344
345 let elem = json::parse(JSON).unwrap();
346 let warnings = Price::from_json(&elem).unwrap_err().into_kind_vec();
347
348 assert_matches!(*warnings, [WarningKind::InvalidType]);
349 }
350
351 #[test]
352 fn should_fail_to_create_from_json_as_excl_vat_is_required() {
353 const JSON: &str = r#"{
354 "incl_vat": 12.3
355 }"#;
356
357 let elem = json::parse(JSON).unwrap();
358 let warnings = Price::from_json(&elem).unwrap_err().into_kind_vec();
359
360 assert_matches!(*warnings, [WarningKind::MissingExclVatField]);
361 }
362
363 #[test]
364 fn should_create_from_json_and_warn_about_excl_vat_greater_than_incl_vat() {
365 const JSON: &str = r#"{
366 "excl_vat": 12.3,
367 "incl_vat": 10.2
368 }"#;
369
370 let elem = json::parse(JSON).unwrap();
371 let (_price, warnings) = Price::from_json(&elem).unwrap().into_parts();
372 let warnings = warnings.into_kind_vec();
373
374 assert_matches!(*warnings, [WarningKind::ExclusiveVatGreaterThanInclusive]);
375 }
376}