diff --git a/crates/immo-core/src/loan_offer.rs b/crates/immo-core/src/loan_offer.rs index 1e194db..3ac886a 100644 --- a/crates/immo-core/src/loan_offer.rs +++ b/crates/immo-core/src/loan_offer.rs @@ -11,8 +11,23 @@ pub enum LoanOfferError { ZeroLoanDuration, #[error("borrowed capital should be strictly positive: {0}")] NonPositiveBorrowedCapital(Euros), + #[error("borrowed capital should be lower than {MAX_BORROWED_CAPITAL}: {0}")] + MaxBorrowedCapitalExceeded(Euros), + #[error("annual nominal rate should be lower than {MAX_ANNUAL_NOMINAL_RATE}: {0}")] + MaxAnnualNominalRateExceeded(Decimal), + #[error("loan duration in months should be lower than {MAX_LOAN_DURATION_IN_MONTHS}: {0}")] + MaxLoanDurationExceeded(u64), } +/// 1b€ is a pretty expensive house... +pub const MAX_BORROWED_CAPITAL: Euros = Euros::from_cents_as_i64(999_999_999_99); + +/// 99% nominal rate will hopefully never happen +pub const MAX_ANNUAL_NOMINAL_RATE: Decimal = Decimal::from_parts(99, 0, 0, false, 2); + +/// Loan duration is theoretically constrained to max 25y in France, let's use 40 to be sure +pub const MAX_LOAN_DURATION_IN_MONTHS: u64 = 40 * 12; + #[derive(Debug, PartialEq, Eq)] pub struct LoanOffer { borrowed_capital: Euros, @@ -28,10 +43,20 @@ impl LoanOffer { ) -> Result { if annual_nominal_rate < Decimal::ZERO { Err(LoanOfferError::NegativeInterestRate(annual_nominal_rate)) + } else if annual_nominal_rate > MAX_ANNUAL_NOMINAL_RATE { + Err(LoanOfferError::MaxAnnualNominalRateExceeded( + annual_nominal_rate, + )) } else if loan_duration_in_months == 0 { Err(LoanOfferError::ZeroLoanDuration) + } else if loan_duration_in_months > MAX_LOAN_DURATION_IN_MONTHS { + Err(LoanOfferError::MaxLoanDurationExceeded( + loan_duration_in_months, + )) } else if borrowed_capital <= Euros::from_cents_as_i64(0) { Err(LoanOfferError::NonPositiveBorrowedCapital(borrowed_capital)) + } else if borrowed_capital > MAX_BORROWED_CAPITAL { + Err(LoanOfferError::MaxBorrowedCapitalExceeded(borrowed_capital)) } else { Ok(LoanOffer { borrowed_capital, @@ -77,6 +102,48 @@ mod tests { ); } + #[test] + fn creating_loan_offer_with_borrowed_capital_higher_than_max_borrowed_capital_returns_max_borrowed_capital_exceeded_error() + { + let euros = MAX_BORROWED_CAPITAL + Euros::from_cents_as_i64(1); + let ret = LoanOffer::new(euros, Decimal::new(45, 2), 25); + assert_eq!(ret, Err(LoanOfferError::MaxBorrowedCapitalExceeded(euros))); + } + + #[test] + fn creating_loan_offer_with_annual_nominal_rate_higher_than_max_annual_nominal_rate_returns_max_annual_nominal_rate_exceeded_error() + { + let annual_nominal_rate = MAX_ANNUAL_NOMINAL_RATE + Decimal::new(1, 2); + let ret = LoanOffer::new( + Euros::from_cents_as_i64(100_000_00), + annual_nominal_rate, + 25, + ); + assert_eq!( + ret, + Err(LoanOfferError::MaxAnnualNominalRateExceeded( + annual_nominal_rate + )) + ); + } + + #[test] + fn creating_loan_offer_with_loan_duration_higher_than_max_loan_duration_returns_max_loan_duration_exceeded_error() + { + let loan_duration_in_months = MAX_LOAN_DURATION_IN_MONTHS + 1; + let ret = LoanOffer::new( + Euros::from_cents_as_i64(100_000_00), + Decimal::new(314, 4), + loan_duration_in_months, + ); + assert_eq!( + ret, + Err(LoanOfferError::MaxLoanDurationExceeded( + loan_duration_in_months + )) + ); + } + #[test] fn creating_loan_offer_with_null_borrowed_capital_returns_non_positive_borrowed_capital_error() {