allsource_core/domain/value_objects/
money.rs1use crate::error::Result;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4use std::ops::{Add, Sub};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub enum Currency {
9 USD,
11 USDC,
13 SOL,
15}
16
17impl Currency {
18 pub fn decimals(&self) -> u8 {
20 match self {
21 Currency::USD => 2,
22 Currency::USDC => 6, Currency::SOL => 9, }
25 }
26
27 pub fn symbol(&self) -> &'static str {
29 match self {
30 Currency::USD => "$",
31 Currency::USDC => "USDC",
32 Currency::SOL => "SOL",
33 }
34 }
35}
36
37impl fmt::Display for Currency {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 write!(f, "{}", self.symbol())
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
60pub struct Money {
61 amount: u64,
63 currency: Currency,
65}
66
67impl Money {
68 pub fn new(amount: u64, currency: Currency) -> Self {
80 Self { amount, currency }
81 }
82
83 pub fn from_decimal(decimal: f64, currency: Currency) -> Self {
94 let multiplier = 10_u64.pow(currency.decimals() as u32);
95 let amount = (decimal * multiplier as f64).round() as u64;
96 Self { amount, currency }
97 }
98
99 pub fn zero(currency: Currency) -> Self {
101 Self {
102 amount: 0,
103 currency,
104 }
105 }
106
107 pub fn usd_cents(cents: u64) -> Self {
109 Self::new(cents, Currency::USD)
110 }
111
112 pub fn usd(dollars: f64) -> Self {
114 Self::from_decimal(dollars, Currency::USD)
115 }
116
117 pub fn usdc(amount: u64) -> Self {
119 Self::new(amount, Currency::USDC)
120 }
121
122 pub fn usdc_decimal(amount: f64) -> Self {
124 Self::from_decimal(amount, Currency::USDC)
125 }
126
127 pub fn lamports(lamports: u64) -> Self {
129 Self::new(lamports, Currency::SOL)
130 }
131
132 pub fn sol(amount: f64) -> Self {
134 Self::from_decimal(amount, Currency::SOL)
135 }
136
137 pub fn amount(&self) -> u64 {
139 self.amount
140 }
141
142 pub fn currency(&self) -> Currency {
144 self.currency
145 }
146
147 pub fn as_decimal(&self) -> f64 {
149 let divisor = 10_u64.pow(self.currency.decimals() as u32);
150 self.amount as f64 / divisor as f64
151 }
152
153 pub fn is_zero(&self) -> bool {
155 self.amount == 0
156 }
157
158 pub fn is_positive(&self) -> bool {
160 self.amount > 0
161 }
162
163 pub fn at_least(&self, minimum: &Money) -> Result<()> {
165 if self.currency != minimum.currency {
166 return Err(crate::error::AllSourceError::InvalidInput(format!(
167 "Cannot compare {} with {}",
168 self.currency, minimum.currency
169 )));
170 }
171
172 if self.amount < minimum.amount {
173 return Err(crate::error::AllSourceError::ValidationError(format!(
174 "Amount {} is less than minimum {}",
175 self.as_decimal(),
176 minimum.as_decimal()
177 )));
178 }
179
180 Ok(())
181 }
182
183 pub fn percentage(&self, percent: u64) -> Self {
194 let amount = (self.amount * percent) / 100;
195 Self {
196 amount,
197 currency: self.currency,
198 }
199 }
200
201 pub fn subtract_percentage(&self, percent: u64) -> Self {
212 let fee = (self.amount * percent) / 100;
213 Self {
214 amount: self.amount.saturating_sub(fee),
215 currency: self.currency,
216 }
217 }
218}
219
220impl fmt::Display for Money {
221 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222 match self.currency {
223 Currency::USD => write!(f, "${:.2}", self.as_decimal()),
224 Currency::USDC => write!(f, "{:.2} USDC", self.as_decimal()),
225 Currency::SOL => write!(f, "{:.4} SOL", self.as_decimal()),
226 }
227 }
228}
229
230impl Add for Money {
231 type Output = Result<Money>;
232
233 fn add(self, other: Money) -> Self::Output {
234 if self.currency != other.currency {
235 return Err(crate::error::AllSourceError::InvalidInput(format!(
236 "Cannot add {} to {}",
237 self.currency, other.currency
238 )));
239 }
240
241 Ok(Money {
242 amount: self.amount + other.amount,
243 currency: self.currency,
244 })
245 }
246}
247
248impl Sub for Money {
249 type Output = Result<Money>;
250
251 fn sub(self, other: Money) -> Self::Output {
252 if self.currency != other.currency {
253 return Err(crate::error::AllSourceError::InvalidInput(format!(
254 "Cannot subtract {} from {}",
255 other.currency, self.currency
256 )));
257 }
258
259 Ok(Money {
260 amount: self.amount.saturating_sub(other.amount),
261 currency: self.currency,
262 })
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn test_create_money_from_smallest_unit() {
272 let money = Money::new(50, Currency::USD);
273 assert_eq!(money.amount(), 50);
274 assert_eq!(money.currency(), Currency::USD);
275 }
276
277 #[test]
278 fn test_create_money_from_decimal() {
279 let money = Money::from_decimal(0.50, Currency::USD);
280 assert_eq!(money.amount(), 50);
281 assert_eq!(money.currency(), Currency::USD);
282
283 let money = Money::from_decimal(1.00, Currency::USDC);
284 assert_eq!(money.amount(), 1_000_000); }
286
287 #[test]
288 fn test_usd_helpers() {
289 let cents = Money::usd_cents(150);
290 assert_eq!(cents.amount(), 150);
291 assert_eq!(cents.as_decimal(), 1.50);
292
293 let dollars = Money::usd(1.50);
294 assert_eq!(dollars.amount(), 150);
295 }
296
297 #[test]
298 fn test_usdc_helpers() {
299 let usdc = Money::usdc_decimal(1.0);
300 assert_eq!(usdc.amount(), 1_000_000);
301 assert_eq!(usdc.currency(), Currency::USDC);
302 }
303
304 #[test]
305 fn test_sol_helpers() {
306 let sol = Money::sol(1.0);
307 assert_eq!(sol.amount(), 1_000_000_000);
308 assert_eq!(sol.currency(), Currency::SOL);
309
310 let lamports = Money::lamports(1_000_000_000);
311 assert_eq!(lamports.as_decimal(), 1.0);
312 }
313
314 #[test]
315 fn test_zero() {
316 let zero = Money::zero(Currency::USD);
317 assert!(zero.is_zero());
318 assert!(!zero.is_positive());
319 }
320
321 #[test]
322 fn test_is_positive() {
323 let money = Money::usd_cents(100);
324 assert!(money.is_positive());
325 assert!(!money.is_zero());
326 }
327
328 #[test]
329 fn test_at_least() {
330 let amount = Money::usd_cents(100);
331 let minimum = Money::usd_cents(50);
332
333 assert!(amount.at_least(&minimum).is_ok());
334
335 let small = Money::usd_cents(25);
336 assert!(small.at_least(&minimum).is_err());
337 }
338
339 #[test]
340 fn test_at_least_different_currency_fails() {
341 let usd = Money::usd_cents(100);
342 let sol = Money::lamports(100);
343
344 assert!(usd.at_least(&sol).is_err());
345 }
346
347 #[test]
348 fn test_percentage() {
349 let money = Money::usd_cents(1000); let fee = money.percentage(7); assert_eq!(fee.amount(), 70); }
353
354 #[test]
355 fn test_subtract_percentage() {
356 let money = Money::usd_cents(1000); let after_fee = money.subtract_percentage(7); assert_eq!(after_fee.amount(), 930); }
360
361 #[test]
362 fn test_add_same_currency() {
363 let a = Money::usd_cents(100);
364 let b = Money::usd_cents(50);
365 let result = (a + b).unwrap();
366 assert_eq!(result.amount(), 150);
367 }
368
369 #[test]
370 fn test_add_different_currency_fails() {
371 let usd = Money::usd_cents(100);
372 let sol = Money::lamports(100);
373 let result = usd + sol;
374 assert!(result.is_err());
375 }
376
377 #[test]
378 fn test_sub_same_currency() {
379 let a = Money::usd_cents(100);
380 let b = Money::usd_cents(50);
381 let result = (a - b).unwrap();
382 assert_eq!(result.amount(), 50);
383 }
384
385 #[test]
386 fn test_sub_saturating() {
387 let a = Money::usd_cents(50);
388 let b = Money::usd_cents(100);
389 let result = (a - b).unwrap();
390 assert_eq!(result.amount(), 0); }
392
393 #[test]
394 fn test_sub_different_currency_fails() {
395 let usd = Money::usd_cents(100);
396 let sol = Money::lamports(100);
397 let result = usd - sol;
398 assert!(result.is_err());
399 }
400
401 #[test]
402 fn test_display_usd() {
403 let money = Money::usd_cents(150);
404 assert_eq!(format!("{}", money), "$1.50");
405 }
406
407 #[test]
408 fn test_display_usdc() {
409 let money = Money::usdc_decimal(1.50);
410 assert_eq!(format!("{}", money), "1.50 USDC");
411 }
412
413 #[test]
414 fn test_display_sol() {
415 let money = Money::sol(0.001);
416 assert_eq!(format!("{}", money), "0.0010 SOL");
417 }
418
419 #[test]
420 fn test_currency_decimals() {
421 assert_eq!(Currency::USD.decimals(), 2);
422 assert_eq!(Currency::USDC.decimals(), 6);
423 assert_eq!(Currency::SOL.decimals(), 9);
424 }
425
426 #[test]
427 fn test_currency_symbol() {
428 assert_eq!(Currency::USD.symbol(), "$");
429 assert_eq!(Currency::USDC.symbol(), "USDC");
430 assert_eq!(Currency::SOL.symbol(), "SOL");
431 }
432
433 #[test]
434 fn test_equality() {
435 let a = Money::usd_cents(100);
436 let b = Money::usd_cents(100);
437 let c = Money::usd_cents(200);
438 let d = Money::new(100, Currency::SOL);
439
440 assert_eq!(a, b);
441 assert_ne!(a, c);
442 assert_ne!(a, d); }
444
445 #[test]
446 fn test_cloning() {
447 let a = Money::usd_cents(100);
448 let b = a; assert_eq!(a, b);
450 }
451
452 #[test]
453 fn test_hash_consistency() {
454 use std::collections::HashSet;
455
456 let a = Money::usd_cents(100);
457 let b = Money::usd_cents(100);
458
459 let mut set = HashSet::new();
460 set.insert(a);
461
462 assert!(set.contains(&b));
463 }
464
465 #[test]
466 fn test_serde_serialization() {
467 let money = Money::usd_cents(150);
468
469 let json = serde_json::to_string(&money).unwrap();
471
472 let deserialized: Money = serde_json::from_str(&json).unwrap();
474 assert_eq!(deserialized, money);
475 }
476
477 #[test]
478 fn test_as_decimal_precision() {
479 let usd = Money::usd_cents(123);
481 assert!((usd.as_decimal() - 1.23).abs() < f64::EPSILON);
482
483 let usdc = Money::usdc(1_234_567);
485 assert!((usdc.as_decimal() - 1.234567).abs() < 0.000001);
486
487 let sol = Money::lamports(1_234_567_890);
489 assert!((sol.as_decimal() - 1.23456789).abs() < 0.000000001);
490 }
491}