1use std::{borrow::Cow, fmt};
3
4use rust_decimal::Decimal;
5use rust_decimal_macros::dec;
6
7use crate::{
8 from_warning_set_to, impl_dec_newtype, into_caveat,
9 json::{self, FieldsAsExt as _},
10 number::{self, FromDecimal as _},
11 warning::{self, GatherWarnings as _, IntoCaveat},
12 SaturatingAdd as _, Verdict,
13};
14
15pub trait Cost: Copy {
17 fn cost(&self, money: Money) -> Money;
19}
20
21impl Cost for () {
22 fn cost(&self, money: Money) -> Money {
23 money
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) => kind.id(),
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)]
80#[cfg_attr(test, derive(serde::Deserialize))]
81pub struct Price {
82 pub excl_vat: Money,
84
85 #[cfg_attr(test, 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 if let Some(incl_vat) = self.incl_vat {
98 write!(f, "{{ -vat: {}, +vat: {} }}", self.excl_vat, incl_vat)
99 } else {
100 fmt::Display::fmt(&self.excl_vat, f)
101 }
102 }
103}
104
105impl json::FromJson<'_, '_> for Price {
106 type WarningKind = WarningKind;
107
108 fn from_json(elem: &json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
109 let mut warnings = warning::Set::new();
110 let value = elem.as_value();
111
112 let Some(fields) = value.as_object_fields() else {
113 warnings.with_elem(WarningKind::InvalidType, elem);
114 return Err(warnings);
115 };
116
117 let Some(excl_vat) = fields.find_field("excl_vat") else {
118 warnings.with_elem(WarningKind::MissingExclVatField, elem);
119 return Err(warnings);
120 };
121
122 let excl_vat = Money::from_json(excl_vat.element())?.gather_warnings_into(&mut warnings);
123
124 let incl_vat = fields
125 .find_field("incl_vat")
126 .map(|f| Money::from_json(f.element()))
127 .transpose()?
128 .gather_warnings_into(&mut warnings);
129
130 if let Some(incl_vat) = incl_vat {
131 if excl_vat > incl_vat {
132 warnings.with_elem(WarningKind::ExclusiveVatGreaterThanInclusive, elem);
133 }
134 }
135
136 Ok(Self { excl_vat, incl_vat }.into_caveat(warnings))
137 }
138}
139
140into_caveat!(Price);
141
142impl Price {
143 pub fn zero() -> Self {
144 Self {
145 excl_vat: Money::zero(),
146 incl_vat: Some(Money::zero()),
147 }
148 }
149
150 pub fn is_zero(&self) -> bool {
151 self.excl_vat.is_zero() && self.incl_vat.is_none_or(|v| v.is_zero())
152 }
153
154 #[must_use]
156 pub fn rescale(self) -> Self {
157 Self {
158 excl_vat: self.excl_vat.rescale(),
159 incl_vat: self.incl_vat.map(Money::rescale),
160 }
161 }
162
163 #[must_use]
165 pub(crate) fn saturating_add(self, rhs: Self) -> Self {
166 let incl_vat = self
167 .incl_vat
168 .zip(rhs.incl_vat)
169 .map(|(lhs, rhs)| lhs.saturating_add(rhs));
170
171 Self {
172 excl_vat: self.excl_vat.saturating_add(rhs.excl_vat),
173 incl_vat,
174 }
175 }
176
177 #[must_use]
178 pub fn round_dp(self, digits: u32) -> Self {
179 Self {
180 excl_vat: self.excl_vat.round_dp(digits),
181 incl_vat: self.incl_vat.map(|v| v.round_dp(digits)),
182 }
183 }
184}
185
186impl Default for Price {
187 fn default() -> Self {
188 Self::zero()
189 }
190}
191
192#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
194#[cfg_attr(test, derive(serde::Deserialize))]
195pub struct Money(Decimal);
196
197impl_dec_newtype!(Money, "ยค");
198
199impl Money {
200 #[must_use]
202 pub fn apply_vat(self, vat: Vat) -> Self {
203 const ONE: Decimal = dec!(1);
204
205 let x = vat.as_unit_interval().saturating_add(ONE);
206 Self(self.0.saturating_mul(x))
207 }
208}
209
210#[derive(Debug, PartialEq, Eq, Clone, Copy)]
212pub struct Vat(Decimal);
213
214impl_dec_newtype!(Vat, "%");
215
216impl Vat {
217 #[expect(clippy::missing_panics_doc, reason = "The divisor is non-zero")]
218 pub fn as_unit_interval(self) -> Decimal {
219 const PERCENT: Decimal = dec!(100);
220
221 self.0.checked_div(PERCENT).expect("divisor is non-zero")
222 }
223}
224
225#[derive(Clone, Copy, Debug)]
227pub enum VatApplicable {
228 Unknown,
232
233 Inapplicable,
237
238 Applicable(Vat),
240}
241
242impl json::FromJson<'_, '_> for VatApplicable {
243 type WarningKind = number::WarningKind;
244
245 fn from_json(elem: &'_ json::Element<'_>) -> Verdict<Self, Self::WarningKind> {
246 let vat = Decimal::from_json(elem)?;
247 Ok(vat.map(|d| Self::Applicable(Vat::from_decimal(d))))
248 }
249}
250
251#[cfg(test)]
252mod test {
253 use crate::test::ApproxEq;
254
255 use super::Price;
256
257 impl ApproxEq for Price {
258 fn approx_eq(&self, other: &Self) -> bool {
259 let incl_eq = match (self.incl_vat, other.incl_vat) {
260 (Some(a), Some(b)) => a.approx_eq(&b),
261 (None, None) => true,
262 _ => return false,
263 };
264
265 incl_eq && self.excl_vat.approx_eq(&other.excl_vat)
266 }
267 }
268}
269
270#[cfg(test)]
271mod test_price {
272 use assert_matches::assert_matches;
273 use rust_decimal::Decimal;
274 use rust_decimal_macros::dec;
275
276 use crate::json::{self, FromJson as _};
277
278 use super::{Price, WarningKind};
279
280 #[test]
281 fn should_create_from_json_with_only_excl_vat_field() {
282 const JSON: &str = r#"{
283 "excl_vat": 10.2
284 }"#;
285
286 let elem = json::parse(JSON).unwrap();
287 let price = Price::from_json(&elem).unwrap().unwrap();
288
289 assert!(price.incl_vat.is_none());
290 assert_eq!(Decimal::from(price.excl_vat), dec!(10.2));
291 }
292
293 #[test]
294 fn should_create_from_json_with_excl_and_incl_vat_fields() {
295 const JSON: &str = r#"{
296 "excl_vat": 10.2,
297 "incl_vat": 12.3
298 }"#;
299
300 let elem = json::parse(JSON).unwrap();
301 let price = Price::from_json(&elem).unwrap().unwrap();
302
303 assert_eq!(Decimal::from(price.incl_vat.unwrap()), dec!(12.3));
304 assert_eq!(Decimal::from(price.excl_vat), dec!(10.2));
305 }
306
307 #[test]
308 fn should_fail_to_create_from_non_object_json() {
309 const JSON: &str = "12.3";
310
311 let elem = json::parse(JSON).unwrap();
312 let warnings = Price::from_json(&elem).unwrap_err().into_kind_vec();
313
314 assert_matches!(*warnings, [WarningKind::InvalidType]);
315 }
316
317 #[test]
318 fn should_fail_to_create_from_json_as_excl_vat_is_required() {
319 const JSON: &str = r#"{
320 "incl_vat": 12.3
321 }"#;
322
323 let elem = json::parse(JSON).unwrap();
324 let warnings = Price::from_json(&elem).unwrap_err().into_kind_vec();
325
326 assert_matches!(*warnings, [WarningKind::MissingExclVatField]);
327 }
328
329 #[test]
330 fn should_create_from_json_and_warn_about_excl_vat_greater_than_incl_vat() {
331 const JSON: &str = r#"{
332 "excl_vat": 12.3,
333 "incl_vat": 10.2
334 }"#;
335
336 let elem = json::parse(JSON).unwrap();
337 let (_price, warnings) = Price::from_json(&elem).unwrap().into_parts();
338 let warnings = warnings.into_kind_vec();
339
340 assert_matches!(*warnings, [WarningKind::ExclusiveVatGreaterThanInclusive]);
341 }
342}