#include // DO NOT REMOVE #include #include #include #include #include #include #include #include #include #include #include namespace xrpl { namespace test { class LendingHelpers_test : public beast::unit_test::suite { void testComputeRaisedRate() { using namespace jtx; using namespace xrpl::detail; struct TestCase { std::string name; Number periodicRate; std::uint32_t paymentsRemaining; Number expectedRaisedRate; }; auto const testCases = std::vector{ { .name = "Zero payments remaining", .periodicRate = Number{5, -2}, .paymentsRemaining = 0, .expectedRaisedRate = Number{1}, // (1 + r)^0 = 1 }, { .name = "One payment remaining", .periodicRate = Number{5, -2}, .paymentsRemaining = 1, .expectedRaisedRate = Number{105, -2}, }, // 1.05^1 { .name = "Multiple payments remaining", .periodicRate = Number{5, -2}, .paymentsRemaining = 3, .expectedRaisedRate = Number{1157625, -6}, }, // 1.05^3 { .name = "Zero periodic rate", .periodicRate = Number{0}, .paymentsRemaining = 5, .expectedRaisedRate = Number{1}, // (1 + 0)^5 = 1 }}; for (auto const& tc : testCases) { testcase("computeRaisedRate: " + tc.name); auto const computedRaisedRate = computeRaisedRate(tc.periodicRate, tc.paymentsRemaining); BEAST_EXPECTS( computedRaisedRate == tc.expectedRaisedRate, "Raised rate mismatch: expected " + to_string(tc.expectedRaisedRate) + ", got " + to_string(computedRaisedRate)); } } void testComputePaymentFactor() { using namespace jtx; using namespace xrpl::detail; struct TestCase { std::string name; Number periodicRate; std::uint32_t paymentsRemaining; Number expectedPaymentFactor; }; auto const testCases = std::vector{ { .name = "Zero periodic rate", .periodicRate = Number{0}, .paymentsRemaining = 4, .expectedPaymentFactor = Number{25, -2}, }, // 1/4 = 0.25 { .name = "One payment remaining", .periodicRate = Number{5, -2}, .paymentsRemaining = 1, .expectedPaymentFactor = Number{105, -2}, }, // 0.05/1 = 1.05 { .name = "Multiple payments remaining", .periodicRate = Number{5, -2}, .paymentsRemaining = 3, .expectedPaymentFactor = Number{3672085646312450436, -19}, }, // from calc { .name = "Zero payments remaining", .periodicRate = Number{5, -2}, .paymentsRemaining = 0, .expectedPaymentFactor = Number{0}, } // edge case }; for (auto const& tc : testCases) { testcase("computePaymentFactor: " + tc.name); auto const computedPaymentFactor = computePaymentFactor(tc.periodicRate, tc.paymentsRemaining); BEAST_EXPECTS( computedPaymentFactor == tc.expectedPaymentFactor, "Payment factor mismatch: expected " + to_string(tc.expectedPaymentFactor) + ", got " + to_string(computedPaymentFactor)); } } void testLoanPeriodicPayment() { using namespace jtx; using namespace xrpl::detail; struct TestCase { std::string name; Number principalOutstanding; Number periodicRate; std::uint32_t paymentsRemaining; Number expectedPeriodicPayment; }; auto const testCases = std::vector{ { .name = "Zero principal outstanding", .principalOutstanding = Number{0}, .periodicRate = Number{5, -2}, .paymentsRemaining = 5, .expectedPeriodicPayment = Number{0}, }, { .name = "Zero payments remaining", .principalOutstanding = Number{1'000}, .periodicRate = Number{5, -2}, .paymentsRemaining = 0, .expectedPeriodicPayment = Number{0}, }, { .name = "Zero periodic rate", .principalOutstanding = Number{1'000}, .periodicRate = Number{0}, .paymentsRemaining = 4, .expectedPeriodicPayment = Number{250}, }, { .name = "Standard case", .principalOutstanding = Number{1'000}, .periodicRate = loanPeriodicRate(TenthBips32(100'000), 30 * 24 * 60 * 60), .paymentsRemaining = 3, .expectedPeriodicPayment = Number{389569066396123265, -15}, // from calc }, }; for (auto const& tc : testCases) { testcase("loanPeriodicPayment: " + tc.name); auto const computedPeriodicPayment = loanPeriodicPayment(tc.principalOutstanding, tc.periodicRate, tc.paymentsRemaining); BEAST_EXPECTS( computedPeriodicPayment == tc.expectedPeriodicPayment, "Periodic payment mismatch: expected " + to_string(tc.expectedPeriodicPayment) + ", got " + to_string(computedPeriodicPayment)); } } void testLoanPrincipalFromPeriodicPayment() { using namespace jtx; using namespace xrpl::detail; struct TestCase { std::string name; Number periodicPayment; Number periodicRate; std::uint32_t paymentsRemaining; Number expectedPrincipalOutstanding; }; auto const testCases = std::vector{ { .name = "Zero periodic payment", .periodicPayment = Number{0}, .periodicRate = Number{5, -2}, .paymentsRemaining = 5, .expectedPrincipalOutstanding = Number{0}, }, { .name = "Zero payments remaining", .periodicPayment = Number{1'000}, .periodicRate = Number{5, -2}, .paymentsRemaining = 0, .expectedPrincipalOutstanding = Number{0}, }, { .name = "Zero periodic rate", .periodicPayment = Number{250}, .periodicRate = Number{0}, .paymentsRemaining = 4, .expectedPrincipalOutstanding = Number{1'000}, }, { .name = "Standard case", .periodicPayment = Number{389569066396123265, -15}, // from calc .periodicRate = loanPeriodicRate(TenthBips32(100'000), 30 * 24 * 60 * 60), .paymentsRemaining = 3, .expectedPrincipalOutstanding = Number{1'000}, }, }; for (auto const& tc : testCases) { testcase("loanPrincipalFromPeriodicPayment: " + tc.name); auto const computedPrincipalOutstanding = loanPrincipalFromPeriodicPayment( tc.periodicPayment, tc.periodicRate, tc.paymentsRemaining); BEAST_EXPECTS( computedPrincipalOutstanding == tc.expectedPrincipalOutstanding, "Principal outstanding mismatch: expected " + to_string(tc.expectedPrincipalOutstanding) + ", got " + to_string(computedPrincipalOutstanding)); } } void testComputeOverpaymentComponents() { testcase("computeOverpaymentComponents"); using namespace jtx; using namespace xrpl::detail; Account const issuer{"issuer"}; PrettyAsset const IOU = issuer["IOU"]; int32_t const loanScale = 1; auto const overpayment = Number{1'000}; auto const overpaymentInterestRate = TenthBips32{10'000}; // 10% auto const overpaymentFeeRate = TenthBips32{50'000}; // 50% auto const managementFeeRate = TenthBips16{10'000}; // 10% auto const expectedOverpaymentFee = Number{500}; // 50% of 1,000 auto const expectedOverpaymentInterestGross = Number{100}; // 10% of 1,000 auto const expectedOverpaymentInterestNet = Number{90}; // 100 - 10% of 100 auto const expectedOverpaymentManagementFee = Number{10}; // 10% of 100 auto const expectedPrincipalPortion = Number{400}; // 1,000 - 100 - 500 auto const components = detail::computeOverpaymentComponents( IOU, loanScale, overpayment, overpaymentInterestRate, overpaymentFeeRate, managementFeeRate); BEAST_EXPECT(components.untrackedManagementFee == expectedOverpaymentFee); BEAST_EXPECT(components.untrackedInterest == expectedOverpaymentInterestNet); BEAST_EXPECT(components.trackedInterestPart() == expectedOverpaymentInterestNet); BEAST_EXPECT(components.trackedManagementFeeDelta == expectedOverpaymentManagementFee); BEAST_EXPECT(components.trackedPrincipalDelta == expectedPrincipalPortion); BEAST_EXPECT( components.trackedManagementFeeDelta + components.untrackedInterest == expectedOverpaymentInterestGross); BEAST_EXPECT( components.trackedManagementFeeDelta + components.untrackedInterest + components.trackedPrincipalDelta + components.untrackedManagementFee == overpayment); } void testComputeInterestAndFeeParts() { using namespace jtx; using namespace xrpl::detail; struct TestCase { std::string name; Number interest; TenthBips16 managementFeeRate; Number expectedInterestPart; Number expectedFeePart; }; Account const issuer{"issuer"}; PrettyAsset const IOU = issuer["IOU"]; std::int32_t const loanScale = 1; auto const testCases = std::vector{ {.name = "Zero interest", .interest = Number{0}, .managementFeeRate = TenthBips16{10'000}, .expectedInterestPart = Number{0}, .expectedFeePart = Number{0}}, {.name = "Zero fee rate", .interest = Number{1'000}, .managementFeeRate = TenthBips16{0}, .expectedInterestPart = Number{1'000}, .expectedFeePart = Number{0}}, {.name = "10% fee rate", .interest = Number{1'000}, .managementFeeRate = TenthBips16{10'000}, .expectedInterestPart = Number{900}, .expectedFeePart = Number{100}}, }; for (auto const& tc : testCases) { testcase("computeInterestAndFeeParts: " + tc.name); auto const [computedInterestPart, computedFeePart] = computeInterestAndFeeParts(IOU, tc.interest, tc.managementFeeRate, loanScale); BEAST_EXPECTS( computedInterestPart == tc.expectedInterestPart, "Interest part mismatch: expected " + to_string(tc.expectedInterestPart) + ", got " + to_string(computedInterestPart)); BEAST_EXPECTS( computedFeePart == tc.expectedFeePart, "Fee part mismatch: expected " + to_string(tc.expectedFeePart) + ", got " + to_string(computedFeePart)); } } void testLoanLatePaymentInterest() { using namespace jtx; using namespace xrpl::detail; struct TestCase { std::string name; Number principalOutstanding; TenthBips32 lateInterestRate; NetClock::time_point parentCloseTime; std::uint32_t nextPaymentDueDate; Number expectedLateInterest; }; auto const testCases = std::vector{ { .name = "On-time payment", .principalOutstanding = Number{1'000}, .lateInterestRate = TenthBips32{10'000}, // 10% .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}}, .nextPaymentDueDate = 3'000, .expectedLateInterest = Number{0}, }, { .name = "Early payment", .principalOutstanding = Number{1'000}, .lateInterestRate = TenthBips32{10'000}, // 10% .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}}, .nextPaymentDueDate = 4'000, .expectedLateInterest = Number{0}, }, { .name = "No principal outstanding", .principalOutstanding = Number{0}, .lateInterestRate = TenthBips32{10'000}, // 10% .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}}, .nextPaymentDueDate = 2'000, .expectedLateInterest = Number{0}, }, { .name = "No late interest rate", .principalOutstanding = Number{1'000}, .lateInterestRate = TenthBips32{0}, // 0% .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}}, .nextPaymentDueDate = 2'000, .expectedLateInterest = Number{0}, }, { .name = "Late payment", .principalOutstanding = Number{1'000}, .lateInterestRate = TenthBips32{100'000}, // 100% .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}}, .nextPaymentDueDate = 2'000, .expectedLateInterest = Number{317097919837645865, -19}, // from calc }, }; for (auto const& tc : testCases) { testcase("loanLatePaymentInterest: " + tc.name); auto const computedLateInterest = loanLatePaymentInterest( tc.principalOutstanding, tc.lateInterestRate, tc.parentCloseTime, tc.nextPaymentDueDate); BEAST_EXPECTS( computedLateInterest == tc.expectedLateInterest, "Late interest mismatch: expected " + to_string(tc.expectedLateInterest) + ", got " + to_string(computedLateInterest)); } } void testLoanAccruedInterest() { using namespace jtx; using namespace xrpl::detail; struct TestCase { std::string name; Number principalOutstanding; Number periodicRate; NetClock::time_point parentCloseTime; std::uint32_t startDate; std::uint32_t prevPaymentDate; std::uint32_t paymentInterval; Number expectedAccruedInterest; }; auto const testCases = std::vector{ { .name = "Zero principal outstanding", .principalOutstanding = Number{0}, .periodicRate = Number{5, -2}, .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}}, .startDate = 2'000, .prevPaymentDate = 2'500, .paymentInterval = 30 * 24 * 60 * 60, .expectedAccruedInterest = Number{0}, }, { .name = "Before start date", .principalOutstanding = Number{1'000}, .periodicRate = Number{5, -2}, .parentCloseTime = NetClock::time_point{NetClock::duration{1'000}}, .startDate = 2'000, .prevPaymentDate = 1'500, .paymentInterval = 30 * 24 * 60 * 60, .expectedAccruedInterest = Number{0}, }, { .name = "Zero periodic rate", .principalOutstanding = Number{1'000}, .periodicRate = Number{0}, .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}}, .startDate = 2'000, .prevPaymentDate = 2'500, .paymentInterval = 30 * 24 * 60 * 60, .expectedAccruedInterest = Number{0}, }, { .name = "Zero payment interval", .principalOutstanding = Number{1'000}, .periodicRate = Number{5, -2}, .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}}, .startDate = 2'000, .prevPaymentDate = 2'500, .paymentInterval = 0, .expectedAccruedInterest = Number{0}, }, { .name = "Standard case", .principalOutstanding = Number{1'000}, .periodicRate = Number{5, -2}, .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}}, .startDate = 1'000, .prevPaymentDate = 2'000, .paymentInterval = 30 * 24 * 60 * 60, .expectedAccruedInterest = Number{1929012345679012346, -20}, // from calc }, }; for (auto const& tc : testCases) { testcase("loanAccruedInterest: " + tc.name); auto const computedAccruedInterest = loanAccruedInterest( tc.principalOutstanding, tc.periodicRate, tc.parentCloseTime, tc.startDate, tc.prevPaymentDate, tc.paymentInterval); BEAST_EXPECTS( computedAccruedInterest == tc.expectedAccruedInterest, "Accrued interest mismatch: expected " + to_string(tc.expectedAccruedInterest) + ", got " + to_string(computedAccruedInterest)); } } // This test overlaps with testLoanAccruedInterest, the test cases only // exercise the computeFullPaymentInterest parts unique to it. void testComputeFullPaymentInterest() { using namespace jtx; using namespace xrpl::detail; struct TestCase { std::string name; Number rawPrincipalOutstanding; Number periodicRate; NetClock::time_point parentCloseTime; std::uint32_t paymentInterval; std::uint32_t prevPaymentDate; std::uint32_t startDate; TenthBips32 closeInterestRate; Number expectedFullPaymentInterest; }; auto const testCases = std::vector{ { .name = "Zero principal outstanding", .rawPrincipalOutstanding = Number{0}, .periodicRate = Number{5, -2}, .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}}, .paymentInterval = 30 * 24 * 60 * 60, .prevPaymentDate = 2'000, .startDate = 1'000, .closeInterestRate = TenthBips32{10'000}, .expectedFullPaymentInterest = Number{0}, }, { .name = "Zero close interest rate", .rawPrincipalOutstanding = Number{1'000}, .periodicRate = Number{5, -2}, .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}}, .paymentInterval = 30 * 24 * 60 * 60, .prevPaymentDate = 2'000, .startDate = 1'000, .closeInterestRate = TenthBips32{0}, .expectedFullPaymentInterest = Number{1929012345679012346, -20}, // from calc }, { .name = "Standard case", .rawPrincipalOutstanding = Number{1'000}, .periodicRate = Number{5, -2}, .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}}, .paymentInterval = 30 * 24 * 60 * 60, .prevPaymentDate = 2'000, .startDate = 1'000, .closeInterestRate = TenthBips32{10'000}, .expectedFullPaymentInterest = Number{1000192901234567901, -16}, // from calc }, }; for (auto const& tc : testCases) { testcase("computeFullPaymentInterest: " + tc.name); auto const computedFullPaymentInterest = computeFullPaymentInterest( tc.rawPrincipalOutstanding, tc.periodicRate, tc.parentCloseTime, tc.paymentInterval, tc.prevPaymentDate, tc.startDate, tc.closeInterestRate); BEAST_EXPECTS( computedFullPaymentInterest == tc.expectedFullPaymentInterest, "Full payment interest mismatch: expected " + to_string(tc.expectedFullPaymentInterest) + ", got " + to_string(computedFullPaymentInterest)); } } void testTryOverpaymentNoInterestNoFee() { // This test ensures that overpayment with no interest works correctly. testcase("tryOverpayment - No Interest No Fee"); using namespace jtx; using namespace xrpl::detail; Env env{*this}; Account const issuer{"issuer"}; PrettyAsset const asset = issuer["USD"]; std::int32_t const loanScale = -5; TenthBips16 const managementFeeRate{0}; // 0% TenthBips32 const loanInterestRate{0}; // 0% Number const loanPrincipal{1'000}; std::uint32_t const paymentInterval = 30 * 24 * 60 * 60; std::uint32_t const paymentsRemaining = 10; auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval); Number const overpaymentAmount{50}; auto const overpaymentComponents = computeOverpaymentComponents( asset, loanScale, overpaymentAmount, TenthBips32(0), TenthBips32(0), managementFeeRate); auto const loanProperties = computeLoanProperties( asset, loanPrincipal, loanInterestRate, paymentInterval, paymentsRemaining, managementFeeRate, loanScale); auto const ret = tryOverpayment( asset, loanScale, overpaymentComponents, loanProperties.loanState, loanProperties.periodicPayment, periodicRate, paymentsRemaining, managementFeeRate, env.journal); BEAST_EXPECT(ret); auto const& [actualPaymentParts, newLoanProperties] = *ret; auto const& newState = newLoanProperties.loanState; // =========== VALIDATE PAYMENT PARTS =========== BEAST_EXPECTS( actualPaymentParts.valueChange == 0, " valueChange mismatch: expected 0, got " + to_string(actualPaymentParts.valueChange)); BEAST_EXPECTS( actualPaymentParts.feePaid == 0, " feePaid mismatch: expected 0, got " + to_string(actualPaymentParts.feePaid)); BEAST_EXPECTS( actualPaymentParts.interestPaid == 0, " interestPaid mismatch: expected 0, got " + to_string(actualPaymentParts.interestPaid)); BEAST_EXPECTS( actualPaymentParts.principalPaid == overpaymentAmount, " principalPaid mismatch: expected " + to_string(overpaymentAmount) + ", got " + to_string(actualPaymentParts.principalPaid)); // =========== VALIDATE STATE CHANGES =========== BEAST_EXPECTS( loanProperties.loanState.interestDue - newState.interestDue == 0, " interest change mismatch: expected 0, got " + to_string(loanProperties.loanState.interestDue - newState.interestDue)); BEAST_EXPECTS( loanProperties.loanState.managementFeeDue - newState.managementFeeDue == 0, " management fee change mismatch: expected 0, got " + to_string(loanProperties.loanState.managementFeeDue - newState.managementFeeDue)); BEAST_EXPECTS( actualPaymentParts.principalPaid == loanProperties.loanState.principalOutstanding - newState.principalOutstanding, " principalPaid mismatch: expected " + to_string( loanProperties.loanState.principalOutstanding - newState.principalOutstanding) + ", got " + to_string(actualPaymentParts.principalPaid)); } void testTryOverpaymentNoInterestOverpaymentFee() { testcase("tryOverpayment - No Interest With Overpayment Fee"); using namespace jtx; using namespace xrpl::detail; Env env{*this}; Account const issuer{"issuer"}; PrettyAsset const asset = issuer["USD"]; std::int32_t const loanScale = -5; TenthBips16 const managementFeeRate{0}; // 0% TenthBips32 const loanInterestRate{0}; // 0% Number const loanPrincipal{1'000}; std::uint32_t const paymentInterval = 30 * 24 * 60 * 60; std::uint32_t const paymentsRemaining = 10; auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval); auto const overpaymentComponents = computeOverpaymentComponents( asset, loanScale, Number{50, 0}, TenthBips32(0), TenthBips32(10'000), // 10% overpayment fee managementFeeRate); auto const loanProperties = computeLoanProperties( asset, loanPrincipal, loanInterestRate, paymentInterval, paymentsRemaining, managementFeeRate, loanScale); auto const ret = tryOverpayment( asset, loanScale, overpaymentComponents, loanProperties.loanState, loanProperties.periodicPayment, periodicRate, paymentsRemaining, managementFeeRate, env.journal); BEAST_EXPECT(ret); auto const& [actualPaymentParts, newLoanProperties] = *ret; auto const& newState = newLoanProperties.loanState; // =========== VALIDATE PAYMENT PARTS =========== BEAST_EXPECTS( actualPaymentParts.valueChange == 0, " valueChange mismatch: expected 0, got " + to_string(actualPaymentParts.valueChange)); BEAST_EXPECTS( actualPaymentParts.feePaid == 5, " feePaid mismatch: expected 5, got " + to_string(actualPaymentParts.feePaid)); BEAST_EXPECTS( actualPaymentParts.principalPaid == 45, " principalPaid mismatch: expected 45, got `" + to_string(actualPaymentParts.principalPaid)); BEAST_EXPECTS( actualPaymentParts.interestPaid == 0, " interestPaid mismatch: expected 0, got " + to_string(actualPaymentParts.interestPaid)); // =========== VALIDATE STATE CHANGES =========== // With no Loan interest, interest outstanding should not change BEAST_EXPECTS( loanProperties.loanState.interestDue - newState.interestDue == 0, " interest change mismatch: expected 0, got " + to_string(loanProperties.loanState.interestDue - newState.interestDue)); // With no Loan management fee, management fee due should not change BEAST_EXPECTS( loanProperties.loanState.managementFeeDue - newState.managementFeeDue == 0, " management fee change mismatch: expected 0, got " + to_string(loanProperties.loanState.managementFeeDue - newState.managementFeeDue)); BEAST_EXPECTS( actualPaymentParts.principalPaid == loanProperties.loanState.principalOutstanding - newState.principalOutstanding, " principalPaid mismatch: expected " + to_string( loanProperties.loanState.principalOutstanding - newState.principalOutstanding) + ", got " + to_string(actualPaymentParts.principalPaid)); } void testTryOverpaymentLoanInterestNoOverpaymentFees() { testcase("tryOverpayment - Loan Interest, No Overpayment Fees"); using namespace jtx; using namespace xrpl::detail; Env env{*this}; Account const issuer{"issuer"}; PrettyAsset const asset = issuer["USD"]; std::int32_t const loanScale = -5; TenthBips16 const managementFeeRate{0}; // 0% TenthBips32 const loanInterestRate{10'000}; // 10% Number const loanPrincipal{1'000}; std::uint32_t const paymentInterval = 30 * 24 * 60 * 60; std::uint32_t const paymentsRemaining = 10; auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval); auto const overpaymentComponents = computeOverpaymentComponents( asset, loanScale, Number{50, 0}, TenthBips32(0), // no overpayment interest TenthBips32(0), // 0% overpayment fee managementFeeRate); auto const loanProperties = computeLoanProperties( asset, loanPrincipal, loanInterestRate, paymentInterval, paymentsRemaining, managementFeeRate, loanScale); auto const ret = tryOverpayment( asset, loanScale, overpaymentComponents, loanProperties.loanState, loanProperties.periodicPayment, periodicRate, paymentsRemaining, managementFeeRate, env.journal); BEAST_EXPECT(ret); auto const& [actualPaymentParts, newLoanProperties] = *ret; auto const& newState = newLoanProperties.loanState; // =========== VALIDATE PAYMENT PARTS =========== // with no overpayment interest portion, value change should equal // interest decrease BEAST_EXPECTS( (actualPaymentParts.valueChange == Number{-228802, -5}), " valueChange mismatch: expected " + to_string(Number{-228802, -5}) + ", got " + to_string(actualPaymentParts.valueChange)); // with no fee portion, fee paid should be zero BEAST_EXPECTS( actualPaymentParts.feePaid == 0, " feePaid mismatch: expected 0, got " + to_string(actualPaymentParts.feePaid)); BEAST_EXPECTS( actualPaymentParts.principalPaid == 50, " principalPaid mismatch: expected 50, got `" + to_string(actualPaymentParts.principalPaid)); // with no interest portion, interest paid should be zero BEAST_EXPECTS( actualPaymentParts.interestPaid == 0, " interestPaid mismatch: expected 0, got " + to_string(actualPaymentParts.interestPaid)); // =========== VALIDATE STATE CHANGES =========== BEAST_EXPECTS( actualPaymentParts.principalPaid == loanProperties.loanState.principalOutstanding - newState.principalOutstanding, " principalPaid mismatch: expected " + to_string( loanProperties.loanState.principalOutstanding - newState.principalOutstanding) + ", got " + to_string(actualPaymentParts.principalPaid)); BEAST_EXPECTS( actualPaymentParts.valueChange == newState.interestDue - loanProperties.loanState.interestDue, " valueChange mismatch: expected " + to_string(newState.interestDue - loanProperties.loanState.interestDue) + ", got " + to_string(actualPaymentParts.valueChange)); // With no Loan management fee, management fee due should not change BEAST_EXPECTS( loanProperties.loanState.managementFeeDue - newState.managementFeeDue == 0, " management fee change mismatch: expected 0, got " + to_string(loanProperties.loanState.managementFeeDue - newState.managementFeeDue)); } void testTryOverpaymentLoanInterestOverpaymentInterest() { testcase("tryOverpayment - Loan Interest, Overpayment Interest, No Fee"); using namespace jtx; using namespace xrpl::detail; Env env{*this}; Account const issuer{"issuer"}; PrettyAsset const asset = issuer["USD"]; std::int32_t const loanScale = -5; TenthBips16 const managementFeeRate{0}; // 0% TenthBips32 const loanInterestRate{10'000}; // 10% Number const loanPrincipal{1'000}; std::uint32_t const paymentInterval = 30 * 24 * 60 * 60; std::uint32_t const paymentsRemaining = 10; auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval); auto const overpaymentComponents = computeOverpaymentComponents( asset, loanScale, Number{50, 0}, TenthBips32(10'000), // 10% overpayment interest TenthBips32(0), // 0% overpayment fee managementFeeRate); auto const loanProperties = computeLoanProperties( asset, loanPrincipal, loanInterestRate, paymentInterval, paymentsRemaining, managementFeeRate, loanScale); auto const ret = tryOverpayment( asset, loanScale, overpaymentComponents, loanProperties.loanState, loanProperties.periodicPayment, periodicRate, paymentsRemaining, managementFeeRate, env.journal); BEAST_EXPECT(ret); auto const& [actualPaymentParts, newLoanProperties] = *ret; auto const& newState = newLoanProperties.loanState; // =========== VALIDATE PAYMENT PARTS =========== // with overpayment interest portion, interest paid should be 5 BEAST_EXPECTS( actualPaymentParts.interestPaid == 5, " interestPaid mismatch: expected 5, got " + to_string(actualPaymentParts.interestPaid)); // With overpayment interest portion, value change should equal the // interest decrease plus overpayment interest portion BEAST_EXPECTS( (actualPaymentParts.valueChange == Number{-205922, -5} + actualPaymentParts.interestPaid), " valueChange mismatch: expected " + to_string(actualPaymentParts.valueChange - actualPaymentParts.interestPaid) + ", got " + to_string(actualPaymentParts.valueChange)); // with no fee portion, fee paid should be zero BEAST_EXPECTS( actualPaymentParts.feePaid == 0, " feePaid mismatch: expected 0, got " + to_string(actualPaymentParts.feePaid)); BEAST_EXPECTS( actualPaymentParts.principalPaid == 45, " principalPaid mismatch: expected 45, got `" + to_string(actualPaymentParts.principalPaid)); // =========== VALIDATE STATE CHANGES =========== BEAST_EXPECTS( actualPaymentParts.principalPaid == loanProperties.loanState.principalOutstanding - newState.principalOutstanding, " principalPaid mismatch: expected " + to_string( loanProperties.loanState.principalOutstanding - newState.principalOutstanding) + ", got " + to_string(actualPaymentParts.principalPaid)); // The change in interest is equal to the value change sans the // overpayment interest BEAST_EXPECTS( actualPaymentParts.valueChange - actualPaymentParts.interestPaid == newState.interestDue - loanProperties.loanState.interestDue, " valueChange mismatch: expected " + to_string( newState.interestDue - loanProperties.loanState.interestDue + actualPaymentParts.interestPaid) + ", got " + to_string(actualPaymentParts.valueChange)); // With no Loan management fee, management fee due should not change BEAST_EXPECTS( loanProperties.loanState.managementFeeDue - newState.managementFeeDue == 0, " management fee change mismatch: expected 0, got " + to_string(loanProperties.loanState.managementFeeDue - newState.managementFeeDue)); } void testTryOverpaymentLoanInterestFeeOverpaymentInterestNoFee() { testcase( "tryOverpayment - Loan Interest and Fee, Overpayment Interest, No " "Fee"); using namespace jtx; using namespace xrpl::detail; Env env{*this}; Account const issuer{"issuer"}; PrettyAsset const asset = issuer["USD"]; std::int32_t const loanScale = -5; TenthBips16 const managementFeeRate{10'000}; // 10% TenthBips32 const loanInterestRate{10'000}; // 10% Number const loanPrincipal{1'000}; std::uint32_t const paymentInterval = 30 * 24 * 60 * 60; std::uint32_t const paymentsRemaining = 10; auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval); auto const overpaymentComponents = computeOverpaymentComponents( asset, loanScale, Number{50, 0}, TenthBips32(10'000), // 10% overpayment interest TenthBips32(0), // 0% overpayment fee managementFeeRate); auto const loanProperties = computeLoanProperties( asset, loanPrincipal, loanInterestRate, paymentInterval, paymentsRemaining, managementFeeRate, loanScale); auto const ret = tryOverpayment( asset, loanScale, overpaymentComponents, loanProperties.loanState, loanProperties.periodicPayment, periodicRate, paymentsRemaining, managementFeeRate, env.journal); BEAST_EXPECT(ret); auto const& [actualPaymentParts, newLoanProperties] = *ret; auto const& newState = newLoanProperties.loanState; // =========== VALIDATE PAYMENT PARTS =========== // Since there is loan management fee, the fee is charged against // overpayment interest portion first, so interest paid remains 4.5 BEAST_EXPECTS( (actualPaymentParts.interestPaid == Number{45, -1}), " interestPaid mismatch: expected 4.5, got " + to_string(actualPaymentParts.interestPaid)); // With overpayment interest portion, value change should equal the // interest decrease plus overpayment interest portion BEAST_EXPECTS( (actualPaymentParts.valueChange == Number{-18533, -4} + actualPaymentParts.interestPaid), " valueChange mismatch: expected " + to_string(Number{-18533, -4} + actualPaymentParts.interestPaid) + ", got " + to_string(actualPaymentParts.valueChange)); // While there is no overpayment fee, fee paid should equal the // management fee charged against the overpayment interest portion BEAST_EXPECTS( (actualPaymentParts.feePaid == Number{5, -1}), " feePaid mismatch: expected 0.5, got " + to_string(actualPaymentParts.feePaid)); BEAST_EXPECTS( actualPaymentParts.principalPaid == 45, " principalPaid mismatch: expected 45, got `" + to_string(actualPaymentParts.principalPaid)); // =========== VALIDATE STATE CHANGES =========== BEAST_EXPECTS( actualPaymentParts.principalPaid == loanProperties.loanState.principalOutstanding - newState.principalOutstanding, " principalPaid mismatch: expected " + to_string( loanProperties.loanState.principalOutstanding - newState.principalOutstanding) + ", got " + to_string(actualPaymentParts.principalPaid)); // Note that the management fee value change is not captured, as this // value is not needed to correctly update the Vault state. BEAST_EXPECTS( (newState.managementFeeDue - loanProperties.loanState.managementFeeDue == Number{-20592, -5}), " management fee change mismatch: expected " + to_string(Number{-20592, -5}) + ", got " + to_string(newState.managementFeeDue - loanProperties.loanState.managementFeeDue)); BEAST_EXPECTS( actualPaymentParts.valueChange - actualPaymentParts.interestPaid == newState.interestDue - loanProperties.loanState.interestDue, " valueChange mismatch: expected " + to_string(newState.interestDue - loanProperties.loanState.interestDue) + ", got " + to_string(actualPaymentParts.valueChange - actualPaymentParts.interestPaid)); } void testTryOverpaymentLoanInterestFeeOverpaymentInterestFee() { testcase("tryOverpayment - Loan Interest, Fee, Overpayment Interest, Fee"); using namespace jtx; using namespace xrpl::detail; Env env{*this}; Account const issuer{"issuer"}; PrettyAsset const asset = issuer["USD"]; std::int32_t const loanScale = -5; TenthBips16 const managementFeeRate{10'000}; // 10% TenthBips32 const loanInterestRate{10'000}; // 10% Number const loanPrincipal{1'000}; std::uint32_t const paymentInterval = 30 * 24 * 60 * 60; std::uint32_t const paymentsRemaining = 10; auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval); auto const overpaymentComponents = computeOverpaymentComponents( asset, loanScale, Number{50, 0}, TenthBips32(10'000), // 10% overpayment interest TenthBips32(10'000), // 10% overpayment fee managementFeeRate); auto const loanProperties = computeLoanProperties( asset, loanPrincipal, loanInterestRate, paymentInterval, paymentsRemaining, managementFeeRate, loanScale); auto const ret = tryOverpayment( asset, loanScale, overpaymentComponents, loanProperties.loanState, loanProperties.periodicPayment, periodicRate, paymentsRemaining, managementFeeRate, env.journal); BEAST_EXPECT(ret); auto const& [actualPaymentParts, newLoanProperties] = *ret; auto const& newState = newLoanProperties.loanState; // =========== VALIDATE PAYMENT PARTS =========== // Since there is loan management fee, the fee is charged against // overpayment interest portion first, so interest paid remains 4.5 BEAST_EXPECTS( (actualPaymentParts.interestPaid == Number{45, -1}), " interestPaid mismatch: expected 4.5, got " + to_string(actualPaymentParts.interestPaid)); // With overpayment interest portion, value change should equal the // interest decrease plus overpayment interest portion BEAST_EXPECTS( (actualPaymentParts.valueChange == Number{-164737, -5} + actualPaymentParts.interestPaid), " valueChange mismatch: expected " + to_string(Number{-164737, -5} + actualPaymentParts.interestPaid) + ", got " + to_string(actualPaymentParts.valueChange)); // While there is no overpayment fee, fee paid should equal the // management fee charged against the overpayment interest portion BEAST_EXPECTS( (actualPaymentParts.feePaid == Number{55, -1}), " feePaid mismatch: expected 5.5, got " + to_string(actualPaymentParts.feePaid)); BEAST_EXPECTS( actualPaymentParts.principalPaid == 40, " principalPaid mismatch: expected 40, got `" + to_string(actualPaymentParts.principalPaid)); // =========== VALIDATE STATE CHANGES =========== BEAST_EXPECTS( actualPaymentParts.principalPaid == loanProperties.loanState.principalOutstanding - newState.principalOutstanding, " principalPaid mismatch: expected " + to_string( loanProperties.loanState.principalOutstanding - newState.principalOutstanding) + ", got " + to_string(actualPaymentParts.principalPaid)); // Note that the management fee value change is not captured, as this // value is not needed to correctly update the Vault state. BEAST_EXPECTS( (newState.managementFeeDue - loanProperties.loanState.managementFeeDue == Number{-18304, -5}), " management fee change mismatch: expected " + to_string(Number{-18304, -5}) + ", got " + to_string(newState.managementFeeDue - loanProperties.loanState.managementFeeDue)); BEAST_EXPECTS( actualPaymentParts.valueChange - actualPaymentParts.interestPaid == newState.interestDue - loanProperties.loanState.interestDue, " valueChange mismatch: expected " + to_string(newState.interestDue - loanProperties.loanState.interestDue) + ", got " + to_string(actualPaymentParts.valueChange - actualPaymentParts.interestPaid)); } public: void run() override { testTryOverpaymentNoInterestNoFee(); testTryOverpaymentNoInterestOverpaymentFee(); testTryOverpaymentLoanInterestNoOverpaymentFees(); testTryOverpaymentLoanInterestOverpaymentInterest(); testTryOverpaymentLoanInterestFeeOverpaymentInterestNoFee(); testTryOverpaymentLoanInterestFeeOverpaymentInterestFee(); testComputeFullPaymentInterest(); testLoanAccruedInterest(); testLoanLatePaymentInterest(); testLoanPeriodicPayment(); testLoanPrincipalFromPeriodicPayment(); testComputeRaisedRate(); testComputePaymentFactor(); testComputeOverpaymentComponents(); testComputeInterestAndFeeParts(); } }; BEAST_DEFINE_TESTSUITE(LendingHelpers, app, xrpl); } // namespace test } // namespace xrpl