Store the annual nominal rate as a fraction, not a percentage

The field was called annual_rate_in_percent but nothing multiplied it by 100,
so a 3,41 % loan would have been stored as 3.41 and read as 341 %. Rename it
to annual_nominal_rate, which is what it holds, and add an explicit
annual_nominal_rate_in_percent() accessor for callers that want the display
form. Drop the trailing % from the error message, which was making the same
claim.

The nominal qualifier is there because TAEG will join this struct later and
the two must never be confused.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Marcel Enguehard 2026-08-23 15:17:15 +02:00
parent 4506231f02
commit 0cf311bdf4

View file

@ -5,7 +5,7 @@ use crate::euros::Euros;
#[derive(Debug, PartialEq, Eq, Error, Clone)] #[derive(Debug, PartialEq, Eq, Error, Clone)]
pub enum LoanOfferError { pub enum LoanOfferError {
#[error("loan cannot have a negative interest rate: {0}%")] #[error("loan cannot have a negative interest rate: {0}")]
NegativeInterestRate(Decimal), NegativeInterestRate(Decimal),
#[error("loan duration cannot be zero")] #[error("loan duration cannot be zero")]
ZeroLoanDuration, ZeroLoanDuration,
@ -16,18 +16,18 @@ pub enum LoanOfferError {
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct LoanOffer { pub struct LoanOffer {
borrowed_capital: Euros, borrowed_capital: Euros,
annual_rate_in_percent: Decimal, annual_nominal_rate: Decimal,
loan_duration_in_months: u64, loan_duration_in_months: u64,
} }
impl LoanOffer { impl LoanOffer {
pub fn new( pub fn new(
borrowed_capital: Euros, borrowed_capital: Euros,
annual_rate_in_percent: Decimal, annual_nominal_rate: Decimal,
loan_duration_in_months: u64, loan_duration_in_months: u64,
) -> Result<Self, LoanOfferError> { ) -> Result<Self, LoanOfferError> {
if annual_rate_in_percent < Decimal::ZERO { if annual_nominal_rate < Decimal::ZERO {
Err(LoanOfferError::NegativeInterestRate(annual_rate_in_percent)) Err(LoanOfferError::NegativeInterestRate(annual_nominal_rate))
} else if loan_duration_in_months == 0 { } else if loan_duration_in_months == 0 {
Err(LoanOfferError::ZeroLoanDuration) Err(LoanOfferError::ZeroLoanDuration)
} else if borrowed_capital <= Euros::from_cents_as_i64(0) { } else if borrowed_capital <= Euros::from_cents_as_i64(0) {
@ -35,11 +35,15 @@ impl LoanOffer {
} else { } else {
Ok(LoanOffer { Ok(LoanOffer {
borrowed_capital, borrowed_capital,
annual_rate_in_percent, annual_nominal_rate,
loan_duration_in_months, loan_duration_in_months,
}) })
} }
} }
pub fn annual_nominal_rate_in_percent(&self) -> Decimal {
self.annual_nominal_rate * Decimal::ONE_HUNDRED
}
} }
#[cfg(test)] #[cfg(test)]
@ -92,7 +96,7 @@ mod tests {
ret, ret,
Ok(LoanOffer { Ok(LoanOffer {
borrowed_capital: Euros::from_cents_as_i64(10), borrowed_capital: Euros::from_cents_as_i64(10),
annual_rate_in_percent: Decimal::new(45, 2), annual_nominal_rate: Decimal::new(45, 2),
loan_duration_in_months: 15 loan_duration_in_months: 15
}) })
); );
@ -105,9 +109,20 @@ mod tests {
ret, ret,
Ok(LoanOffer { Ok(LoanOffer {
borrowed_capital: Euros::from_cents_as_i64(10), borrowed_capital: Euros::from_cents_as_i64(10),
annual_rate_in_percent: Decimal::ZERO, annual_nominal_rate: Decimal::ZERO,
loan_duration_in_months: 15 loan_duration_in_months: 15
}) })
); );
} }
#[test]
fn conversion_to_annual_nominal_rate_in_percent_works() {
let offer = LoanOffer::new(
Euros::from_cents_as_i64(150_000_00),
Decimal::new(4, 4),
300,
)
.unwrap();
assert_eq!(offer.annual_nominal_rate_in_percent(), Decimal::new(4, 2));
}
} }