diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index f74039ccfd..dbd7ac5b0e 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -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; diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 45a076b2ed..c42367332c 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -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(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; } diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index b63cdcb505..2c6bc16052 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -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 Expected @@ -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 +Expected +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 Expected 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; } diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index edbe192632..f87346f299 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -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 paymentParts = loanMakePayment( - asset, - view, - loanSle, - brokerSle, - amount, - tx.isFlag(tfLoanOverpayment), - j_); + Expected 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");