Implement tfLoanFullPayment; use updated full payment calculations

This commit is contained in:
Ed Hennis
2025-10-23 00:54:58 -04:00
parent 30e2650ff9
commit 9814ec0309
4 changed files with 260 additions and 191 deletions

View File

@@ -290,9 +290,13 @@ constexpr std::uint32_t const tfBatchMask =
// LoanPay: True, indicates any excess in this payment can be used
// as an overpayment. False, no overpayments will be taken.
constexpr std::uint32_t const tfLoanOverpayment = 0x00010000;
// Use two separate mask variables in case the set of flags diverges
constexpr std::uint32_t const tfLoanSetMask = ~(tfUniversal | tfLoanOverpayment);
constexpr std::uint32_t const tfLoanPayMask = ~(tfUniversal | tfLoanOverpayment);
// LoanPay exclusive flags:
// tfLoanFullPayment: True, indicates that the payment is
constexpr std::uint32_t const tfLoanFullPayment = 0x00020000;
constexpr std::uint32_t const tfLoanSetMask = ~(tfUniversal |
tfLoanOverpayment);
constexpr std::uint32_t const tfLoanPayMask = ~(tfUniversal |
tfLoanOverpayment | tfLoanFullPayment);
// LoanManage flags:
constexpr std::uint32_t const tfLoanDefault = 0x00010000;

View File

