use chrono::{DateTime, Utc};
use hourglass_rs::SafeTimeProvider;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use uuid::Uuid;
use crate::config::{FacilityConfig, FacilityType};
use crate::types::RevolvingType;
use crate::decimal::{Money, Rate};
use crate::errors::{FacilityError, Result};
use crate::events::Event;
use crate::facility::Facility;
use crate::types::FacilityStatus;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UtilizationState {
Unused, Low, Moderate, High, Maxed, Overlimit, }
pub struct RevolvingFacility {
facility: Facility,
time: Option<*const SafeTimeProvider>,
credit_limit: Money,
available_credit: Money,
draw_period_ends: Option<DateTime<Utc>>,
repayment_period_ends: Option<DateTime<Utc>>,
is_in_draw_period: bool,
minimum_payment_percentage: Decimal,
}
impl RevolvingFacility {
pub fn new(facility: Facility) -> Result<Self> {
match &facility.config.facility_type {
FacilityType::Revolving(_) => {}
_ => {
return Err(FacilityError::InvalidConfiguration {
message: "Not a revolving facility configuration".to_string(),
});
}
}
let credit_limit = facility.config.limits.credit_limit
.unwrap_or(facility.config.financial_terms.commitment_amount);
let minimum_payment_percentage = facility.config.payment_config
.minimum_payment_percentage
.unwrap_or(dec!(0.02));
Ok(Self {
facility,
time: None,
credit_limit,
available_credit: credit_limit,
draw_period_ends: None,
repayment_period_ends: None,
is_in_draw_period: true,
minimum_payment_percentage,
})
}
pub fn builder() -> RevolvingFacilityBuilder {
RevolvingFacilityBuilder::new()
}
pub fn set_time(&mut self, time: &SafeTimeProvider) {
self.time = Some(time as *const SafeTimeProvider);
}
pub fn draw(&mut self, amount: Money) -> Result<Money> {
let time_ptr = self.time
.ok_or(FacilityError::InvalidConfiguration {
message: "Time provider not set. Call set_time() first".to_string(),
})?;
let time = unsafe { time_ptr.as_ref() }
.ok_or(FacilityError::InvalidConfiguration {
message: "Invalid time provider reference".to_string(),
})?;
self.draw_with_time(amount, time)
}
pub fn process_payment(&mut self, amount: Money) -> Result<()> {
let time_ptr = self.time
.ok_or(FacilityError::InvalidConfiguration {
message: "Time provider not set. Call set_time() first".to_string(),
})?;
let time = unsafe { time_ptr.as_ref() }
.ok_or(FacilityError::InvalidConfiguration {
message: "Invalid time provider reference".to_string(),
})?;
self.process_payment_with_time(amount, time)
}
pub fn activate(&mut self) -> Result<()> {
let time_ptr = self.time
.ok_or(FacilityError::InvalidConfiguration {
message: "Time provider not set. Call set_time() first".to_string(),
})?;
let time = unsafe { time_ptr.as_ref() }
.ok_or(FacilityError::InvalidConfiguration {
message: "Invalid time provider reference".to_string(),
})?;
self.activate_with_time(time)
}
pub fn accrue_interest(&mut self) -> Result<Vec<crate::interest::DailyAccrual>> {
let time_ptr = self.time
.ok_or(FacilityError::InvalidConfiguration {
message: "Time provider not set. Call set_time() first".to_string(),
})?;
let time = unsafe { time_ptr.as_ref() }
.ok_or(FacilityError::InvalidConfiguration {
message: "Invalid time provider reference".to_string(),
})?;
self.facility.accrue_interest(time)
}
pub fn update_daily_status(&mut self) -> Result<()> {
let time_ptr = self.time
.ok_or(FacilityError::InvalidConfiguration {
message: "Time provider not set. Call set_time() first".to_string(),
})?;
let time = unsafe { time_ptr.as_ref() }
.ok_or(FacilityError::InvalidConfiguration {
message: "Invalid time provider reference".to_string(),
})?;
self.facility.update_daily_status(time)
}
pub fn activate_with_time(&mut self, time_provider: &SafeTimeProvider) -> Result<()> {
self.facility.state.activation_date = Some(time_provider.now());
self.facility.state.update_status(FacilityStatus::Active, time_provider.now());
if let FacilityType::Revolving(RevolvingType::HELOC) = &self.facility.config.facility_type {
if let Some(term_months) = self.facility.config.financial_terms.term_months {
let draw_months = (term_months as f64 * 0.6) as i64;
self.draw_period_ends = Some(
time_provider.now() + chrono::Duration::days(draw_months * 30)
);
self.repayment_period_ends = Some(
time_provider.now() + chrono::Duration::days(term_months as i64 * 30)
);
}
}
if let crate::state::FacilitySpecificState::Revolving {
credit_limit,
available_credit,
..
} = &mut self.facility.state.facility_specific {
*credit_limit = self.credit_limit;
*available_credit = self.available_credit;
}
self.facility.events.emit(Event::FacilityActivated {
facility_id: self.facility.id,
first_disbursement: Money::ZERO,
timestamp: time_provider.now(),
});
Ok(())
}
pub fn draw_with_time(
&mut self,
amount: Money,
time_provider: &SafeTimeProvider,
) -> Result<Money> {
if !self.is_in_draw_period {
return Err(FacilityError::DrawPeriodEnded);
}
if amount <= Money::ZERO {
return Err(FacilityError::InvalidDrawAmount { amount });
}
if let Some(min) = self.facility.config.limits.minimum_drawdown {
if amount < min {
return Err(FacilityError::BelowMinimumDrawdown {
minimum: min,
requested: amount
});
}
}
let is_overlimit = amount > self.available_credit;
if is_overlimit {
let total_after_draw = self.facility.state.outstanding_principal + amount;
let overlimit_amount = total_after_draw - self.credit_limit;
if overlimit_amount > self.credit_limit * dec!(0.1) {
return Err(FacilityError::ExceedsCreditLimit {
available: self.available_credit,
requested: amount,
});
}
if let Some(fee) = self.facility.config.fee_config.overlimit_fee {
self.facility.state.accrued_fees += fee;
self.facility.state.total_fees_charged += fee;
self.facility.events.emit(Event::OverlimitOccurred {
facility_id: self.facility.id,
amount_over: overlimit_amount,
fees_applied: fee,
timestamp: time_provider.now(),
});
}
}
self.facility.state.record_disbursement(amount);
self.available_credit = (self.credit_limit - self.facility.state.outstanding_principal)
.max(Money::ZERO);
if let crate::state::FacilitySpecificState::Revolving {
available_credit,
..
} = &mut self.facility.state.facility_specific {
*available_credit = self.available_credit;
}
self.facility.events.emit(Event::FundsDrawn {
facility_id: self.facility.id,
amount,
new_outstanding: self.facility.state.outstanding_principal,
available_credit: self.available_credit,
timestamp: time_provider.now(),
});
Ok(amount)
}
pub fn process_payment_with_time(
&mut self,
amount: Money,
time_provider: &SafeTimeProvider,
) -> Result<()> {
let principal_before = self.facility.state.outstanding_principal;
self.facility.process_payment(amount, time_provider)?;
let principal_after = self.facility.state.outstanding_principal;
let principal_paid = principal_before - principal_after;
self.available_credit = (self.available_credit + principal_paid)
.min(self.credit_limit);
if let crate::state::FacilitySpecificState::Revolving {
available_credit,
..
} = &mut self.facility.state.facility_specific {
*available_credit = self.available_credit;
}
Ok(())
}
pub fn calculate_minimum_payment(&self) -> Money {
let outstanding = self.facility.state.outstanding_principal;
let interest = self.facility.state.accrued_interest;
let fees = self.facility.state.accrued_fees;
let percentage_based = outstanding * self.minimum_payment_percentage + interest + fees;
let absolute_minimum = self.facility.config.payment_config.minimum_payment
.unwrap_or(Money::from_major(25));
if outstanding > self.credit_limit {
let overlimit = outstanding - self.credit_limit;
percentage_based.max(absolute_minimum).max(overlimit + interest + fees)
} else {
percentage_based.max(absolute_minimum)
}
}
pub fn utilization_rate(&self) -> Rate {
if self.credit_limit.is_zero() {
return Rate::from_percentage(0);
}
Rate::from_decimal(
self.facility.state.outstanding_principal.as_decimal() /
self.credit_limit.as_decimal()
)
}
pub fn utilization_state(&self) -> UtilizationState {
let rate = self.utilization_rate();
if rate == Rate::from_percentage(0) {
UtilizationState::Unused
} else if rate < Rate::from_percentage(30) {
UtilizationState::Low
} else if rate < Rate::from_percentage(70) {
UtilizationState::Moderate
} else if rate < Rate::from_percentage(90) {
UtilizationState::High
} else if rate <= Rate::from_percentage(100) {
UtilizationState::Maxed
} else {
UtilizationState::Overlimit
}
}
pub fn is_overlimit(&self) -> bool {
self.facility.state.outstanding_principal > self.credit_limit
}
pub fn charge_commitment_fee(&mut self) -> Result<()> {
let time_ptr = self.time
.ok_or(FacilityError::InvalidConfiguration {
message: "Time provider not set. Call set_time() first".to_string(),
})?;
let time_provider = unsafe { time_ptr.as_ref() }
.ok_or(FacilityError::InvalidConfiguration {
message: "Invalid time provider reference".to_string(),
})?;
if let Some(commitment_rate) = self.facility.config.fee_config.commitment_fee_rate {
let undrawn = self.available_credit;
if undrawn > Money::ZERO {
let monthly_rate = commitment_rate.as_decimal() / dec!(12);
let fee = Money::from_decimal(undrawn.as_decimal() * monthly_rate);
self.facility.state.accrued_fees += fee;
self.facility.state.total_fees_charged += fee;
self.facility.events.emit(Event::CommitmentFeeCharged {
facility_id: self.facility.id,
undrawn_amount: undrawn,
fee,
timestamp: time_provider.now(),
});
}
}
Ok(())
}
pub fn end_draw_period(&mut self) -> Result<()> {
let time_ptr = self.time
.ok_or(FacilityError::InvalidConfiguration {
message: "Time provider not set. Call set_time() first".to_string(),
})?;
let time_provider = unsafe { time_ptr.as_ref() }
.ok_or(FacilityError::InvalidConfiguration {
message: "Invalid time provider reference".to_string(),
})?;
if !self.is_in_draw_period {
return Ok(()); }
self.is_in_draw_period = false;
self.draw_period_ends = Some(time_provider.now());
if let FacilityType::Revolving(RevolvingType::HELOC) = &self.facility.config.facility_type {
self.minimum_payment_percentage = dec!(0.015); }
Ok(())
}
pub fn change_credit_limit(&mut self, new_limit: Money) -> Result<()> {
let time_ptr = self.time
.ok_or(FacilityError::InvalidConfiguration {
message: "Time provider not set. Call set_time() first".to_string(),
})?;
let time_provider = unsafe { time_ptr.as_ref() }
.ok_or(FacilityError::InvalidConfiguration {
message: "Invalid time provider reference".to_string(),
})?;
let old_limit = self.credit_limit;
self.credit_limit = new_limit;
self.available_credit = (new_limit - self.facility.state.outstanding_principal)
.max(Money::ZERO);
if let crate::state::FacilitySpecificState::Revolving {
credit_limit,
available_credit,
..
} = &mut self.facility.state.facility_specific {
*credit_limit = self.credit_limit;
*available_credit = self.available_credit;
}
self.facility.events.emit(Event::CreditLimitChanged {
facility_id: self.facility.id,
old_limit,
new_limit,
timestamp: time_provider.now(),
});
if self.is_overlimit() {
self.facility.events.emit(Event::OverlimitOccurred {
facility_id: self.facility.id,
amount_over: self.facility.state.outstanding_principal - new_limit,
fees_applied: Money::ZERO,
timestamp: time_provider.now(),
});
}
Ok(())
}
pub fn facility(&self) -> &Facility {
&self.facility
}
pub fn facility_mut(&mut self) -> &mut Facility {
&mut self.facility
}
pub fn credit_limit(&self) -> Money {
self.credit_limit
}
pub fn available_credit(&self) -> Money {
self.available_credit
}
pub fn to_json_pretty(&self) -> String {
use super::serialization::{FacilityView, RevolvingView};
let view = RevolvingView {
facility: FacilityView::from_facility(&self.facility),
credit_limit: self.credit_limit,
available_credit: self.available_credit,
utilization_rate: self.utilization_rate(),
is_in_draw_period: self.is_in_draw_period,
};
serde_json::to_string_pretty(&view).unwrap_or_else(|e| format!("JSON error: {}", e))
}
pub fn json(&self) -> String {
self.to_json_pretty()
}
pub fn approve(&mut self) -> Result<()> {
self.activate()
}
pub fn deny(&mut self) -> Result<()> {
let time_ptr = self.time
.ok_or(FacilityError::InvalidConfiguration {
message: "Time provider not set. Call set_time() first".to_string(),
})?;
let time = unsafe { time_ptr.as_ref() }
.ok_or(FacilityError::InvalidConfiguration {
message: "Invalid time provider reference".to_string(),
})?;
let old_status = self.facility.state.status;
self.facility.state.update_status(FacilityStatus::Settled, time.now());
self.facility.events.emit(Event::StatusChanged {
facility_id: self.facility.id,
old_status,
new_status: FacilityStatus::Settled,
reason: "Denied by lender - no funds disbursed".to_string(),
timestamp: time.now(),
});
Ok(())
}
pub fn make_payment(&mut self, amount: Money) -> Result<()> {
self.process_payment(amount)
}
pub fn disburse(&mut self, amount: Money) -> Result<Money> {
self.draw(amount)
}
}
pub struct RevolvingFacilityBuilder {
facility_type: Option<RevolvingType>,
credit_limit: Option<Money>,
rate: Option<Rate>,
minimum_percentage: Option<Decimal>,
commitment_fee_rate: Option<Rate>,
property_value: Option<Money>,
draw_period: Option<u32>,
repayment_period: Option<u32>,
account_number: Option<String>,
customer_id: Option<String>,
time_provider: Option<*const SafeTimeProvider>,
}
impl RevolvingFacilityBuilder {
pub fn new() -> Self {
Self {
facility_type: None,
credit_limit: None,
rate: None,
minimum_percentage: None,
commitment_fee_rate: None,
property_value: None,
draw_period: None,
repayment_period: None,
account_number: None,
customer_id: None,
time_provider: None,
}
}
pub fn set_time(mut self, time: &SafeTimeProvider) -> Self {
self.time_provider = Some(time as *const SafeTimeProvider);
self
}
pub fn facility_type(mut self, facility_type: RevolvingType) -> Self {
self.facility_type = Some(facility_type);
self
}
pub fn credit_limit(mut self, limit: Money) -> Self {
self.credit_limit = Some(limit);
self
}
pub fn rate(mut self, rate: Rate) -> Self {
self.rate = Some(rate);
self
}
pub fn minimum_percentage(mut self, percentage: Decimal) -> Self {
self.minimum_percentage = Some(percentage);
self
}
pub fn commitment_fee_rate(mut self, rate: Rate) -> Self {
self.commitment_fee_rate = Some(rate);
self
}
pub fn property_value(mut self, value: Money) -> Self {
self.property_value = Some(value);
self
}
pub fn draw_period_months(mut self, months: u32) -> Self {
self.draw_period = Some(months);
self
}
pub fn repayment_period_months(mut self, months: u32) -> Self {
self.repayment_period = Some(months);
self
}
pub fn account_number(mut self, account: String) -> Self {
self.account_number = Some(account);
self
}
pub fn customer_id(mut self, customer: String) -> Self {
self.customer_id = Some(customer);
self
}
pub fn build(self) -> Result<RevolvingFacility> {
if let Some(time_ptr) = self.time_provider {
let time = unsafe { time_ptr.as_ref() }
.ok_or(FacilityError::InvalidConfiguration {
message: "Invalid time provider reference".to_string(),
})?;
self.build_with_time(time)
} else {
let time = SafeTimeProvider::new(hourglass_rs::TimeSource::System);
self.build_with_time(&time)
}
}
pub fn build_now(self) -> Result<RevolvingFacility> {
let time = SafeTimeProvider::new(hourglass_rs::TimeSource::System);
self.build_with_time(&time)
}
pub fn build_with_time(self, time_provider: &SafeTimeProvider) -> Result<RevolvingFacility> {
let facility_type = self.facility_type.unwrap_or_else(|| {
if self.property_value.is_some() {
RevolvingType::HELOC } else if self.minimum_percentage == Some(dec!(0.02)) || self.minimum_percentage == Some(dec!(0.025)) {
RevolvingType::CreditCard } else {
RevolvingType::LineOfCredit }
});
let credit_limit = self.credit_limit.ok_or(FacilityError::InvalidConfiguration {
message: "Credit limit required".to_string(),
})?;
let rate = self.rate.ok_or(FacilityError::InvalidConfiguration {
message: "Rate required".to_string(),
})?;
let config = match facility_type {
RevolvingType::CreditCard => {
FacilityConfig::credit_card(
credit_limit,
rate,
self.minimum_percentage.unwrap_or(dec!(0.02)),
)
}
RevolvingType::LineOfCredit => {
FacilityConfig::line_of_credit(
credit_limit,
rate,
self.commitment_fee_rate.unwrap_or(Rate::from_decimal(dec!(0.005))),
)
}
RevolvingType::HELOC => {
let property_value = self.property_value.ok_or(FacilityError::InvalidConfiguration {
message: "Property value required for HELOC".to_string(),
})?;
FacilityConfig::heloc(
credit_limit,
rate,
property_value,
self.draw_period.unwrap_or(120), self.repayment_period.unwrap_or(240), )
}
};
let account_number = self.account_number.unwrap_or_else(|| {
format!("REV-{}", Uuid::new_v4().to_string()[..8].to_uppercase())
});
let customer_id = self.customer_id.unwrap_or_else(|| {
format!("CUST-{}", Uuid::new_v4().to_string()[..8].to_uppercase())
});
let facility = Facility::originate(config, account_number, customer_id, time_provider)?;
let mut revolving = RevolvingFacility::new(facility)?;
if let Some(time_ptr) = self.time_provider {
revolving.time = Some(time_ptr);
}
Ok(revolving)
}
}
#[cfg(test)]
mod tests {
use super::*;
use hourglass_rs::TimeSource;
use chrono::TimeZone;
#[test]
fn test_credit_card_creation() {
let time = SafeTimeProvider::new(TimeSource::Test(
Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap()
));
let card = RevolvingFacility::builder()
.facility_type(RevolvingType::CreditCard)
.credit_limit(Money::from_major(10_000))
.rate(Rate::from_percentage(18))
.minimum_percentage(dec!(0.02))
.set_time(&time)
.build()
.unwrap();
assert_eq!(card.credit_limit, Money::from_major(10_000));
assert_eq!(card.available_credit, Money::from_major(10_000));
assert_eq!(card.utilization_rate(), Rate::from_percentage(0));
}
#[test]
fn test_draw_and_repay_cycle() {
let time = SafeTimeProvider::new(TimeSource::Test(
Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap()
));
let control = time.test_control().unwrap();
let mut card = RevolvingFacility::builder()
.facility_type(RevolvingType::CreditCard)
.credit_limit(Money::from_major(10_000))
.rate(Rate::from_percentage(18))
.set_time(&time).build()
.unwrap();
card.activate().unwrap();
let drawn = card.draw(Money::from_major(3_000)).unwrap();
assert_eq!(drawn, Money::from_major(3_000));
assert_eq!(card.available_credit, Money::from_major(7_000));
assert_eq!(card.utilization_rate(), Rate::from_percentage(30));
control.advance(chrono::Duration::days(30));
card.facility_mut().accrue_interest(&time).unwrap();
card.process_payment(Money::from_major(1_000)).unwrap();
assert!(card.available_credit > Money::from_major(7_000));
card.draw(Money::from_major(500)).unwrap();
assert!(card.available_credit >= Money::from_major(7_400)); }
#[test]
fn test_utilization_states() {
let time = SafeTimeProvider::new(TimeSource::Test(
Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap()
));
let mut card = RevolvingFacility::builder()
.facility_type(RevolvingType::CreditCard)
.credit_limit(Money::from_major(10_000))
.rate(Rate::from_percentage(18))
.set_time(&time).build()
.unwrap();
card.activate().unwrap();
assert_eq!(card.utilization_state(), UtilizationState::Unused);
card.draw(Money::from_major(2_000)).unwrap();
assert_eq!(card.utilization_state(), UtilizationState::Low);
card.draw(Money::from_major(3_000)).unwrap();
assert_eq!(card.utilization_state(), UtilizationState::Moderate);
card.draw(Money::from_major(3_000)).unwrap();
assert_eq!(card.utilization_state(), UtilizationState::High);
card.draw(Money::from_major(1_500)).unwrap();
assert_eq!(card.utilization_state(), UtilizationState::Maxed);
}
#[test]
fn test_overlimit_handling() {
let time = SafeTimeProvider::new(TimeSource::Test(
Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap()
));
let mut card = RevolvingFacility::builder()
.facility_type(RevolvingType::CreditCard)
.credit_limit(Money::from_major(1_000))
.rate(Rate::from_percentage(18))
.set_time(&time).build()
.unwrap();
card.activate().unwrap();
card.draw(Money::from_major(1_000)).unwrap();
assert_eq!(card.available_credit, Money::ZERO);
card.draw(Money::from_major(50)).unwrap();
assert!(card.is_overlimit());
assert_eq!(card.utilization_state(), UtilizationState::Overlimit);
assert!(card.facility.state.accrued_fees > Money::ZERO);
let result = card.draw(Money::from_major(100));
assert!(result.is_err());
}
#[test]
fn test_minimum_payment_calculation() {
let time = SafeTimeProvider::new(TimeSource::Test(
Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap()
));
let control = time.test_control().unwrap();
let mut card = RevolvingFacility::builder()
.facility_type(RevolvingType::CreditCard)
.credit_limit(Money::from_major(10_000))
.rate(Rate::from_percentage(18))
.minimum_percentage(dec!(0.02))
.set_time(&time).build()
.unwrap();
card.activate().unwrap();
card.draw(Money::from_major(1_000)).unwrap();
control.advance(chrono::Duration::days(30));
card.facility_mut().accrue_interest(&time).unwrap();
let minimum = card.calculate_minimum_payment();
let expected = Money::from_major(20) + card.facility.state.accrued_interest;
assert!(minimum >= expected);
assert!(minimum >= Money::from_major(25));
}
#[test]
fn test_commitment_fee() {
let time = SafeTimeProvider::new(TimeSource::Test(
Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap()
));
let mut loc = RevolvingFacility::builder()
.facility_type(RevolvingType::LineOfCredit)
.credit_limit(Money::from_major(500_000))
.rate(Rate::from_percentage(8))
.commitment_fee_rate(Rate::from_decimal(dec!(0.005)))
.set_time(&time).build()
.unwrap();
loc.activate().unwrap();
loc.draw(Money::from_major(200_000)).unwrap();
let initial_fees = loc.facility.state.accrued_fees;
loc.charge_commitment_fee().unwrap();
let expected_commitment_fee = Money::from_decimal(dec!(300_000) * dec!(0.005) / dec!(12));
assert_eq!(loc.facility.state.accrued_fees - initial_fees, expected_commitment_fee);
}
#[test]
fn test_credit_limit_change() {
let time = SafeTimeProvider::new(TimeSource::Test(
Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap()
));
let mut card = RevolvingFacility::builder()
.facility_type(RevolvingType::CreditCard)
.credit_limit(Money::from_major(10_000))
.rate(Rate::from_percentage(18))
.set_time(&time).build()
.unwrap();
card.activate().unwrap();
card.draw(Money::from_major(4_000)).unwrap();
card.change_credit_limit(Money::from_major(15_000)).unwrap();
assert_eq!(card.credit_limit, Money::from_major(15_000));
assert_eq!(card.available_credit, Money::from_major(11_000));
card.change_credit_limit(Money::from_major(3_000)).unwrap();
assert!(card.is_overlimit());
assert_eq!(card.available_credit, Money::ZERO);
}
#[test]
fn test_heloc_draw_period() {
let time = SafeTimeProvider::new(TimeSource::Test(
Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap()
));
let mut heloc = RevolvingFacility::builder()
.facility_type(RevolvingType::HELOC)
.credit_limit(Money::from_major(100_000))
.rate(Rate::from_percentage(6))
.property_value(Money::from_major(400_000))
.draw_period_months(120)
.repayment_period_months(240)
.set_time(&time).build()
.unwrap();
heloc.activate().unwrap();
assert!(heloc.is_in_draw_period);
heloc.draw(Money::from_major(50_000)).unwrap();
heloc.end_draw_period().unwrap();
assert!(!heloc.is_in_draw_period);
let result = heloc.draw(Money::from_major(10_000));
assert!(result.is_err());
let minimum = heloc.calculate_minimum_payment();
assert!(minimum > Money::from_major(750)); }
}