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"
|
||||
dependencies = [
|
||||
"rust_decimal",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -492,6 +493,26 @@ version = "1.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "tinyvec"
|
||||
version = "1.12.0"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ rust-version.workspace = true
|
|||
|
||||
[dependencies]
|
||||
rust_decimal = "1.42.1"
|
||||
thiserror = "2.0.19"
|
||||
|
||||
[lints.clippy]
|
||||
inconsistent_digit_grouping = "allow"
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
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