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