@@ -1323,19 +1323,23 @@ class Loan_test : public beast::unit_test::suite
LoanState& state,
STAmount const& payoffAmount,
std::uint32_t numPayments,
std::uint32_t baseFlag) {
std::uint32_t baseFlag,
std::uint32_t txFlags) {
// toEndOfLife
//
verifyLoanStatus(state);
// Send some bogus pay transactions
env(pay(borrower, keylet::loan(uint256(0)).key, broker.asset(10)),
env(pay(borrower,
keylet::loan(uint256(0)).key,
broker.asset(10),
txFlags),
ter(temINVALID));
env(pay(borrower, loanKeylet.key, broker.asset(-100)),
env(pay(borrower, loanKeylet.key, broker.asset(-100), txFlags),
ter(temBAD_AMOUNT));
env(pay(borrower, broker.brokerID, broker.asset(100)),
env(pay(borrower, broker.brokerID, broker.asset(100), txFlags),
ter(tecNO_ENTRY));
env(pay(evan, loanKeylet.key, broker.asset(500)),
env(pay(evan, loanKeylet.key, broker.asset(500), txFlags),
ter(tecNO_PERMISSION));
// TODO: Write a general "isFlag" function? See STObject::isFlag.
@@ -1347,7 +1351,7 @@ class Loan_test : public beast::unit_test::suite
env(pay(borrower,
loanKeylet.key,
broker.asset(state.periodicPayment * 2),
tfLoanOverpayment),
tfLoanOverpayment | txFlags),
ter(temINVALID_FLAG));
}
@@ -1355,12 +1359,15 @@ class Loan_test : public beast::unit_test::suite
auto const otherAsset = broker.asset.raw() == assets[0].raw()
? assets[1]
: assets[0];
env(pay(borrower, loanKeylet.key, otherAsset(100)),
env(pay(borrower, loanKeylet.key, otherAsset(100), txFlags),
ter(tecWRONG_ASSET));
}
// Amount doesn't cover a single payment
env(pay(borrower, loanKeylet.key, STAmount{broker.asset, 1}),
env(pay(borrower,
loanKeylet.key,
STAmount{broker.asset, 1},
txFlags),
ter(tecINSUFFICIENT_PAYMENT));
// Get the balance after these failed transactions take
@@ -1379,10 +1386,11 @@ class Loan_test : public beast::unit_test::suite
loanKeylet.key,
STAmount{
broker.asset,
borrowerBalanceBeforePayment.number() * 2}),
borrowerBalanceBeforePayment.number() * 2},
txFlags),
ter(tecINSUFFICIENT_FUNDS));
env(pay(borrower, loanKeylet.key, transactionAmount));
env(pay(borrower, loanKeylet.key, transactionAmount, txFlags));
env.close();
@@ -1474,7 +1482,8 @@ class Loan_test : public beast::unit_test::suite
state,
payoffAmount,
1,
baseFlag);
baseFlag,
tfLoanFullPayment);
};
};
@@ -1506,7 +1515,8 @@ class Loan_test : public beast::unit_test::suite
state,
payoffAmount,
state.paymentRemaining,
baseFlag);
baseFlag,
0);
};
};
@@ -1676,7 +1686,7 @@ class Loan_test : public beast::unit_test::suite
testcase << "\tPayment components: "
<< "Payments remaining, rawInterest, rawPrincipal, "
"rawMFee, roundedInterest, roundedPrincipal, "
"roundedMFee, final, extra";
"roundedMFee, special";
auto const serviceFee = broker.asset(2);
@@ -1758,8 +1768,12 @@ class Loan_test : public beast::unit_test::suite
<< paymentComponents.roundedInterest << ", "
<< paymentComponents.roundedPrincipal << ", "
<< paymentComponents.roundedManagementFee << ", "
<< (paymentComponents.final ? "true" : "false") << ", "
<< (paymentComponents.extra ? "true" : "false");
<< (paymentComponents.specialCase == SpecialCase::final
? "final"
: paymentComponents.specialCase ==
SpecialCase::final
? "extra"
: "none");
auto const totalDueAmount = STAmount{
broker.asset,
@@ -1776,7 +1790,8 @@ class Loan_test : public beast::unit_test::suite
// IOUs, the difference should be after the 8th digit.
Number const diff = totalDue - totalDueAmount;
BEAST_EXPECT(
paymentComponents.final || diff == beast::zero ||
paymentComponents.specialCase == SpecialCase::final ||
diff == beast::zero ||
(diff > beast::zero &&
((broker.asset.integral() &&
(static_cast<Number>(diff) < 3)) ||
@@ -1803,11 +1818,11 @@ class Loan_test : public beast::unit_test::suite
paymentComponents.roundedPrincipal <=
state.principalOutstanding);
BEAST_EXPECT(
!paymentComponents.final ||
paymentComponents.specialCase != SpecialCase::final ||
paymentComponents.roundedPrincipal ==
state.principalOutstanding);
BEAST_EXPECT(
paymentComponents.final ||
paymentComponents.specialCase == SpecialCase::final ||
(state.periodicPayment.exponent() -
(paymentComponents.rawPrincipal +
paymentComponents.rawInterest +
@@ -1846,7 +1861,7 @@ class Loan_test : public beast::unit_test::suite
--state.paymentRemaining;
state.previousPaymentDate = state.nextPaymentDate;
if (paymentComponents.final)
if (paymentComponents.specialCase == SpecialCase::final)
{
state.paymentRemaining = 0;
}

View File

@@ -45,6 +45,8 @@ roundPeriodicPayment(
return roundToAsset(asset, periodicPayment, scale, Number::upward);
}
enum class SpecialCase { none, final, extra };
/// This structure is used internally to compute the breakdown of a
/// single loan payment
struct PaymentComponents
@@ -58,10 +60,7 @@ struct PaymentComponents
// periodic payment that goes toward the Broker's management fee, which is
// tracked by sfManagementFeeOutstanding
Number roundedManagementFee;
//// We may not need roundedPayment
// Number roundedPayment;
bool final = false;
bool extra = false;
SpecialCase specialCase = SpecialCase::none;
};
/// This structure is explained in the XLS-66 spec, section 3.2.4.4 (Failure
@@ -79,12 +78,8 @@ struct LoanPaymentParts
* This is 0 for regular payments.
*/
Number valueChange;
/// managementFeePaid is amount of fee that is tracked by
/// sfManagementFeeOutstanding
Number managementFeePaid;
/// extraFeePaid is the amount of fee that the payment covered not tracked
/// by sfManagementFeeOutstanding.
Number extraFeePaid;
/// feePaid is amount of fee that is paid to the broker
Number feePaid;
LoanPaymentParts&
operator+=(LoanPaymentParts const& other)
@@ -92,8 +87,7 @@ struct LoanPaymentParts
principalPaid += other.principalPaid;
interestPaid += other.interestPaid;
valueChange += other.valueChange;
extraFeePaid += other.extraFeePaid;
managementFeePaid += other.managementFeePaid;
feePaid += other.feePaid;
return *this;
}
};
@@ -509,40 +503,41 @@ doPayment(
nextDueDateProxy,
"ripple::detail::doPayment",
"Next due date proxy set");
auto const totalValueDelta = payment.roundedPrincipal +
payment.roundedInterest + payment.roundedManagementFee -
payment.valueChange;
if (!payment.extra)
if (payment.specialCase == SpecialCase::final)
{
if (payment.final)
XRPL_ASSERT_PARTS(
principalOutstandingProxy == payment.roundedPrincipal,
"ripple::detail::doPayment",
"Full principal payment");
XRPL_ASSERT_PARTS(
totalValueOutstandingProxy == totalValueDelta,
"ripple::detail::doPayment",
"Full value payment");
XRPL_ASSERT_PARTS(
managementFeeOutstandingProxy == payment.roundedManagementFee,
"ripple::detail::doPayment",
"Full management fee payment");
paymentRemainingProxy = 0;
prevPaymentDateProxy = *nextDueDateProxy;
// Remove the field. This is the only condition where nextDueDate is
// allowed to be removed.
nextDueDateProxy = std::nullopt;
// Always zero out the the tracked values on a final payment
principalOutstandingProxy = 0;
totalValueOutstandingProxy = 0;
managementFeeOutstandingProxy = 0;
}
else
{
if (payment.specialCase != SpecialCase::extra)
{
XRPL_ASSERT_PARTS(
principalOutstandingProxy == payment.roundedPrincipal,
"ripple::detail::doPayment",
"Full principal payment");
XRPL_ASSERT_PARTS(
totalValueOutstandingProxy == totalValueDelta,
"ripple::detail::doPayment",
"Full value payment");
paymentRemainingProxy = 0;
prevPaymentDateProxy = *nextDueDateProxy;
// Remove the field. This is the only condition where nextDueDate is
// allowed to be removed.
nextDueDateProxy = std::nullopt;
}
else
{
XRPL_ASSERT_PARTS(
principalOutstandingProxy > payment.roundedPrincipal,
"ripple::detail::doPayment",
"Partial principal payment");
XRPL_ASSERT_PARTS(
totalValueOutstandingProxy > totalValueDelta,
"ripple::detail::doPayment",
"Partial value payment");
paymentRemainingProxy -= 1;
prevPaymentDateProxy = *nextDueDateProxy;
@@ -550,17 +545,25 @@ doPayment(
// old-fashioned way.
nextDueDateProxy = *nextDueDateProxy + paymentInterval;
}
XRPL_ASSERT_PARTS(
principalOutstandingProxy > payment.roundedPrincipal,
"ripple::detail::doPayment",
"Partial principal payment");
XRPL_ASSERT_PARTS(
totalValueOutstandingProxy > totalValueDelta,
"ripple::detail::doPayment",
"Partial value payment");
// Management fees are expected to be relatively small, and could get to
// zero before the loan is paid off
XRPL_ASSERT_PARTS(
managementFeeOutstandingProxy >= payment.roundedManagementFee,
"ripple::detail::doPayment",
"Valid management fee");
}
principalOutstandingProxy -= payment.roundedPrincipal;
totalValueOutstandingProxy -= totalValueDelta;
managementFeeOutstandingProxy -= payment.roundedManagementFee;
principalOutstandingProxy -= payment.roundedPrincipal;
totalValueOutstandingProxy -= totalValueDelta;
managementFeeOutstandingProxy -= payment.roundedManagementFee;
}
XRPL_ASSERT_PARTS(
// Use an explicit cast because the template parameter can be
@@ -580,8 +583,9 @@ doPayment(
.principalPaid = payment.roundedPrincipal,
.interestPaid = payment.roundedInterest,
.valueChange = payment.valueChange,
.managementFeePaid = payment.roundedManagementFee,
.extraFeePaid = payment.extraFee};
// Now that the adjustments have been made, the fee parts can be
// combined
.feePaid = payment.roundedManagementFee + payment.extraFee};
}
// This function mainly exists to guarantee isolation of the "sandbox"
@@ -765,14 +769,11 @@ computeOverpayment(
"ripple::detail::computeOverpayment",
"value change matches");
XRPL_ASSERT_PARTS(
loanPaymentParts.extraFeePaid == overpaymentComponents.extraFee,
loanPaymentParts.feePaid ==
overpaymentComponents.extraFee +
overpaymentComponents.roundedManagementFee,
"ripple::detail::computeOverpayment",
"extra fee payment matches");
XRPL_ASSERT_PARTS(
loanPaymentParts.managementFeePaid ==
overpaymentComponents.roundedManagementFee,
"ripple::detail::computeOverpayment",
"management fee payment matches");
"fee payment matches");
// Update the loan object (via proxies)
totalValueOutstandingProxy = totalValueOutstanding;
@@ -885,9 +886,9 @@ computeLatePayment(
/* Handle possible full payments.
*
* If this function processed a full payment, the return value will be
* a PaymentComponentsPlus object. If the payment should not be considered as a
* full payment, the return will be an Unexpected(tesSUCCESS). Otherwise, it'll
* be an Unexpected with the error code the caller is expected to return.
* a PaymentComponentsPlus object. Otherwise, it'll be an Unexpected with the
* error code the caller is expected to return. It should NEVER return
* tesSUCCESS
*/
template <AssetType A>
Expected<PaymentComponentsPlus, TER>
@@ -912,7 +913,7 @@ computeFullPayment(
{
if (paymentRemaining <= 1)
// If this is the last payment, it has to be a regular payment
return Unexpected(tesSUCCESS);
return Unexpected(tecKILLED);
Number const rawPrincipalOutstanding = loanPrincipalFromPeriodicPayment(
periodicPayment, periodicRate, paymentRemaining);
@@ -930,22 +931,16 @@ computeFullPayment(
auto const [rawFullInterest, rawFullManagementFee] =
computeInterestAndFeeParts(fullPaymentInterest, managementFeeRate);
auto const
[roundedFullInterest, roundedFullManagementFee, roundedFullExtraFee] =
[&]() {
auto const interest =
roundToAsset(asset, fullPaymentInterest, loanScale);
auto const parts = computeInterestAndFeeParts(
asset, interest, managementFeeRate, loanScale);
// Apply as much of the fee to the outstanding fee, but no
// more
if (parts.second <= managementFeeOutstanding)
return std::make_tuple(parts.first, parts.second, Number{});
return std::make_tuple(
parts.first,
managementFeeOutstanding,
parts.second - managementFeeOutstanding);
}();
auto const [roundedFullInterest, roundedFullManagementFee] = [&]() {
auto const interest =
roundToAsset(asset, fullPaymentInterest, loanScale);
auto const parts = computeInterestAndFeeParts(
asset, interest, managementFeeRate, loanScale);
// Apply as much of the fee to the outstanding fee, but no
// more
return std::make_tuple(parts.first, parts.second);
}();
PaymentComponentsPlus const full{
PaymentComponents{
@@ -954,11 +949,16 @@ computeFullPayment(
.rawManagementFee = rawFullManagementFee,
.roundedInterest = roundedFullInterest,
.roundedPrincipal = principalOutstanding,
.roundedManagementFee = roundedFullManagementFee,
.final = true},
// A full payment pays the single close payment fee, plus whatever part
// of the computed management fee is not outstanding in the Loan
closePaymentFee + roundedFullExtraFee,
// to make the accounting work later, the tracked part of the fee
// must be paid in full
.roundedManagementFee = managementFeeOutstanding,
.specialCase = SpecialCase::final},
// A full payment pays the single close payment fee, plus the computed
// management fee part of the interest portion, but for tracking, the
// outstanding part is removed. That could make this value negative, but
// that's ok, because it's not used until it's recombined with
// roundedManagementFee.
closePaymentFee + roundedFullManagementFee - managementFeeOutstanding,
// A full payment decreases the value of the loan by the
// difference between the interest paid and the expected
// outstanding interest return
@@ -972,7 +972,7 @@ computeFullPayment(
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.
return Unexpected(tesSUCCESS);
return Unexpected(tecINSUFFICIENT_PAYMENT);
return full;
}
@@ -1038,8 +1038,7 @@ computePaymentComponents(
.roundedInterest = interest,
.roundedPrincipal = principalOutstanding,
.roundedManagementFee = managementFeeOutstanding,
//.roundedPayment = totalValueOutstanding,
.final = true};
.specialCase = SpecialCase::final};
}
/*
@@ -1137,7 +1136,6 @@ computePaymentComponents(
.roundedInterest = roundedInterest,
.roundedPrincipal = roundedPrincipal,
.roundedManagementFee = roundedFee,
//.roundedPayment = roundedPeriodicPayment
};
}
@@ -1409,6 +1407,112 @@ isRounded(A const& asset, Number const& value, std::int32_t scale)
roundToAsset(asset, value, scale, Number::upward);
}
template <AssetType A>
Expected<LoanPaymentParts, TER>
loanMakeFullPayment(
A 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
}
template <AssetType A>
Expected<LoanPaymentParts, TER>
loanMakePayment(
@@ -1524,46 +1628,6 @@ loanMakePayment(
// means nothing was done, so continue.
return Unexpected(latePaymentComponents.error());
// -------------------------------------------------------------
// 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());
// -------------------------------------------------------------
// regular periodic payment handling
@@ -1635,13 +1699,13 @@ loanMakePayment(
paymentInterval);
++numPayments;
if (nextPayment.final)
if (nextPayment.specialCase == SpecialCase::final)
break;
}
XRPL_ASSERT_PARTS(
totalParts.principalPaid + totalParts.interestPaid +
totalParts.extraFeePaid + totalParts.managementFeePaid ==
totalParts.feePaid ==
totalPaid,
"ripple::loanMakePayment",
"payment parts add up");
@@ -1649,10 +1713,6 @@ loanMakePayment(
totalParts.valueChange == 0,
"ripple::loanMakePayment",
"no value change");
XRPL_ASSERT_PARTS(
totalParts.extraFeePaid == periodic.extraFee * numPayments,
"ripple::loanMakePayment",
"fee parts add up");
// -------------------------------------------------------------
// overpayment handling
@@ -1675,8 +1735,6 @@ loanMakePayment(
Number const payment = overpayment - fee;
// TODO: Is the overpaymentInterestRate an APR or flat?
auto const [rawOverpaymentInterest, rawOverpaymentManagementFee] =
[&]() {
Number const interest =
@@ -1701,7 +1759,7 @@ loanMakePayment(
.roundedInterest = roundedOverpaymentInterest,
.roundedPrincipal = payment - roundedOverpaymentInterest,
.roundedManagementFee = 0,
.extra = true},
.specialCase = SpecialCase::extra},
fee,
roundedOverpaymentInterest};
@@ -1747,11 +1805,8 @@ loanMakePayment(
isRounded(asset, totalParts.valueChange, loanScale),
"ripple::loanMakePayment : loan value change rounded");
XRPL_ASSERT(
isRounded(asset, totalParts.extraFeePaid, loanScale),
"ripple::loanMakePayment : extra fee paid rounded");
XRPL_ASSERT(
isRounded(asset, totalParts.managementFeePaid, loanScale),
"ripple::loanMakePayment : management fee paid rounded");
isRounded(asset, totalParts.feePaid, loanScale),
"ripple::loanMakePayment : fee paid rounded");
return totalParts;
}

View File

@@ -55,6 +55,12 @@ XRPAmount
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)
// payment
return normalCost;
auto const paymentsPerFeeIncrement = 20;
// The fee is based on the potential number of payments, unless the loan is
@@ -99,21 +105,6 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx)
// This is definitely paying fewer than paymentsPerFeeIncrement payments
return normalCost;
if (auto const fullInterest = calculateFullPaymentInterest(
loanSle->at(sfPeriodicPayment),
loanPeriodicRate(
TenthBips32(loanSle->at(sfInterestRate)),
loanSle->at(sfPaymentInterval)),
loanSle->at(sfPaymentRemaining),
view.parentCloseTime(),
loanSle->at(sfPaymentInterval),
loanSle->at(sfPreviousPaymentDate),
loanSle->at(sfStartDate),
TenthBips32(loanSle->at(sfCloseInterestRate)));
amount > loanSle->at(sfPrincipalOutstanding) + fullInterest +
loanSle->at(sfClosePaymentFee))
return normalCost;
NumberRoundModeGuard mg(Number::downward);
// Figure out how many payments will be made
auto const numPaymentEstimate =
@@ -304,14 +295,23 @@ LoanPay::doApply()
LoanManage::unimpairLoan(view, loanSle, vaultSle, j_);
}
Expected<LoanPaymentParts, TER> paymentParts = loanMakePayment(
asset,
view,
loanSle,
brokerSle,
amount,
tx.isFlag(tfLoanOverpayment),
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_);
if (!paymentParts)
{
@@ -342,16 +342,12 @@ LoanPay::doApply()
"ripple::LoanPay::doApply",
"valid principal paid");
XRPL_ASSERT_PARTS(
paymentParts->extraFeePaid >= 0,
paymentParts->feePaid >= 0,
"ripple::LoanPay::doApply",
"valid fee paid");
XRPL_ASSERT_PARTS(
paymentParts->managementFeePaid >= 0,
"ripple::LoanPay::doApply",
"valid management fee paid");
if (paymentParts->principalPaid < 0 || paymentParts->interestPaid < 0 ||
paymentParts->extraFeePaid < 0 || paymentParts->managementFeePaid < 0)
paymentParts->feePaid < 0)
{
// LCOV_EXCL_START
JLOG(j_.fatal()) << "Loan payment computation returned invalid values.";
@@ -375,13 +371,12 @@ LoanPay::doApply()
auto const totalPaidToVaultForDebt =
totalPaidToVaultRaw - paymentParts->valueChange;
auto const totalPaidToBroker =
paymentParts->managementFeePaid + paymentParts->extraFeePaid;
auto const totalPaidToBroker = paymentParts->feePaid;
XRPL_ASSERT_PARTS(
(totalPaidToVaultRaw + totalPaidToBroker) ==
(paymentParts->principalPaid + paymentParts->interestPaid +
paymentParts->managementFeePaid + paymentParts->extraFeePaid),
paymentParts->feePaid),
"ripple::LoanPay::doApply",
"payments add up");