mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-29 07:25:51 +00:00
Continue progress updating the LoanHelpers
- May not build
This commit is contained in:
@@ -31,7 +31,9 @@ struct PreflightContext;
|
|||||||
bool
|
bool
|
||||||
checkLendingProtocolDependencies(PreflightContext const& ctx);
|
checkLendingProtocolDependencies(PreflightContext const& ctx);
|
||||||
|
|
||||||
struct PaymentParts
|
// This structure is used internally to compute the breakdown of a
|
||||||
|
// single loan payment
|
||||||
|
struct PaymentComponents
|
||||||
{
|
{
|
||||||
Number rawInterest;
|
Number rawInterest;
|
||||||
Number rawPrincipal;
|
Number rawPrincipal;
|
||||||
@@ -42,6 +44,25 @@ struct PaymentParts
|
|||||||
bool final = false;
|
bool final = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// This structure is explained in the XLS-66 spec, section 3.2.4.4 (Failure
|
||||||
|
// Conditions)
|
||||||
|
struct LoanPaymentParts
|
||||||
|
{
|
||||||
|
/// principal_paid is the amount of principal that the payment covered.
|
||||||
|
Number principalPaid;
|
||||||
|
/// interest_paid is the amount of interest that the payment covered.
|
||||||
|
Number interestPaid;
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
/// fee_paid is the amount of fee that the payment covered.
|
||||||
|
Number feeToPay;
|
||||||
|
};
|
||||||
|
|
||||||
namespace detail {
|
namespace detail {
|
||||||
// These functions should rarely be used directly. More often, the ultimate
|
// These functions should rarely be used directly. More often, the ultimate
|
||||||
// result needs to be roundToAsset'd.
|
// result needs to be roundToAsset'd.
|
||||||
@@ -79,14 +100,14 @@ loanAccruedInterest(
|
|||||||
std::uint32_t paymentInterval);
|
std::uint32_t paymentInterval);
|
||||||
|
|
||||||
inline Number
|
inline Number
|
||||||
minusManagementFee(Number const& value, TenthBips32 managementFeeRate)
|
minusFee(Number const& value, TenthBips32 managementFeeRate)
|
||||||
{
|
{
|
||||||
return tenthBipsOfValue(value, tenthBipsPerUnity - managementFeeRate);
|
return tenthBipsOfValue(value, tenthBipsPerUnity - managementFeeRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
template <AssetType A>
|
template <AssetType A>
|
||||||
PaymentParts
|
PaymentComponents
|
||||||
computePaymentParts(
|
computePaymentComponents(
|
||||||
A const& asset,
|
A const& asset,
|
||||||
std::int32_t scale,
|
std::int32_t scale,
|
||||||
Number const& totalValueOutstanding,
|
Number const& totalValueOutstanding,
|
||||||
@@ -103,7 +124,7 @@ computePaymentParts(
|
|||||||
XRPL_ASSERT_PARTS(
|
XRPL_ASSERT_PARTS(
|
||||||
isRounded(asset, totalValueOutstanding, scale) &&
|
isRounded(asset, totalValueOutstanding, scale) &&
|
||||||
isRounded(asset, principalOutstanding, scale),
|
isRounded(asset, principalOutstanding, scale),
|
||||||
"ripple::detail::computePaymentParts",
|
"ripple::detail::computePaymentComponents",
|
||||||
"Outstanding values are rounded");
|
"Outstanding values are rounded");
|
||||||
auto const roundedPeriodicPayment =
|
auto const roundedPeriodicPayment =
|
||||||
roundToAsset(asset, periodicPayment, scale, Number::upward);
|
roundToAsset(asset, periodicPayment, scale, Number::upward);
|
||||||
@@ -121,7 +142,7 @@ computePaymentParts(
|
|||||||
XRPL_ASSERT_PARTS(
|
XRPL_ASSERT_PARTS(
|
||||||
rawInterest + referencePrincipal ==
|
rawInterest + referencePrincipal ==
|
||||||
roundedInterest + principalOutstanding,
|
roundedInterest + principalOutstanding,
|
||||||
"ripple::detail::computePaymentParts",
|
"ripple::detail::computePaymentComponents",
|
||||||
"last payment is complete");
|
"last payment is complete");
|
||||||
|
|
||||||
Number const interest = totalValueOutstanding - principalOutstanding;
|
Number const interest = totalValueOutstanding - principalOutstanding;
|
||||||
@@ -130,7 +151,7 @@ computePaymentParts(
|
|||||||
.rawPrincipal = referencePrincipal,
|
.rawPrincipal = referencePrincipal,
|
||||||
.roundedInterest = roundedInterest,
|
.roundedInterest = roundedInterest,
|
||||||
.roundedPrincipal = principalOutstanding,
|
.roundedPrincipal = principalOutstanding,
|
||||||
.roundedPayment = roundedPeriodicPayment,
|
.roundedPayment = roundedInterest + principalOutstanding,
|
||||||
.final = true};
|
.final = true};
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
@@ -144,11 +165,11 @@ computePaymentParts(
|
|||||||
Number const rawPrincipal = periodicPayment - rawInterest;
|
Number const rawPrincipal = periodicPayment - rawInterest;
|
||||||
XRPL_ASSERT_PARTS(
|
XRPL_ASSERT_PARTS(
|
||||||
rawInterest >= 0,
|
rawInterest >= 0,
|
||||||
"ripple::detail::computePaymentParts",
|
"ripple::detail::computePaymentComponents",
|
||||||
"valid raw interest");
|
"valid raw interest");
|
||||||
XRPL_ASSERT_PARTS(
|
XRPL_ASSERT_PARTS(
|
||||||
rawPrincipal > 0 && rawPrincipal <= referencePrincipal,
|
rawPrincipal > 0 && rawPrincipal <= referencePrincipal,
|
||||||
"ripple::detail::computePaymentParts",
|
"ripple::detail::computePaymentComponents",
|
||||||
"valid raw principal");
|
"valid raw principal");
|
||||||
|
|
||||||
// if (count($A20), MIN(Z19, Z19 - FLOOR(AA19 - Y20, 1)), "")
|
// if (count($A20), MIN(Z19, Z19 - FLOOR(AA19 - Y20, 1)), "")
|
||||||
@@ -190,15 +211,15 @@ computePaymentParts(
|
|||||||
Number const roundedInterest = roundedPeriodicPayment - roundedPrincipal;
|
Number const roundedInterest = roundedPeriodicPayment - roundedPrincipal;
|
||||||
XRPL_ASSERT_PARTS(
|
XRPL_ASSERT_PARTS(
|
||||||
roundedInterest >= 0 && isRounded(asset, roundedInterest, scale),
|
roundedInterest >= 0 && isRounded(asset, roundedInterest, scale),
|
||||||
"ripple::detail::computePaymentParts",
|
"ripple::detail::computePaymentComponents",
|
||||||
"valid rounded interest");
|
"valid rounded interest");
|
||||||
XRPL_ASSERT_PARTS(
|
XRPL_ASSERT_PARTS(
|
||||||
roundedPrincipal >= 0 && roundedPrincipal <= principalOutstanding,
|
roundedPrincipal >= 0 && roundedPrincipal <= principalOutstanding,
|
||||||
"ripple::detail::computePaymentParts",
|
"ripple::detail::computePaymentComponents",
|
||||||
"valid rounded principal");
|
"valid rounded principal");
|
||||||
XRPL_ASSERT_PARTS(
|
XRPL_ASSERT_PARTS(
|
||||||
isRounded(asset, roundedPrincipal, scale),
|
isRounded(asset, roundedPrincipal, scale),
|
||||||
"ripple::detail::computePaymentParts",
|
"ripple::detail::computePaymentComponents",
|
||||||
"principal is rounded");
|
"principal is rounded");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -209,6 +230,229 @@ computePaymentParts(
|
|||||||
.roundedPayment = roundedPeriodicPayment};
|
.roundedPayment = roundedPeriodicPayment};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct PaymentComponentsPlus : public PaymentComponents
|
||||||
|
{
|
||||||
|
Number fee{0};
|
||||||
|
Number valueChange{0};
|
||||||
|
|
||||||
|
PaymentComponentsPlus(
|
||||||
|
PaymentComponents const& p,
|
||||||
|
Number f,
|
||||||
|
Number v = Number{})
|
||||||
|
: PaymentComponents(p), fee(f), valueChange(v)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class NumberProxy, class Int32Proxy>
|
||||||
|
LoanPaymentParts
|
||||||
|
doPayment(
|
||||||
|
PaymentComponentsPlus const& payment,
|
||||||
|
NumberProxy& totalValueOutstandingProxy,
|
||||||
|
NumberProxy& principalOutstandingProxy,
|
||||||
|
NumberProxy& referencePrincipalProxy,
|
||||||
|
Int32Proxy& paymentRemainingProxy,
|
||||||
|
Int32Proxy& prevPaymentDateProxy,
|
||||||
|
Int32Proxy& nextDueDateProxy,
|
||||||
|
std::uint32_t paymentInterval)
|
||||||
|
{
|
||||||
|
if (payment.final)
|
||||||
|
{
|
||||||
|
paymentRemainingProxy = 0;
|
||||||
|
XRPL_ASSERT_PARTS(
|
||||||
|
referencePrincipalProxy == payment.rawPrincipal,
|
||||||
|
"ripple::detail::doPayment",
|
||||||
|
"Full reference principal payment");
|
||||||
|
XRPL_ASSERT_PARTS(
|
||||||
|
principalOutstandingProxy == payment.roundedPrincipal,
|
||||||
|
"ripple::detail::doPayment",
|
||||||
|
"Full principal payment");
|
||||||
|
XRPL_ASSSERT_PARTS(
|
||||||
|
totalValueOutstandingProxy ==
|
||||||
|
payment.roundedPrincipal + payment.roundedInterest,
|
||||||
|
"ripple::detail::doPayment",
|
||||||
|
"Full value payment");
|
||||||
|
|
||||||
|
prevPaymentDateProxy = nextDueDateProxy;
|
||||||
|
// May as well...
|
||||||
|
nextDueDateProxy = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
paymentRemainingProxy -= 1;
|
||||||
|
|
||||||
|
prevPaymentDateProxy = nextDueDateProxy;
|
||||||
|
nextDueDateProxy += paymentInterval;
|
||||||
|
}
|
||||||
|
// A single payment always pays the same amount of principal. Only the
|
||||||
|
// interest and fees are extra for a late payment
|
||||||
|
referencePrincipalProxy -= payment.rawPrincipal;
|
||||||
|
principalOutstandingProxy -= payment.roundedPrincipal;
|
||||||
|
totalValueOutstandingProxy -=
|
||||||
|
payment.roundedPrincipal + payment.roundedInterest;
|
||||||
|
|
||||||
|
return LoanPaymentParts{
|
||||||
|
.principalPaid = payment.roundedPrincipal,
|
||||||
|
.interestPaid = payment.roundedInterest,
|
||||||
|
.valueChange = payment.valueChange,
|
||||||
|
.feeToPay = payment.fee};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Handle possible late payments.
|
||||||
|
*
|
||||||
|
* If this function processed a late payment, the return value will be
|
||||||
|
* a LoanPaymentParts object. If the loan is not late, the return will be an
|
||||||
|
* Unexpected(tesSUCCESS). Otherwise, it'll be an Unexpected with the error code
|
||||||
|
* the caller is expected to return.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* This function is an implementation of the XLS-66 spec, based on
|
||||||
|
* * section 3.2.4.3 (Transaction Pseudo-code), specifically the bit
|
||||||
|
* labeled "the payment is late"
|
||||||
|
* * section 3.2.4.1.2 (Late Payment)
|
||||||
|
*/
|
||||||
|
template <AssetType A, class NumberProxy, class Int32Proxy>
|
||||||
|
Expected<PaymentComponentsPlus, TER>
|
||||||
|
handleLatePayment(
|
||||||
|
A const& asset,
|
||||||
|
ApplyView& view,
|
||||||
|
NumberProxy& principalOutstandingProxy,
|
||||||
|
Int32Proxy& nextDueDateProxy,
|
||||||
|
PaymentComponentsPlus const& periodic,
|
||||||
|
TenthBips32 const lateInterestRate,
|
||||||
|
std::int32_t loanScale,
|
||||||
|
Number const& latePaymentFee,
|
||||||
|
STAmount const& amount,
|
||||||
|
beast::Journal j)
|
||||||
|
{
|
||||||
|
if (!hasExpired(view, nextDueDateProxy))
|
||||||
|
return Unexpected(tesSUCCESS);
|
||||||
|
|
||||||
|
// the payment is late
|
||||||
|
// Late payment interest is only the part of the interest that comes from
|
||||||
|
// being late, as computed by 3.2.4.1.2.
|
||||||
|
auto const latePaymentInterest = loanLatePaymentInterest(
|
||||||
|
asset,
|
||||||
|
principalOutstandingProxy,
|
||||||
|
lateInterestRate,
|
||||||
|
view.parentCloseTime(),
|
||||||
|
nextDueDateProxy,
|
||||||
|
loanScale);
|
||||||
|
XRPL_ASSERT(
|
||||||
|
latePaymentInterest >= 0,
|
||||||
|
"ripple::detail::handleLatePayment : valid late interest");
|
||||||
|
PaymentComponentsPlus const late{
|
||||||
|
PaymentComponents{
|
||||||
|
.rawInterest = periodic.rawInterest + latePaymentInterest,
|
||||||
|
.rawPrincipal = periodic.rawPrincipal,
|
||||||
|
.roundedInterest = periodic.roundedInterest + latePaymentInterest,
|
||||||
|
.roundedPrincipal = periodic.roundedPrincipal,
|
||||||
|
.roundedPayment = periodic.roundedPayment},
|
||||||
|
// A late payment pays both the normal fee, and the extra fee
|
||||||
|
periodic.fee + latePaymentFee,
|
||||||
|
// A late payment increases the value of the loan by the difference
|
||||||
|
// between periodic and late payment interest
|
||||||
|
latePaymentInterest};
|
||||||
|
auto const totalDue =
|
||||||
|
late.roundedPrincipal + late.roundedInterest + late.fee;
|
||||||
|
XRPL_ASSERT_PARTS(
|
||||||
|
isRounded(asset, totalDue, loanScale),
|
||||||
|
"ripple::detail::handleLatePayment",
|
||||||
|
"total due is rounded");
|
||||||
|
|
||||||
|
if (amount < totalDue)
|
||||||
|
{
|
||||||
|
JLOG(j.warn()) << "Late loan payment amount is insufficient. Due: "
|
||||||
|
<< totalDue << ", paid: " << amount;
|
||||||
|
return Unexpected(tecINSUFFICIENT_PAYMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return late;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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.
|
||||||
|
*/
|
||||||
|
template <AssetType A, class NumberProxy, class Int32Proxy>
|
||||||
|
Expected<PaymentComponentsPlus, TER>
|
||||||
|
handleFullPayment(
|
||||||
|
A const& asset,
|
||||||
|
ApplyView& view,
|
||||||
|
NumberProxy& principalOutstandingProxy,
|
||||||
|
NumberProxy& referencePrincipalProxy,
|
||||||
|
Int32Proxy& paymentRemainingProxy,
|
||||||
|
Int32Proxy& prevPaymentDateProxy,
|
||||||
|
std::uint32_t const startDate,
|
||||||
|
std::uint32_t const paymentInterval,
|
||||||
|
TenthBips32 const closeInterestRate,
|
||||||
|
std::int32_t loanScale,
|
||||||
|
Number const& totalInterestOutstanding,
|
||||||
|
Number const& periodicRate,
|
||||||
|
Number const& closePaymentFee,
|
||||||
|
STAmount const& amount,
|
||||||
|
beast::Journal j)
|
||||||
|
{
|
||||||
|
if (paymentRemainingProxy <= 1)
|
||||||
|
// If this is the last payment, it has to be a regular payment
|
||||||
|
return Unexpected(tesSUCCESS);
|
||||||
|
|
||||||
|
// If there is more than one payment remaining, see if enough was
|
||||||
|
// paid for a full payment
|
||||||
|
auto const accruedInterest = roundToAsset(
|
||||||
|
asset,
|
||||||
|
detail::loanAccruedInterest(
|
||||||
|
principalOutstandingProxy,
|
||||||
|
periodicRate,
|
||||||
|
view.parentCloseTime(),
|
||||||
|
startDate,
|
||||||
|
prevPaymentDateProxy,
|
||||||
|
paymentInterval),
|
||||||
|
loanScale);
|
||||||
|
XRPL_ASSERT(
|
||||||
|
accruedInterest >= 0,
|
||||||
|
"ripple::detail::handleFullPayment : valid accrued interest");
|
||||||
|
auto const prepaymentPenalty = roundToAsset(
|
||||||
|
asset,
|
||||||
|
tenthBipsOfValue(principalOutstandingProxy.value(), closeInterestRate),
|
||||||
|
loanScale);
|
||||||
|
XRPL_ASSERT(
|
||||||
|
prepaymentPenalty >= 0,
|
||||||
|
"ripple::detail::handleFullPayment : valid prepayment "
|
||||||
|
"interest");
|
||||||
|
auto const totalInterest = accruedInterest + prepaymentPenalty;
|
||||||
|
auto const closeFullPayment =
|
||||||
|
principalOutstandingProxy + totalInterest + closePaymentFee;
|
||||||
|
|
||||||
|
if (amount < closeFullPayment)
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Make a full payment
|
||||||
|
|
||||||
|
PaymentComponentsPlus const result{
|
||||||
|
PaymentComponents{
|
||||||
|
.rawInterest = principalOutstandingProxy + totalInterest -
|
||||||
|
referencePrincipalProxy,
|
||||||
|
.rawPrincipal = referencePrincipalProxy,
|
||||||
|
.roundedInterest = totalInterest,
|
||||||
|
.roundedPrincipal = principalOutstandingProxy,
|
||||||
|
.roundedPayment = closeFullPayment,
|
||||||
|
.final = true},
|
||||||
|
// A full payment only pays the single close payment fee
|
||||||
|
closePaymentFee,
|
||||||
|
// A full payment decreases the value of the loan by the
|
||||||
|
// difference between the interest paid and the expected
|
||||||
|
// outstanding interest return
|
||||||
|
totalInterest - totalInterestOutstanding};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
|
|
||||||
template <AssetType A>
|
template <AssetType A>
|
||||||
@@ -220,7 +464,7 @@ valueMinusFee(
|
|||||||
std::int32_t scale)
|
std::int32_t scale)
|
||||||
{
|
{
|
||||||
return roundToAsset(
|
return roundToAsset(
|
||||||
asset, detail::minusManagementFee(value, managementFeeRate), scale);
|
asset, detail::minusFee(value, managementFeeRate), scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LoanProperties
|
struct LoanProperties
|
||||||
@@ -272,7 +516,7 @@ computeLoanProperties(
|
|||||||
auto const firstPaymentPrincipal = [&]() {
|
auto const firstPaymentPrincipal = [&]() {
|
||||||
// Compute the unrounded parts for the first payment. Ensure that the
|
// Compute the unrounded parts for the first payment. Ensure that the
|
||||||
// principal payment will actually change the principal.
|
// principal payment will actually change the principal.
|
||||||
auto const paymentParts = detail::computePaymentParts(
|
auto const paymentComponents = detail::computePaymentComponents(
|
||||||
asset,
|
asset,
|
||||||
loanScale,
|
loanScale,
|
||||||
totalValueOutstanding,
|
totalValueOutstanding,
|
||||||
@@ -284,12 +528,13 @@ computeLoanProperties(
|
|||||||
|
|
||||||
// We only care about the unrounded principal part. It needs to be large
|
// We only care about the unrounded principal part. It needs to be large
|
||||||
// enough that it will affect the reference principal.
|
// enough that it will affect the reference principal.
|
||||||
auto const remaining = referencePrincipal - paymentParts.rawPrincipal;
|
auto const remaining =
|
||||||
|
referencePrincipal - paymentComponents.rawPrincipal;
|
||||||
if (remaining == referencePrincipal)
|
if (remaining == referencePrincipal)
|
||||||
// No change, so the first payment effectively pays no principal.
|
// No change, so the first payment effectively pays no principal.
|
||||||
// Whether that's a problem is left to the caller.
|
// Whether that's a problem is left to the caller.
|
||||||
return Number{0};
|
return Number{0};
|
||||||
return paymentParts.rawPrincipal;
|
return paymentComponents.rawPrincipal;
|
||||||
}();
|
}();
|
||||||
|
|
||||||
auto const interestOwedToVault = valueMinusFee(
|
auto const interestOwedToVault = valueMinusFee(
|
||||||
@@ -498,207 +743,8 @@ template <AssetType A>
|
|||||||
bool
|
bool
|
||||||
isRounded(A const& asset, Number const& value, std::int32_t scale)
|
isRounded(A const& asset, Number const& value, std::int32_t scale)
|
||||||
{
|
{
|
||||||
return roundToAsset(asset, value, scale, Number::downward) == value &&
|
return roundToAsset(asset, value, scale, Number::downward) ==
|
||||||
roundToAsset(asset, value, scale, Number::upward) == value;
|
roundToAsset(asset, value, scale, Number::upward);
|
||||||
}
|
|
||||||
|
|
||||||
// This structure is explained in the XLS-66 spec, section 3.2.4.4 (Failure
|
|
||||||
// Conditions)
|
|
||||||
struct LoanPaymentParts
|
|
||||||
{
|
|
||||||
/// principal_paid is the amount of principal that the payment covered.
|
|
||||||
Number principalPaid;
|
|
||||||
/// interest_paid is the amount of interest that the payment covered.
|
|
||||||
Number interestPaid;
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
/// fee_paid is the amount of fee that the payment covered.
|
|
||||||
Number feeToPay;
|
|
||||||
};
|
|
||||||
|
|
||||||
template <class NumberProxy, class Int32Proxy>
|
|
||||||
void
|
|
||||||
doPayment(
|
|
||||||
PaymentParts const& payment,
|
|
||||||
NumberProxy& totalValueOutstandingProxy,
|
|
||||||
NumberProxy& principalOutstandingProxy,
|
|
||||||
NumberProxy& referencePrincipalProxy,
|
|
||||||
Int32Proxy& paymentRemainingProxy,
|
|
||||||
Int32Proxy& prevPaymentDateProxy,
|
|
||||||
Int32Proxy& nextDueDateProxy,
|
|
||||||
std::uint32_t paymentInterval)
|
|
||||||
{
|
|
||||||
paymentRemainingProxy -= 1;
|
|
||||||
// A single payment always pays the same amount of principal. Only the
|
|
||||||
// interest and fees are extra for a late payment
|
|
||||||
referencePrincipalProxy -= payment.rawPrincipal;
|
|
||||||
principalOutstandingProxy -= payment.roundedPrincipal;
|
|
||||||
totalValueOutstandingProxy -=
|
|
||||||
payment.roundedPrincipal + payment.roundedInterest;
|
|
||||||
|
|
||||||
// Make sure this does an assignment
|
|
||||||
prevPaymentDateProxy = nextDueDateProxy;
|
|
||||||
nextDueDateProxy += paymentInterval;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Handle possible late payments.
|
|
||||||
*
|
|
||||||
* If this function processed a late payment, the return value will be
|
|
||||||
* a LoanPaymentParts object. If the loan is not late, the return will be an
|
|
||||||
* Unexpected(tesSUCCESS). Otherwise, it'll be an Unexpected with the error code
|
|
||||||
* the caller is expected to return.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* This function is an implementation of the XLS-66 spec, based on
|
|
||||||
* * section 3.2.4.3 (Transaction Pseudo-code), specifically the bit
|
|
||||||
* labeled "the payment is late"
|
|
||||||
* * section 3.2.4.1.2 (Late Payment)
|
|
||||||
*/
|
|
||||||
template <AssetType A, class NumberProxy, class Int32Proxy>
|
|
||||||
Expected<std::pair<PaymentParts, LoanPaymentParts>, TER>
|
|
||||||
handleLatePayment(
|
|
||||||
A const& asset,
|
|
||||||
ApplyView& view,
|
|
||||||
NumberProxy& principalOutstandingProxy,
|
|
||||||
Int32Proxy& nextDueDateProxy,
|
|
||||||
PaymentParts const& periodic,
|
|
||||||
TenthBips32 const lateInterestRate,
|
|
||||||
std::int32_t loanScale,
|
|
||||||
Number const& paymentFee,
|
|
||||||
Number const& latePaymentFee,
|
|
||||||
STAmount const& amount,
|
|
||||||
beast::Journal j)
|
|
||||||
{
|
|
||||||
if (!hasExpired(view, nextDueDateProxy))
|
|
||||||
return Unexpected(tesSUCCESS);
|
|
||||||
|
|
||||||
// the payment is late
|
|
||||||
// Late payment interest is only the part of the interest that comes from
|
|
||||||
// being late, as computed by 3.2.4.1.2.
|
|
||||||
auto const latePaymentInterest = loanLatePaymentInterest(
|
|
||||||
asset,
|
|
||||||
principalOutstandingProxy,
|
|
||||||
lateInterestRate,
|
|
||||||
view.parentCloseTime(),
|
|
||||||
nextDueDateProxy,
|
|
||||||
loanScale);
|
|
||||||
XRPL_ASSERT(
|
|
||||||
latePaymentInterest >= 0,
|
|
||||||
"ripple::handleLatePayment : valid late interest");
|
|
||||||
PaymentParts const late{
|
|
||||||
.rawInterest = periodic.rawInterest + latePaymentInterest,
|
|
||||||
.rawPrincipal = periodic.rawPrincipal,
|
|
||||||
.roundedInterest = periodic.roundedInterest + latePaymentInterest,
|
|
||||||
.roundedPrincipal = periodic.roundedPrincipal,
|
|
||||||
.roundedPayment = periodic.roundedPayment};
|
|
||||||
auto const fee = paymentFee + latePaymentFee;
|
|
||||||
auto const totalDue = late.roundedPrincipal + late.roundedInterest + fee;
|
|
||||||
XRPL_ASSERT_PARTS(
|
|
||||||
isRounded(asset, totalDue, loanScale),
|
|
||||||
"ripple::handleLatePayment",
|
|
||||||
"total due is rounded");
|
|
||||||
|
|
||||||
if (amount < totalDue)
|
|
||||||
{
|
|
||||||
JLOG(j.warn()) << "Late loan payment amount is insufficient. Due: "
|
|
||||||
<< totalDue << ", paid: " << amount;
|
|
||||||
return Unexpected(tecINSUFFICIENT_PAYMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A late payment increases the value of the loan by the difference
|
|
||||||
// between periodic and late payment interest
|
|
||||||
return std::make_pair(
|
|
||||||
late,
|
|
||||||
LoanPaymentParts{
|
|
||||||
.principalPaid = late.roundedPrincipal,
|
|
||||||
.interestPaid = late.roundedInterest,
|
|
||||||
.valueChange = latePaymentInterest,
|
|
||||||
.feeToPay = fee});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Handle possible full payments.
|
|
||||||
*
|
|
||||||
* If this function processed a full payment, the return value will be
|
|
||||||
* a LoanPaymentParts 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.
|
|
||||||
*/
|
|
||||||
template <AssetType A, class NumberProxy, class Int32Proxy>
|
|
||||||
Expected<LoanPaymentParts, TER>
|
|
||||||
handleFullPayment(
|
|
||||||
A const& asset,
|
|
||||||
ApplyView& view,
|
|
||||||
NumberProxy& principalOutstandingProxy,
|
|
||||||
Int32Proxy& paymentRemainingProxy,
|
|
||||||
Int32Proxy& prevPaymentDateProxy,
|
|
||||||
std::uint32_t const startDate,
|
|
||||||
std::uint32_t const paymentInterval,
|
|
||||||
TenthBips32 const closeInterestRate,
|
|
||||||
std::int32_t loanScale,
|
|
||||||
Number const& totalInterestOutstanding,
|
|
||||||
Number const& periodicRate,
|
|
||||||
Number const& closePaymentFee,
|
|
||||||
STAmount const& amount,
|
|
||||||
beast::Journal j)
|
|
||||||
{
|
|
||||||
if (paymentRemainingProxy <= 1)
|
|
||||||
// If this is the last payment, it has to be a regular payment
|
|
||||||
return Unexpected(tesSUCCESS);
|
|
||||||
|
|
||||||
// If there is more than one payment remaining, see if enough was
|
|
||||||
// paid for a full payment
|
|
||||||
auto const accruedInterest = roundToAsset(
|
|
||||||
asset,
|
|
||||||
detail::loanAccruedInterest(
|
|
||||||
principalOutstandingProxy,
|
|
||||||
periodicRate,
|
|
||||||
view.parentCloseTime(),
|
|
||||||
startDate,
|
|
||||||
prevPaymentDateProxy,
|
|
||||||
paymentInterval),
|
|
||||||
loanScale);
|
|
||||||
XRPL_ASSERT(
|
|
||||||
accruedInterest >= 0,
|
|
||||||
"ripple::handleFullPayment : valid accrued interest");
|
|
||||||
auto const prepaymentPenalty = roundToAsset(
|
|
||||||
asset,
|
|
||||||
tenthBipsOfValue(principalOutstandingProxy.value(), closeInterestRate),
|
|
||||||
loanScale);
|
|
||||||
XRPL_ASSERT(
|
|
||||||
prepaymentPenalty >= 0,
|
|
||||||
"ripple::handleFullPayment : valid prepayment "
|
|
||||||
"interest");
|
|
||||||
auto const totalInterest = accruedInterest + prepaymentPenalty;
|
|
||||||
auto const closeFullPayment =
|
|
||||||
principalOutstandingProxy + totalInterest + closePaymentFee;
|
|
||||||
|
|
||||||
if (amount < closeFullPayment)
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// Make a full payment
|
|
||||||
|
|
||||||
// A full payment decreases the value of the loan by the
|
|
||||||
// difference between the interest paid and the expected
|
|
||||||
// outstanding interest return
|
|
||||||
auto const valueChange = totalInterest - totalInterestOutstanding;
|
|
||||||
|
|
||||||
LoanPaymentParts const result{
|
|
||||||
.principalPaid = principalOutstandingProxy,
|
|
||||||
.interestPaid = totalInterest,
|
|
||||||
.valueChange = valueChange,
|
|
||||||
.feeToPay = closePaymentFee};
|
|
||||||
|
|
||||||
paymentRemainingProxy = 0;
|
|
||||||
principalOutstandingProxy = 0;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
template <AssetType A>
|
template <AssetType A>
|
||||||
@@ -766,19 +812,21 @@ loanMakePayment(
|
|||||||
|
|
||||||
view.update(loan);
|
view.update(loan);
|
||||||
|
|
||||||
auto const periodic = detail::computePaymentParts(
|
detail::PaymentComponentsPlus const periodic{
|
||||||
asset,
|
detail::computePaymentComponents(
|
||||||
loanScale,
|
asset,
|
||||||
totalValueOutstandingProxy,
|
loanScale,
|
||||||
principalOutstandingProxy,
|
totalValueOutstandingProxy,
|
||||||
referencePrincipalProxy,
|
principalOutstandingProxy,
|
||||||
periodicPayment,
|
referencePrincipalProxy,
|
||||||
periodicRate,
|
periodicPayment,
|
||||||
paymentRemainingProxy);
|
periodicRate,
|
||||||
|
paymentRemainingProxy),
|
||||||
|
serviceFee};
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// late payment handling
|
// late payment handling
|
||||||
if (auto const latePaymentParts = handleLatePayment(
|
if (auto const latePaymentComponents = detail::handleLatePayment(
|
||||||
asset,
|
asset,
|
||||||
view,
|
view,
|
||||||
principalOutstandingProxy,
|
principalOutstandingProxy,
|
||||||
@@ -786,13 +834,12 @@ loanMakePayment(
|
|||||||
periodic,
|
periodic,
|
||||||
lateInterestRate,
|
lateInterestRate,
|
||||||
loanScale,
|
loanScale,
|
||||||
serviceFee,
|
|
||||||
latePaymentFee,
|
latePaymentFee,
|
||||||
amount,
|
amount,
|
||||||
j))
|
j))
|
||||||
{
|
{
|
||||||
doPayment(
|
return doPayment(
|
||||||
latePaymentParts->first,
|
*latePaymentComponents,
|
||||||
totalValueOutstandingProxy,
|
totalValueOutstandingProxy,
|
||||||
principalOutstandingProxy,
|
principalOutstandingProxy,
|
||||||
referencePrincipalProxy,
|
referencePrincipalProxy,
|
||||||
@@ -800,21 +847,23 @@ loanMakePayment(
|
|||||||
prevPaymentDateProxy,
|
prevPaymentDateProxy,
|
||||||
nextDueDateProxy,
|
nextDueDateProxy,
|
||||||
paymentInterval);
|
paymentInterval);
|
||||||
|
|
||||||
return latePaymentParts->second;
|
|
||||||
}
|
}
|
||||||
else if (latePaymentParts.error())
|
else if (latePaymentComponents.error())
|
||||||
return Unexpected(latePaymentParts.error());
|
// error() will be the TER returned if a payment is not made. It will
|
||||||
|
// only evaluate to true if it's an error. Otherwise, tesSUCCESS means
|
||||||
|
// nothing was done, so continue.
|
||||||
|
return Unexpected(latePaymentComponents.error());
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// full payment handling
|
// full payment handling
|
||||||
auto const totalInterestOutstanding =
|
auto const totalInterestOutstanding =
|
||||||
totalValueOutstandingProxy - principalOutstandingProxy;
|
totalValueOutstandingProxy - principalOutstandingProxy;
|
||||||
|
|
||||||
if (auto const fullPaymentParts = handleFullPayment(
|
if (auto const fullPaymentComponents = detail::handleFullPayment(
|
||||||
asset,
|
asset,
|
||||||
view,
|
view,
|
||||||
principalOutstandingProxy,
|
principalOutstandingProxy,
|
||||||
|
referencePrincipalProxy,
|
||||||
paymentRemainingProxy,
|
paymentRemainingProxy,
|
||||||
prevPaymentDateProxy,
|
prevPaymentDateProxy,
|
||||||
startDate,
|
startDate,
|
||||||
@@ -826,91 +875,92 @@ loanMakePayment(
|
|||||||
closePaymentFee,
|
closePaymentFee,
|
||||||
amount,
|
amount,
|
||||||
j))
|
j))
|
||||||
return *fullPaymentParts;
|
return doPayment(
|
||||||
else if (fullPaymentParts.error())
|
*fullPaymentComponents,
|
||||||
return fullPaymentParts;
|
totalValueOutstandingProxy,
|
||||||
|
principalOutstandingProxy,
|
||||||
|
referencePrincipalProxy,
|
||||||
|
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 an error. Otherwise, tesSUCCESS means
|
||||||
|
// nothing was done, so continue.
|
||||||
|
return Unexpected(fullPaymentComponents.error());
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// regular periodic payment handling
|
// regular periodic payment handling
|
||||||
|
|
||||||
return Unexpected(temDISABLED);
|
|
||||||
#if LOANCOMPLETE
|
|
||||||
// if the payment is not late nor if it's a full payment, then it must
|
// if the payment is not late nor if it's a full payment, then it must
|
||||||
// be a periodic one, with possible overpayments
|
// be a periodic one, with possible overpayments
|
||||||
|
|
||||||
auto const totalDue = periodic.interest + periodic.principal + periodic.fee;
|
// This will keep a running total of what is actually paid, if the payment
|
||||||
|
// is sufficient for a single payment
|
||||||
|
Number totalPaid =
|
||||||
|
periodic.roundedInterest + periodic.roundedPrincipal + periodic.fee;
|
||||||
|
|
||||||
// TODO: Don't attempt to figure out the number of payments beforehand. Just
|
if (amount < totalPaid)
|
||||||
// loop over making payments until the `amount` is used up or the loan is
|
|
||||||
// paid off.
|
|
||||||
std::optional<NumberRoundModeGuard> mg(Number::downward);
|
|
||||||
std::int64_t const fullPeriodicPayments = [&]() {
|
|
||||||
std::int64_t const full{amount / totalDue};
|
|
||||||
return full < paymentRemainingProxy ? full : paymentRemainingProxy;
|
|
||||||
}();
|
|
||||||
mg.reset();
|
|
||||||
// Temporary asserts
|
|
||||||
XRPL_ASSERT(
|
|
||||||
amount >= totalDue || fullPeriodicPayments == 0,
|
|
||||||
"temp full periodic rounding");
|
|
||||||
XRPL_ASSERT(
|
|
||||||
amount < totalDue || fullPeriodicPayments >= 1,
|
|
||||||
"temp full periodic rounding");
|
|
||||||
|
|
||||||
if (fullPeriodicPayments < 1)
|
|
||||||
{
|
{
|
||||||
JLOG(j.warn()) << "Periodic loan payment amount is insufficient. Due: "
|
JLOG(j.warn()) << "Periodic loan payment amount is insufficient. Due: "
|
||||||
<< totalDue << ", paid: " << amount;
|
<< totalPaid << ", paid: " << amount;
|
||||||
return Unexpected(tecINSUFFICIENT_PAYMENT);
|
return Unexpected(tecINSUFFICIENT_PAYMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
nextDueDateProxy += paymentInterval * fullPeriodicPayments;
|
LoanPaymentParts totalParts = detail::doPayment(
|
||||||
prevPaymentDateProxy = nextDueDateProxy - paymentInterval;
|
periodic,
|
||||||
|
totalValueOutstandingProxy,
|
||||||
|
principalOutstandingProxy,
|
||||||
|
referencePrincipalProxy,
|
||||||
|
paymentRemainingProxy,
|
||||||
|
prevPaymentDateProxy,
|
||||||
|
nextDueDateProxy,
|
||||||
|
paymentInterval);
|
||||||
|
|
||||||
Number totalPrincipalPaid = 0;
|
while (totalPaid < amount && paymentRemainingProxy > 0)
|
||||||
Number totalInterestPaid = 0;
|
|
||||||
Number loanValueChange = 0;
|
|
||||||
|
|
||||||
std::optional<PaymentParts> future = periodic;
|
|
||||||
for (int i = 0; i < fullPeriodicPayments; ++i)
|
|
||||||
{
|
{
|
||||||
// Only do the work if we need to
|
// Try to make more payments
|
||||||
if (!future)
|
detail::PaymentComponentsPlus const nextPayment{
|
||||||
future = computePaymentParts(
|
detail::computePaymentComponents(
|
||||||
asset,
|
asset,
|
||||||
loanScale,
|
loanScale,
|
||||||
totalValueOutstandingProxy,
|
totalValueOutstandingProxy,
|
||||||
principalOutstandingProxy,
|
principalOutstandingProxy,
|
||||||
|
referencePrincipalProxy,
|
||||||
periodicPayment,
|
periodicPayment,
|
||||||
serviceFee,
|
|
||||||
periodicRate,
|
periodicRate,
|
||||||
paymentRemainingProxy);
|
paymentRemainingProxy),
|
||||||
|
periodic.fee};
|
||||||
XRPL_ASSERT(
|
XRPL_ASSERT(
|
||||||
future->interest <= periodic.interest,
|
nextPayment.rawInterest <= periodic.rawInterest,
|
||||||
"ripple::loanMakePayment : decreasing interest");
|
"ripple::loanMakePayment : decreasing interest");
|
||||||
XRPL_ASSERT(
|
XRPL_ASSERT(
|
||||||
future->principal >= periodic.principal,
|
nextPayment.rawPrincipal >= periodic.rawPrincipal,
|
||||||
"ripple::loanMakePayment : increasing principal");
|
"ripple::loanMakePayment : increasing principal");
|
||||||
|
|
||||||
totalPrincipalPaid += future->principal;
|
// the fee part doesn't change
|
||||||
totalInterestPaid += future->interest;
|
auto const due = nextPayment.roundedInterest +
|
||||||
paymentRemainingProxy -= 1;
|
nextPayment.roundedPrincipal + periodic.fee;
|
||||||
principalOutstandingProxy -= future->principal;
|
|
||||||
|
|
||||||
future.reset();
|
if (amount < totalPaid + due)
|
||||||
|
// We're done making payments.
|
||||||
|
break;
|
||||||
|
|
||||||
|
totalPaid += due;
|
||||||
|
totalParts += detail::doPayment(
|
||||||
|
nextPayment,
|
||||||
|
totalValueOutstandingProxy,
|
||||||
|
principalOutstandingProxy,
|
||||||
|
referencePrincipalProxy,
|
||||||
|
paymentRemainingProxy,
|
||||||
|
prevPaymentDateProxy,
|
||||||
|
nextDueDateProxy,
|
||||||
|
paymentInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
Number totalfeeToPay = serviceFee * fullPeriodicPayments;
|
return Unexpected(temDISABLED);
|
||||||
|
#if LOANCOMPLETE
|
||||||
Number const newInterest = loanTotalInterestOutstanding(
|
|
||||||
asset,
|
|
||||||
loanScale,
|
|
||||||
principalOutstandingProxy,
|
|
||||||
interestRate,
|
|
||||||
paymentInterval,
|
|
||||||
paymentRemainingProxy) +
|
|
||||||
totalInterestPaid;
|
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// overpayment handling
|
// overpayment handling
|
||||||
Number overpaymentInterestPortion = 0;
|
Number overpaymentInterestPortion = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user