mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
Implement tfLoanFullPayment; use updated full payment calculations
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user