Continue progress updating the LoanHelpers

- May not build
This commit is contained in:
Ed Hennis
2025-10-04 18:07:28 -05:00
parent 96d0258f51
commit 97e2c10359

View File

@@ -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;