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