1use rust_decimal::prelude::*;
2use rust_decimal::Decimal;
3use std::ops::{Add, Sub};
4use std::str::FromStr;
5
6use crate::currency::Currency;
7use crate::error::{MoneyError, Result};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11#[must_use]
12#[cfg_attr(feature = "serde_impl", derive(serde::Serialize, serde::Deserialize))]
13#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
14pub struct CurrencyAmount {
15 pub amount: Decimal,
17 pub currency: Currency,
19}
20
21impl CurrencyAmount {
22 pub fn new(amount: impl Into<Decimal>, currency: Currency) -> Self {
24 Self {
25 amount: amount.into(),
26 currency,
27 }
28 }
29
30 pub fn from_str_values(amount: &str, currency: Currency) -> Result<Self> {
36 let decimal = Decimal::from_str(amount).map_err(|e| {
37 MoneyError::InvalidAmount(format!("Invalid decimal '{amount}': {e}"))
38 })?;
39 Ok(Self::new(decimal, currency))
40 }
41
42 pub fn is_zero(&self) -> bool {
44 self.amount.is_zero()
45 }
46
47 pub fn is_positive(&self) -> bool {
49 self.amount.is_sign_positive() && !self.amount.is_zero()
50 }
51
52 pub fn is_negative(&self) -> bool {
54 self.amount.is_sign_negative() && !self.amount.is_zero()
55 }
56
57 pub fn abs(&self) -> Self {
59 Self {
60 amount: self.amount.abs(),
61 currency: self.currency,
62 }
63 }
64
65 pub fn negate(&self) -> Self {
67 Self {
68 amount: -self.amount,
69 currency: self.currency,
70 }
71 }
72
73 pub fn round(&self) -> Self {
75 let places = self.currency.decimal_places();
76 Self {
77 amount: self.amount.round_dp(places),
78 currency: self.currency,
79 }
80 }
81
82 pub fn round_to(&self, decimal_places: u32) -> Self {
84 Self {
85 amount: self.amount.round_dp(decimal_places),
86 currency: self.currency,
87 }
88 }
89
90 pub fn to_f64(&self) -> Option<f64> {
92 self.amount.to_f64()
93 }
94
95 pub fn parts(&self) -> (Decimal, Decimal) {
97 let rounded = self.round();
98 let major = rounded.amount.floor();
99 let minor = rounded.amount - major;
100 (major, minor)
101 }
102}
103
104impl Add for CurrencyAmount {
105 type Output = Result<Self>;
106
107 fn add(self, rhs: Self) -> Self::Output {
108 if self.currency != rhs.currency {
109 return Err(MoneyError::CurrencyMismatch {
110 left: self.currency.code().to_string(),
111 right: rhs.currency.code().to_string(),
112 });
113 }
114 let amount = self.amount + rhs.amount;
115 Ok(Self {
116 amount,
117 currency: self.currency,
118 })
119 }
120}
121
122impl Sub for CurrencyAmount {
123 type Output = Result<Self>;
124
125 fn sub(self, rhs: Self) -> Self::Output {
126 if self.currency != rhs.currency {
127 return Err(MoneyError::CurrencyMismatch {
128 left: self.currency.code().to_string(),
129 right: rhs.currency.code().to_string(),
130 });
131 }
132 let amount = self.amount - rhs.amount;
133 Ok(Self {
134 amount,
135 currency: self.currency,
136 })
137 }
138}
139
140impl std::fmt::Display for CurrencyAmount {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 let symbol = self.currency.symbol();
143 let places = self.currency.decimal_places();
144 let places = places as usize;
145 let formatted = format!("{:.places$}", self.amount);
146 write!(f, "{symbol}{formatted}")
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn test_new_amount() {
156 let amount = CurrencyAmount::new(Decimal::from(100), Currency::USD);
157 assert_eq!(amount.amount, Decimal::from(100));
158 assert_eq!(amount.currency, Currency::USD);
159 }
160
161 #[test]
162 fn test_from_str_values() {
163 let amount = CurrencyAmount::from_str_values("19.99", Currency::USD).unwrap();
164 assert_eq!(amount.amount, Decimal::try_from("19.99").unwrap());
165 assert!(CurrencyAmount::from_str_values("abc", Currency::USD).is_err());
166 }
167
168 #[test]
169 fn test_predicates() {
170 let zero = CurrencyAmount::new(Decimal::ZERO, Currency::USD);
171 let positive = CurrencyAmount::new(Decimal::from(5), Currency::USD);
172 let negative = CurrencyAmount::new(Decimal::from(-5), Currency::USD);
173
174 assert!(zero.is_zero());
175 assert!(!zero.is_positive());
176 assert!(!zero.is_negative());
177 assert!(positive.is_positive());
178 assert!(negative.is_negative());
179 }
180
181 #[test]
182 fn test_addition() {
183 let a = CurrencyAmount::new(Decimal::from(10), Currency::USD);
184 let b = CurrencyAmount::new(Decimal::from(20), Currency::USD);
185 let result = (a + b).unwrap();
186 assert_eq!(result.amount, Decimal::from(30));
187 }
188
189 #[test]
190 fn test_addition_currency_mismatch() {
191 let a = CurrencyAmount::new(Decimal::from(10), Currency::USD);
192 let b = CurrencyAmount::new(Decimal::from(20), Currency::EUR);
193 assert!(matches!(a + b, Err(MoneyError::CurrencyMismatch { .. })));
194 }
195
196 #[test]
197 fn test_subtraction() {
198 let a = CurrencyAmount::new(Decimal::from(30), Currency::USD);
199 let b = CurrencyAmount::new(Decimal::from(10), Currency::USD);
200 let result = (a - b).unwrap();
201 assert_eq!(result.amount, Decimal::from(20));
202 }
203
204 #[test]
205 fn test_display() {
206 let amount = CurrencyAmount::new(Decimal::try_from("19.99").unwrap(), Currency::USD);
207 assert_eq!(format!("{amount}"), "$19.99");
208 }
209
210 #[test]
211 fn test_round() {
212 let amount = CurrencyAmount::new(Decimal::try_from("19.999").unwrap(), Currency::USD);
213 let rounded = amount.round();
214 assert_eq!(rounded.amount, Decimal::try_from("20.00").unwrap());
215 }
216}