Add tfLoanLatePayment flag; full payment is no longer a special case

- A regular payment that is late, or a tfLoanLatePayment that is not
  late will fail.
- Flags are mutually exclusive.
- Add a few interest computation shortcuts and overflow prevention
  checks that return 0 if there's no time to compute for.
This commit is contained in:
Ed Hennis
2025-11-10 23:17:42 -05:00
parent 8d22409ab5
commit 4396b77c4b
5 changed files with 260 additions and 244 deletions

View File

@@ -274,10 +274,11 @@ constexpr std::uint32_t const tfLoanOverpayment = 0x00010000;
// LoanPay exclusive flags:
// tfLoanFullPayment: True, indicates that the payment is
constexpr std::uint32_t const tfLoanFullPayment = 0x00020000;
constexpr std::uint32_t const tfLoanLatePayment = 0x00040000;
constexpr std::uint32_t const tfLoanSetMask = ~(tfUniversal |
tfLoanOverpayment);
constexpr std::uint32_t const tfLoanPayMask = ~(tfUniversal |
tfLoanOverpayment | tfLoanFullPayment);
tfLoanOverpayment | tfLoanFullPayment | tfLoanLatePayment);
// LoanManage flags:
constexpr std::uint32_t const tfLoanDefault = 0x00010000;

View File

