mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
documents overpayment
This commit is contained in:
@@ -353,10 +353,16 @@ doPayment(
|
|||||||
payment.trackedManagementFeeDelta + payment.untrackedManagementFee};
|
payment.trackedManagementFeeDelta + payment.untrackedManagementFee};
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function mainly exists to guarantee isolation of the "sandbox"
|
/* Simulates an overpayment to validate it won't break the loan's amortization.
|
||||||
// variables from the real / proxy variables that will affect actual
|
*
|
||||||
// ledger data in the caller.
|
* When a borrower pays more than the scheduled amount, the loan needs to be
|
||||||
|
* re-amortized with a lower principal. This function performs that calculation
|
||||||
|
* in a "sandbox" using temporary variables, allowing the caller to validate
|
||||||
|
* the result before committing changes to the actual ledger.
|
||||||
|
*
|
||||||
|
* The function preserves accumulated rounding errors across the re-amortization
|
||||||
|
* to ensure the loan state remains consistent with its payment history.
|
||||||
|
*/
|
||||||
Expected<LoanPaymentParts, TER>
|
Expected<LoanPaymentParts, TER>
|
||||||
tryOverpayment(
|
tryOverpayment(
|
||||||
Asset const& asset,
|
Asset const& asset,
|
||||||
@@ -375,18 +381,30 @@ tryOverpayment(
|
|||||||
TenthBips16 const managementFeeRate,
|
TenthBips16 const managementFeeRate,
|
||||||
beast::Journal j)
|
beast::Journal j)
|
||||||
{
|
{
|
||||||
|
// Calculate what the loan state SHOULD be theoretically (at full precision)
|
||||||
auto const raw = calculateRawLoanState(
|
auto const raw = calculateRawLoanState(
|
||||||
periodicPayment, periodicRate, paymentRemaining, managementFeeRate);
|
periodicPayment, periodicRate, paymentRemaining, managementFeeRate);
|
||||||
|
|
||||||
|
// Get the actual loan state (with accumulated rounding from past payments)
|
||||||
auto const rounded = constructRoundedLoanState(
|
auto const rounded = constructRoundedLoanState(
|
||||||
totalValueOutstanding, principalOutstanding, managementFeeOutstanding);
|
totalValueOutstanding, principalOutstanding, managementFeeOutstanding);
|
||||||
|
|
||||||
|
// Calculate the accumulated rounding errors. These need to be preserved
|
||||||
|
// across the re-amortization to maintain consistency with the loan's
|
||||||
|
// payment history. Without preserving these errors, the loan could end
|
||||||
|
// up with a different total value than what the borrower has actually paid.
|
||||||
auto const errors = rounded - raw;
|
auto const errors = rounded - raw;
|
||||||
|
|
||||||
|
// Compute the new principal by applying the overpayment to the raw
|
||||||
|
// (theoretical) principal. Use max with 0 to ensure we never go negative.
|
||||||
auto const newRawPrincipal = std::max(
|
auto const newRawPrincipal = std::max(
|
||||||
raw.principalOutstanding - overpaymentComponents.trackedPrincipalDelta,
|
raw.principalOutstanding - overpaymentComponents.trackedPrincipalDelta,
|
||||||
Number{0});
|
Number{0});
|
||||||
|
|
||||||
auto newLoanProperties = computeLoanProperties(
|
// Compute new loan properties based on the reduced principal. This
|
||||||
|
// recalculates the periodic payment, total value, and management fees
|
||||||
|
// for the remaining payment schedule.
|
||||||
|
auto const newLoanProperties = computeLoanProperties(
|
||||||
asset,
|
asset,
|
||||||
newRawPrincipal,
|
newRawPrincipal,
|
||||||
interestRate,
|
interestRate,
|
||||||
@@ -402,6 +420,7 @@ tryOverpayment(
|
|||||||
<< ", first payment principal: "
|
<< ", first payment principal: "
|
||||||
<< newLoanProperties.firstPaymentPrincipal;
|
<< newLoanProperties.firstPaymentPrincipal;
|
||||||
|
|
||||||
|
// Calculate what the new loan state should be with the new periodic payment
|
||||||
auto const newRaw = calculateRawLoanState(
|
auto const newRaw = calculateRawLoanState(
|
||||||
newLoanProperties.periodicPayment,
|
newLoanProperties.periodicPayment,
|
||||||
periodicRate,
|
periodicRate,
|
||||||
@@ -412,6 +431,9 @@ tryOverpayment(
|
|||||||
JLOG(j.debug()) << "new raw value: " << newRaw.valueOutstanding
|
JLOG(j.debug()) << "new raw value: " << newRaw.valueOutstanding
|
||||||
<< ", principal: " << newRaw.principalOutstanding
|
<< ", principal: " << newRaw.principalOutstanding
|
||||||
<< ", interest gross: " << newRaw.interestOutstanding();
|
<< ", interest gross: " << newRaw.interestOutstanding();
|
||||||
|
// Update the loan state variables with the new values PLUS the preserved
|
||||||
|
// rounding errors. This ensures the loan's tracked state remains
|
||||||
|
// consistent with its payment history.
|
||||||
|
|
||||||
principalOutstanding = std::clamp(
|
principalOutstanding = std::clamp(
|
||||||
roundToAsset(
|
roundToAsset(
|
||||||
@@ -440,6 +462,7 @@ tryOverpayment(
|
|||||||
<< ", principal: " << newRounded.principalOutstanding
|
<< ", principal: " << newRounded.principalOutstanding
|
||||||
<< ", interest gross: " << newRounded.interestOutstanding();
|
<< ", interest gross: " << newRounded.interestOutstanding();
|
||||||
|
|
||||||
|
// Update the periodic payment to reflect the re-amortized schedule
|
||||||
periodicPayment = newLoanProperties.periodicPayment;
|
periodicPayment = newLoanProperties.periodicPayment;
|
||||||
|
|
||||||
// check that the loan is still valid
|
// check that the loan is still valid
|
||||||
@@ -457,10 +480,12 @@ tryOverpayment(
|
|||||||
{
|
{
|
||||||
JLOG(j.warn()) << "Principal overpayment would cause the loan to be in "
|
JLOG(j.warn()) << "Principal overpayment would cause the loan to be in "
|
||||||
"an invalid state. Ignore the overpayment";
|
"an invalid state. Ignore the overpayment";
|
||||||
|
|
||||||
return Unexpected(tesSUCCESS);
|
return Unexpected(tesSUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that the other computed values are valid
|
// Validate that all computed properties are reasonable. These checks should
|
||||||
|
// never fail under normal circumstances, but we validate defensively.
|
||||||
if (newLoanProperties.periodicPayment <= 0 ||
|
if (newLoanProperties.periodicPayment <= 0 ||
|
||||||
newLoanProperties.totalValueOutstanding <= 0 ||
|
newLoanProperties.totalValueOutstanding <= 0 ||
|
||||||
newLoanProperties.managementFeeOwedToBroker < 0)
|
newLoanProperties.managementFeeOwedToBroker < 0)
|
||||||
@@ -483,6 +508,9 @@ tryOverpayment(
|
|||||||
auto const hypotheticalValueOutstanding =
|
auto const hypotheticalValueOutstanding =
|
||||||
rounded.valueOutstanding - deltas.principal;
|
rounded.valueOutstanding - deltas.principal;
|
||||||
|
|
||||||
|
// Calculate how the loan's value changed due to the overpayment.
|
||||||
|
// This should be negative (value decreased) or zero. A principal
|
||||||
|
// overpayment should never increase the loan's value.
|
||||||
auto const valueChange =
|
auto const valueChange =
|
||||||
newRounded.valueOutstanding - hypotheticalValueOutstanding;
|
newRounded.valueOutstanding - hypotheticalValueOutstanding;
|
||||||
if (valueChange > 0)
|
if (valueChange > 0)
|
||||||
@@ -493,15 +521,33 @@ tryOverpayment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return LoanPaymentParts{
|
return LoanPaymentParts{
|
||||||
|
// Principal paid is the reduction in principal outstanding
|
||||||
.principalPaid = deltas.principal,
|
.principalPaid = deltas.principal,
|
||||||
|
// Interest paid is the reduction in interest due
|
||||||
.interestPaid =
|
.interestPaid =
|
||||||
deltas.interest + overpaymentComponents.untrackedInterest,
|
deltas.interest + overpaymentComponents.untrackedInterest,
|
||||||
|
// Value change includes both the reduction from paying down principal
|
||||||
|
// (negative) and any untracked interest penalties (positive, e.g., if
|
||||||
|
// the overpayment itself incurs a fee)
|
||||||
.valueChange =
|
.valueChange =
|
||||||
valueChange + overpaymentComponents.trackedInterestPart(),
|
valueChange + overpaymentComponents.trackedInterestPart(),
|
||||||
|
// Fee paid includes both the reduction in tracked management fees and
|
||||||
|
// any untracked fees on the overpayment itself
|
||||||
.feePaid = deltas.managementFee +
|
.feePaid = deltas.managementFee +
|
||||||
overpaymentComponents.untrackedManagementFee};
|
overpaymentComponents.untrackedManagementFee};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Validates and applies an overpayment to the loan state.
|
||||||
|
*
|
||||||
|
* This function acts as a wrapper around tryOverpayment(), performing the
|
||||||
|
* re-amortization calculation in a sandbox (using temporary copies of the
|
||||||
|
* loan state), then validating the results before committing them to the
|
||||||
|
* actual ledger via the proxy objects.
|
||||||
|
*
|
||||||
|
* The two-step process (try in sandbox, then commit) ensures that if the
|
||||||
|
* overpayment would leave the loan in an invalid state, we can reject it
|
||||||
|
* gracefully without corrupting the ledger data.
|
||||||
|
*/
|
||||||
template <class NumberProxy>
|
template <class NumberProxy>
|
||||||
Expected<LoanPaymentParts, TER>
|
Expected<LoanPaymentParts, TER>
|
||||||
doOverpayment(
|
doOverpayment(
|
||||||
@@ -521,8 +567,9 @@ doOverpayment(
|
|||||||
TenthBips16 const managementFeeRate,
|
TenthBips16 const managementFeeRate,
|
||||||
beast::Journal j)
|
beast::Journal j)
|
||||||
{
|
{
|
||||||
// Use temp variables to do the payment, so they can be thrown away if
|
// Create temporary copies of the loan state that can be safely modified
|
||||||
// they don't work
|
// and discarded if the overpayment doesn't work out. This prevents
|
||||||
|
// corrupting the actual ledger data if validation fails.
|
||||||
Number totalValueOutstanding = totalValueOutstandingProxy;
|
Number totalValueOutstanding = totalValueOutstandingProxy;
|
||||||
Number principalOutstanding = principalOutstandingProxy;
|
Number principalOutstanding = principalOutstandingProxy;
|
||||||
Number managementFeeOutstanding = managementFeeOutstandingProxy;
|
Number managementFeeOutstanding = managementFeeOutstandingProxy;
|
||||||
@@ -539,6 +586,9 @@ doOverpayment(
|
|||||||
<< ", untrackedInterest: " << overpaymentComponents.untrackedInterest
|
<< ", untrackedInterest: " << overpaymentComponents.untrackedInterest
|
||||||
<< ", totalDue: " << overpaymentComponents.totalDue
|
<< ", totalDue: " << overpaymentComponents.totalDue
|
||||||
<< ", payments remaining :" << paymentRemaining;
|
<< ", payments remaining :" << paymentRemaining;
|
||||||
|
|
||||||
|
// Attempt to re-amortize the loan with the overpayment applied.
|
||||||
|
// This modifies the temporary copies, leaving the proxies unchanged.
|
||||||
auto const ret = tryOverpayment(
|
auto const ret = tryOverpayment(
|
||||||
asset,
|
asset,
|
||||||
loanScale,
|
loanScale,
|
||||||
@@ -560,6 +610,9 @@ doOverpayment(
|
|||||||
|
|
||||||
auto const& loanPaymentParts = *ret;
|
auto const& loanPaymentParts = *ret;
|
||||||
|
|
||||||
|
// Safety check: the principal must have decreased. If it didn't (or
|
||||||
|
// increased!), something went wrong in the calculation and we should
|
||||||
|
// reject the overpayment.
|
||||||
if (principalOutstandingProxy <= principalOutstanding)
|
if (principalOutstandingProxy <= principalOutstanding)
|
||||||
{
|
{
|
||||||
// LCOV_EXCL_START
|
// LCOV_EXCL_START
|
||||||
@@ -571,8 +624,10 @@ doOverpayment(
|
|||||||
// LCOV_EXCL_STOP
|
// LCOV_EXCL_STOP
|
||||||
}
|
}
|
||||||
|
|
||||||
// We haven't updated the proxies yet, so they still have the original
|
// The proxies still hold the original (pre-overpayment) values, which
|
||||||
// values. Use those to do some checks.
|
// allows us to compute deltas and verify they match what we expect
|
||||||
|
// from the overpaymentComponents and loanPaymentParts.
|
||||||
|
|
||||||
XRPL_ASSERT_PARTS(
|
XRPL_ASSERT_PARTS(
|
||||||
overpaymentComponents.trackedPrincipalDelta ==
|
overpaymentComponents.trackedPrincipalDelta ==
|
||||||
principalOutstandingProxy - principalOutstanding,
|
principalOutstandingProxy - principalOutstanding,
|
||||||
@@ -622,7 +677,8 @@ doOverpayment(
|
|||||||
"ripple::detail::doOverpayment",
|
"ripple::detail::doOverpayment",
|
||||||
"fee payment matches");
|
"fee payment matches");
|
||||||
|
|
||||||
// Update the loan object (via proxies)
|
// All validations passed, so update the proxy objects (which will
|
||||||
|
// modify the actual Loan ledger object)
|
||||||
totalValueOutstandingProxy = totalValueOutstanding;
|
totalValueOutstandingProxy = totalValueOutstanding;
|
||||||
principalOutstandingProxy = principalOutstanding;
|
principalOutstandingProxy = principalOutstanding;
|
||||||
managementFeeOutstandingProxy = managementFeeOutstanding;
|
managementFeeOutstandingProxy = managementFeeOutstanding;
|
||||||
|
|||||||
Reference in New Issue
Block a user