Skip to main content

nautilus_model/types/
money.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Represents an amount of money in a specified currency denomination.
17//!
18//! [`Money`] is an immutable value type for representing monetary amounts with an associated
19//! currency. It supports both positive and negative values (for debits, losses, etc.) and
20//! enforces currency consistency in arithmetic operations.
21//!
22//! # Arithmetic behavior
23//!
24//! Adding or subtracting two `Money` values requires matching effective fixed-point scales.
25//! These operations panic on a scale mismatch.
26//! Comparisons and hashes account for scale differences without rounding.
27//!
28//! | Operation         | Result    | Notes                             |
29//! |-------------------|-----------|-----------------------------------|
30//! | `Money + Money`   | `Money`   | Panics if currencies don't match. |
31//! | `Money - Money`   | `Money`   | Panics if currencies don't match. |
32//! | `Money + Decimal` | `Decimal` |                                   |
33//! | `Money - Decimal` | `Decimal` |                                   |
34//! | `Money * Decimal` | `Decimal` |                                   |
35//! | `Money / Decimal` | `Decimal` |                                   |
36//! | `Money + f64`     | `f64`     |                                   |
37//! | `Money - f64`     | `f64`     |                                   |
38//! | `Money * f64`     | `f64`     |                                   |
39//! | `Money / f64`     | `f64`     |                                   |
40//! | `-Money`          | `Money`   |                                   |
41//!
42//! # Ordering behavior
43//!
44//! Rust ordering compares currency codes lexicographically, then scale-adjusted amounts.
45//! This order supports sorting and ordered collections across currencies, but it does not convert
46//! amounts to a common currency. Values with the same currency code compare by numeric value.
47//! Python comparisons reject values with different currency codes.
48//!
49//! # Currency constraints
50//!
51//! When performing arithmetic between two `Money` values, both must have the same currency.
52//! Attempting to add or subtract money with different currencies raises an error.
53//!
54//! # Immutability
55//!
56//! `Money` is immutable. All arithmetic operations return new instances.
57
58use std::{
59    cmp::Ordering,
60    fmt::{Debug, Display},
61    hash::{Hash, Hasher},
62    ops::{Add, Div, Mul, Neg, Sub},
63    str::FromStr,
64};
65
66use nautilus_core::{
67    correctness::{
68        CorrectnessError, CorrectnessResult, CorrectnessResultExt, FAILED,
69        check_in_range_inclusive_f64, check_predicate_false, check_predicate_true,
70    },
71    string::formatting::Separable,
72};
73use rust_decimal::{Decimal, RoundingStrategy};
74use serde::{Deserialize, Deserializer, Serialize};
75
76#[cfg(not(any(feature = "defi", feature = "high-precision")))]
77use super::fixed::{f64_to_fixed_i64, fixed_i64_to_f64};
78#[cfg(any(feature = "defi", feature = "high-precision"))]
79use super::fixed::{f64_to_fixed_i128, fixed_i128_to_f64};
80#[cfg(feature = "defi")]
81use crate::types::fixed::MAX_FLOAT_PRECISION;
82use crate::{
83    deserialization::StringInput,
84    types::{
85        Currency, Quantity,
86        fixed::{
87            FIXED_PRECISION, FIXED_SCALAR, canonical_raw, check_fixed_precision,
88            check_fixed_raw_i128, check_fixed_raw_u128, compare_raw_signed, format_scaled_i128,
89            mantissa_exponent_to_fixed_i128, mantissa_exponent_to_raw_checked,
90            parse_decimal_mantissa, raw_scales_match, remove_underscores, scaled_raw_to_decimal,
91        },
92    },
93};
94
95// -----------------------------------------------------------------------------
96// MoneyRaw
97// -----------------------------------------------------------------------------
98
99#[cfg(feature = "high-precision")]
100pub type MoneyRaw = i128;
101
102#[cfg(not(feature = "high-precision"))]
103pub type MoneyRaw = i64;
104
105// -----------------------------------------------------------------------------
106
107/// The maximum raw money integer value.
108///
109/// # Safety
110///
111/// `MONEY_MAX` and `FIXED_SCALAR` are cast to `MoneyRaw` before multiplying, so the
112/// scaling uses exact integer arithmetic rather than a lossy `f64` product. The result
113/// fits within `MoneyRaw`'s range in both high-precision (i128) and standard-precision
114/// (i64) modes, so the multiplication cannot overflow.
115#[unsafe(no_mangle)]
116#[allow(unsafe_code)]
117pub static MONEY_RAW_MAX: MoneyRaw = (MONEY_MAX as MoneyRaw) * (FIXED_SCALAR as MoneyRaw);
118
119/// The minimum raw money integer value.
120///
121/// # Safety
122///
123/// `MONEY_MIN` and `FIXED_SCALAR` are cast to `MoneyRaw` before multiplying, so the
124/// scaling uses exact integer arithmetic rather than a lossy `f64` product. The result
125/// fits within `MoneyRaw`'s range in both high-precision (i128) and standard-precision
126/// (i64) modes, so the multiplication cannot overflow.
127#[unsafe(no_mangle)]
128#[allow(unsafe_code)]
129pub static MONEY_RAW_MIN: MoneyRaw = (MONEY_MIN as MoneyRaw) * (FIXED_SCALAR as MoneyRaw);
130
131// -----------------------------------------------------------------------------
132// MONEY_MAX
133// -----------------------------------------------------------------------------
134
135#[cfg(feature = "high-precision")]
136/// The maximum valid money amount that can be represented.
137pub const MONEY_MAX: f64 = 17_014_118_346_046.0;
138
139#[cfg(not(feature = "high-precision"))]
140/// The maximum valid money amount that can be represented.
141pub const MONEY_MAX: f64 = 9_223_372_036.0;
142
143// -----------------------------------------------------------------------------
144// MONEY_MIN
145// -----------------------------------------------------------------------------
146
147#[cfg(feature = "high-precision")]
148/// The minimum valid money amount that can be represented.
149pub const MONEY_MIN: f64 = -17_014_118_346_046.0;
150
151#[cfg(not(feature = "high-precision"))]
152/// The minimum valid money amount that can be represented.
153pub const MONEY_MIN: f64 = -9_223_372_036.0;
154
155// -----------------------------------------------------------------------------
156
157/// Represents an amount of money in a specified currency denomination.
158///
159/// The currency precision supplies default rounding and minimum display precision.
160/// Exact constructors and raw construction retain finer native fixed-point amounts.
161///
162/// - [`MONEY_MAX`] - Maximum representable money amount
163/// - [`MONEY_MIN`] - Minimum representable money amount
164#[repr(C)]
165#[derive(Clone, Copy, Eq)]
166#[cfg_attr(
167    feature = "python",
168    pyo3::pyclass(module = "nautilus_trader.model", frozen, from_py_object)
169)]
170#[cfg_attr(
171    feature = "python",
172    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
173)]
174pub struct Money {
175    pub(crate) raw: MoneyRaw,
176    /// The currency denomination associated with the monetary amount.
177    pub currency: Currency,
178}
179
180impl Money {
181    /// Creates a new [`Money`] instance with correctness checking.
182    ///
183    /// # Errors
184    ///
185    /// Returns an error if:
186    /// - `amount` is invalid outside the representable range [`MONEY_MIN`, `MONEY_MAX`].
187    /// - `currency.precision` exceeds the maximum fixed precision.
188    ///
189    /// # Notes
190    ///
191    /// PyO3 requires a `Result` type for proper error handling and stacktrace printing in Python.
192    pub fn new_checked(amount: f64, currency: Currency) -> CorrectnessResult<Self> {
193        // check_in_range_inclusive_f64 already validates that amount is finite
194        // (not NaN or infinite) as part of its range validation logic, so no additional
195        // infinity checks are needed here.
196        check_in_range_inclusive_f64(amount, MONEY_MIN, MONEY_MAX, "amount")?;
197
198        #[cfg(feature = "defi")]
199        if currency.precision > MAX_FLOAT_PRECISION {
200            // Floats are only reliable up to ~16 decimal digits of precision regardless of feature flags
201            return Err(CorrectnessError::PredicateViolation {
202                message: format!(
203                    "`currency.precision` exceeded maximum float precision ({MAX_FLOAT_PRECISION}), use `Money::from_wei()` for wei values instead"
204                ),
205            });
206        }
207
208        check_fixed_precision(currency.precision)?;
209
210        #[cfg(feature = "high-precision")]
211        let raw = f64_to_fixed_i128(amount, currency.precision);
212
213        #[cfg(not(feature = "high-precision"))]
214        let raw = f64_to_fixed_i64(amount, currency.precision);
215
216        Ok(Self { raw, currency })
217    }
218
219    /// Creates a new [`Money`] instance.
220    ///
221    /// # Panics
222    ///
223    /// Panics if a correctness check fails. See [`Money::new_checked`] for more details.
224    #[must_use]
225    pub fn new(amount: f64, currency: Currency) -> Self {
226        Self::new_checked(amount, currency).expect_display(FAILED)
227    }
228
229    /// Creates a new [`Money`] instance from the given `raw` fixed-point value and the specified `currency`.
230    ///
231    /// # Panics
232    ///
233    /// Panics if a correctness check fails. See [`Money::from_raw_checked`] for more details.
234    #[must_use]
235    pub fn from_raw(raw: MoneyRaw, currency: Currency) -> Self {
236        Self::from_raw_checked(raw, currency).expect_display(FAILED)
237    }
238
239    /// Creates a new [`Money`] instance from the given `raw` fixed-point value and the specified
240    /// `currency` with correctness checking.
241    ///
242    /// # Errors
243    ///
244    /// Returns an error if:
245    /// - `raw` is outside the representable range [`MONEY_RAW_MIN`, `MONEY_RAW_MAX`].
246    /// - `currency.precision` exceeds the maximum fixed precision.
247    pub fn from_raw_checked(raw: MoneyRaw, currency: Currency) -> CorrectnessResult<Self> {
248        if raw < MONEY_RAW_MIN || raw > MONEY_RAW_MAX {
249            return Err(CorrectnessError::PredicateViolation {
250                message: format!(
251                    "`raw` value {raw} exceeded bounds [{MONEY_RAW_MIN}, {MONEY_RAW_MAX}] for Money"
252                ),
253            });
254        }
255
256        check_fixed_precision(currency.precision)?;
257
258        Ok(Self { raw, currency })
259    }
260
261    /// Creates a new [`Money`] from a mantissa/exponent pair using pure integer arithmetic.
262    ///
263    /// The value is `mantissa * 10^exponent`. This avoids all floating-point and Decimal
264    /// operations, making it ideal for exchange data that arrives as mantissa/exponent pairs.
265    ///
266    /// # Panics
267    ///
268    /// Panics if the resulting raw value exceeds [`MONEY_RAW_MAX`] or [`MONEY_RAW_MIN`].
269    #[must_use]
270    pub fn from_mantissa_exponent(mantissa: i64, exponent: i8, currency: Currency) -> Self {
271        check_fixed_precision(currency.precision).expect_display(FAILED);
272
273        if mantissa == 0 {
274            return Self { raw: 0, currency };
275        }
276
277        let raw_i128 =
278            mantissa_exponent_to_fixed_i128(i128::from(mantissa), exponent, currency.precision)
279                .expect("Overflow in Money::from_mantissa_exponent");
280
281        #[allow(
282            clippy::useless_conversion,
283            reason = "i128 to MoneyRaw is real when not high-precision"
284        )]
285        let raw: MoneyRaw = raw_i128
286            .try_into()
287            .expect("Raw value exceeds MoneyRaw range in Money::from_mantissa_exponent");
288        assert!(
289            raw >= MONEY_RAW_MIN && raw <= MONEY_RAW_MAX,
290            "`raw` value {raw} exceeded bounds [{MONEY_RAW_MIN}, {MONEY_RAW_MAX}] for Money"
291        );
292
293        Self { raw, currency }
294    }
295
296    /// Creates a new [`Money`] instance with a value of zero with the given [`Currency`].
297    ///
298    /// # Panics
299    ///
300    /// Panics if `currency.precision` exceeds the maximum allowed by `check_fixed_precision`.
301    #[must_use]
302    pub fn zero(currency: Currency) -> Self {
303        check_fixed_precision(currency.precision).expect_display(FAILED);
304        Self { raw: 0, currency }
305    }
306
307    /// Returns a copy with raw value rounded to currency precision,
308    /// stripping any sub-scale bits.
309    #[must_use]
310    pub fn normalized(&self) -> Self {
311        #[cfg(feature = "high-precision")]
312        let raw = super::fixed::correct_raw_i128(self.raw, self.currency.precision);
313
314        #[cfg(not(feature = "high-precision"))]
315        let raw = super::fixed::correct_raw_i64(self.raw, self.currency.precision);
316
317        Self {
318            raw,
319            currency: self.currency,
320        }
321    }
322
323    /// Returns the stored fixed-point integer without rescaling.
324    ///
325    /// Use this for serialization and explicit fixed-point conversions. Prefer domain
326    /// operations for calculations; the storage scale can differ from display precision.
327    ///
328    /// Direct field access is restricted to this crate:
329    ///
330    /// ```compile_fail
331    /// use nautilus_model::types::Money;
332    /// let value = Money::from("1 USD");
333    /// let raw = value.raw;
334    /// ```
335    #[must_use]
336    #[inline]
337    pub const fn raw(&self) -> MoneyRaw {
338        self.raw
339    }
340
341    /// Returns `true` if the value of this instance is zero.
342    #[must_use]
343    pub fn is_zero(&self) -> bool {
344        self.raw == 0
345    }
346
347    /// Returns `true` if the value of this instance is positive (> 0).
348    #[must_use]
349    pub fn is_positive(&self) -> bool {
350        self.raw > 0
351    }
352
353    /// Returns `true` if the value of this instance is negative (< 0).
354    #[must_use]
355    pub fn is_negative(&self) -> bool {
356        self.raw < 0
357    }
358
359    /// Returns the absolute amount in the same currency.
360    #[must_use]
361    pub fn abs(self) -> Self {
362        if self.is_negative() { -self } else { self }
363    }
364
365    /// Performs a checked addition, returning `None` on raw integer overflow, when
366    /// the result falls outside `[MONEY_RAW_MIN, MONEY_RAW_MAX]`, or when the operands
367    /// have mixed raw scales (e.g. a wei-scaled `Money` and a `FIXED_SCALAR`-scaled
368    /// `Money`, even if their currency codes match).
369    ///
370    /// # Panics
371    ///
372    /// Panics if `self.currency` and `rhs.currency` differ by code (currency mismatch
373    /// is a type-system invariant violation, not a recoverable arithmetic condition).
374    #[must_use]
375    pub fn checked_add(self, rhs: Self) -> Option<Self> {
376        assert_eq!(
377            self.currency, rhs.currency,
378            "Currency mismatch: cannot add {} to {}",
379            rhs.currency.code, self.currency.code
380        );
381
382        if !raw_scales_match(self.currency.precision, rhs.currency.precision) {
383            return None;
384        }
385
386        let raw = self.raw.checked_add(rhs.raw)?;
387        if raw < MONEY_RAW_MIN || raw > MONEY_RAW_MAX {
388            return None;
389        }
390
391        Some(Self {
392            raw,
393            currency: self.currency,
394        })
395    }
396
397    /// Performs a checked subtraction, returning `None` on raw integer underflow, when
398    /// the result falls outside `[MONEY_RAW_MIN, MONEY_RAW_MAX]`, or when the operands
399    /// have mixed raw scales (e.g. a wei-scaled `Money` and a `FIXED_SCALAR`-scaled
400    /// `Money`, even if their currency codes match).
401    ///
402    /// # Panics
403    ///
404    /// Panics if `self.currency` and `rhs.currency` differ by code (currency mismatch
405    /// is a type-system invariant violation, not a recoverable arithmetic condition).
406    #[must_use]
407    pub fn checked_sub(self, rhs: Self) -> Option<Self> {
408        assert_eq!(
409            self.currency, rhs.currency,
410            "Currency mismatch: cannot subtract {} from {}",
411            rhs.currency.code, self.currency.code
412        );
413
414        if !raw_scales_match(self.currency.precision, rhs.currency.precision) {
415            return None;
416        }
417
418        let raw = self.raw.checked_sub(rhs.raw)?;
419        if raw < MONEY_RAW_MIN || raw > MONEY_RAW_MAX {
420            return None;
421        }
422
423        Some(Self {
424            raw,
425            currency: self.currency,
426        })
427    }
428
429    #[cfg(feature = "high-precision")]
430    /// Returns the value of this instance as an `f64`.
431    ///
432    /// # Panics
433    ///
434    /// Panics if the `defi` feature is enabled and precision exceeds `MAX_FLOAT_PRECISION` (16).
435    #[must_use]
436    pub fn as_f64(&self) -> f64 {
437        #[cfg(feature = "defi")]
438        assert!(
439            self.currency.precision <= MAX_FLOAT_PRECISION,
440            "Invalid f64 conversion beyond `MAX_FLOAT_PRECISION` (16)"
441        );
442
443        fixed_i128_to_f64(self.raw)
444    }
445
446    #[cfg(not(feature = "high-precision"))]
447    /// Returns the value of this instance as an `f64`.
448    #[must_use]
449    pub fn as_f64(&self) -> f64 {
450        fixed_i64_to_f64(self.raw)
451    }
452
453    /// Returns the value of this instance as a `Decimal`.
454    ///
455    /// Preserves stored fractional digits beyond the currency precision. Values whose
456    /// significant digits exceed the 96-bit mantissa of Rust `Decimal` can lose scale during conversion.
457    #[must_use]
458    pub fn as_decimal(&self) -> Decimal {
459        let (raw, precision) = self.raw_at_precision();
460        scaled_raw_to_decimal(raw, precision)
461    }
462
463    /// Returns a formatted string representation of this instance.
464    #[must_use]
465    pub fn to_formatted_string(&self) -> String {
466        let amount_str = self.to_decimal_string();
467        format!(
468            "{} {}",
469            amount_str.separate_with_underscores(),
470            self.currency.code
471        )
472    }
473
474    pub(crate) fn to_decimal_string(self) -> String {
475        let (raw, precision) = self.raw_at_precision();
476        format_scaled_i128(raw, precision)
477    }
478
479    /// Converts a quantity denominated in `currency` to money without rounding.
480    ///
481    /// # Errors
482    ///
483    /// Returns an error if the quantity is invalid, the amount is not exactly representable
484    /// at the currency precision, or scaling exceeds the money bounds.
485    #[allow(
486        clippy::useless_conversion,
487        reason = "the raw width differs when high-precision is disabled"
488    )]
489    pub fn from_quantity(quantity: Quantity, currency: Currency) -> CorrectnessResult<Self> {
490        check_predicate_false(quantity.is_undefined(), "quantity was undefined")?;
491        Quantity::from_raw_checked(quantity.raw, quantity.precision)?;
492        check_fixed_raw_u128(u128::from(quantity.raw), quantity.precision).map_err(|e| {
493            CorrectnessError::PredicateViolation {
494                message: e.to_string(),
495            }
496        })?;
497
498        let raw = i128::try_from(u128::from(quantity.raw)).map_err(|_| {
499            CorrectnessError::PredicateViolation {
500                message: format!("quantity for {currency} exceeds signed raw bounds"),
501            }
502        })?;
503
504        Self::from_rescaled_raw(raw, quantity.precision, currency, "quantity")
505    }
506
507    fn raw_at_precision(&self) -> (i128, u8) {
508        let precision_diff = FIXED_PRECISION.saturating_sub(self.currency.precision);
509        let scale = MoneyRaw::pow(10, u32::from(precision_diff));
510        let mut raw = self.raw;
511        if raw % scale == 0 {
512            return (Self::raw_as_i128(raw / scale), self.currency.precision);
513        }
514
515        let mut precision = FIXED_PRECISION;
516
517        while raw % 10 == 0 {
518            raw /= 10;
519            precision -= 1;
520        }
521
522        (Self::raw_as_i128(raw), precision)
523    }
524
525    fn raw_as_i128(raw: MoneyRaw) -> i128 {
526        #[allow(
527            clippy::useless_conversion,
528            reason = "i128::from is a widening conversion when MoneyRaw is i64"
529        )]
530        i128::from(raw)
531    }
532
533    /// Creates a new [`Money`] from a `Decimal` value with specified currency.
534    ///
535    /// This method provides more reliable parsing by using Decimal arithmetic
536    /// to avoid floating-point precision issues during conversion.
537    ///
538    /// # Errors
539    ///
540    /// Returns an error if:
541    /// - The decimal value cannot be converted to the raw representation.
542    /// - Overflow occurs during scaling.
543    pub fn from_decimal(decimal: Decimal, currency: Currency) -> CorrectnessResult<Self> {
544        let exponent = -(decimal.scale() as i8);
545        let raw_i128 =
546            mantissa_exponent_to_fixed_i128(decimal.mantissa(), exponent, currency.precision)?;
547
548        #[allow(
549            clippy::useless_conversion,
550            reason = "i128 to MoneyRaw is real when not high-precision"
551        )]
552        let raw: MoneyRaw =
553            raw_i128
554                .try_into()
555                .map_err(|_| CorrectnessError::PredicateViolation {
556                    message: format!(
557                        "Decimal value exceeds MoneyRaw range [{MONEY_RAW_MIN}, {MONEY_RAW_MAX}]"
558                    ),
559                })?;
560
561        if !(raw >= MONEY_RAW_MIN && raw <= MONEY_RAW_MAX) {
562            return Err(CorrectnessError::PredicateViolation {
563                message: format!(
564                    "Raw value {raw} exceeded bounds [{MONEY_RAW_MIN}, {MONEY_RAW_MAX}] for Money"
565                ),
566            });
567        }
568
569        Ok(Self { raw, currency })
570    }
571
572    /// Creates money rounded to the native fixed-point scale with midpoint-to-even rounding.
573    ///
574    /// Uses the greater of the native scale and the currency precision. The currency
575    /// identity and its default display precision remain unchanged.
576    ///
577    /// # Errors
578    ///
579    /// Returns an error if the currency precision is invalid or the rounded amount exceeds money bounds.
580    pub fn from_decimal_native(decimal: Decimal, currency: Currency) -> CorrectnessResult<Self> {
581        let rounded = decimal.round_dp_with_strategy(
582            u32::from(FIXED_PRECISION.max(currency.precision)),
583            RoundingStrategy::MidpointNearestEven,
584        );
585        Self::from_decimal_exact(rounded, currency)
586    }
587
588    /// Creates money without rounding to the currency precision.
589    ///
590    /// The native fixed-point scale limits the amount precision. The currency
591    /// identity and its default precision remain unchanged.
592    ///
593    /// # Errors
594    ///
595    /// Returns an error if the currency precision is invalid, the amount loses precision at the
596    /// native scale, or the amount exceeds money bounds.
597    pub fn from_decimal_exact(decimal: Decimal, currency: Currency) -> CorrectnessResult<Self> {
598        Self::from_exact_parts(decimal.mantissa(), decimal.scale() as i32, currency)
599    }
600
601    /// Parses an amount and currency without rounding to the currency precision.
602    ///
603    /// # Errors
604    ///
605    /// Returns an error if the input is invalid, its currency is unknown, or its amount
606    /// is not exactly representable at the native scale within money bounds.
607    pub fn from_str_exact(value: &str) -> Result<Self, String> {
608        Self::parse(value, true)
609    }
610
611    pub(crate) fn from_decimal_str_exact(value: &str, currency: Currency) -> Result<Self, String> {
612        let invalid_decimal = || format!("Invalid decimal value '{value}'");
613
614        let (amount, exponent) = match value.split_once(['e', 'E']) {
615            Some((amount, exponent)) => {
616                let exponent = exponent.parse::<i16>().map_err(|_| invalid_decimal())?;
617                (amount, i32::from(exponent))
618            }
619            None => (value, 0),
620        };
621
622        let amount = if let Some((_, fraction)) = amount.split_once('.') {
623            if !fraction.bytes().all(|byte| byte.is_ascii_digit()) {
624                return Err(invalid_decimal());
625            }
626
627            let trimmed = amount.trim_end_matches('0');
628            let trimmed = trimmed.strip_suffix('.').unwrap_or(trimmed);
629
630            if matches!(trimmed, "" | "+" | "-") && !fraction.is_empty() {
631                "0"
632            } else {
633                trimmed
634            }
635        } else {
636            amount
637        };
638
639        let (amount, whole_zeros) = if amount.contains('.') {
640            (amount, 0)
641        } else {
642            let trimmed = amount.trim_end_matches('0');
643            let zeros = i32::try_from(amount.len() - trimmed.len()).map_err(|e| e.to_string())?;
644            if zeros > 0 && matches!(trimmed, "" | "+" | "-") {
645                ("0", zeros)
646            } else {
647                (trimmed, zeros)
648            }
649        };
650
651        let (mantissa, precision) = parse_decimal_mantissa(amount)?;
652        Self::from_exact_parts(
653            mantissa,
654            i32::from(precision) - exponent - whole_zeros,
655            currency,
656        )
657        .map_err(|e| e.to_string())
658    }
659
660    fn from_exact_parts(
661        mut mantissa: i128,
662        mut precision: i32,
663        currency: Currency,
664    ) -> CorrectnessResult<Self> {
665        check_fixed_precision(currency.precision)?;
666        let target_precision = currency.precision.max(FIXED_PRECISION);
667        if mantissa == 0 {
668            return Ok(Self::zero(currency));
669        }
670
671        while precision > i32::from(target_precision) && mantissa % 10 == 0 {
672            mantissa /= 10;
673            precision -= 1;
674        }
675
676        if precision > i32::from(target_precision) {
677            return Err(CorrectnessError::PredicateViolation {
678                message: format!("amount exceeds native money precision {target_precision}"),
679            });
680        }
681
682        let exponent =
683            i8::try_from(-precision).map_err(|_| CorrectnessError::PredicateViolation {
684                message: "amount exceeds native money exponent range".to_string(),
685            })?;
686
687        let raw = mantissa_exponent_to_raw_checked::<MoneyRaw>(
688            mantissa,
689            exponent,
690            target_precision,
691            "exact Money construction",
692            "MoneyRaw",
693            "Money",
694        )?;
695        Self::from_raw_checked(raw, currency)
696    }
697
698    #[allow(
699        clippy::useless_conversion,
700        reason = "the raw width differs when high-precision is disabled"
701    )]
702    pub(crate) fn from_rescaled_raw(
703        raw: i128,
704        source_precision: u8,
705        currency: Currency,
706        subject: &str,
707    ) -> CorrectnessResult<Self> {
708        check_fixed_precision(source_precision)?;
709        check_fixed_precision(currency.precision)?;
710        let source_precision = source_precision.max(FIXED_PRECISION);
711        let target_precision = currency.precision.max(FIXED_PRECISION);
712
713        let raw = match source_precision.cmp(&target_precision) {
714            Ordering::Less => {
715                let scale = 10_i128.pow(u32::from(target_precision - source_precision));
716                raw.checked_mul(scale)
717                    .ok_or_else(|| CorrectnessError::PredicateViolation {
718                        message: format!(
719                            "{subject} for {currency} overflowed while increasing raw scale"
720                        ),
721                    })?
722            }
723            Ordering::Greater => {
724                let scale = 10_i128.pow(u32::from(source_precision - target_precision));
725                check_predicate_true(
726                    raw % scale == 0,
727                    &format!("{subject} for {currency} loses precision when decreasing raw scale"),
728                )?;
729                raw / scale
730            }
731            Ordering::Equal => raw,
732        };
733
734        check_fixed_raw_i128(raw, currency.precision).map_err(|e| {
735            CorrectnessError::PredicateViolation {
736                message: e.to_string(),
737            }
738        })?;
739
740        let raw: MoneyRaw = raw
741            .try_into()
742            .map_err(|_| CorrectnessError::PredicateViolation {
743                message: format!("{subject} for {currency} exceeds Money raw bounds"),
744            })?;
745
746        Self::from_raw_checked(raw, currency)
747    }
748
749    fn parse(value: &str, exact: bool) -> Result<Self, String> {
750        let mut parts = value.split_whitespace();
751
752        // Ensure we have both the amount and currency
753        let (Some(amount), Some(currency), None) = (parts.next(), parts.next(), parts.next())
754        else {
755            return Err(format!(
756                "Error invalid input format '{value}'. Expected '<amount> <currency>'"
757            ));
758        };
759
760        let clean_amount = remove_underscores(amount);
761
762        let currency = Currency::from_str(currency).map_err(|e| e.to_string())?;
763        if exact {
764            return Self::from_decimal_str_exact(&clean_amount, currency);
765        }
766
767        if clean_amount.contains('e') || clean_amount.contains('E') {
768            let decimal = Decimal::from_scientific(&clean_amount)
769                .map_err(|e| format!("Error parsing amount '{amount}' as Decimal: {e}"))?;
770            return Self::from_decimal(decimal, currency).map_err(|e| e.to_string());
771        }
772
773        let (mantissa, amount_precision) = parse_decimal_mantissa(&clean_amount)?;
774        let exponent = -i8::try_from(amount_precision).map_err(|e| e.to_string())?;
775        let raw = mantissa_exponent_to_raw_checked::<MoneyRaw>(
776            mantissa,
777            exponent,
778            currency.precision,
779            "Money::from_str",
780            "MoneyRaw",
781            "Money",
782        )
783        .map_err(|e| e.to_string())?;
784        Self::from_raw_checked(raw, currency).map_err(|e| e.to_string())
785    }
786}
787
788impl FromStr for Money {
789    type Err = String;
790
791    fn from_str(value: &str) -> Result<Self, Self::Err> {
792        Self::parse(value, false)
793    }
794}
795
796impl<T: AsRef<str>> From<T> for Money {
797    fn from(value: T) -> Self {
798        Self::from_str(value.as_ref()).expect(FAILED)
799    }
800}
801
802impl From<Money> for f64 {
803    fn from(money: Money) -> Self {
804        money.as_f64()
805    }
806}
807
808impl From<&Money> for f64 {
809    fn from(money: &Money) -> Self {
810        money.as_f64()
811    }
812}
813
814impl Hash for Money {
815    fn hash<H: Hasher>(&self, state: &mut H) {
816        self.raw.signum().hash(state);
817        canonical_raw(self.raw.unsigned_abs(), self.currency.precision).hash(state);
818        self.currency.hash(state);
819    }
820}
821
822impl PartialEq for Money {
823    fn eq(&self, other: &Self) -> bool {
824        self.cmp(other) == Ordering::Equal
825    }
826}
827
828impl PartialOrd for Money {
829    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
830        Some(self.cmp(other))
831    }
832}
833
834impl Ord for Money {
835    fn cmp(&self, other: &Self) -> Ordering {
836        self.currency.code.cmp(&other.currency.code).then_with(|| {
837            compare_raw_signed(
838                self.raw,
839                self.currency.precision,
840                other.raw,
841                other.currency.precision,
842            )
843        })
844    }
845}
846
847impl Neg for Money {
848    type Output = Self;
849    fn neg(self) -> Self::Output {
850        Self {
851            raw: -self.raw,
852            currency: self.currency,
853        }
854    }
855}
856
857impl Add for Money {
858    type Output = Self;
859    fn add(self, rhs: Self) -> Self::Output {
860        assert_eq!(
861            self.currency, rhs.currency,
862            "Currency mismatch: cannot add {} to {}",
863            rhs.currency.code, self.currency.code
864        );
865        assert!(
866            raw_scales_match(self.currency.precision, rhs.currency.precision),
867            "Cannot add `Money` values with mismatched decimal scales"
868        );
869        Self {
870            raw: self
871                .raw
872                .checked_add(rhs.raw)
873                .expect("Overflow occurred when adding `Money`"),
874            currency: self.currency,
875        }
876    }
877}
878
879impl Sub for Money {
880    type Output = Self;
881    fn sub(self, rhs: Self) -> Self::Output {
882        assert_eq!(
883            self.currency, rhs.currency,
884            "Currency mismatch: cannot subtract {} from {}",
885            rhs.currency.code, self.currency.code
886        );
887        assert!(
888            raw_scales_match(self.currency.precision, rhs.currency.precision),
889            "Cannot subtract `Money` values with mismatched decimal scales"
890        );
891        Self {
892            raw: self
893                .raw
894                .checked_sub(rhs.raw)
895                .expect("Underflow occurred when subtracting `Money`"),
896            currency: self.currency,
897        }
898    }
899}
900
901impl Add<Decimal> for Money {
902    type Output = Decimal;
903    fn add(self, rhs: Decimal) -> Self::Output {
904        self.as_decimal() + rhs
905    }
906}
907
908impl Sub<Decimal> for Money {
909    type Output = Decimal;
910    fn sub(self, rhs: Decimal) -> Self::Output {
911        self.as_decimal() - rhs
912    }
913}
914
915impl Mul<Decimal> for Money {
916    type Output = Decimal;
917    fn mul(self, rhs: Decimal) -> Self::Output {
918        self.as_decimal() * rhs
919    }
920}
921
922impl Div<Decimal> for Money {
923    type Output = Decimal;
924    fn div(self, rhs: Decimal) -> Self::Output {
925        self.as_decimal() / rhs
926    }
927}
928
929impl Add<f64> for Money {
930    type Output = f64;
931    fn add(self, rhs: f64) -> Self::Output {
932        self.as_f64() + rhs
933    }
934}
935
936impl Sub<f64> for Money {
937    type Output = f64;
938    fn sub(self, rhs: f64) -> Self::Output {
939        self.as_f64() - rhs
940    }
941}
942
943impl Mul<f64> for Money {
944    type Output = f64;
945    fn mul(self, rhs: f64) -> Self::Output {
946        self.as_f64() * rhs
947    }
948}
949
950impl Div<f64> for Money {
951    type Output = f64;
952    fn div(self, rhs: f64) -> Self::Output {
953        self.as_f64() / rhs
954    }
955}
956
957impl Debug for Money {
958    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
959        let amount = self.to_decimal_string();
960        write!(f, "Money({amount}, {})", self.currency)
961    }
962}
963
964impl Display for Money {
965    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
966        let amount = self.to_decimal_string();
967        write!(f, "{amount} {}", self.currency)
968    }
969}
970
971impl Serialize for Money {
972    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
973    where
974        S: serde::Serializer,
975    {
976        serializer.serialize_str(&self.to_string())
977    }
978}
979
980impl<'de> Deserialize<'de> for Money {
981    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
982    where
983        D: Deserializer<'de>,
984    {
985        let StringInput(money_str) = Deserialize::deserialize(deserializer)?;
986        Self::from_str_exact(money_str.as_ref()).map_err(serde::de::Error::custom)
987    }
988}
989
990/// Checks if the money `value` is positive.
991///
992/// # Errors
993///
994/// Returns an error if `value` is not positive.
995#[inline(always)]
996pub fn check_positive_money(value: Money, param: &str) -> CorrectnessResult<()> {
997    if value.raw <= 0 {
998        return Err(CorrectnessError::NotPositive {
999            param: param.to_string(),
1000            value: value.to_string(),
1001            type_name: "`Money`",
1002        });
1003    }
1004    Ok(())
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009    use nautilus_core::{approx_eq, correctness::CorrectnessError};
1010    use rstest::rstest;
1011    use rust_decimal_macros::dec;
1012
1013    use super::*;
1014    use crate::enums::CurrencyType;
1015
1016    #[rstest]
1017    #[case(dec!(1.0001), "1.0001 USD")]
1018    #[case(dec!(0.000001), "0.000001 USD")]
1019    #[case(dec!(-0.000001), "-0.000001 USD")]
1020    #[case(dec!(1234.500100), "1234.5001 USD")]
1021    #[case(dec!(1.230000), "1.23 USD")]
1022    #[case(dec!(0.000000), "0.00 USD")]
1023    fn test_exact_usd_roundtrip(#[case] amount: Decimal, #[case] expected: &str) {
1024        let money = Money::from_decimal_exact(amount, Currency::USD()).unwrap();
1025        let text = money.to_string();
1026        let json = serde_json::to_vec(&money).unwrap();
1027        let msgpack = rmp_serde::to_vec(&money).unwrap();
1028        let from_text = Money::from_str_exact(&text).unwrap();
1029        let from_json: Money = serde_json::from_slice(&json).unwrap();
1030        let from_msgpack: Money = rmp_serde::from_slice(&msgpack).unwrap();
1031
1032        assert_eq!(money.currency, Currency::USD());
1033        assert_eq!(money.currency.precision, 2);
1034        assert_eq!(money.as_decimal(), amount);
1035        assert_eq!(text, expected);
1036        assert_eq!(
1037            format!("{money:?}"),
1038            format!(
1039                "Money({}, USD)",
1040                expected.split_whitespace().next().unwrap()
1041            )
1042        );
1043        assert_eq!(from_text, money);
1044        assert_eq!(from_json, money);
1045        assert_eq!(from_msgpack, money);
1046    }
1047
1048    #[rstest]
1049    fn test_exact_money_arithmetic() {
1050        let balance = Money::from_decimal_exact(dec!(1.0001), Currency::USD()).unwrap();
1051        let fee = Money::from_decimal_exact(dec!(0.000001), Currency::USD()).unwrap();
1052
1053        assert_eq!((balance + fee).as_decimal(), dec!(1.000101));
1054        assert_eq!((balance - fee).as_decimal(), dec!(1.000099));
1055        assert_eq!((-fee).as_decimal(), dec!(-0.000001));
1056        assert_eq!((fee * dec!(3)), dec!(0.000003));
1057        assert_eq!(balance.to_formatted_string(), "1.0001 USD");
1058        assert_eq!(balance.normalized().as_decimal(), dec!(1.00));
1059    }
1060
1061    #[rstest]
1062    #[case(1)]
1063    #[case(-1)]
1064    fn test_exact_money_native_precision_roundtrip(#[case] raw: MoneyRaw) {
1065        let sign = if raw < 0 { "-" } else { "" };
1066        let amount = format!("{sign}0.{}1", "0".repeat(usize::from(FIXED_PRECISION - 1)));
1067        let decimal = Decimal::from_str(&amount).unwrap();
1068        let expected = Money::from_raw(raw, Currency::USD());
1069        let from_decimal = Money::from_decimal_exact(decimal, Currency::USD()).unwrap();
1070        let text = format!("{amount} USD");
1071        let from_text = Money::from_str_exact(&text).unwrap();
1072
1073        assert_eq!(from_decimal, expected);
1074        assert_eq!(from_text, expected);
1075        assert_eq!(from_decimal.as_decimal(), decimal);
1076        assert_eq!(from_decimal.to_string(), text);
1077        assert_eq!(from_decimal.currency.precision, 2);
1078    }
1079
1080    #[rstest]
1081    fn test_exact_money_rejects_native_precision_loss() {
1082        let amount = format!("0.{}1", "0".repeat(usize::from(FIXED_PRECISION)));
1083        let decimal = Decimal::from_str(&amount).unwrap();
1084
1085        assert!(Money::from_decimal_exact(decimal, Currency::USD()).is_err());
1086        assert!(Money::from_str_exact(&format!("{amount} USD")).is_err());
1087    }
1088
1089    #[rstest]
1090    #[case(usize::from(FIXED_PRECISION) + 1)]
1091    #[case(39)]
1092    fn test_exact_money_accepts_extra_zero_digits(#[case] zeros: usize) {
1093        let amount = format!("1.{}", "0".repeat(zeros));
1094        let money = Money::from_str_exact(&format!("{amount} USD")).unwrap();
1095
1096        assert_eq!(money.as_decimal(), dec!(1.00));
1097        assert_eq!(money.to_string(), "1.00 USD");
1098    }
1099
1100    #[rstest]
1101    #[case("")]
1102    #[case("-")]
1103    fn test_exact_money_normalizes_scientific_integer(#[case] sign: &str) {
1104        let amount = format!("{sign}1{}e-39 USD", "0".repeat(39));
1105        let money = Money::from_str_exact(&amount).unwrap();
1106        let decimal_amount = format!("{sign}1{}.0e-39 USD", "0".repeat(39));
1107        let decimal_money = Money::from_str_exact(&decimal_amount).unwrap();
1108
1109        assert_eq!(money.to_string(), format!("{sign}1.00 USD"));
1110        assert_eq!(money.currency, Currency::USD());
1111        assert_eq!(decimal_money, money);
1112    }
1113
1114    #[rstest]
1115    #[case("e0")]
1116    #[case("E0")]
1117    #[case("")]
1118    fn test_exact_money_rejects_digits_beyond_decimal_capacity(#[case] exponent: &str) {
1119        let amount = format!("1.{}1{exponent} USD", "0".repeat(28));
1120
1121        assert_eq!(
1122            Money::from_str_exact(&amount),
1123            Err(format!(
1124                "amount exceeds native money precision {FIXED_PRECISION}"
1125            ))
1126        );
1127    }
1128
1129    #[rstest]
1130    #[case("1.0001e0 USD", "1.0001 USD")]
1131    #[case("1.234e-3 USD", "0.001234 USD")]
1132    #[case("1.000001E+2 USD", "100.0001 USD")]
1133    #[case("-.0000 USD", "0.00 USD")]
1134    #[case("0e32767 USD", "0.00 USD")]
1135    fn test_exact_money_scientific(#[case] input: &str, #[case] expected: &str) {
1136        let money = Money::from_str_exact(input).unwrap();
1137
1138        assert_eq!(money.to_string(), expected);
1139        assert_eq!(money.currency, Currency::USD());
1140    }
1141
1142    #[rstest]
1143    #[case("not-a-number")]
1144    #[case("None")]
1145    #[case("1e")]
1146    #[case("1e1.5")]
1147    fn test_exact_money_rejects_invalid_exponent(#[case] amount: &str) {
1148        let result = Money::from_str_exact(&format!("{amount} USD"));
1149
1150        assert_eq!(result, Err(format!("Invalid decimal value '{amount}'")));
1151    }
1152
1153    #[rstest]
1154    #[case("1..0")]
1155    #[case("1.2.3")]
1156    #[case("-")]
1157    #[case(".")]
1158    #[case("+-1")]
1159    #[case("1.5x0")]
1160    fn test_exact_money_rejects_malformed_amount(#[case] amount: &str) {
1161        let result = Money::from_str_exact(&format!("{amount} USD"));
1162
1163        assert!(result.is_err());
1164    }
1165
1166    #[rstest]
1167    fn test_exact_money_overflow_context() {
1168        let result = Money::from_str_exact("1e100 USD");
1169
1170        assert_eq!(
1171            result,
1172            Err(format!(
1173                "Overflow in exact Money construction (mantissa=1, exponent=100, precision={FIXED_PRECISION})"
1174            ))
1175        );
1176    }
1177
1178    #[rstest]
1179    fn test_exact_money_rejects_overflow() {
1180        let maximum = Money::from_raw(MONEY_RAW_MAX, Currency::USD()).as_decimal();
1181        let minimum = Money::from_raw(MONEY_RAW_MIN, Currency::USD()).as_decimal();
1182
1183        assert!(Money::from_decimal_exact(maximum + Decimal::ONE, Currency::USD()).is_err());
1184        assert!(Money::from_decimal_exact(minimum - Decimal::ONE, Currency::USD()).is_err());
1185        assert_eq!(
1186            Money::from_decimal_exact(maximum, Currency::USD())
1187                .unwrap()
1188                .raw(),
1189            MONEY_RAW_MAX
1190        );
1191        assert_eq!(
1192            Money::from_decimal_exact(minimum, Currency::USD())
1193                .unwrap()
1194                .raw(),
1195            MONEY_RAW_MIN
1196        );
1197    }
1198
1199    #[cfg(feature = "high-precision")]
1200    #[rstest]
1201    fn test_as_decimal_above_decimal_mantissa() {
1202        // Regression: a precision-16 currency amount above roughly 7.92e12 rescales to a raw
1203        // value beyond `Decimal`'s 96-bit mantissa, which used to panic during conversion.
1204        let currency = Currency::new("XYZ", 16, 0, "XYZ", crate::enums::CurrencyType::Crypto);
1205        let money = Money::from_raw(MONEY_RAW_MAX, currency);
1206
1207        assert_eq!(money.as_decimal(), dec!(17014118346046));
1208    }
1209
1210    #[rstest]
1211    fn test_money_ordering_is_structural_across_currencies() {
1212        use std::collections::BTreeSet;
1213
1214        let aud_low = Money::new(-1.0, Currency::AUD());
1215        let aud_high = Money::new(100.0, Currency::AUD());
1216        let usd_low = Money::new(-100.0, Currency::USD());
1217        let usd_high = Money::new(1.0, Currency::USD());
1218
1219        assert_eq!(aud_high.cmp(&usd_low), Ordering::Less);
1220        assert_eq!(aud_high.partial_cmp(&usd_low), Some(Ordering::Less));
1221        assert_eq!(
1222            (
1223                aud_high < usd_low,
1224                aud_high <= usd_low,
1225                aud_high > usd_low,
1226                aud_high >= usd_low,
1227                usd_low < aud_high,
1228                usd_low <= aud_high,
1229                usd_low > aud_high,
1230                usd_low >= aud_high,
1231            ),
1232            (true, true, false, false, false, false, true, true),
1233        );
1234
1235        let expected = vec![aud_low, aud_high, usd_low, usd_high];
1236        let mut sorted = vec![usd_high, aud_high, usd_low, aud_low];
1237        sorted.sort();
1238        let ordered = BTreeSet::from([usd_high, aud_high, usd_low, aud_low]);
1239
1240        assert_eq!(sorted, expected);
1241        assert_eq!(ordered.into_iter().collect::<Vec<_>>(), expected);
1242    }
1243
1244    #[rstest]
1245    fn test_money_cmp_equal_matches_equality() {
1246        use crate::enums::CurrencyType;
1247
1248        let currency = Currency::new("TST", 2, 1, "Test fiat", CurrencyType::Fiat);
1249        let same_code = Currency::new("TST", 8, 2, "Test crypto", CurrencyType::Crypto);
1250        let other_code = Currency::new("TSU", 2, 1, "Other fiat", CurrencyType::Fiat);
1251        let money = Money::from_raw(1, currency);
1252        let cases = [
1253            (Money::from_raw(1, same_code), true),
1254            (Money::from_raw(2, same_code), false),
1255            (Money::from_raw(1, other_code), false),
1256        ];
1257
1258        for (other, expected_equal) in cases {
1259            assert_eq!(money == other, expected_equal);
1260            assert_eq!(money.cmp(&other) == Ordering::Equal, expected_equal);
1261        }
1262    }
1263
1264    #[rstest]
1265    fn test_from_quantity_rejects_noncanonical_raw() {
1266        let precision = FIXED_PRECISION - 1;
1267        let quantity = Quantity::from_raw(1, precision);
1268        let currency = Currency::new("TOKEN", FIXED_PRECISION, 0, "Token", CurrencyType::Crypto);
1269        let result = Money::from_quantity(quantity, currency);
1270
1271        assert_eq!(
1272            result,
1273            Err(CorrectnessError::PredicateViolation {
1274                message: format!(
1275                    "Invalid fixed-point raw value 1 for precision {precision}: \
1276                             remainder 1 when divided by scale 10. Raw value should be a multiple of 10. \
1277                             This indicates data corruption or incorrect precision/scaling upstream"
1278                ),
1279            })
1280        );
1281    }
1282
1283    #[rstest]
1284    fn test_extreme_money_round_trips_through_raw() {
1285        // Regression: a lossy `f64` scalar previously left `MONEY_RAW_MAX`/`MONEY_RAW_MIN`
1286        // beyond the raw produced by `new` at the bounds, causing spurious panics and errors.
1287        let max = Money::new(MONEY_MAX, Currency::USD());
1288        let min = Money::new(MONEY_MIN, Currency::USD());
1289
1290        assert_eq!(max.raw, MONEY_RAW_MAX);
1291        assert_eq!(min.raw, MONEY_RAW_MIN);
1292        assert!(Money::from_raw_checked(max.raw, Currency::USD()).is_ok());
1293        assert!(Money::from_raw_checked(min.raw, Currency::USD()).is_ok());
1294    }
1295
1296    #[cfg(all(feature = "defi", feature = "high-precision"))]
1297    #[rstest]
1298    fn test_wei_above_decimal_mantissa_formats_exactly() {
1299        let raw = 80_000_000_000_000_000_250_000_000_000_i128;
1300        let currency = Currency::new("ETH", 18, 0, "Ether", crate::enums::CurrencyType::Crypto);
1301        let money = Money::from_raw(raw, currency);
1302
1303        assert_eq!(money.to_string(), "80000000000.000000250000000000 ETH");
1304        assert_eq!(
1305            money.to_formatted_string(),
1306            "80_000_000_000.000000250000000000 ETH"
1307        );
1308    }
1309
1310    #[cfg(all(feature = "defi", feature = "high-precision"))]
1311    #[rstest]
1312    fn test_exact_money_wei_precision_boundary() {
1313        let currency = Currency::new(
1314            "EXACT18",
1315            crate::defi::WEI_PRECISION,
1316            0,
1317            "Exact token",
1318            CurrencyType::Crypto,
1319        );
1320        Currency::register(currency, true).unwrap();
1321        let amount = dec!(0.000000000000000001);
1322        let text = "0.000000000000000001 EXACT18";
1323        let from_decimal = Money::from_decimal_exact(amount, currency).unwrap();
1324        let from_text = Money::from_str_exact(text).unwrap();
1325        let too_fine = dec!(0.0000000000000000001);
1326        let expected = format!(
1327            "amount exceeds native money precision {}",
1328            currency.precision
1329        );
1330
1331        assert_eq!(from_decimal.raw(), 1);
1332        assert_eq!(from_decimal.currency, currency);
1333        assert_eq!(from_decimal.currency.precision, 18);
1334        assert_eq!(from_decimal.as_decimal(), amount);
1335        assert_eq!(from_decimal.to_string(), text);
1336        assert_eq!(from_text, from_decimal);
1337        assert_eq!(
1338            Money::from_decimal_exact(too_fine, currency),
1339            Err(CorrectnessError::PredicateViolation {
1340                message: expected.clone(),
1341            })
1342        );
1343        assert_eq!(
1344            Money::from_str_exact("0.0000000000000000001 EXACT18"),
1345            Err(expected)
1346        );
1347    }
1348
1349    #[cfg(all(feature = "defi", feature = "high-precision"))]
1350    #[rstest]
1351    fn test_wei_money_serde_json_round_trip() {
1352        let currency = Currency::new(
1353            "WXW",
1354            18,
1355            0,
1356            "Wei exact",
1357            crate::enums::CurrencyType::Crypto,
1358        );
1359        Currency::register(currency, true).unwrap();
1360        let money = Money::from_raw(80_000_000_000_000_000_250_000_000_000_i128, currency);
1361
1362        let json = serde_json::to_string(&money).unwrap();
1363        let deserialized = serde_json::from_str::<Money>(&json).unwrap();
1364
1365        assert_eq!(json, "\"80000000000.000000250000000000 WXW\"");
1366        assert_eq!(deserialized, money);
1367    }
1368
1369    #[rstest]
1370    fn test_debug() {
1371        let money = Money::new(1010.12, Currency::USD());
1372        let result = format!("{money:?}");
1373        let expected = "Money(1010.12, USD)";
1374        assert_eq!(result, expected);
1375    }
1376
1377    #[rstest]
1378    #[case(dec!(9007199253.999999999), "Money(9007199253.999999999, TST9)")]
1379    #[case(dec!(-9007199253.999999999), "Money(-9007199253.999999999, TST9)")]
1380    fn test_debug_preserves_exact_amount(#[case] amount: Decimal, #[case] expected: &str) {
1381        use crate::enums::CurrencyType;
1382
1383        let currency = Currency::new("TST9", 9, 0, "Test 9dp", CurrencyType::Crypto);
1384        let money = Money::from_decimal(amount, currency).unwrap();
1385
1386        assert_eq!(format!("{money:?}"), expected);
1387    }
1388
1389    #[rstest]
1390    fn test_debug_preserves_domain_maximum() {
1391        use crate::enums::CurrencyType;
1392
1393        let currency = Currency::new(
1394            "TST",
1395            FIXED_PRECISION,
1396            0,
1397            "Test fixed precision",
1398            CurrencyType::Crypto,
1399        );
1400        let money = Money::from_raw(MONEY_RAW_MAX, currency);
1401        let expected = if cfg!(feature = "high-precision") {
1402            "Money(17014118346046.0000000000000000, TST)"
1403        } else {
1404            "Money(9223372036.000000000, TST)"
1405        };
1406
1407        assert_eq!(format!("{money:?}"), expected);
1408    }
1409
1410    #[rstest]
1411    fn test_display() {
1412        let money = Money::new(1010.12, Currency::USD());
1413        let result = format!("{money}");
1414        let expected = "1010.12 USD";
1415        assert_eq!(result, expected);
1416    }
1417
1418    #[rstest]
1419    #[case(42.0, 0, "JPY", "Money(42, JPY)", "42 JPY")]
1420    #[case(0.0, 2, "USD", "Money(0.00, USD)", "0.00 USD")]
1421    #[case(-1010.12, 2, "USD", "Money(-1010.12, USD)", "-1010.12 USD")]
1422    #[case(1010.12, 2, "USD", "Money(1010.12, USD)", "1010.12 USD")] // Normal precision
1423    #[case(123.456_789, 8, "BTC", "Money(123.45678900, BTC)", "123.45678900 BTC")] // At max normal precision
1424    fn test_formatting_normal_precision(
1425        #[case] value: f64,
1426        #[case] precision: u8,
1427        #[case] currency_code: &str,
1428        #[case] expected_debug: &str,
1429        #[case] expected_display: &str,
1430    ) {
1431        use crate::enums::CurrencyType;
1432        let currency = Currency::new(
1433            currency_code,
1434            precision,
1435            0,
1436            currency_code,
1437            CurrencyType::Fiat,
1438        );
1439        let money = Money::new(value, currency);
1440
1441        assert_eq!(format!("{money:?}"), expected_debug);
1442        assert_eq!(format!("{money}"), expected_display);
1443    }
1444
1445    #[rstest]
1446    #[cfg(feature = "defi")]
1447    #[case(
1448        1_000_000_000_000_000_000_i128,
1449        18,
1450        "wei",
1451        "Money(1.000000000000000000, wei)",
1452        "1.000000000000000000 wei"
1453    )] // High precision
1454    #[case(
1455        2_500_000_000_000_000_000_i128,
1456        18,
1457        "ETH",
1458        "Money(2.500000000000000000, ETH)",
1459        "2.500000000000000000 ETH"
1460    )] // High precision
1461    fn test_formatting_high_precision(
1462        #[case] raw_value: i128,
1463        #[case] precision: u8,
1464        #[case] currency_code: &str,
1465        #[case] expected_debug: &str,
1466        #[case] expected_display: &str,
1467    ) {
1468        use crate::enums::CurrencyType;
1469        let currency = Currency::new(
1470            currency_code,
1471            precision,
1472            0,
1473            currency_code,
1474            CurrencyType::Crypto,
1475        );
1476        let money = Money::from_raw(raw_value, currency);
1477
1478        assert_eq!(format!("{money:?}"), expected_debug);
1479        assert_eq!(format!("{money}"), expected_display);
1480        assert_eq!(
1481            serde_json::to_value(money).unwrap(),
1482            serde_json::Value::String(expected_display.to_string()),
1483        );
1484    }
1485
1486    #[rstest]
1487    fn test_zero_constructor() {
1488        let usd = Currency::USD();
1489        let money = Money::zero(usd);
1490        assert_eq!(money.raw, 0);
1491        assert_eq!(money.currency, usd);
1492    }
1493
1494    #[rstest]
1495    #[should_panic(expected = "Currency mismatch")]
1496    fn test_money_different_currency_addition() {
1497        let usd = Money::new(1000.0, Currency::USD());
1498        let btc = Money::new(1.0, Currency::BTC());
1499        let _ = usd + btc; // This should panic since currencies are different
1500    }
1501
1502    #[rstest] // Test does not panic rather than exact value
1503    fn test_with_maximum_value() {
1504        let money = Money::new_checked(MONEY_MAX, Currency::USD());
1505        assert!(money.is_ok());
1506    }
1507
1508    #[rstest] // Test does not panic rather than exact value
1509    fn test_with_minimum_value() {
1510        let money = Money::new_checked(MONEY_MIN, Currency::USD());
1511        assert!(money.is_ok());
1512    }
1513
1514    #[rstest]
1515    fn test_new_checked_returns_typed_error_with_stable_display() {
1516        let error = Money::new_checked(MONEY_MAX + 1.0, Currency::USD()).unwrap_err();
1517
1518        assert!(matches!(error, CorrectnessError::OutOfRange { .. }));
1519        assert_eq!(
1520            error.to_string(),
1521            format!(
1522                "invalid f64 for 'amount' not in range [{MONEY_MIN}, {MONEY_MAX}], was {}",
1523                MONEY_MAX + 1.0
1524            )
1525        );
1526    }
1527
1528    #[cfg(not(feature = "defi"))]
1529    #[rstest]
1530    fn test_new_checked_invalid_currency_precision_returns_error() {
1531        let mut currency = Currency::USD();
1532        currency.precision = FIXED_PRECISION + 1;
1533
1534        let error = Money::new_checked(1.0, currency).unwrap_err();
1535        assert!(
1536            error
1537                .to_string()
1538                .contains("`precision` exceeded maximum `FIXED_PRECISION`"),
1539            "unexpected message: {error}"
1540        );
1541    }
1542
1543    #[cfg(feature = "defi")]
1544    #[rstest]
1545    fn test_new_checked_rejects_float_for_wei_currency() {
1546        use crate::enums::CurrencyType;
1547
1548        let currency = Currency::new("TST18", 18, 0, "Test token", CurrencyType::Crypto);
1549        let error = Money::new_checked(1.0, currency).unwrap_err();
1550        let message = "`currency.precision` exceeded maximum float precision (16), use \
1551                       `Money::from_wei()` for wei values instead";
1552
1553        assert_eq!(
1554            error,
1555            CorrectnessError::PredicateViolation {
1556                message: message.to_string(),
1557            }
1558        );
1559        assert_eq!(error.to_string(), message);
1560    }
1561
1562    #[rstest]
1563    fn test_money_is_zero() {
1564        let zero_usd = Money::new(0.0, Currency::USD());
1565        assert!(zero_usd.is_zero());
1566        assert_eq!(zero_usd, Money::from("0.0 USD"));
1567
1568        let non_zero_usd = Money::new(100.0, Currency::USD());
1569        assert!(!non_zero_usd.is_zero());
1570    }
1571
1572    #[rstest]
1573    fn test_money_is_positive() {
1574        let usd = Currency::USD();
1575        assert!(Money::new(100.0, usd).is_positive());
1576        assert!(!Money::new(0.0, usd).is_positive());
1577        assert!(!Money::new(-100.0, usd).is_positive());
1578    }
1579
1580    #[rstest]
1581    fn test_money_comparisons() {
1582        let usd = Currency::USD();
1583        let m1 = Money::new(100.0, usd);
1584        let m2 = Money::new(200.0, usd);
1585
1586        assert!(m1 < m2);
1587        assert!(m2 > m1);
1588        assert!(m1 <= m2);
1589        assert!(m2 >= m1);
1590        assert_eq!(m1.cmp(&m2), Ordering::Less);
1591        assert_eq!(m2.cmp(&m1), Ordering::Greater);
1592
1593        // Equality
1594        let m3 = Money::new(100.0, usd);
1595        assert_eq!(m1, m3);
1596    }
1597
1598    #[rstest]
1599    fn test_add() {
1600        let a = 1000.0;
1601        let b = 500.0;
1602        let money1 = Money::new(a, Currency::USD());
1603        let money2 = Money::new(b, Currency::USD());
1604        let money3 = money1 + money2;
1605        assert_eq!(money3.raw, Money::new(a + b, Currency::USD()).raw);
1606    }
1607
1608    #[rstest]
1609    fn test_sub() {
1610        let usd = Currency::USD();
1611        let money1 = Money::new(1000.0, usd);
1612        let money2 = Money::new(250.0, usd);
1613        let result = money1 - money2;
1614        assert!(approx_eq!(f64, result.as_f64(), 750.0, epsilon = 1e-9));
1615        assert_eq!(result.currency, usd);
1616    }
1617
1618    #[rstest]
1619    fn test_money_checked_add_within_bounds() {
1620        let usd = Currency::USD();
1621        let a = Money::new(100.0, usd);
1622        let b = Money::new(50.0, usd);
1623        assert_eq!(a.checked_add(b), Some(Money::new(150.0, usd)));
1624    }
1625
1626    #[rstest]
1627    fn test_money_checked_add_above_max_returns_none() {
1628        let usd = Currency::USD();
1629        let near_max = Money::from_raw(MONEY_RAW_MAX, usd);
1630        let one = Money::new(1.0, usd);
1631        assert_eq!(near_max.checked_add(one), None);
1632    }
1633
1634    #[rstest]
1635    fn test_money_checked_sub_within_bounds() {
1636        let usd = Currency::USD();
1637        let a = Money::new(100.0, usd);
1638        let b = Money::new(40.0, usd);
1639        assert_eq!(a.checked_sub(b), Some(Money::new(60.0, usd)));
1640    }
1641
1642    #[rstest]
1643    fn test_money_checked_sub_below_min_returns_none() {
1644        let usd = Currency::USD();
1645        let near_min = Money::from_raw(MONEY_RAW_MIN, usd);
1646        let one = Money::new(1.0, usd);
1647        assert_eq!(near_min.checked_sub(one), None);
1648    }
1649
1650    #[rstest]
1651    fn test_money_checked_add_below_min_returns_none() {
1652        let usd = Currency::USD();
1653        let minus_one_unit = Money::from_raw(-1, usd);
1654
1655        assert_eq!(
1656            Money::from_raw(MONEY_RAW_MIN, usd).checked_add(minus_one_unit),
1657            None
1658        );
1659        assert_eq!(
1660            Money::from_raw(MONEY_RAW_MIN + 1, usd).checked_add(minus_one_unit),
1661            Some(Money::from_raw(MONEY_RAW_MIN, usd))
1662        );
1663    }
1664
1665    #[rstest]
1666    fn test_money_checked_sub_above_max_returns_none() {
1667        let usd = Currency::USD();
1668        let minus_one_unit = Money::from_raw(-1, usd);
1669
1670        assert_eq!(
1671            Money::from_raw(MONEY_RAW_MAX, usd).checked_sub(minus_one_unit),
1672            None
1673        );
1674        assert_eq!(
1675            Money::from_raw(MONEY_RAW_MAX - 1, usd).checked_sub(minus_one_unit),
1676            Some(Money::from_raw(MONEY_RAW_MAX, usd))
1677        );
1678    }
1679
1680    #[rstest]
1681    #[should_panic(expected = "Currency mismatch")]
1682    fn test_money_checked_add_currency_mismatch_panics() {
1683        let usd = Money::new(100.0, Currency::USD());
1684        let aud = Money::new(50.0, Currency::AUD());
1685        let _ = usd.checked_add(aud);
1686    }
1687
1688    #[rstest]
1689    #[should_panic(expected = "Currency mismatch")]
1690    fn test_money_checked_sub_currency_mismatch_panics() {
1691        let usd = Money::new(100.0, Currency::USD());
1692        let aud = Money::new(50.0, Currency::AUD());
1693        let _ = usd.checked_sub(aud);
1694    }
1695
1696    #[rstest]
1697    fn test_money_checked_add_at_exact_max_returns_some() {
1698        let usd = Currency::USD();
1699        let near_max = Money::from_raw(MONEY_RAW_MAX - 1, usd);
1700        let one_unit = Money::from_raw(1, usd);
1701        assert_eq!(
1702            near_max.checked_add(one_unit),
1703            Some(Money::from_raw(MONEY_RAW_MAX, usd)),
1704        );
1705    }
1706
1707    #[rstest]
1708    fn test_money_checked_sub_at_exact_min_returns_some() {
1709        let usd = Currency::USD();
1710        let near_min = Money::from_raw(MONEY_RAW_MIN + 1, usd);
1711        let one_unit = Money::from_raw(1, usd);
1712        assert_eq!(
1713            near_min.checked_sub(one_unit),
1714            Some(Money::from_raw(MONEY_RAW_MIN, usd)),
1715        );
1716    }
1717
1718    #[rstest]
1719    fn test_money_negation() {
1720        let money = Money::new(100.0, Currency::USD());
1721        let result = -money;
1722        assert_eq!(result, Money::from("-100.0 USD"));
1723        assert_eq!(result.currency, Currency::USD().clone());
1724    }
1725
1726    #[rstest]
1727    fn test_money_addition_decimal() {
1728        let money = Money::new(100.0, Currency::USD());
1729        let result = money + dec!(50.25);
1730        assert_eq!(result, dec!(150.25));
1731    }
1732
1733    #[rstest]
1734    fn test_money_subtraction_decimal() {
1735        let money = Money::new(100.0, Currency::USD());
1736        let result = money - dec!(30.50);
1737        assert_eq!(result, dec!(69.50));
1738    }
1739
1740    #[rstest]
1741    fn test_money_multiplication_decimal() {
1742        let money = Money::new(100.0, Currency::USD());
1743        let result = money * dec!(1.5);
1744        assert_eq!(result, dec!(150.00));
1745    }
1746
1747    #[rstest]
1748    fn test_money_division_decimal() {
1749        let money = Money::new(100.0, Currency::USD());
1750        let result = money / dec!(4);
1751        assert_eq!(result, dec!(25.00));
1752    }
1753
1754    #[rstest]
1755    fn test_money_addition_f64() {
1756        let money = Money::new(100.0, Currency::USD());
1757        let result = money + 50.25;
1758        assert!(approx_eq!(f64, result, 150.25, epsilon = 1e-9));
1759    }
1760
1761    #[rstest]
1762    fn test_money_subtraction_f64() {
1763        let money = Money::new(100.0, Currency::USD());
1764        let result = money - 30.50;
1765        assert!(approx_eq!(f64, result, 69.50, epsilon = 1e-9));
1766    }
1767
1768    #[rstest]
1769    fn test_money_multiplication_f64() {
1770        let money = Money::new(100.0, Currency::USD());
1771        let result = money * 1.5;
1772        assert!(approx_eq!(f64, result, 150.0, epsilon = 1e-9));
1773    }
1774
1775    #[rstest]
1776    fn test_money_division_f64() {
1777        let money = Money::new(100.0, Currency::USD());
1778        let result = money / 4.0;
1779        assert!(approx_eq!(f64, result, 25.0, epsilon = 1e-9));
1780    }
1781
1782    #[rstest]
1783    fn test_money_new_usd() {
1784        let money = Money::new(1000.0, Currency::USD());
1785        assert_eq!(money.currency.code.as_str(), "USD");
1786        assert_eq!(money.currency.precision, 2);
1787        assert_eq!(money.to_string(), "1000.00 USD");
1788        assert_eq!(money.to_formatted_string(), "1_000.00 USD");
1789        assert_eq!(money.as_decimal(), dec!(1000.00));
1790        assert!(approx_eq!(f64, money.as_f64(), 1000.0, epsilon = 0.001));
1791    }
1792
1793    #[rstest]
1794    fn test_money_new_btc() {
1795        let money = Money::new(10.3, Currency::BTC());
1796        assert_eq!(money.currency.code.as_str(), "BTC");
1797        assert_eq!(money.currency.precision, 8);
1798        assert_eq!(money.to_string(), "10.30000000 BTC");
1799        assert_eq!(money.to_formatted_string(), "10.30000000 BTC");
1800    }
1801
1802    #[rstest]
1803    fn test_to_formatted_string_preserves_digits_beyond_f64_precision() {
1804        use crate::enums::CurrencyType;
1805
1806        // 19 significant digits exceed f64's ~16-digit resolution; the previous
1807        // f64-based formatting printed the nearest float instead.
1808        let currency = Currency::new("TST9", 9, 0, "Test 9dp", CurrencyType::Crypto);
1809        let money = Money::from_decimal(dec!(1234567890.123456789), currency).unwrap();
1810
1811        assert_eq!(money.to_formatted_string(), "1_234_567_890.123456789 TST9");
1812    }
1813
1814    #[rstest]
1815    #[case("0USD")] // <-- No whitespace separator
1816    #[case("0x00 USD")] // <-- Invalid float
1817    #[case("0 US")] // <-- Invalid currency
1818    #[case("0 USD USD")] // <-- Too many parts
1819    #[should_panic(expected = "Condition failed")]
1820    fn test_from_str_invalid_input(#[case] input: &str) {
1821        let _ = Money::from(input);
1822    }
1823
1824    #[rstest]
1825    #[case("invalid UNKNOWN_FROM_STR", "Unknown currency: UNKNOWN_FROM_STR")]
1826    #[case(
1827        "1__e+ USD",
1828        "Error parsing amount '1__e+' as Decimal: Failed to parse"
1829    )]
1830    fn test_from_str_preserves_error_order_and_input(#[case] input: &str, #[case] expected: &str) {
1831        assert_eq!(input.parse::<Money>(), Err(expected.to_string()));
1832    }
1833
1834    #[rstest]
1835    #[case("0 USD", Currency::USD(), dec!(0.00))]
1836    #[case("1.1 AUD", Currency::AUD(), dec!(1.10))]
1837    #[case("1.12345678 BTC", Currency::BTC(), dec!(1.12345678))]
1838    #[case("10_000.10 USD", Currency::USD(), dec!(10000.10))]
1839    fn test_from_str_valid_input(
1840        #[case] input: &str,
1841        #[case] expected_currency: Currency,
1842        #[case] expected_dec: Decimal,
1843    ) {
1844        let money = Money::from(input);
1845        assert_eq!(money.currency, expected_currency);
1846        assert_eq!(money.as_decimal(), expected_dec);
1847    }
1848
1849    #[rstest]
1850    #[case("1.005 USD", dec!(1.00))]
1851    #[case("1.015 USD", dec!(1.02))]
1852    #[case("-1.005 USD", dec!(-1.00))]
1853    #[case("-1.015 USD", dec!(-1.02))]
1854    fn test_from_str_rounds_half_to_even(#[case] input: &str, #[case] expected: Decimal) {
1855        let money = input.parse::<Money>().unwrap();
1856
1857        assert_eq!(
1858            money,
1859            Money::from_decimal(expected, Currency::USD()).unwrap()
1860        );
1861        assert_eq!(money.currency, Currency::USD());
1862        assert_eq!(money.as_decimal(), expected);
1863    }
1864
1865    #[rstest]
1866    fn test_money_from_str_negative() {
1867        let money = Money::from("-123.45 USD");
1868        assert!(approx_eq!(f64, money.as_f64(), -123.45, epsilon = 1e-9));
1869        assert_eq!(money.currency, Currency::USD());
1870    }
1871
1872    #[rstest]
1873    #[case("1e7 USD", 10_000_000.0)]
1874    #[case("2.5e3 EUR", 2_500.0)]
1875    #[case("1.234e-2 GBP", 0.01)] // GBP has 2 decimal places, so 0.01234 becomes 0.01
1876    #[case("5E-3 JPY", 0.0)] // JPY has 0 decimal places, so 0.005 becomes 0
1877    fn test_from_str_scientific_notation(#[case] input: &str, #[case] expected_value: f64) {
1878        let money = Money::from_str(input).unwrap();
1879        assert!(approx_eq!(
1880            f64,
1881            money.as_f64(),
1882            expected_value,
1883            epsilon = 1e-10
1884        ));
1885    }
1886
1887    #[rstest]
1888    #[case("1_234.56 USD", 1234.56)]
1889    #[case("1_000_000 EUR", 1_000_000.0)]
1890    #[case("99_999.99 GBP", 99_999.99)]
1891    fn test_from_str_with_underscores(#[case] input: &str, #[case] expected_value: f64) {
1892        let money = Money::from_str(input).unwrap();
1893        assert!(approx_eq!(
1894            f64,
1895            money.as_f64(),
1896            expected_value,
1897            epsilon = 1e-10
1898        ));
1899    }
1900
1901    #[rstest]
1902    fn test_from_decimal_precision_preservation() {
1903        use rust_decimal::Decimal;
1904
1905        let decimal = Decimal::from_str("123.45").unwrap();
1906        let money = Money::from_decimal(decimal, Currency::USD()).unwrap();
1907        assert_eq!(money.currency.precision, 2);
1908        assert!(approx_eq!(f64, money.as_f64(), 123.45, epsilon = 1e-10));
1909
1910        // Verify raw value is exact for USD (2 decimal places)
1911        let expected_raw = 12345 * 10_i64.pow(u32::from(FIXED_PRECISION - 2));
1912        assert_eq!(money.raw, MoneyRaw::from(expected_raw));
1913    }
1914
1915    #[rstest]
1916    fn test_from_decimal_rounding() {
1917        use rust_decimal::Decimal;
1918
1919        // Test banker's rounding with USD (2 decimal places)
1920        let decimal = Decimal::from_str("1.005").unwrap();
1921        let money = Money::from_decimal(decimal, Currency::USD()).unwrap();
1922        assert_eq!(money.as_f64(), 1.0); // 1.005 rounds to 1.00 (even)
1923
1924        let decimal = Decimal::from_str("1.015").unwrap();
1925        let money = Money::from_decimal(decimal, Currency::USD()).unwrap();
1926        assert_eq!(money.as_f64(), 1.02); // 1.015 rounds to 1.02 (even)
1927    }
1928
1929    #[rstest]
1930    fn test_money_hash() {
1931        use std::{
1932            collections::hash_map::DefaultHasher,
1933            hash::{Hash, Hasher},
1934        };
1935
1936        let m1 = Money::new(100.0, Currency::USD());
1937        let m2 = Money::new(100.0, Currency::USD());
1938        let m3 = Money::new(100.0, Currency::AUD());
1939
1940        let mut s1 = DefaultHasher::new();
1941        let mut s2 = DefaultHasher::new();
1942        let mut s3 = DefaultHasher::new();
1943
1944        m1.hash(&mut s1);
1945        m2.hash(&mut s2);
1946        m3.hash(&mut s3);
1947
1948        assert_eq!(
1949            s1.finish(),
1950            s2.finish(),
1951            "Same amount + same currency => same hash"
1952        );
1953        assert_ne!(
1954            s1.finish(),
1955            s3.finish(),
1956            "Same amount + different currency => different hash"
1957        );
1958    }
1959
1960    #[rstest]
1961    fn test_money_serialization_deserialization() {
1962        let money = Money::new(123.45, Currency::USD());
1963        let serialized = serde_json::to_string(&money);
1964        let deserialized: Money = serde_json::from_str(&serialized.unwrap()).unwrap();
1965        assert_eq!(money, deserialized);
1966    }
1967
1968    #[rstest]
1969    fn test_money_deserialize_from_owned_value() {
1970        let money = Money::new(123.45, Currency::USD());
1971        let value = serde_json::to_value(money).unwrap();
1972
1973        let deserialized: Money = serde_json::from_value(value).unwrap();
1974        assert_eq!(money, deserialized);
1975    }
1976
1977    #[rstest]
1978    fn test_money_deserialize_invalid_format_returns_error() {
1979        let result = serde_json::from_str::<Money>("\"100.00\"");
1980        let error = result.unwrap_err();
1981        assert!(
1982            error.to_string().contains("Expected '<amount> <currency>'"),
1983            "unexpected message: {error}"
1984        );
1985    }
1986
1987    #[rstest]
1988    fn test_money_deserialize_unknown_currency_returns_error() {
1989        let result = serde_json::from_str::<Money>("\"100.00 ZZZZ\"");
1990        let error = result.unwrap_err();
1991        assert!(
1992            error.to_string().contains("Unknown currency"),
1993            "unexpected message: {error}"
1994        );
1995    }
1996
1997    #[rstest]
1998    #[should_panic(expected = "`raw` value")]
1999    fn test_money_from_raw_out_of_range_panics() {
2000        let usd = Currency::USD();
2001        let raw = MONEY_RAW_MAX.saturating_add(1);
2002        let _ = Money::from_raw(raw, usd);
2003    }
2004
2005    #[rstest]
2006    fn test_money_from_raw_checked_valid() {
2007        let usd = Currency::USD();
2008        let money = Money::from_raw_checked(123_450_000_000, usd).unwrap();
2009        assert_eq!(money.currency, usd);
2010    }
2011
2012    #[rstest]
2013    fn test_money_from_raw_checked_above_max_returns_error() {
2014        let usd = Currency::USD();
2015        let raw = MONEY_RAW_MAX.saturating_add(1);
2016        let error = Money::from_raw_checked(raw, usd).unwrap_err();
2017        assert!(matches!(error, CorrectnessError::PredicateViolation { .. }));
2018    }
2019
2020    #[rstest]
2021    fn test_money_from_raw_checked_below_min_returns_error() {
2022        let usd = Currency::USD();
2023        let raw = MONEY_RAW_MIN.saturating_sub(1);
2024        let error = Money::from_raw_checked(raw, usd).unwrap_err();
2025        assert!(matches!(error, CorrectnessError::PredicateViolation { .. }));
2026    }
2027
2028    #[rstest]
2029    fn test_from_decimal_rejects_out_of_range() {
2030        let huge = Decimal::from_str("99999999999999999999.99").unwrap();
2031        let result = Money::from_decimal(huge, Currency::USD());
2032        assert!(result.is_err());
2033    }
2034
2035    #[rstest]
2036    fn test_from_decimal_rejects_raw_between_money_and_raw_bounds() {
2037        let usd = Currency::USD();
2038        let at_max = Decimal::try_from(MONEY_MAX).unwrap();
2039        let above_max = at_max + dec!(0.5);
2040        let expected_raw = MONEY_RAW_MAX + 5 * MoneyRaw::pow(10, u32::from(FIXED_PRECISION - 1));
2041
2042        let error = Money::from_decimal(above_max, usd).unwrap_err();
2043
2044        assert_eq!(
2045            error.to_string(),
2046            format!(
2047                "Raw value {expected_raw} exceeded bounds [{MONEY_RAW_MIN}, {MONEY_RAW_MAX}] for Money"
2048            )
2049        );
2050        assert_eq!(Money::from_decimal(at_max, usd).unwrap().raw, MONEY_RAW_MAX);
2051    }
2052
2053    #[rstest]
2054    fn test_from_decimal_out_of_range_returns_typed_error_with_stable_display() {
2055        let huge = Decimal::from_str("99999999999999999999.99").unwrap();
2056        let error = Money::from_decimal(huge, Currency::USD()).unwrap_err();
2057        match error {
2058            CorrectnessError::PredicateViolation { ref message } => {
2059                assert!(
2060                    message.contains("MoneyRaw range") || message.contains("Money"),
2061                    "unexpected message: {message:?}",
2062                );
2063            }
2064            _ => panic!("expected PredicateViolation, was {error:?}"),
2065        }
2066    }
2067
2068    #[rstest]
2069    fn test_from_mantissa_exponent_exact_precision() {
2070        let money = Money::from_mantissa_exponent(12345, -2, Currency::USD());
2071        assert_eq!(money.as_f64(), 123.45);
2072    }
2073
2074    #[rstest]
2075    fn test_from_mantissa_exponent_excess_rounds_down() {
2076        // 12.345 rounds to 12.34 (4 is even, banker's rounding)
2077        let money = Money::from_mantissa_exponent(12345, -3, Currency::USD());
2078        assert_eq!(money.as_f64(), 12.34);
2079    }
2080
2081    #[rstest]
2082    fn test_from_mantissa_exponent_excess_rounds_up() {
2083        // 12.355 rounds to 12.36 (5 is odd, banker's rounding)
2084        let money = Money::from_mantissa_exponent(12355, -3, Currency::USD());
2085        assert_eq!(money.as_f64(), 12.36);
2086    }
2087
2088    #[rstest]
2089    fn test_from_mantissa_exponent_positive_exponent() {
2090        let money = Money::from_mantissa_exponent(5, 2, Currency::USD());
2091        assert_eq!(money.as_f64(), 500.0);
2092    }
2093
2094    #[rstest]
2095    #[should_panic(expected = "Money::from_mantissa_exponent")]
2096    fn test_from_mantissa_exponent_overflow_panics() {
2097        let _ = Money::from_mantissa_exponent(i64::MAX, 9, Currency::USD());
2098    }
2099
2100    #[rstest]
2101    #[should_panic(expected = "exceeds i128 range")]
2102    fn test_from_mantissa_exponent_large_exponent_panics() {
2103        let _ = Money::from_mantissa_exponent(1, 119, Currency::USD());
2104    }
2105
2106    #[rstest]
2107    fn test_from_mantissa_exponent_zero_with_large_exponent() {
2108        let money = Money::from_mantissa_exponent(0, 119, Currency::USD());
2109        assert_eq!(money.as_f64(), 0.0);
2110    }
2111
2112    #[rstest]
2113    fn test_from_mantissa_exponent_very_negative_exponent_rounds_to_zero() {
2114        // exponent=-120, frac_digits=120, excess=118 for USD (precision 2)
2115        let money = Money::from_mantissa_exponent(12345, -120, Currency::USD());
2116        assert_eq!(money.as_f64(), 0.0);
2117    }
2118
2119    #[rstest]
2120    #[case(42.0, true, "positive value")]
2121    #[case(0.0, false, "zero value")]
2122    #[case( -13.5,  false, "negative value")]
2123    #[expect(
2124        clippy::used_underscore_binding,
2125        reason = "rstest case name documents the parameterized input"
2126    )]
2127    fn test_check_positive_money(
2128        #[case] amount: f64,
2129        #[case] should_succeed: bool,
2130        #[case] _case_name: &str,
2131    ) {
2132        let money = Money::new(amount, Currency::USD());
2133
2134        let res = check_positive_money(money, "money");
2135
2136        if should_succeed {
2137            assert!(res.is_ok(), "expected Ok(..) for {amount}");
2138        } else {
2139            assert!(res.is_err(), "expected Err(..) for {amount}");
2140            let msg = res.unwrap_err().to_string();
2141            assert!(
2142                msg.contains("not positive"),
2143                "error message should mention positivity; got: {msg:?}"
2144            );
2145        }
2146    }
2147
2148    #[rstest]
2149    fn test_check_positive_money_returns_typed_error_with_stable_display() {
2150        let error = check_positive_money(Money::new(0.0, Currency::USD()), "money").unwrap_err();
2151
2152        assert_eq!(
2153            error,
2154            CorrectnessError::NotPositive {
2155                param: "money".to_string(),
2156                value: "0.00 USD".to_string(),
2157                type_name: "`Money`",
2158            }
2159        );
2160        assert_eq!(
2161            error.to_string(),
2162            "invalid `Money` for 'money' not positive, was 0.00 USD"
2163        );
2164    }
2165}
2166
2167#[cfg(test)]
2168mod property_tests {
2169    use proptest::prelude::*;
2170    use rstest::rstest;
2171
2172    use super::*;
2173
2174    fn currency_strategy() -> impl Strategy<Value = Currency> {
2175        prop_oneof![
2176            Just(Currency::USD()),
2177            Just(Currency::EUR()),
2178            Just(Currency::GBP()),
2179            Just(Currency::JPY()),
2180            Just(Currency::AUD()),
2181            Just(Currency::CAD()),
2182            Just(Currency::CHF()),
2183            Just(Currency::BTC()),
2184            Just(Currency::ETH()),
2185            Just(Currency::USDT()),
2186        ]
2187    }
2188
2189    fn money_amount_strategy() -> impl Strategy<Value = f64> {
2190        prop_oneof![
2191            -1000.0..1000.0,
2192            -100_000.0..100_000.0,
2193            -1_000_000.0..1_000_000.0,
2194            Just(0.0),
2195            Just(MONEY_MIN / 2.0),
2196            Just(MONEY_MAX / 2.0),
2197            Just(MONEY_MIN + 1.0),
2198            Just(MONEY_MAX - 1.0),
2199            Just(MONEY_MIN),
2200            Just(MONEY_MAX),
2201        ]
2202    }
2203
2204    fn money_strategy() -> impl Strategy<Value = Money> {
2205        (money_amount_strategy(), currency_strategy())
2206            .prop_filter_map("constructible money", |(amount, currency)| {
2207                Money::new_checked(amount, currency).ok()
2208            })
2209    }
2210
2211    proptest! {
2212        #[rstest]
2213        fn prop_money_construction_roundtrip(
2214            amount in money_amount_strategy(),
2215            currency in currency_strategy()
2216        ) {
2217            if let Ok(money) = Money::new_checked(amount, currency) {
2218                let roundtrip = money.as_f64();
2219                let precision_epsilon = if currency.precision == 0 {
2220                    1.0
2221                } else {
2222                    let currency_epsilon = 10.0_f64.powi(-i32::from(currency.precision));
2223                    let magnitude_epsilon = amount.abs() * 1e-10;
2224                    currency_epsilon.max(magnitude_epsilon)
2225                };
2226                prop_assert!((roundtrip - amount).abs() <= precision_epsilon,
2227                    "Roundtrip failed: {} -> {} -> {} (precision: {}, epsilon: {})",
2228                    amount, money.raw, roundtrip, currency.precision, precision_epsilon);
2229                prop_assert_eq!(money.currency, currency);
2230            }
2231        }
2232
2233        #[rstest]
2234        fn prop_money_addition_commutative(
2235            money1 in money_strategy(),
2236            money2 in money_strategy(),
2237        ) {
2238            if money1.currency == money2.currency
2239                && let (Some(_), Some(_)) = (
2240                    money1.raw.checked_add(money2.raw),
2241                    money2.raw.checked_add(money1.raw)
2242                )
2243            {
2244                let sum1 = money1 + money2;
2245                let sum2 = money2 + money1;
2246                prop_assert_eq!(sum1, sum2, "Addition should be commutative");
2247                prop_assert_eq!(sum1.currency, money1.currency);
2248            }
2249        }
2250
2251        #[rstest]
2252        fn prop_money_addition_associative(
2253            money1 in money_strategy(),
2254            money2 in money_strategy(),
2255            money3 in money_strategy(),
2256        ) {
2257            if money1.currency == money2.currency
2258                && money2.currency == money3.currency
2259                && let (Some(sum1), Some(sum2)) = (
2260                    money1.raw.checked_add(money2.raw),
2261                    money2.raw.checked_add(money3.raw)
2262                )
2263                && let (Some(left), Some(right)) = (
2264                    sum1.checked_add(money3.raw),
2265                    money1.raw.checked_add(sum2)
2266                )
2267                && (MONEY_RAW_MIN..=MONEY_RAW_MAX).contains(&left)
2268                && (MONEY_RAW_MIN..=MONEY_RAW_MAX).contains(&right)
2269            {
2270                let left_result = Money::from_raw(left, money1.currency);
2271                let right_result = Money::from_raw(right, money1.currency);
2272                prop_assert_eq!(left_result, right_result, "Addition should be associative");
2273            }
2274        }
2275
2276        #[rstest]
2277        fn prop_money_subtraction_inverse(
2278            money1 in money_strategy(),
2279            money2 in money_strategy(),
2280        ) {
2281            if money1.currency == money2.currency
2282                && let Some(sum_raw) = money1.raw.checked_add(money2.raw)
2283                && (MONEY_RAW_MIN..=MONEY_RAW_MAX).contains(&sum_raw)
2284            {
2285                let sum = Money::from_raw(sum_raw, money1.currency);
2286                let diff = sum - money2;
2287                prop_assert_eq!(diff, money1, "Subtraction should be inverse of addition");
2288            }
2289        }
2290
2291        /// Property: checked_add agrees with raw checked_add when result is in bounds and
2292        /// currencies match; returns None when out of bounds.
2293        #[rstest]
2294        fn prop_money_checked_add_matches_spec(
2295            raw1 in MONEY_RAW_MIN..=MONEY_RAW_MAX,
2296            raw2 in MONEY_RAW_MIN..=MONEY_RAW_MAX,
2297            currency in currency_strategy(),
2298        ) {
2299            let m1 = Money::from_raw(raw1, currency);
2300            let m2 = Money::from_raw(raw2, currency);
2301            let expected = m1.raw
2302                .checked_add(m2.raw)
2303                .filter(|r| (MONEY_RAW_MIN..=MONEY_RAW_MAX).contains(r))
2304                .map(|raw| Money { raw, currency });
2305            prop_assert_eq!(m1.checked_add(m2), expected);
2306        }
2307
2308        /// Property: checked_sub agrees with raw checked_sub when result is in bounds and
2309        /// currencies match; returns None when out of bounds.
2310        #[rstest]
2311        fn prop_money_checked_sub_matches_spec(
2312            raw1 in MONEY_RAW_MIN..=MONEY_RAW_MAX,
2313            raw2 in MONEY_RAW_MIN..=MONEY_RAW_MAX,
2314            currency in currency_strategy(),
2315        ) {
2316            let m1 = Money::from_raw(raw1, currency);
2317            let m2 = Money::from_raw(raw2, currency);
2318            let expected = m1.raw
2319                .checked_sub(m2.raw)
2320                .filter(|r| (MONEY_RAW_MIN..=MONEY_RAW_MAX).contains(r))
2321                .map(|raw| Money { raw, currency });
2322            prop_assert_eq!(m1.checked_sub(m2), expected);
2323        }
2324
2325        #[rstest]
2326        fn prop_money_zero_identity(money in money_strategy()) {
2327            let zero = Money::zero(money.currency);
2328            prop_assert_eq!(money + zero, money, "Zero should be additive identity");
2329            prop_assert_eq!(zero + money, money, "Zero should be additive identity (commutative)");
2330            prop_assert!(zero.is_zero(), "Zero should be recognized as zero");
2331        }
2332
2333        #[rstest]
2334        fn prop_money_negation_inverse(money in money_strategy()) {
2335            let negated = -money;
2336            let double_neg = -negated;
2337            prop_assert_eq!(money, double_neg, "Double negation should equal original");
2338            prop_assert_eq!(negated.currency, money.currency, "Negation preserves currency");
2339
2340            if let Some(sum_raw) = money.raw.checked_add(negated.raw)
2341                && (MONEY_RAW_MIN..=MONEY_RAW_MAX).contains(&sum_raw) {
2342                    let sum = Money::from_raw(sum_raw, money.currency);
2343                    prop_assert!(sum.is_zero(), "Money + (-Money) should equal zero");
2344                }
2345        }
2346
2347        #[rstest]
2348        fn prop_money_comparison_consistency(
2349            money1 in money_strategy(),
2350            money2 in money_strategy(),
2351        ) {
2352            let eq = money1 == money2;
2353            let lt = money1 < money2;
2354            let gt = money1 > money2;
2355            let le = money1 <= money2;
2356            let ge = money1 >= money2;
2357
2358            let exclusive_count = [eq, lt, gt].iter().filter(|&&x| x).count();
2359            prop_assert_eq!(exclusive_count, 1, "Exactly one of ==, <, > should be true");
2360
2361            prop_assert_eq!(le, eq || lt, "<= should equal == || <");
2362            prop_assert_eq!(ge, eq || gt, ">= should equal == || >");
2363            prop_assert_eq!(lt, money2 > money1, "< should be symmetric with >");
2364            prop_assert_eq!(le, money2 >= money1, "<= should be symmetric with >=");
2365        }
2366
2367        #[rstest]
2368        fn prop_money_decimal_conversion(money in money_strategy()) {
2369            let decimal = money.as_decimal();
2370
2371            // Scale must always match currency precision
2372            prop_assert_eq!(decimal.scale(), u32::from(money.currency.precision));
2373
2374            #[cfg(feature = "defi")]
2375            {
2376                let decimal_f64: f64 = decimal.try_into().unwrap_or(0.0);
2377                prop_assert!(decimal_f64.is_finite(), "Decimal should convert to finite f64");
2378            }
2379            #[cfg(not(feature = "defi"))]
2380            {
2381                let decimal_f64: f64 = decimal.try_into().unwrap_or(0.0);
2382                let original_f64 = money.as_f64();
2383
2384                let base_epsilon = 10.0_f64.powi(-i32::from(money.currency.precision));
2385                let precision_epsilon = if cfg!(feature = "high-precision") {
2386                    base_epsilon.max(1e-10)
2387                } else {
2388                    base_epsilon
2389                };
2390                let diff = (decimal_f64 - original_f64).abs();
2391                prop_assert!(diff <= precision_epsilon,
2392                    "Decimal conversion should preserve value within currency precision: {} vs {} (diff: {}, epsilon: {})",
2393                    original_f64, decimal_f64, diff, precision_epsilon);
2394            }
2395        }
2396
2397        #[rstest]
2398        fn prop_money_arithmetic_with_f64(
2399            money in money_strategy(),
2400            factor in -1000.0..1000.0_f64,
2401        ) {
2402            if factor != 0.0 {
2403                let original_f64 = money.as_f64();
2404
2405                let mul_result = money * factor;
2406                let expected_mul = original_f64 * factor;
2407                prop_assert!((mul_result - expected_mul).abs() < 0.01,
2408                    "Multiplication with f64 should be accurate");
2409
2410                let div_result = money / factor;
2411                let expected_div = original_f64 / factor;
2412                if expected_div.is_finite() {
2413                    prop_assert!((div_result - expected_div).abs() < 0.01,
2414                        "Division with f64 should be accurate");
2415                }
2416
2417                let add_result = money + factor;
2418                let expected_add = original_f64 + factor;
2419                prop_assert!((add_result - expected_add).abs() < 0.01,
2420                    "Addition with f64 should be accurate");
2421
2422                let sub_result = money - factor;
2423                let expected_sub = original_f64 - factor;
2424                prop_assert!((sub_result - expected_sub).abs() < 0.01,
2425                    "Subtraction with f64 should be accurate");
2426            }
2427        }
2428    }
2429}