@@ -1059,8 +1059,7 @@ protected:
// Make the payment
env(pay(borrower, loanKeylet.key, transactionAmount));
env.close(
d{(state.previousPaymentDate + state.nextPaymentDate) / 2});
env.close(d{state.paymentInterval / 2});
// Need to account for fees if the loan is in XRP
PrettyAmount adjustment = broker.asset(0);
@@ -2227,14 +2226,29 @@ protected:
(Number{15, -1} / loanPaymentsPerFeeIncrement + 1)}),
ter(temINVALID_FLAG));
}
// Try to send a payment marked as both full payment and
// overpayment. Do not include `txFlags`, so we don't duplicate the
// prior test transaction.
// Try to send a payment marked as multiple mutually exclusive
// payment types. Do not include `txFlags`, so we don't duplicate
// the prior test transaction.
env(pay(borrower,
loanKeylet.key,
broker.asset(state.periodicPayment * 2),
tfLoanLatePayment | tfLoanFullPayment),
ter(temINVALID_FLAG));
env(pay(borrower,
loanKeylet.key,
broker.asset(state.periodicPayment * 2),
tfLoanLatePayment | tfLoanOverpayment),
ter(temINVALID_FLAG));
env(pay(borrower,
loanKeylet.key,
broker.asset(state.periodicPayment * 2),
tfLoanOverpayment | tfLoanFullPayment),
ter(temINVALID_FLAG));
env(pay(borrower,
loanKeylet.key,
broker.asset(state.periodicPayment * 2),
tfLoanLatePayment | tfLoanOverpayment | tfLoanFullPayment),
ter(temINVALID_FLAG));
{
auto const otherAsset = broker.asset.raw() == assets[0].raw()
@@ -4562,7 +4576,11 @@ protected:
issuer, lender["IOU"](1'000), tfClearFreeze | tfClearDeepFreeze));
env.close();
env(pay(borrower, loanKeylet.key, debtMaximumRequest));
// The payment is late by this point
env(pay(borrower, loanKeylet.key, debtMaximumRequest), ter(tecEXPIRED));
env.close();
env(pay(
borrower, loanKeylet.key, debtMaximumRequest, tfLoanLatePayment));
env.close();
// preclaim: tecKILLED
@@ -6777,11 +6795,10 @@ public:
testPoC_UnsignedUnderflowOnFullPayAfterEarlyPeriodic();
testLoanCoverMinimumRoundingExploit();
#endif
testDustManipulation();
testIssuerLoan();
testDisabled();
testSelfLoan();
testIssuerLoan();
testLoanSet();
testLifecycle();
testServiceFeeOnBrokerDeepFreeze();
@@ -6806,6 +6823,7 @@ public:
testLoanNextPaymentDueDateOverflow();
testRequireAuth();
testDustManipulation();
testRIPD3831();
testRIPD3459();

View File

@@ -30,21 +30,24 @@ roundPeriodicPayment(
struct LoanPaymentParts
{
/// principal_paid is the amount of principal that the payment covered.
Number principalPaid;
Number principalPaid = numZero;
/// interest_paid is the amount of interest that the payment covered.
Number interestPaid;
Number interestPaid = numZero;
/**
* 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;
Number valueChange = numZero;
/// feePaid is amount of fee that is paid to the broker
Number feePaid;
Number feePaid = numZero;
LoanPaymentParts&
operator+=(LoanPaymentParts const& other);
bool
operator==(LoanPaymentParts const& other) const;
};
/** This structure describes the initial "computed" properties of a loan.
@@ -240,15 +243,11 @@ computeLoanProperties(
bool
isRounded(Asset const& asset, Number const& value, std::int32_t scale);
Expected<LoanPaymentParts, TER>
loanMakeFullPayment(
Asset const& asset,
ApplyView& view,
SLE::ref loan,
SLE::const_ref brokerSle,
STAmount const& amount,
bool const overpaymentAllowed,
beast::Journal j);
// Indicates what type of payment is being made.
// regular, late, and full are mutually exclusive.
// overpayment is an "add on" to a regular payment, and follows that path with
// potential extra work at the end.
enum class LoanPaymentType { regular = 0, late, full, overpayment };
Expected<LoanPaymentParts, TER>
loanMakePayment(
@@ -257,7 +256,7 @@ loanMakePayment(
SLE::ref loan,
SLE::const_ref brokerSle,
STAmount const& amount,
bool const overpaymentAllowed,
LoanPaymentType const paymentType,
beast::Journal j);
} // namespace ripple

View File

@@ -35,6 +35,14 @@ LoanPaymentParts::operator+=(LoanPaymentParts const& other)
return *this;
}
bool
LoanPaymentParts::operator==(LoanPaymentParts const& other) const
{
return principalPaid == other.principalPaid &&
interestPaid == other.interestPaid &&
valueChange == other.valueChange && feePaid == other.feePaid;
}
Number
loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval)
{
@@ -170,6 +178,12 @@ loanLatePaymentInterest(
*
* The spec is to be updated to base the duration on the next due date
*/
// If the payment is not late by any amount of time, then there's no late
// interest
if (parentCloseTime.time_since_epoch().count() <= nextPaymentDueDate)
return 0;
auto const secondsOverdue =
parentCloseTime.time_since_epoch().count() - nextPaymentDueDate;
@@ -193,6 +207,11 @@ loanAccruedInterest(
*/
auto const lastPaymentDate = std::max(prevPaymentDate, startDate);
// If the loan has been paid ahead, then "lastPaymentDate" is in the future,
// and no interest has accrued.
if (parentCloseTime.time_since_epoch().count() <= lastPaymentDate)
return 0;
auto const secondsSinceLastPayment =
parentCloseTime.time_since_epoch().count() - lastPaymentDate;
@@ -818,7 +837,7 @@ computeLatePayment(
beast::Journal j)
{
if (!hasExpired(view, nextDueDate))
return Unexpected(tesSUCCESS);
return Unexpected(tecTOO_SOON);
// the payment is late
// Late payment interest is only the part of the interest that comes
@@ -972,9 +991,19 @@ computeFullPayment(
"ripple::detail::computeFullPayment",
"total due is rounded");
JLOG(j.trace()) << "computeFullPayment result: periodicPayment: "
<< periodicPayment << ", periodicRate: " << periodicRate
<< ", paymentRemaining: " << paymentRemaining
<< ", rawPrincipalOutstanding: " << rawPrincipalOutstanding
<< ", fullPaymentInterest: " << fullPaymentInterest
<< ", roundedFullInterest: " << roundedFullInterest
<< ", roundedFullManagementFee: "
<< roundedFullManagementFee
<< ", untrackedInterest: " << full.untrackedInterest;
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.
// sufficient to be a full payment.
return Unexpected(tecINSUFFICIENT_PAYMENT);
return full;
@@ -1639,111 +1668,6 @@ computeLoanProperties(
.firstPaymentPrincipal = firstPaymentPrincipal};
}
Expected<LoanPaymentParts, TER>
loanMakeFullPayment(
Asset 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
}
Expected<LoanPaymentParts, TER>
loanMakePayment(
Asset const& asset,
@@ -1751,7 +1675,7 @@ loanMakePayment(
SLE::ref loan,
SLE::const_ref brokerSle,
STAmount const& amount,
bool const overpaymentAllowed,
LoanPaymentType const paymentType,
beast::Journal j)
{
/*
@@ -1785,16 +1709,14 @@ loanMakePayment(
std::int32_t const loanScale = loan->at(sfLoanScale);
TenthBips32 const interestRate{loan->at(sfInterestRate)};
TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)};
TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)};
Number const serviceFee = loan->at(sfLoanServiceFee);
Number const latePaymentFee = loan->at(sfLatePaymentFee);
TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
Number 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.
@@ -1810,7 +1732,7 @@ loanMakePayment(
view.update(loan);
detail::PaymentComponentsPlus const periodic{
detail::PaymentComponentsPlus periodic{
detail::computePaymentComponents(
asset,
loanScale,
@@ -1829,21 +1751,141 @@ loanMakePayment(
// -------------------------------------------------------------
// late payment handling
if (auto const latePaymentComponents = detail::computeLatePayment(
asset,
view,
principalOutstandingProxy,
*nextDueDateProxy,
periodic,
lateInterestRate,
loanScale,
latePaymentFee,
amount,
managementFeeRate,
j))
if (paymentType == LoanPaymentType::late)
{
return doPayment(
*latePaymentComponents,
TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)};
Number const latePaymentFee = loan->at(sfLatePaymentFee);
if (auto const latePaymentComponents = detail::computeLatePayment(
asset,
view,
principalOutstandingProxy,
*nextDueDateProxy,
periodic,
lateInterestRate,
loanScale,
latePaymentFee,
amount,
managementFeeRate,
j))
{
return doPayment(
*latePaymentComponents,
totalValueOutstandingProxy,
principalOutstandingProxy,
managementFeeOutstandingProxy,
paymentRemainingProxy,
prevPaymentDateProxy,
nextDueDateProxy,
paymentInterval);
}
else if (latePaymentComponents.error())
{
// error() will be the TER returned if a payment is not made. It
// will only evaluate to true if it's unsuccessful.
return Unexpected(latePaymentComponents.error());
}
// LCOV_EXCL_START
UNREACHABLE("ripple::loanMakePayment : invalid late payment result");
return Unexpected(tecINTERNAL);
// LCOV_EXCL_STOP
}
else if (hasExpired(view, nextDueDateProxy))
{
// If the payment is late, and the late flag was not set, it's not valid
JLOG(j.warn())
<< "Loan payment is overdue. Use the tfLoanLatePayment transaction "
"flag to make a late payment. Loan was created on "
<< startDate << ", prev payment due date is "
<< prevPaymentDateProxy << ", next payment due date is "
<< *nextDueDateProxy << ", ledger time is "
<< view.parentCloseTime().time_since_epoch().count();
return Unexpected(tecEXPIRED);
}
// -------------------------------------------------------------
// full payment handling
if (paymentType == LoanPaymentType::full)
{
TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)};
Number const closePaymentFee =
roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale);
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::loanMakePayment : invalid full payment result");
return Unexpected(tecINTERNAL);
// LCOV_EXCL_STOP
}
// -------------------------------------------------------------
// regular periodic payment handling
XRPL_ASSERT_PARTS(
paymentType == LoanPaymentType::regular ||
paymentType == LoanPaymentType::overpayment,
"ripple::loanMakePayment",
"regular payment type");
// This will keep a running total of what is actually paid, if the payment
// is sufficient for any payment
LoanPaymentParts totalParts;
Number totalPaid;
std::size_t numPayments = 0;
while (amount >= totalPaid + periodic.totalDue &&
paymentRemainingProxy > 0 &&
numPayments < loanMaximumPaymentsPerTransaction)
{
// Try to make more payments
XRPL_ASSERT_PARTS(
periodic.trackedPrincipalDelta >= 0,
"ripple::loanMakePayment",
"payment pays non-negative principal");
totalPaid += periodic.totalDue;
totalParts += detail::doPayment(
periodic,
totalValueOutstandingProxy,
principalOutstandingProxy,
managementFeeOutstandingProxy,
@@ -1851,47 +1893,19 @@ loanMakePayment(
prevPaymentDateProxy,
nextDueDateProxy,
paymentInterval);
}
else if (latePaymentComponents.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(latePaymentComponents.error());
++numPayments;
// -------------------------------------------------------------
// regular periodic payment handling
XRPL_ASSERT_PARTS(
(periodic.specialCase == detail::PaymentSpecialCase::final) ==
(paymentRemainingProxy == 0),
"ripple::loanMakePayment",
"final payment is the final payment");
// if the payment is not late nor if it's a full payment, then it must
// be a periodic one, with possible overpayments
// Don't compute the next payment if this was the last payment
if (periodic.specialCase == detail::PaymentSpecialCase::final)
break;
// This will keep a running total of what is actually paid, if the payment
// is sufficient for a single payment
Number totalPaid = periodic.totalDue;
if (amount < totalPaid)
{
JLOG(j.warn()) << "Periodic loan payment amount is insufficient. Due: "
<< totalPaid << ", paid: " << amount;
return Unexpected(tecINSUFFICIENT_PAYMENT);
}
LoanPaymentParts totalParts = detail::doPayment(
periodic,
totalValueOutstandingProxy,
principalOutstandingProxy,
managementFeeOutstandingProxy,
paymentRemainingProxy,
prevPaymentDateProxy,
nextDueDateProxy,
paymentInterval);
std::size_t numPayments = 1;
while (totalPaid < amount && paymentRemainingProxy > 0 &&
numPayments < loanMaximumPaymentsPerTransaction)
{
// Try to make more payments
detail::PaymentComponentsPlus const nextPayment{
periodic = detail::PaymentComponentsPlus{
detail::computePaymentComponents(
asset,
loanScale,
@@ -1903,40 +1917,13 @@ loanMakePayment(
paymentRemainingProxy,
managementFeeRate),
serviceFee};
XRPL_ASSERT_PARTS(
nextPayment.trackedPrincipalDelta >= 0,
"ripple::loanMakePayment",
"additional payment pays non-negative principal");
#if LOANCOMPLETE
XRPL_ASSERT(
nextPayment.rawInterest <= periodic.rawInterest,
"ripple::loanMakePayment : decreasing interest");
XRPL_ASSERT(
nextPayment.rawPrinicpal >= periodic.rawPrincipal,
"ripple::loanMakePayment : increasing principal");
#endif
}
if (amount < totalPaid + nextPayment.totalDue)
// We're done making payments.
break;
totalPaid += nextPayment.totalDue;
totalParts += detail::doPayment(
nextPayment,
totalValueOutstandingProxy,
principalOutstandingProxy,
managementFeeOutstandingProxy,
paymentRemainingProxy,
prevPaymentDateProxy,
nextDueDateProxy,
paymentInterval);
++numPayments;
XRPL_ASSERT_PARTS(
(nextPayment.specialCase == detail::PaymentSpecialCase::final) ==
(paymentRemainingProxy == 0),
"ripple::loanMakePayment",
"final payment is the final payment");
if (numPayments == 0)
{
JLOG(j.warn()) << "Regular loan payment amount is insufficient. Due: "
<< periodic.totalDue << ", paid: " << amount;
return Unexpected(tecINSUFFICIENT_PAYMENT);
}
XRPL_ASSERT_PARTS(
@@ -1952,8 +1939,9 @@ loanMakePayment(
// -------------------------------------------------------------
// overpayment handling
if (overpaymentAllowed && loan->isFlag(lsfLoanOverpayment) &&
paymentRemainingProxy > 0 && nextDueDateProxy && totalPaid < amount &&
if (paymentType == LoanPaymentType::overpayment &&
loan->isFlag(lsfLoanOverpayment) && paymentRemainingProxy > 0 &&
nextDueDateProxy && totalPaid < amount &&
numPayments < loanMaximumPaymentsPerTransaction)
{
TenthBips32 const overpaymentInterestRate{

View File

@@ -30,9 +30,21 @@ LoanPay::preflight(PreflightContext const& ctx)
if (ctx.tx[sfAmount] <= beast::zero)
return temBAD_AMOUNT;
// isFlag requires an exact match - all flags to be set - to return true.
if (ctx.tx.isFlag(tfLoanOverpayment | tfLoanFullPayment))
// The loan payment flags are all mutually exclusive. If more than one is
// set, the tx is malformed.
int flagsSet = 0;
for (auto const flag :
{tfLoanLatePayment, tfLoanFullPayment, tfLoanOverpayment})
{
if (ctx.tx.isFlag(flag))
++flagsSet;
}
if (flagsSet > 1)
{
JLOG(ctx.j.warn()) << "Only one LoanPay flag can be set per tx. "
<< flagsSet << " is too many.";
return temINVALID_FLAG;
}
return tesSUCCESS;
}
@@ -42,8 +54,8 @@ 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)
if (tx.isFlag(tfLoanFullPayment) || tx.isFlag(tfLoanLatePayment))
// The loan will be making one set of calculations for one full or late
// payment
return normalCost;
@@ -65,7 +77,8 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx)
}
if (hasExpired(view, loanSle->at(sfNextPaymentDueDate)))
// If the payment is late, it'll only make one payment
// If the payment is late, and the late payment flag is not set, it'll
// fail
return normalCost;
auto const brokerSle =
@@ -97,6 +110,7 @@ LoanPay::calculateBaseFee(ReadView const& view, STTx const& tx)
// Estimate how many payments will be made
Number const numPaymentEstimate =
static_cast<std::int64_t>(amount / regularPayment);
// Charge one base fee per paymentsPerFeeIncrement payments, rounding up.
Number::setround(Number::upward);
auto const feeIncrements = std::max(
@@ -291,23 +305,19 @@ LoanPay::doApply()
LoanManage::unimpairLoan(view, loanSle, vaultSle, j_);
}
Expected<LoanPaymentParts, TER> const paymentParts =
tx.isFlag(tfLoanFullPayment) ? loanMakeFullPayment(
asset,
view,
loanSle,
brokerSle,
amount,
tx.isFlag(tfLoanOverpayment),
j_)
: loanMakePayment(
asset,
view,
loanSle,
brokerSle,
amount,
tx.isFlag(tfLoanOverpayment),
j_);
LoanPaymentType const paymentType = [&tx]() {
// preflight already checked that at most one flag is set.
if (tx.isFlag(tfLoanLatePayment))
return LoanPaymentType::late;
if (tx.isFlag(tfLoanFullPayment))
return LoanPaymentType::full;
if (tx.isFlag(tfLoanOverpayment))
return LoanPaymentType::overpayment;
return LoanPaymentType::regular;
}();
Expected<LoanPaymentParts, TER> const paymentParts = loanMakePayment(
asset, view, loanSle, brokerSle, amount, paymentType, j_);
if (!paymentParts)
{