diff --git a/pallets/loans/src/entities/loans.rs b/pallets/loans/src/entities/loans.rs index 6a99dd20a4..dff883539e 100644 --- a/pallets/loans/src/entities/loans.rs +++ b/pallets/loans/src/entities/loans.rs @@ -357,19 +357,19 @@ impl ActiveLoan { Error::::from(BorrowLoanError::MaturityDatePassed) ); - // TODO - // If the loan has an interest or pay down schedule other than None, - // then we should only allow borrowing more if no interest or principal - // payments are overdue. - // - // This is required because after borrowing more, it is not possible - // to validate anymore whether previous cashflows matched the repayment - // schedule, as we don't store historic data of the principal. - // - // Therefore, in `borrow()` we set repayments_on_schedule_until to now. - // - // TODO: check total_repaid_interest >= total_expected_interest - // and total_repaid_principal >= total_expected_principal + if self.schedule.has_cashflow() { + let expected_payment = self.schedule.expected_payment( + self.origination_date, + self.principal()?, + self.pricing.interest().rate(), + now, + )?; + + ensure!( + self.total_repaid.effective()? >= expected_payment, + DispatchError::Other("payment overdue") + ) + } Ok(()) } @@ -388,6 +388,8 @@ impl ActiveLoan { } } + self.repayments_on_schedule_until = T::Time::now(); + Ok(()) } diff --git a/pallets/loans/src/types/cashflow.rs b/pallets/loans/src/types/cashflow.rs index 79256a4dd7..d86f576fa4 100644 --- a/pallets/loans/src/types/cashflow.rs +++ b/pallets/loans/src/types/cashflow.rs @@ -18,7 +18,8 @@ use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use sp_runtime::{ traits::{ - EnsureAddAssign, EnsureDiv, EnsureFixedPointNumber, EnsureInto, EnsureSub, EnsureSubAssign, + EnsureAdd, EnsureAddAssign, EnsureDiv, EnsureFixedPointNumber, EnsureInto, EnsureSub, + EnsureSubAssign, }, ArithmeticError, DispatchError, FixedPointNumber, FixedPointOperand, }; @@ -103,6 +104,8 @@ pub enum InterestPayments { /// The associated value correspond to the paydown day in the month, /// from 1-31. /// The day will be adjusted to the month. + /// + /// NOTE: Only day 1 is supported by now Monthly(u8), } @@ -133,6 +136,20 @@ impl RepaymentSchedule { self.maturity.is_valid(now) } + pub fn has_cashflow(&self) -> bool { + let has_interest_payments = match self.interest_payments { + InterestPayments::None => false, + _ => true, + }; + + let has_pay_down_schedule = match self.pay_down_schedule { + PayDownSchedule::None => false, + _ => true, + }; + + has_interest_payments || has_pay_down_schedule + } + pub fn generate_cashflows( &self, origination_date: Seconds, @@ -169,6 +186,30 @@ impl RepaymentSchedule { }) .collect() } + + pub fn expected_payment( + &self, + origination_date: Seconds, + principal: Balance, + interest_rate: &InterestRate, + until: Seconds, + ) -> Result + where + Balance: FixedPointOperand + EnsureAdd, + Rate: FixedPointNumber, + { + let cashflow = self.generate_cashflows(origination_date, principal, interest_rate)?; + + let until_date = seconds_to_date(until)?; + + let total_amount = cashflow + .iter() + .take_while(|(date, _)| *date < until) + .map(|(_, amount)| amount) + .try_fold(Balance::zero(), |a, b| a.ensure_add(*b))?; + + Ok(total_amount) + } } fn monthly_dates( @@ -268,6 +309,7 @@ mod tests { from_ymd(year, month, day) .and_hms_opt(hour, min, seconds) .unwrap() + .and_utc() .timestamp() as Seconds }