mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-01 16:35:53 +00:00
Add tfLoanLatePayment flag; full payment is no longer a special case
- A regular payment that is late, or a tfLoanLatePayment that is not late will fail. - Flags are mutually exclusive. - Add a few interest computation shortcuts and overflow prevention checks that return 0 if there's no time to compute for.
This commit is contained in:
@@ -274,10 +274,11 @@ constexpr std::uint32_t const tfLoanOverpayment = 0x00010000;
|
||||
// LoanPay exclusive flags:
|
||||
// tfLoanFullPayment: True, indicates that the payment is
|
||||
constexpr std::uint32_t const tfLoanFullPayment = 0x00020000;
|
||||
constexpr std::uint32_t const tfLoanLatePayment = 0x00040000;
|
||||
constexpr std::uint32_t const tfLoanSetMask = ~(tfUniversal |
|
||||
tfLoanOverpayment);
|
||||
constexpr std::uint32_t const tfLoanPayMask = ~(tfUniversal |
|
||||
tfLoanOverpayment | tfLoanFullPayment);
|
||||
tfLoanOverpayment | tfLoanFullPayment | tfLoanLatePayment);
|
||||
|
||||
// LoanManage flags:
|
||||
constexpr std::uint32_t const tfLoanDefault = 0x00010000;
|
||||
|
||||
@@ -1059,8 +1059,7 @@ protected:
|
||||
// Make the payment
|
||||
env(pay(borrower, loanKeylet.key, transactionAmount));
|
||||
|
||||
env.close(
|
||||
d{(state.previousPaymentDate + state.nextPaymentDate) / 2});
|
||||
env.close(d{state.paymentInterval / 2});
|
||||
|
||||
// Need to account for fees if the loan is in XRP
|
||||
PrettyAmount adjustment = broker.asset(0);
|
||||
@@ -2227,14 +2226,29 @@ protected:
|
||||
(Number{15, -1} / loanPaymentsPerFeeIncrement + 1)}),
|
||||
ter(temINVALID_FLAG));
|
||||
}
|
||||
// Try to send a payment marked as both full payment and
|
||||
// overpayment. Do not include `txFlags`, so we don't duplicate the
|
||||
// prior test transaction.
|
||||
// Try to send a payment marked as multiple mutually exclusive
|
||||
// payment types. Do not include `txFlags`, so we don't duplicate
|
||||
// the prior test transaction.
|
||||
env(pay(borrower,
|
||||
loanKeylet.key,
|
||||
broker.asset(state.periodicPayment * 2),
|
||||
tfLoanLatePayment | tfLoanFullPayment),
|
||||
ter(temINVALID_FLAG));
|
||||
env(pay(borrower,
|
||||
loanKeylet.key,
|
||||
broker.asset(state.periodicPayment * 2),
|
||||
tfLoanLatePayment | tfLoanOverpayment),
|
||||
ter(temINVALID_FLAG));
|
||||
env(pay(borrower,
|
||||
loanKeylet.key,
|
||||
broker.asset(state.periodicPayment * 2),
|
||||
tfLoanOverpayment | tfLoanFullPayment),
|
||||
ter(temINVALID_FLAG));
|
||||
env(pay(borrower,
|
||||
loanKeylet.key,
|
||||
broker.asset(state.periodicPayment * 2),
|
||||
tfLoanLatePayment | tfLoanOverpayment | tfLoanFullPayment),
|
||||
ter(temINVALID_FLAG));
|
||||
|
||||
{
|
||||
auto const otherAsset = broker.asset.raw() == assets[0].raw()
|
||||
@@ -4562,7 +4576,11 @@ protected:
|
||||
issuer, lender["IOU"](1'000), tfClearFreeze | tfClearDeepFreeze));
|
||||
env.close();
|
||||
|
||||
env(pay(borrower, loanKeylet.key, debtMaximumRequest));
|
||||
// The payment is late by this point
|
||||
env(pay(borrower, loanKeylet.key, debtMaximumRequest), ter(tecEXPIRED));
|
||||
env.close();
|
||||
env(pay(
|
||||
borrower, loanKeylet.key, debtMaximumRequest, tfLoanLatePayment));
|
||||
env.close();
|
||||
|
||||
// preclaim: tecKILLED
|
||||
@@ -6777,11 +6795,10 @@ public:
|
||||
testPoC_UnsignedUnderflowOnFullPayAfterEarlyPeriodic();
|
||||
testLoanCoverMinimumRoundingExploit();
|
||||
#endif
|
||||
testDustManipulation();
|
||||
|
||||
testIssuerLoan();
|
||||
testDisabled();
|
||||
testSelfLoan();
|
||||
testIssuerLoan();
|
||||
testLoanSet();
|
||||
testLifecycle();
|
||||
testServiceFeeOnBrokerDeepFreeze();
|
||||
@@ -6806,6 +6823,7 @@ public:
|
||||
testLoanNextPaymentDueDateOverflow();
|
||||
|
||||
testRequireAuth();
|
||||
testDustManipulation();
|
||||
|
||||
testRIPD3831();
|
||||
testRIPD3459();
|
||||
|
||||
@@ -30,21 +30,24 @@ roundPeriodicPayment(
|
||||
struct LoanPaymentParts
|
||||
{
|
||||
/// principal_paid is the amount of principal that the payment covered.
|
||||
Number principalPaid;
|
||||
Number principalPaid = numZero;
|
||||
/// interest_paid is the amount of interest that the payment covered.
|
||||
Number interestPaid;
|
||||
Number interestPaid = numZero;
|
||||
/**
|
||||
* value_change is the amount by which the total value of the Loan changed.
|
||||
* If value_change < 0, Loan value decreased.
|
||||
* If value_change > 0, Loan value increased.
|
||||
* This is 0 for regular payments.
|
||||
*/
|
||||
Number valueChange;
|
||||
Number valueChange = numZero;
|
||||
/// feePaid is amount of fee that is paid to the broker
|
||||
Number feePaid;
|
||||
Number feePaid = numZero;
|
||||
|
||||
LoanPaymentParts&
|
||||
operator+=(LoanPaymentParts const& other);
|
||||
|
||||
bool
|
||||
operator==(LoanPaymentParts const& other) const;
|
||||
};
|
||||
|
||||
/** This structure describes the initial "computed" properties of a loan.
|
||||
@@ -240,15 +243,11 @@ computeLoanProperties(
|
||||
bool
|
||||
isRounded(Asset const& asset, Number const& value, std::int32_t scale);
|
||||
|
||||
Expected<LoanPaymentParts, TER>
|
||||
loanMakeFullPayment(
|
||||
Asset const& asset,
|
||||
ApplyView& view,
|
||||
SLE::ref loan,
|
||||
SLE::const_ref brokerSle,
|
||||
STAmount const& amount,
|
||||
bool const overpaymentAllowed,
|
||||
beast::Journal j);
|
||||
// Indicates what type of payment is being made.
|
||||
// regular, late, and full are mutually exclusive.
|
||||
// overpayment is an "add on" to a regular payment, and follows that path with
|
||||
// potential extra work at the end.
|
||||
enum class LoanPaymentType { regular = 0, late, full, overpayment };
|
||||
|
||||
Expected<LoanPaymentParts, TER>
|
||||
loanMakePayment(
|
||||
@@ -257,7 +256,7 @@ loanMakePayment(
|
||||
SLE::ref loan,
|
||||
SLE::const_ref brokerSle,
|
||||
STAmount const& amount,
|
||||
bool const overpaymentAllowed,
|
||||
LoanPaymentType const paymentType,
|
||||
beast::Journal j);
|
||||
|
||||
} // namespace ripple
|
||||
|
||||
@@ -35,6 +35,14 @@ LoanPaymentParts::operator+=(LoanPaymentParts const& other)
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool
|
||||
LoanPaymentParts::operator==(LoanPaymentParts const& other) const
|
||||
{
|
||||
return principalPaid == other.principalPaid &&
|
||||
interestPaid == other.interestPaid &&
|
||||
valueChange == other.valueChange && feePaid == other.feePaid;
|
||||
}
|
||||
|
||||
Number
|
||||
loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval)
|
||||
{
|
||||
@@ -170,6 +178,12 @@ loanLatePaymentInterest(
|
||||
*
|
||||
* The spec is to be updated to base the duration on the next due date
|
||||
*/
|
||||
|
||||
// If the payment is not late by any amount of time, then there's no late
|
||||
// interest
|
||||
if (parentCloseTime.time_since_epoch().count() <= nextPaymentDueDate)
|
||||
return 0;
|
||||
|
||||
auto const secondsOverdue =
|
||||
parentCloseTime.time_since_epoch().count() - nextPaymentDueDate;
|
||||
|
||||
@@ -193,6 +207,11 @@ loanAccruedInterest(
|
||||
*/
|
||||
auto const lastPaymentDate = std::max(prevPaymentDate, startDate);
|
||||
|
||||
// If the loan has been paid ahead, then "lastPaymentDate" is in the future,
|
||||
// and no interest has accrued.
|
||||
if (parentCloseTime.time_since_epoch().count() <= lastPaymentDate)
|
||||
return 0;
|
||||
|
||||
auto const secondsSinceLastPayment =
|
||||
parentCloseTime.time_since_epoch().count() - lastPaymentDate;
|
||||
|
||||
@@ -818,7 +837,7 @@ computeLatePayment(
|
||||
beast::Journal j)
|
||||
{
|
||||
if (!hasExpired(view, nextDueDate))
|
||||
return Unexpected(tesSUCCESS);
|
||||
return Unexpected(tecTOO_SOON);
|
||||
|
||||
// the payment is late
|
||||
// Late payment interest is only the part of the interest that comes
|
||||
@@ -972,9 +991,19 @@ computeFullPayment(
|
||||
"ripple::detail::computeFullPayment",
|
||||
"total due is rounded");
|
||||
|
||||
JLOG(j.trace()) << "computeFullPayment result: periodicPayment: "
|
||||
<< periodicPayment << ", periodicRate: " << periodicRate
|
||||
<< ", paymentRemaining: " << paymentRemaining
|
||||
<< ", rawPrincipalOutstanding: " << rawPrincipalOutstanding
|
||||
<< ", fullPaymentInterest: " << fullPaymentInterest
|
||||
<< ", roundedFullInterest: " << roundedFullInterest
|
||||
<< ", roundedFullManagementFee: "
|
||||
<< roundedFullManagementFee
|
||||
<< ", untrackedInterest: " << full.untrackedInterest;
|
||||
|
||||
if (amount < full.totalDue)
|
||||
// If the payment is less than the full payment amount, it's not
|
||||
// sufficient to be a full payment, but that's not an error.
|
||||
// sufficient to be a full payment.
|
||||
return Unexpected(tecINSUFFICIENT_PAYMENT);
|
||||
|
||||
return full;
|
||||
@@ -1639,111 +1668,6 @@ computeLoanProperties(
|
||||
.firstPaymentPrincipal = firstPaymentPrincipal};
|
||||
}
|
||||
|
||||
Expected<LoanPaymentParts, TER>
|
||||
loanMakeFullPayment(
|
||||
Asset const& asset,
|
||||
ApplyView& view,
|
||||
SLE::ref loan,
|
||||
SLE::const_ref brokerSle,
|
||||
STAmount const& amount,
|
||||
bool const overpaymentAllowed,
|
||||
beast::Journal j)
|
||||
{
|
||||
auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding);
|
||||
auto paymentRemainingProxy = loan->at(sfPaymentRemaining);
|
||||
|
||||
if (paymentRemainingProxy == 0 || principalOutstandingProxy == 0)
|
||||
{
|
||||
// Loan complete
|
||||
JLOG(j.warn()) << "Loan is already paid off.";
|
||||
return Unexpected(tecKILLED);
|
||||
}
|
||||
|
||||
auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding);
|
||||
auto managementFeeOutstandingProxy = loan->at(sfManagementFeeOutstanding);
|
||||
|
||||
// Next payment due date must be set unless the loan is complete
|
||||
auto nextDueDateProxy = loan->at(~sfNextPaymentDueDate);
|
||||
if (!nextDueDateProxy)
|
||||
{
|
||||
JLOG(j.warn()) << "Loan next payment due date is not set.";
|
||||
return Unexpected(tecINTERNAL);
|
||||
}
|
||||
|
||||
std::int32_t const loanScale = loan->at(sfLoanScale);
|
||||
|
||||
TenthBips32 const interestRate{loan->at(sfInterestRate)};
|
||||
TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)};
|
||||
|
||||
Number const closePaymentFee =
|
||||
roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale);
|
||||
TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
|
||||
|
||||
auto const periodicPayment = loan->at(sfPeriodicPayment);
|
||||
|
||||
auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDate);
|
||||
std::uint32_t const startDate = loan->at(sfStartDate);
|
||||
|
||||
std::uint32_t const paymentInterval = loan->at(sfPaymentInterval);
|
||||
// Compute the normal periodic rate, payment, etc.
|
||||
// We'll need it in the remaining calculations
|
||||
Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval);
|
||||
XRPL_ASSERT(
|
||||
interestRate == 0 || periodicRate > 0,
|
||||
"ripple::loanMakeFullPayment : valid rate");
|
||||
|
||||
XRPL_ASSERT(
|
||||
*totalValueOutstandingProxy > 0,
|
||||
"ripple::loanMakeFullPayment : valid total value");
|
||||
|
||||
view.update(loan);
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// full payment handling
|
||||
LoanState const roundedLoanState = calculateRoundedLoanState(
|
||||
totalValueOutstandingProxy,
|
||||
principalOutstandingProxy,
|
||||
managementFeeOutstandingProxy);
|
||||
|
||||
if (auto const fullPaymentComponents = detail::computeFullPayment(
|
||||
asset,
|
||||
view,
|
||||
principalOutstandingProxy,
|
||||
managementFeeOutstandingProxy,
|
||||
periodicPayment,
|
||||
paymentRemainingProxy,
|
||||
prevPaymentDateProxy,
|
||||
startDate,
|
||||
paymentInterval,
|
||||
closeInterestRate,
|
||||
loanScale,
|
||||
roundedLoanState.interestDue,
|
||||
periodicRate,
|
||||
closePaymentFee,
|
||||
amount,
|
||||
managementFeeRate,
|
||||
j))
|
||||
return doPayment(
|
||||
*fullPaymentComponents,
|
||||
totalValueOutstandingProxy,
|
||||
principalOutstandingProxy,
|
||||
managementFeeOutstandingProxy,
|
||||
paymentRemainingProxy,
|
||||
prevPaymentDateProxy,
|
||||
nextDueDateProxy,
|
||||
paymentInterval);
|
||||
else if (fullPaymentComponents.error())
|
||||
// error() will be the TER returned if a payment is not made. It
|
||||
// will only evaluate to true if it's unsuccessful. Otherwise,
|
||||
// tesSUCCESS means nothing was done, so continue.
|
||||
return Unexpected(fullPaymentComponents.error());
|
||||
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("ripple::loanMakeFullPayment : invalid result");
|
||||
return Unexpected(tecINTERNAL);
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
Expected<LoanPaymentParts, TER>
|
||||
loanMakePayment(
|
||||
Asset const& asset,
|
||||
@@ -1751,7 +1675,7 @@ loanMakePayment(
|
||||
SLE::ref loan,
|
||||
SLE::const_ref brokerSle,
|
||||
STAmount const& amount,
|
||||
bool const overpaymentAllowed,
|
||||
LoanPaymentType const paymentType,
|
||||
beast::Journal j)
|
||||
{
|
||||
/*
|
||||
@@ -1785,16 +1709,14 @@ loanMakePayment(
|
||||
std::int32_t const loanScale = loan->at(sfLoanScale);
|
||||
|
||||
TenthBips32 const interestRate{loan->at(sfInterestRate)};
|
||||
TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)};
|
||||
TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)};
|
||||
|
||||
Number const serviceFee = loan->at(sfLoanServiceFee);
|
||||
Number const latePaymentFee = loan->at(sfLatePaymentFee);
|
||||
TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
|
||||
|
||||
Number const periodicPayment = loan->at(sfPeriodicPayment);
|
||||
|
||||
auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDate);
|
||||
std::uint32_t const startDate = loan->at(sfStartDate);
|
||||
|
||||
std::uint32_t const paymentInterval = loan->at(sfPaymentInterval);
|
||||
// Compute the normal periodic rate, payment, etc.
|
||||
@@ -1810,7 +1732,7 @@ loanMakePayment(
|
||||
|
||||
view.update(loan);
|
||||
|
||||
detail::PaymentComponentsPlus const periodic{
|
||||
detail::PaymentComponentsPlus periodic{
|
||||
detail::computePaymentComponents(
|
||||
asset,
|
||||
loanScale,
|
||||
@@ -1829,21 +1751,141 @@ loanMakePayment(
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// late payment handling
|
||||
if (auto const latePaymentComponents = detail::computeLatePayment(
|
||||
asset,
|
||||
view,
|
||||
principalOutstandingProxy,
|
||||
*nextDueDateProxy,
|
||||
periodic,
|
||||
lateInterestRate,
|
||||
loanScale,
|
||||
latePaymentFee,
|
||||
amount,
|
||||
managementFeeRate,
|
||||
j))
|
||||
if (paymentType == LoanPaymentType::late)
|
||||
{
|
||||
return doPayment(
|
||||
*latePaymentComponents,
|
||||
TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)};
|
||||
Number const latePaymentFee = loan->at(sfLatePaymentFee);
|
||||
|
||||
if (auto const latePaymentComponents = detail::computeLatePayment(
|
||||
asset,
|
||||
view,
|
||||
principalOutstandingProxy,
|
||||
*nextDueDateProxy,
|
||||
periodic,
|
||||
lateInterestRate,
|
||||
loanScale,
|
||||
latePaymentFee,
|
||||
amount,
|
||||
managementFeeRate,
|
||||
j))
|
||||
{
|
||||
return doPayment(
|
||||
*latePaymentComponents,
|
||||
totalValueOutstandingProxy,
|
||||
principalOutstandingProxy,
|
||||
managementFeeOutstandingProxy,
|
||||
paymentRemainingProxy,
|
||||
prevPaymentDateProxy,
|
||||
nextDueDateProxy,
|
||||
paymentInterval);
|
||||
}
|
||||
else if (latePaymentComponents.error())
|
||||
{
|
||||
// error() will be the TER returned if a payment is not made. It
|
||||
// will only evaluate to true if it's unsuccessful.
|
||||
return Unexpected(latePaymentComponents.error());
|
||||
}
|
||||
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("ripple::loanMakePayment : invalid late payment result");
|
||||
return Unexpected(tecINTERNAL);
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
else if (hasExpired(view, nextDueDateProxy))
|
||||
{
|
||||
// If the payment is late, and the late flag was not set, it's not valid
|
||||
JLOG(j.warn())
|
||||
<< "Loan payment is overdue. Use the tfLoanLatePayment transaction "
|
||||
"flag to make a late payment. Loan was created on "
|
||||
<< startDate << ", prev payment due date is "
|
||||
<< prevPaymentDateProxy << ", next payment due date is "
|
||||
<< *nextDueDateProxy << ", ledger time is "
|
||||
<< view.parentCloseTime().time_since_epoch().count();
|
||||
return Unexpected(tecEXPIRED);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// full payment handling
|
||||
if (paymentType == LoanPaymentType::full)
|
||||
{
|
||||
TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)};
|
||||
Number const closePaymentFee =
|
||||
roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale);
|
||||
|
||||
LoanState const roundedLoanState = calculateRoundedLoanState(
|
||||
totalValueOutstandingProxy,
|
||||
principalOutstandingProxy,
|
||||
managementFeeOutstandingProxy);
|
||||
|
||||
if (auto const fullPaymentComponents = detail::computeFullPayment(
|
||||
asset,
|
||||
view,
|
||||
principalOutstandingProxy,
|
||||
managementFeeOutstandingProxy,
|
||||
periodicPayment,
|
||||
paymentRemainingProxy,
|
||||
prevPaymentDateProxy,
|
||||
startDate,
|
||||
paymentInterval,
|
||||
closeInterestRate,
|
||||
loanScale,
|
||||
roundedLoanState.interestDue,
|
||||
periodicRate,
|
||||
closePaymentFee,
|
||||
amount,
|
||||
managementFeeRate,
|
||||
j))
|
||||
{
|
||||
return doPayment(
|
||||
*fullPaymentComponents,
|
||||
totalValueOutstandingProxy,
|
||||
principalOutstandingProxy,
|
||||
managementFeeOutstandingProxy,
|
||||
paymentRemainingProxy,
|
||||
prevPaymentDateProxy,
|
||||
nextDueDateProxy,
|
||||
paymentInterval);
|
||||
}
|
||||
else if (fullPaymentComponents.error())
|
||||
// error() will be the TER returned if a payment is not made. It
|
||||
// will only evaluate to true if it's unsuccessful. Otherwise,
|
||||
// tesSUCCESS means nothing was done, so continue.
|
||||
return Unexpected(fullPaymentComponents.error());
|
||||
|
||||
// LCOV_EXCL_START
|
||||
UNREACHABLE("ripple::loanMakePayment : invalid full payment result");
|
||||
return Unexpected(tecINTERNAL);
|
||||
// LCOV_EXCL_STOP
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// regular periodic payment handling
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
paymentType == LoanPaymentType::regular ||
|
||||
paymentType == LoanPaymentType::overpayment,
|
||||
"ripple::loanMakePayment",
|
||||
"regular payment type");
|
||||
|
||||
// This will keep a running total of what is actually paid, if the payment
|
||||
// is sufficient for any payment
|
||||
LoanPaymentParts totalParts;
|
||||
Number totalPaid;
|
||||
std::size_t numPayments = 0;
|
||||
|
||||
while (amount >= totalPaid + periodic.totalDue &&
|
||||
paymentRemainingProxy > 0 &&
|
||||
numPayments < loanMaximumPaymentsPerTransaction)
|
||||
{
|
||||
// Try to make more payments
|
||||
XRPL_ASSERT_PARTS(
|
||||
periodic.trackedPrincipalDelta >= 0,
|
||||
"ripple::loanMakePayment",
|
||||
"payment pays non-negative principal");
|
||||
|
||||
totalPaid += periodic.totalDue;
|
||||
totalParts += detail::doPayment(
|
||||
periodic,
|
||||
totalValueOutstandingProxy,
|
||||
principalOutstandingProxy,
|
||||
managementFeeOutstandingProxy,
|
||||
@@ -1851,47 +1893,19 @@ loanMakePayment(
|
||||
prevPaymentDateProxy,
|
||||
nextDueDateProxy,
|
||||
paymentInterval);
|
||||
}
|
||||
else if (latePaymentComponents.error())
|
||||
// error() will be the TER returned if a payment is not made. It will
|
||||
// only evaluate to true if it's unsuccessful. Otherwise, tesSUCCESS
|
||||
// means nothing was done, so continue.
|
||||
return Unexpected(latePaymentComponents.error());
|
||||
++numPayments;
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// regular periodic payment handling
|
||||
XRPL_ASSERT_PARTS(
|
||||
(periodic.specialCase == detail::PaymentSpecialCase::final) ==
|
||||
(paymentRemainingProxy == 0),
|
||||
"ripple::loanMakePayment",
|
||||
"final payment is the final payment");
|
||||
|
||||
// if the payment is not late nor if it's a full payment, then it must
|
||||
// be a periodic one, with possible overpayments
|
||||
// Don't compute the next payment if this was the last payment
|
||||
if (periodic.specialCase == detail::PaymentSpecialCase::final)
|
||||
break;
|
||||
|
||||
// This will keep a running total of what is actually paid, if the payment
|
||||
// is sufficient for a single payment
|
||||
Number totalPaid = periodic.totalDue;
|
||||
|
||||
if (amount < totalPaid)
|
||||
{
|
||||
JLOG(j.warn()) << "Periodic loan payment amount is insufficient. Due: "
|
||||
<< totalPaid << ", paid: " << amount;
|
||||
return Unexpected(tecINSUFFICIENT_PAYMENT);
|
||||
}
|
||||
|
||||
LoanPaymentParts totalParts = detail::doPayment(
|
||||
periodic,
|
||||
totalValueOutstandingProxy,
|
||||
principalOutstandingProxy,
|
||||
managementFeeOutstandingProxy,
|
||||
paymentRemainingProxy,
|
||||
prevPaymentDateProxy,
|
||||
nextDueDateProxy,
|
||||
paymentInterval);
|
||||
|
||||
std::size_t numPayments = 1;
|
||||
|
||||
while (totalPaid < amount && paymentRemainingProxy > 0 &&
|
||||
numPayments < loanMaximumPaymentsPerTransaction)
|
||||
{
|
||||
// Try to make more payments
|
||||
detail::PaymentComponentsPlus const nextPayment{
|
||||
periodic = detail::PaymentComponentsPlus{
|
||||
detail::computePaymentComponents(
|
||||
asset,
|
||||
loanScale,
|
||||
@@ -1903,40 +1917,13 @@ loanMakePayment(
|
||||
paymentRemainingProxy,
|
||||
managementFeeRate),
|
||||
serviceFee};
|
||||
XRPL_ASSERT_PARTS(
|
||||
nextPayment.trackedPrincipalDelta >= 0,
|
||||
"ripple::loanMakePayment",
|
||||
"additional payment pays non-negative principal");
|
||||
#if LOANCOMPLETE
|
||||
XRPL_ASSERT(
|
||||
nextPayment.rawInterest <= periodic.rawInterest,
|
||||
"ripple::loanMakePayment : decreasing interest");
|
||||
XRPL_ASSERT(
|
||||
nextPayment.rawPrinicpal >= periodic.rawPrincipal,
|
||||
"ripple::loanMakePayment : increasing principal");
|
||||
#endif
|
||||
}
|
||||
|
||||
if (amount < totalPaid + nextPayment.totalDue)
|
||||
// We're done making payments.
|
||||
break;
|
||||
|
||||
totalPaid += nextPayment.totalDue;
|
||||
totalParts += detail::doPayment(
|
||||
nextPayment,
|
||||
totalValueOutstandingProxy,
|
||||
principalOutstandingProxy,
|
||||
managementFeeOutstandingProxy,
|
||||
paymentRemainingProxy,
|
||||
prevPaymentDateProxy,
|
||||
nextDueDateProxy,
|
||||
paymentInterval);
|
||||
++numPayments;
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
(nextPayment.specialCase == detail::PaymentSpecialCase::final) ==
|
||||
(paymentRemainingProxy == 0),
|
||||
"ripple::loanMakePayment",
|
||||
"final payment is the final payment");
|
||||
if (numPayments == 0)
|
||||
{
|
||||
JLOG(j.warn()) << "Regular loan payment amount is insufficient. Due: "
|
||||
<< periodic.totalDue << ", paid: " << amount;
|
||||
return Unexpected(tecINSUFFICIENT_PAYMENT);
|
||||
}
|
||||
|
||||
XRPL_ASSERT_PARTS(
|
||||
@@ -1952,8 +1939,9 @@ loanMakePayment(
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// overpayment handling
|
||||
if (overpaymentAllowed && loan->isFlag(lsfLoanOverpayment) &&
|
||||
paymentRemainingProxy > 0 && nextDueDateProxy && totalPaid < amount &&
|
||||
if (paymentType == LoanPaymentType::overpayment &&
|
||||
loan->isFlag(lsfLoanOverpayment) && paymentRemainingProxy > 0 &&
|
||||
nextDueDateProxy && totalPaid < amount &&
|
||||
numPayments < loanMaximumPaymentsPerTransaction)
|
||||
{
|
||||
TenthBips32 const overpaymentInterestRate{
|
||||
|
||||
@@ -30,9 +30,21 @@ LoanPay::preflight(PreflightContext const& ctx)
|
||||
if (ctx.tx[sfAmount] <= beast::zero)
|
||||
return temBAD_AMOUNT;
|
||||
|
||||
// isFlag requires an exact match - all flags to be set - to return true.
|
||||
if (ctx.tx.isFlag(tfLoanOverpayment | tfLoanFullPayment))
|
||||
// The loan payment flags are all mutually exclusive. If more than one is
|
||||
// set, the tx is malformed.
|
||||
int flagsSet = 0;
|
||||
for (auto const flag :
|
||||
{tfLoanLatePayment, tfLoanFullPayment, tfLoanOverpayment})
|
||||
{
|
||||
if (ctx.tx.isFlag(flag))
|
||||
++flagsSet;
|
||||
}
|
||||
if (flagsSet > 1)
|
||||
{
|
||||
JLOG(ctx.j.warn()) << "Only one LoanPay flag can be set per tx. "
|
||||
<< flagsSet << " is too many.";
|
||||
return temINVALID_FLAG;
|
||||
}
|
||||
|
||||
return tesSUCCESS;
|
||||
}
|
||||
@@ -42,8 +54,8 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
{
|
||||
auto const normalCost = Transactor::calculateBaseFee(view, tx);
|
||||
|
||||
if (tx.isFlag(tfLoanFullPayment))
|
||||
// The loan will be making one set of calculations for one (large)
|
||||
if (tx.isFlag(tfLoanFullPayment) || tx.isFlag(tfLoanLatePayment))
|
||||
// The loan will be making one set of calculations for one full or late
|
||||
// payment
|
||||
return normalCost;
|
||||
|
||||
@@ -65,7 +77,8 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
}
|
||||
|
||||
if (hasExpired(view, loanSle->at(sfNextPaymentDueDate)))
|
||||
// If the payment is late, it'll only make one payment
|
||||
// If the payment is late, and the late payment flag is not set, it'll
|
||||
// fail
|
||||
return normalCost;
|
||||
|
||||
auto const brokerSle =
|
||||
@@ -97,6 +110,7 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx)
|
||||
// Estimate how many payments will be made
|
||||
Number const numPaymentEstimate =
|
||||
static_cast<std::int64_t>(amount / regularPayment);
|
||||
|
||||
// Charge one base fee per paymentsPerFeeIncrement payments, rounding up.
|
||||
Number::setround(Number::upward);
|
||||
auto const feeIncrements = std::max(
|
||||
@@ -291,23 +305,19 @@ LoanPay::doApply()
|
||||
LoanManage::unimpairLoan(view, loanSle, vaultSle, j_);
|
||||
}
|
||||
|
||||
Expected<LoanPaymentParts, TER> const paymentParts =
|
||||
tx.isFlag(tfLoanFullPayment) ? loanMakeFullPayment(
|
||||
asset,
|
||||
view,
|
||||
loanSle,
|
||||
brokerSle,
|
||||
amount,
|
||||
tx.isFlag(tfLoanOverpayment),
|
||||
j_)
|
||||
: loanMakePayment(
|
||||
asset,
|
||||
view,
|
||||
loanSle,
|
||||
brokerSle,
|
||||
amount,
|
||||
tx.isFlag(tfLoanOverpayment),
|
||||
j_);
|
||||
LoanPaymentType const paymentType = [&tx]() {
|
||||
// preflight already checked that at most one flag is set.
|
||||
if (tx.isFlag(tfLoanLatePayment))
|
||||
return LoanPaymentType::late;
|
||||
if (tx.isFlag(tfLoanFullPayment))
|
||||
return LoanPaymentType::full;
|
||||
if (tx.isFlag(tfLoanOverpayment))
|
||||
return LoanPaymentType::overpayment;
|
||||
return LoanPaymentType::regular;
|
||||
}();
|
||||
|
||||
Expected<LoanPaymentParts, TER> const paymentParts = loanMakePayment(
|
||||
asset, view, loanSle, brokerSle, amount, paymentType, j_);
|
||||
|
||||
if (!paymentParts)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user