1use crate::error::AmountError;
4use rust_decimal::Decimal;
5use std::fmt;
6use std::str::FromStr;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub struct InvoiceAmount(Decimal);
11
12pub type Amount = InvoiceAmount;
14
15impl InvoiceAmount {
16 pub const ZERO: Self = Self(Decimal::ZERO);
17
18 pub fn from_minor(cents: i64) -> Self {
19 Self(Decimal::new(cents, 2))
20 }
21
22 pub fn try_new(value: Decimal) -> Result<Self, AmountError> {
23 if value.scale() > 2 {
24 return Err(AmountError::TooManyDecimals);
25 }
26 Ok(Self(value))
27 }
28
29 pub fn parse(s: &str) -> Result<Self, AmountError> {
30 let d = Decimal::from_str(s.trim()).map_err(|_| AmountError::TooManyDecimals)?;
31 Self::try_new(d)
32 }
33
34 pub fn raw(self) -> Decimal {
35 self.0
36 }
37
38 pub fn is_zero(self) -> bool {
39 self.0.is_zero()
40 }
41
42 pub fn checked_add(self, other: Self) -> Option<Self> {
43 self.0
44 .checked_add(other.0)
45 .and_then(|d| Self::try_new(d).ok())
46 }
47
48 pub fn checked_sub(self, other: Self) -> Option<Self> {
49 self.0
50 .checked_sub(other.0)
51 .and_then(|d| Self::try_new(d).ok())
52 }
53
54 pub fn abs(self) -> Self {
55 Self(self.0.abs())
56 }
57
58 pub fn checked_sum(amounts: impl IntoIterator<Item = Self>) -> Option<Self> {
59 let mut acc = Self::ZERO;
60 for a in amounts {
61 acc = acc.checked_add(a)?;
62 }
63 Some(acc)
64 }
65
66 pub fn from_decimal_rounded(value: Decimal) -> Result<Self, AmountError> {
69 use rust_decimal::RoundingStrategy;
70 let rounded = value.round_dp_with_strategy(2, RoundingStrategy::MidpointAwayFromZero);
71 Self::try_new(rounded)
72 }
73}
74
75impl fmt::Display for InvoiceAmount {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 write!(f, "{}", self.0.round_dp(2))
78 }
79}
80
81impl FromStr for InvoiceAmount {
82 type Err = AmountError;
83 fn from_str(s: &str) -> Result<Self, Self::Err> {
84 Self::parse(s)
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
90pub struct UnitPriceAmount(Decimal);
91
92impl UnitPriceAmount {
93 pub const ZERO: Self = Self(Decimal::ZERO);
94
95 pub fn new(value: Decimal) -> Self {
96 Self(value)
97 }
98
99 pub fn parse(s: &str) -> Result<Self, rust_decimal::Error> {
100 Ok(Self(Decimal::from_str(s.trim())?))
101 }
102
103 pub fn raw(self) -> Decimal {
104 self.0
105 }
106}
107
108impl fmt::Display for UnitPriceAmount {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 write!(f, "{}", self.0)
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn refuses_third_decimal() {
120 assert!(InvoiceAmount::parse("0.005").is_err());
121 assert!(InvoiceAmount::try_new(Decimal::new(5, 3)).is_err());
122 }
123
124 #[test]
125 fn accepts_two_or_fewer() {
126 assert!(InvoiceAmount::parse("100.00").is_ok());
127 assert!(InvoiceAmount::parse("100").is_ok());
128 assert!(InvoiceAmount::parse("100.1").is_ok());
129 assert_eq!(
130 InvoiceAmount::from_minor(10000).raw(),
131 Decimal::new(10000, 2)
132 );
133 }
134
135 #[test]
136 fn unit_price_keeps_four_decimals() {
137 let p = UnitPriceAmount::parse("10000.1234").unwrap();
138 assert_eq!(p.to_string(), "10000.1234");
139 }
140
141 #[test]
142 fn checked_add_no_saturate() {
143 let a = InvoiceAmount::parse("1.00").unwrap();
144 let b = InvoiceAmount::parse("2.50").unwrap();
145 assert_eq!(a.checked_add(b).unwrap().to_string(), "3.50");
146 }
147}