1#![forbid(unsafe_code)]
12
13use super::error::DomainError;
14use rust_decimal::Decimal;
15use serde::{Deserialize, Serialize};
16use std::fmt;
17use std::marker::PhantomData;
18use std::str::FromStr;
19
20pub trait Currency: Copy + Send + Sync + 'static {
22 fn code() -> &'static str;
24 fn symbol() -> &'static str;
26 fn scale() -> u32;
28}
29
30#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
32pub struct Brl;
33
34impl Currency for Brl {
35 fn code() -> &'static str {
36 "BRL"
37 }
38 fn symbol() -> &'static str {
39 "R$"
40 }
41 fn scale() -> u32 {
42 2
43 }
44}
45
46#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
48pub struct Usd;
49
50impl Currency for Usd {
51 fn code() -> &'static str {
52 "USD"
53 }
54 fn symbol() -> &'static str {
55 "$"
56 }
57 fn scale() -> u32 {
58 2
59 }
60}
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(bound = "")]
67pub struct Money<C: Currency> {
68 #[serde(with = "rust_decimal::serde::str")]
69 amount: Decimal,
70 #[serde(skip)]
71 _currency: PhantomData<C>,
72}
73
74impl<C: Currency> Money<C> {
75 pub fn try_new(amount: Decimal) -> Result<Self, DomainError> {
77 if amount.is_sign_negative() {
78 return Err(DomainError::new(
79 "money",
80 format!("{} amount must not be negative", C::code()),
81 ));
82 }
83 let amount = amount.round_dp(C::scale());
84 Ok(Self {
85 amount,
86 _currency: PhantomData,
87 })
88 }
89
90 pub fn try_from_str(s: impl AsRef<str>) -> Result<Self, DomainError> {
92 let d = Decimal::from_str(s.as_ref().trim())
93 .map_err(|e| DomainError::new("money", e.to_string()))?;
94 Self::try_new(d)
95 }
96
97 #[must_use]
99 pub fn zero() -> Self {
100 Self {
101 amount: Decimal::ZERO,
102 _currency: PhantomData,
103 }
104 }
105
106 #[must_use]
108 pub const fn amount(&self) -> Decimal {
109 self.amount
110 }
111
112 #[must_use]
114 pub fn code(&self) -> &'static str {
115 C::code()
116 }
117}
118
119impl<C: Currency> fmt::Display for Money<C> {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 write!(f, "{} {}", self.amount, C::code())
122 }
123}
124
125impl<C: Currency> std::ops::Add for Money<C> {
126 type Output = Self;
127
128 fn add(self, rhs: Self) -> Self::Output {
129 Self {
130 amount: (self.amount + rhs.amount).round_dp(C::scale()),
131 _currency: PhantomData,
132 }
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use rust_decimal::dec;
140
141 #[test]
142 fn brl_add_and_serde_str() {
143 let a = Money::<Brl>::try_new(dec!(99.99)).unwrap();
144 let b = Money::<Brl>::try_new(dec!(0.01)).unwrap();
145 let t = a + b;
146 assert_eq!(t.amount(), dec!(100.00));
147 let j = serde_json::to_string(&t).unwrap();
148 assert!(j.contains('\"') && j.contains("100"), "{j}");
150 let back: Money<Brl> = serde_json::from_str(&j).unwrap();
151 assert_eq!(back.amount(), dec!(100.00));
152 }
153
154 #[test]
155 fn rejects_negative() {
156 assert!(Money::<Usd>::try_new(dec!(-1)).is_err());
157 }
158
159 #[test]
160 fn from_str() {
161 let m = Money::<Usd>::try_from_str("12.34").unwrap();
162 assert_eq!(m.amount(), dec!(12.34));
163 }
164}