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