Compare commits
No commits in common. "8d00f6a936e93593b5034dce2fbd451b59bb9a27" and "85b0836d6e0eeaa5cbf7fa72a1bac05eb98f64b1" have entirely different histories.
8d00f6a936
...
85b0836d6e
7 changed files with 33 additions and 6130 deletions
|
|
@ -68,7 +68,7 @@ docs.rs for that exact version.
|
||||||
## Error handling and tests
|
## Error handling and tests
|
||||||
|
|
||||||
- `thiserror` for typed errors in `immo-core`; `anyhow` only at the `immo-web` boundary.
|
- `thiserror` for typed errors in `immo-core`; `anyhow` only at the `immo-web` boundary.
|
||||||
- No `unwrap()` or `panic!()` outside tests and `main`. `expect()` can only be used for cases where the Result will provably be `Ok()`
|
- No `unwrap()`, `expect()`, or `panic!()` outside tests and `main`.
|
||||||
- Every `core` function producing a number the user sees has a known-answer test, with the worked
|
- Every `core` function producing a number the user sees has a known-answer test, with the worked
|
||||||
example stated in the test name or a comment above it.
|
example stated in the test name or a comment above it.
|
||||||
- Test the amortisation schedule against hand-checked values, never against the implementation's
|
- Test the amortisation schedule against hand-checked values, never against the implementation's
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rust_decimal = { version = "1.42.1", features = ["maths"] }
|
rust_decimal = "1.42.1"
|
||||||
thiserror = "2.0.19"
|
thiserror = "2.0.19"
|
||||||
|
|
||||||
[lints.clippy]
|
[lints.clippy]
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,6 @@
|
||||||
use std::{fmt, ops::Add};
|
use std::fmt;
|
||||||
|
|
||||||
use rust_decimal::{Decimal, prelude::ToPrimitive};
|
use rust_decimal::Decimal;
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Error, Clone)]
|
|
||||||
pub enum EurosError {
|
|
||||||
#[error("Can only create from decimal with scale = 2: {0}")]
|
|
||||||
WrongDecimalScale(Decimal),
|
|
||||||
#[error("Decimal cannot be represented as i64: {0}")]
|
|
||||||
DecimalIsTooBig(Decimal),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialOrd, PartialEq, Ord, Eq, Clone, Copy, Debug)]
|
#[derive(PartialOrd, PartialEq, Ord, Eq, Clone, Copy, Debug)]
|
||||||
pub struct Euros {
|
pub struct Euros {
|
||||||
|
|
@ -17,22 +8,10 @@ pub struct Euros {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Euros {
|
impl Euros {
|
||||||
pub const fn from_cents_as_i64(value_as_cents: i64) -> Self {
|
pub const fn from_cents(value_as_cents: i64) -> Self {
|
||||||
Euros { value_as_cents }
|
Euros { value_as_cents }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_euros_as_decimal(value_as_euros: Decimal) -> Result<Self, EurosError> {
|
|
||||||
// Euros should explicitely have scale 2 to validate cents precision
|
|
||||||
if value_as_euros.scale() != 2 {
|
|
||||||
Err(EurosError::WrongDecimalScale(value_as_euros))
|
|
||||||
} else {
|
|
||||||
(value_as_euros * Decimal::ONE_HUNDRED)
|
|
||||||
.to_i64()
|
|
||||||
.map(Euros::from_cents_as_i64)
|
|
||||||
.ok_or(EurosError::DecimalIsTooBig(value_as_euros))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn as_cents(self) -> i64 {
|
pub const fn as_cents(self) -> i64 {
|
||||||
self.value_as_cents
|
self.value_as_cents
|
||||||
}
|
}
|
||||||
|
|
@ -48,29 +27,19 @@ impl fmt::Display for Euros {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Add for Euros {
|
|
||||||
type Output = Self;
|
|
||||||
|
|
||||||
fn add(self, rhs: Self) -> Self::Output {
|
|
||||||
Euros {
|
|
||||||
value_as_cents: self.value_as_cents + rhs.value_as_cents,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn from_cents_to_euros_to_cents_stays_equals() {
|
fn from_cents_to_euros_to_cents_stays_equals() {
|
||||||
let euros = Euros::from_cents_as_i64(123_456_00);
|
let euros = Euros::from_cents(123_456_00);
|
||||||
assert_eq!(euros.as_cents(), 123_456_00);
|
assert_eq!(euros.as_cents(), 123_456_00);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn from_positive_cents_to_decimal_has_scale_2_and_correct_mantissa() {
|
fn from_positive_cents_to_decimal_has_scale_2_and_correct_mantissa() {
|
||||||
let euros = Euros::from_cents_as_i64(123_456_78);
|
let euros = Euros::from_cents(123_456_78);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
euros.as_decimal().scale(),
|
euros.as_decimal().scale(),
|
||||||
2,
|
2,
|
||||||
|
|
@ -85,7 +54,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn from_negative_cents_to_decimal_has_correct_scale_and_mantissa() {
|
fn from_negative_cents_to_decimal_has_correct_scale_and_mantissa() {
|
||||||
let euros = Euros::from_cents_as_i64(-123_456_78);
|
let euros = Euros::from_cents(-123_456_78);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
euros.as_decimal().scale(),
|
euros.as_decimal().scale(),
|
||||||
2,
|
2,
|
||||||
|
|
@ -100,7 +69,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn from_max_i64_cents_to_decimal_has_correct_scale_and_mantissa() {
|
fn from_max_i64_cents_to_decimal_has_correct_scale_and_mantissa() {
|
||||||
let euros = Euros::from_cents_as_i64(i64::MAX);
|
let euros = Euros::from_cents(i64::MAX);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
euros.as_decimal().scale(),
|
euros.as_decimal().scale(),
|
||||||
2,
|
2,
|
||||||
|
|
@ -115,59 +84,26 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn from_0_cents_to_decimal_stays_zero() {
|
fn from_0_cents_to_decimal_stays_zero() {
|
||||||
let euros = Euros::from_cents_as_i64(0);
|
let euros = Euros::from_cents(0);
|
||||||
assert_eq!(euros.as_decimal().scale(), 2);
|
assert_eq!(euros.as_decimal().scale(), 2);
|
||||||
assert_eq!(euros.as_decimal(), Decimal::ZERO);
|
assert_eq!(euros.as_decimal(), Decimal::ZERO);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn standard_euros_prints_correctly() {
|
fn standard_euros_prints_correctly() {
|
||||||
let euros = Euros::from_cents_as_i64(123_45);
|
let euros = Euros::from_cents(123_45);
|
||||||
assert_eq!(format!("{}", euros), "123.45 €");
|
assert_eq!(format!("{}", euros), "123.45 €");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn euros_without_cents_prints_with_trailing_zeros() {
|
fn euros_without_cents_prints_with_trailing_zeros() {
|
||||||
let euros = Euros::from_cents_as_i64(123_00);
|
let euros = Euros::from_cents(123_00);
|
||||||
assert_eq!(format!("{}", euros), "123.00 €");
|
assert_eq!(format!("{}", euros), "123.00 €");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn euros_with_negative_amount_prints_with_minus() {
|
fn euros_with_negative_amount_print_with_minus() {
|
||||||
let euros = Euros::from_cents_as_i64(-123_00);
|
let euros = Euros::from_cents(-123_00);
|
||||||
assert_eq!(format!("{}", euros), "-123.00 €");
|
assert_eq!(format!("{}", euros), "-123.00 €");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_euros_as_decimal_fails_if_scale_is_larger_than_2() {
|
|
||||||
let dec = Decimal::new(3141, 3);
|
|
||||||
let ret = Euros::from_euros_as_decimal(dec);
|
|
||||||
assert!(ret.is_err());
|
|
||||||
assert_eq!(ret.unwrap_err(), EurosError::WrongDecimalScale(dec));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_euros_as_decimal_fails_if_scale_is_lower_than_2() {
|
|
||||||
let dec = Decimal::new(3141, 1);
|
|
||||||
let ret = Euros::from_euros_as_decimal(dec);
|
|
||||||
assert!(ret.is_err());
|
|
||||||
assert_eq!(ret.unwrap_err(), EurosError::WrongDecimalScale(dec));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_euros_as_decimal_yields_correct_result_if_scale_is_2() {
|
|
||||||
let dec = Decimal::new(3141, 2);
|
|
||||||
let ret = Euros::from_euros_as_decimal(dec);
|
|
||||||
assert!(ret.is_ok());
|
|
||||||
assert_eq!(ret.unwrap().as_cents(), 3141);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn from_euros_as_decimal_yields_error_with_decimal_too_big() {
|
|
||||||
let mut dec = Decimal::from(u64::MAX);
|
|
||||||
dec.rescale(2);
|
|
||||||
let ret = Euros::from_euros_as_decimal(dec);
|
|
||||||
assert!(ret.is_err());
|
|
||||||
assert_eq!(ret.unwrap_err(), EurosError::DecimalIsTooBig(dec));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1 +0,0 @@
|
||||||
pub mod loan_offer_fixture;
|
|
||||||
|
|
@ -1,5 +1,2 @@
|
||||||
pub mod euros;
|
pub mod euros;
|
||||||
pub mod loan_offer;
|
pub mod loan_offer;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub mod fixtures;
|
|
||||||
|
|
|
||||||
|
|
@ -1,104 +1,54 @@
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
use rust_decimal::MathematicalOps;
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::euros::Euros;
|
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,
|
||||||
#[error("borrowed capital should be strictly positive: {0}")]
|
#[error("borrowed capital should be strictly positive: {0}")]
|
||||||
NonPositiveBorrowedCapital(Euros),
|
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)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct LoanOffer {
|
pub struct LoanOffer {
|
||||||
borrowed_capital: Euros,
|
borrowed_capital: Euros,
|
||||||
annual_nominal_rate: Decimal,
|
annual_rate_in_percent: 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_nominal_rate: Decimal,
|
annual_rate_in_percent: Decimal,
|
||||||
loan_duration_in_months: u64,
|
loan_duration_in_months: u64,
|
||||||
) -> Result<Self, LoanOfferError> {
|
) -> Result<Self, LoanOfferError> {
|
||||||
if annual_nominal_rate < Decimal::ZERO {
|
if annual_rate_in_percent < Decimal::ZERO {
|
||||||
Err(LoanOfferError::NegativeInterestRate(annual_nominal_rate))
|
Err(LoanOfferError::NegativeInterestRate(annual_rate_in_percent))
|
||||||
} else if annual_nominal_rate > MAX_ANNUAL_NOMINAL_RATE {
|
|
||||||
Err(LoanOfferError::MaxAnnualNominalRateExceeded(
|
|
||||||
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 loan_duration_in_months > MAX_LOAN_DURATION_IN_MONTHS {
|
} else if borrowed_capital <= Euros::from_cents(0) {
|
||||||
Err(LoanOfferError::MaxLoanDurationExceeded(
|
|
||||||
loan_duration_in_months,
|
|
||||||
))
|
|
||||||
} else if borrowed_capital <= Euros::from_cents_as_i64(0) {
|
|
||||||
Err(LoanOfferError::NonPositiveBorrowedCapital(borrowed_capital))
|
Err(LoanOfferError::NonPositiveBorrowedCapital(borrowed_capital))
|
||||||
} else if borrowed_capital > MAX_BORROWED_CAPITAL {
|
|
||||||
Err(LoanOfferError::MaxBorrowedCapitalExceeded(borrowed_capital))
|
|
||||||
} else {
|
} else {
|
||||||
Ok(LoanOffer {
|
Ok(LoanOffer {
|
||||||
borrowed_capital,
|
borrowed_capital,
|
||||||
annual_nominal_rate,
|
annual_rate_in_percent,
|
||||||
loan_duration_in_months,
|
loan_duration_in_months,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn annual_nominal_rate_in_percent(&self) -> Decimal {
|
|
||||||
self.annual_nominal_rate * Decimal::ONE_HUNDRED
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn proportional_monthly_interest_rate(&self) -> Decimal {
|
|
||||||
self.annual_nominal_rate / Decimal::from(12)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn monthly_repayment(&self) -> Euros {
|
|
||||||
let monthly_rate = self.proportional_monthly_interest_rate();
|
|
||||||
let mut repayment_in_euros = if monthly_rate == Decimal::ZERO {
|
|
||||||
(self.borrowed_capital.as_decimal() / Decimal::from(self.loan_duration_in_months))
|
|
||||||
.round_dp(2)
|
|
||||||
} else {
|
|
||||||
let repayment_numerator = self.borrowed_capital.as_decimal() * monthly_rate;
|
|
||||||
let repayment_denominator = Decimal::ONE
|
|
||||||
- (Decimal::ONE / (Decimal::ONE + monthly_rate).powu(self.loan_duration_in_months));
|
|
||||||
(repayment_numerator / repayment_denominator).round_dp(2)
|
|
||||||
};
|
|
||||||
repayment_in_euros.rescale(2);
|
|
||||||
Euros::from_euros_as_decimal(repayment_in_euros).expect("Borrowed capital and annual rate are constrained on construction, repayment_in_euros cannot exceed 10b, and scale=2 is guaranteed through rescale")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::fixtures::loan_offer_fixture::*;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn creating_loan_offer_with_negative_interest_rate_returns_negative_interest_rate_error() {
|
fn creating_loan_offer_with_negative_interest_rate_returns_negative_interest_rate_error() {
|
||||||
let ret = LoanOffer::new(Euros::from_cents_as_i64(123), Decimal::new(-45, 2), 5);
|
let ret = LoanOffer::new(Euros::from_cents(123), Decimal::new(-45, 2), 5);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ret,
|
ret,
|
||||||
Err(LoanOfferError::NegativeInterestRate(Decimal::new(-45, 2)))
|
Err(LoanOfferError::NegativeInterestRate(Decimal::new(-45, 2)))
|
||||||
|
|
@ -107,60 +57,18 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn creating_loan_offer_with_null_loan_duration_returns_zero_loan_duration_error() {
|
fn creating_loan_offer_with_null_loan_duration_returns_zero_loan_duration_error() {
|
||||||
let ret = LoanOffer::new(Euros::from_cents_as_i64(123), Decimal::new(45, 2), 0);
|
let ret = LoanOffer::new(Euros::from_cents(123), Decimal::new(45, 2), 0);
|
||||||
assert_eq!(ret, Err(LoanOfferError::ZeroLoanDuration));
|
assert_eq!(ret, Err(LoanOfferError::ZeroLoanDuration));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn creating_loan_offer_with_negative_borrowed_capital_returns_non_positive_borrowed_capital_error()
|
fn creating_loan_offer_with_negative_borrowed_capital_returns_non_positive_borrowed_capital_error()
|
||||||
{
|
{
|
||||||
let ret = LoanOffer::new(Euros::from_cents_as_i64(-1), Decimal::new(45, 2), 15);
|
let ret = LoanOffer::new(Euros::from_cents(-1), Decimal::new(45, 2), 15);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ret,
|
ret,
|
||||||
Err(LoanOfferError::NonPositiveBorrowedCapital(
|
Err(LoanOfferError::NonPositiveBorrowedCapital(
|
||||||
Euros::from_cents_as_i64(-1)
|
Euros::from_cents(-1)
|
||||||
))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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
|
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -168,87 +76,38 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn creating_loan_offer_with_null_borrowed_capital_returns_non_positive_borrowed_capital_error()
|
fn creating_loan_offer_with_null_borrowed_capital_returns_non_positive_borrowed_capital_error()
|
||||||
{
|
{
|
||||||
let ret = LoanOffer::new(Euros::from_cents_as_i64(0), Decimal::new(45, 2), 15);
|
let ret = LoanOffer::new(Euros::from_cents(0), Decimal::new(45, 2), 15);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ret,
|
ret,
|
||||||
Err(LoanOfferError::NonPositiveBorrowedCapital(
|
Err(LoanOfferError::NonPositiveBorrowedCapital(
|
||||||
Euros::from_cents_as_i64(0)
|
Euros::from_cents(0)
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn creating_loan_offer_with_correct_inputs_works() {
|
fn creating_loan_offer_with_correct_inputs_works() {
|
||||||
let ret = LoanOffer::new(Euros::from_cents_as_i64(10), Decimal::new(45, 2), 15);
|
let ret = LoanOffer::new(Euros::from_cents(10), Decimal::new(45, 2), 15);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ret,
|
ret,
|
||||||
Ok(LoanOffer {
|
Ok(LoanOffer {
|
||||||
borrowed_capital: Euros::from_cents_as_i64(10),
|
borrowed_capital: Euros::from_cents(10),
|
||||||
annual_nominal_rate: Decimal::new(45, 2),
|
annual_rate_in_percent: Decimal::new(45, 2),
|
||||||
loan_duration_in_months: 15
|
loan_duration_in_months: 15
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn creating_loan_offer_with_limit_inputs_works() {
|
|
||||||
let ret = LoanOffer::new(
|
|
||||||
MAX_BORROWED_CAPITAL,
|
|
||||||
MAX_ANNUAL_NOMINAL_RATE,
|
|
||||||
MAX_LOAN_DURATION_IN_MONTHS,
|
|
||||||
);
|
|
||||||
assert!(ret.is_ok());
|
|
||||||
// Should not panic - checks that expect assertion is valid
|
|
||||||
ret.unwrap().monthly_repayment();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn creating_loan_offer_with_null_interest_rate_works() {
|
fn creating_loan_offer_with_null_interest_rate_works() {
|
||||||
let ret = LoanOffer::new(Euros::from_cents_as_i64(10), Decimal::ZERO, 15);
|
let ret = LoanOffer::new(Euros::from_cents(10), Decimal::ZERO, 15);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ret,
|
ret,
|
||||||
Ok(LoanOffer {
|
Ok(LoanOffer {
|
||||||
borrowed_capital: Euros::from_cents_as_i64(10),
|
borrowed_capital: Euros::from_cents(10),
|
||||||
annual_nominal_rate: Decimal::ZERO,
|
annual_rate_in_percent: 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));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn monthly_repayment_works_for_standard_loan_offer() {
|
|
||||||
let fixture = offre_a();
|
|
||||||
let offer = LoanOffer::new(
|
|
||||||
fixture.borrowed_capital,
|
|
||||||
fixture.annual_nominal_rate,
|
|
||||||
fixture.total_duration_in_months,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(offer.monthly_repayment(), fixture.monthly_repayment);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn monthly_repayment_works_for_zero_annual_nominal_rate() {
|
|
||||||
let fixture = offre_c_ptz();
|
|
||||||
let offer = LoanOffer::new(
|
|
||||||
fixture.borrowed_capital,
|
|
||||||
fixture.annual_nominal_rate,
|
|
||||||
fixture.total_duration_in_months,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(offer.monthly_repayment(), fixture.monthly_repayment);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue