Add LoanOffer with validating constructor
LoanOffer::new is the only way in: fields are private and the constructor rejects a negative rate, a zero duration, and a non-positive capital. A zero rate is accepted on purpose — the PTZ (prêt à taux zéro) is a real French instrument. Add thiserror for the typed error, as required for immo-core. Rate and duration take no upper bound for now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
f3d428dcbe
commit
91f96e7b57
4 changed files with 136 additions and 0 deletions
21
Cargo.lock
generated
21
Cargo.lock
generated
|
|
@ -174,6 +174,7 @@ name = "immo-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rust_decimal",
|
"rust_decimal",
|
||||||
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -492,6 +493,26 @@ version = "1.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "2.0.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.12.0"
|
version = "1.12.0"
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rust_decimal = "1.42.1"
|
rust_decimal = "1.42.1"
|
||||||
|
thiserror = "2.0.19"
|
||||||
|
|
||||||
[lints.clippy]
|
[lints.clippy]
|
||||||
inconsistent_digit_grouping = "allow"
|
inconsistent_digit_grouping = "allow"
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
pub mod euros;
|
pub mod euros;
|
||||||
|
pub mod loan_offer;
|
||||||
|
|
|
||||||
113
crates/immo-core/src/loan_offer.rs
Normal file
113
crates/immo-core/src/loan_offer.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
use rust_decimal::Decimal;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::euros::Euros;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Error, Clone)]
|
||||||
|
pub enum LoanOfferError {
|
||||||
|
#[error("loan cannot have a negative interest rate: {0}%")]
|
||||||
|
NegativeInterestRate(Decimal),
|
||||||
|
#[error("loan duration cannot be zero")]
|
||||||
|
ZeroLoanDuration,
|
||||||
|
#[error("borrowed capital should be strictly positive: {0}")]
|
||||||
|
NonPositiveBorrowedCapital(Euros),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub struct LoanOffer {
|
||||||
|
borrowed_capital: Euros,
|
||||||
|
annual_rate_in_percent: Decimal,
|
||||||
|
loan_duration_in_months: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoanOffer {
|
||||||
|
pub fn new(
|
||||||
|
borrowed_capital: Euros,
|
||||||
|
annual_rate_in_percent: Decimal,
|
||||||
|
loan_duration_in_months: u64,
|
||||||
|
) -> Result<Self, LoanOfferError> {
|
||||||
|
if annual_rate_in_percent < Decimal::ZERO {
|
||||||
|
Err(LoanOfferError::NegativeInterestRate(annual_rate_in_percent))
|
||||||
|
} else if loan_duration_in_months == 0 {
|
||||||
|
Err(LoanOfferError::ZeroLoanDuration)
|
||||||
|
} else if borrowed_capital <= Euros::from_cents(0) {
|
||||||
|
Err(LoanOfferError::NonPositiveBorrowedCapital(borrowed_capital))
|
||||||
|
} else {
|
||||||
|
Ok(LoanOffer {
|
||||||
|
borrowed_capital,
|
||||||
|
annual_rate_in_percent,
|
||||||
|
loan_duration_in_months,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn creating_loan_offer_with_negative_interest_rate_returns_negative_interest_rate_error() {
|
||||||
|
let ret = LoanOffer::new(Euros::from_cents(123), Decimal::new(-45, 2), 5);
|
||||||
|
assert_eq!(
|
||||||
|
ret,
|
||||||
|
Err(LoanOfferError::NegativeInterestRate(Decimal::new(-45, 2)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn creating_loan_offer_with_null_loan_duration_returns_zero_loan_duration_error() {
|
||||||
|
let ret = LoanOffer::new(Euros::from_cents(123), Decimal::new(45, 2), 0);
|
||||||
|
assert_eq!(ret, Err(LoanOfferError::ZeroLoanDuration));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn creating_loan_offer_with_negative_borrowed_capital_returns_non_positive_borrowed_capital_error()
|
||||||
|
{
|
||||||
|
let ret = LoanOffer::new(Euros::from_cents(-1), Decimal::new(45, 2), 15);
|
||||||
|
assert_eq!(
|
||||||
|
ret,
|
||||||
|
Err(LoanOfferError::NonPositiveBorrowedCapital(
|
||||||
|
Euros::from_cents(-1)
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn creating_loan_offer_with_null_borrowed_capital_returns_non_positive_borrowed_capital_error()
|
||||||
|
{
|
||||||
|
let ret = LoanOffer::new(Euros::from_cents(0), Decimal::new(45, 2), 15);
|
||||||
|
assert_eq!(
|
||||||
|
ret,
|
||||||
|
Err(LoanOfferError::NonPositiveBorrowedCapital(
|
||||||
|
Euros::from_cents(0)
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn creating_loan_offer_with_correct_inputs_works() {
|
||||||
|
let ret = LoanOffer::new(Euros::from_cents(10), Decimal::new(45, 2), 15);
|
||||||
|
assert_eq!(
|
||||||
|
ret,
|
||||||
|
Ok(LoanOffer {
|
||||||
|
borrowed_capital: Euros::from_cents(10),
|
||||||
|
annual_rate_in_percent: Decimal::new(45, 2),
|
||||||
|
loan_duration_in_months: 15
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn creating_loan_offer_with_null_interest_rate_works() {
|
||||||
|
let ret = LoanOffer::new(Euros::from_cents(10), Decimal::ZERO, 15);
|
||||||
|
assert_eq!(
|
||||||
|
ret,
|
||||||
|
Ok(LoanOffer {
|
||||||
|
borrowed_capital: Euros::from_cents(10),
|
||||||
|
annual_rate_in_percent: Decimal::ZERO,
|
||||||
|
loan_duration_in_months: 15
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue