FINERACT-2435: fix as of data api
This commit is contained in:
committed by
Arnold Gálovics
parent
2b4de8e655
commit
bf879064af
+27
@@ -285,6 +285,10 @@ public class LoanSummary {
|
|||||||
this.totalFeeChargesOutstanding = totalFeeChargesOutstanding;
|
this.totalFeeChargesOutstanding = totalFeeChargesOutstanding;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateFeeChargesCharged(final BigDecimal totalFeeChargesCharged) {
|
||||||
|
this.totalFeeChargesCharged = totalFeeChargesCharged;
|
||||||
|
}
|
||||||
|
|
||||||
public void updatePenaltyChargeOutstanding(final BigDecimal totalPenaltyChargesOutstanding) {
|
public void updatePenaltyChargeOutstanding(final BigDecimal totalPenaltyChargesOutstanding) {
|
||||||
this.totalPenaltyChargesOutstanding = totalPenaltyChargesOutstanding;
|
this.totalPenaltyChargesOutstanding = totalPenaltyChargesOutstanding;
|
||||||
}
|
}
|
||||||
@@ -309,6 +313,29 @@ public class LoanSummary {
|
|||||||
this.totalWaived = totalWaived;
|
this.totalWaived = totalWaived;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateTotalExpectedRepayment(final BigDecimal totalExpectedRepayment) {
|
||||||
|
this.totalExpectedRepayment = totalExpectedRepayment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateTotalExpectedCostOfLoan(final BigDecimal totalExpectedCostOfLoan) {
|
||||||
|
this.totalExpectedCostOfLoan = totalExpectedCostOfLoan;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recalculateDerivedTotalsForAdjustedFeeCharged(final BigDecimal adjustedFeeCharged) {
|
||||||
|
this.totalFeeChargesCharged = adjustedFeeCharged;
|
||||||
|
|
||||||
|
this.totalFeeChargesOutstanding = adjustedFeeCharged.subtract(this.totalFeeChargesRepaid).subtract(this.totalFeeChargesWaived)
|
||||||
|
.subtract(this.totalFeeChargesWrittenOff);
|
||||||
|
|
||||||
|
this.totalOutstanding = this.totalPrincipalOutstanding.add(this.totalInterestOutstanding).add(this.totalFeeChargesOutstanding)
|
||||||
|
.add(this.totalPenaltyChargesOutstanding);
|
||||||
|
|
||||||
|
this.totalExpectedRepayment = this.totalPrincipal.add(this.totalInterestCharged).add(adjustedFeeCharged)
|
||||||
|
.add(this.totalPenaltyChargesCharged);
|
||||||
|
|
||||||
|
this.totalExpectedCostOfLoan = this.totalInterestCharged.add(adjustedFeeCharged).add(this.totalPenaltyChargesCharged);
|
||||||
|
}
|
||||||
|
|
||||||
protected Money calculateTotalPrincipalRepaid(final List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments,
|
protected Money calculateTotalPrincipalRepaid(final List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments,
|
||||||
final MonetaryCurrency currency) {
|
final MonetaryCurrency currency) {
|
||||||
Money total = Money.zero(currency);
|
Money total = Money.zero(currency);
|
||||||
|
|||||||
+65
-8
@@ -32,9 +32,12 @@ import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
|||||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||||
|
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||||
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanPointInTimeData;
|
import org.apache.fineract.portfolio.loanaccount.data.LoanPointInTimeData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
|
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.arrears.LoanArrearsData;
|
import org.apache.fineract.portfolio.loanaccount.domain.arrears.LoanArrearsData;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -73,15 +76,14 @@ public class LoanPointInTimeServiceImpl implements LoanPointInTimeService {
|
|||||||
int afterRemovalTxCount = loan.getLoanTransactions().size();
|
int afterRemovalTxCount = loan.getLoanTransactions().size();
|
||||||
int afterRemovalChargeCount = loan.getCharges().size();
|
int afterRemovalChargeCount = loan.getCharges().size();
|
||||||
|
|
||||||
// In case the loan is cumulative and is being prepaid by the latest repayment tx, we need the
|
boolean needsScheduleRegeneration = txCount != afterRemovalTxCount || chargeCount != afterRemovalChargeCount;
|
||||||
// recalculateFrom and recalculateTill
|
|
||||||
// set to the same date which is the prepaying transaction's date
|
if (needsScheduleRegeneration) {
|
||||||
// currently this is not implemented and opens up buggy edge cases
|
|
||||||
// we work this around only for cases when the loan is already closed or the requested date doesn't change
|
|
||||||
// the loan's state
|
|
||||||
if (txCount != afterRemovalTxCount || chargeCount != afterRemovalChargeCount) {
|
|
||||||
ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null, null);
|
ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null, null);
|
||||||
loanScheduleService.regenerateScheduleWithReprocessingTransactions(loan, scheduleGeneratorDTO);
|
loanScheduleService.regenerateScheduleWithReprocessingTransactions(loan, scheduleGeneratorDTO);
|
||||||
|
recalculateSummaryForInstallmentsUpToDate(loan, date);
|
||||||
|
} else if (!loan.isClosed()) {
|
||||||
|
recalculateSummaryForInstallmentsUpToDate(loan, date);
|
||||||
}
|
}
|
||||||
|
|
||||||
LoanArrearsData arrearsData = arrearsAgingService.calculateArrearsForLoan(loan);
|
LoanArrearsData arrearsData = arrearsAgingService.calculateArrearsForLoan(loan);
|
||||||
@@ -97,7 +99,62 @@ public class LoanPointInTimeServiceImpl implements LoanPointInTimeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void removeAfterDateCharges(Loan loan, LocalDate date) {
|
private void removeAfterDateCharges(Loan loan, LocalDate date) {
|
||||||
loan.removeCharges(c -> DateUtils.isAfter(c.getEffectiveDueDate(), date));
|
// Don't remove installment fees based on effectiveDueDate since they span multiple installments
|
||||||
|
// For installment fees, effectiveDueDate returns the first UNPAID installment's due date,
|
||||||
|
// which would incorrectly remove the entire fee even if some installments are already paid/due
|
||||||
|
// The recalculateSummaryForInstallmentsUpToDate method handles installment fee adjustments separately
|
||||||
|
loan.removeCharges(c -> !c.isInstalmentFee() && DateUtils.isAfter(c.getEffectiveDueDate(), date));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recalculateSummaryForInstallmentsUpToDate(Loan loan, LocalDate date) {
|
||||||
|
var currency = loan.getCurrency();
|
||||||
|
var summary = loan.getSummary();
|
||||||
|
|
||||||
|
// Calculate fee charged based only on charges due by the specified date.
|
||||||
|
// This excludes after-date charges and only includes installment fee portions for installments due by the date.
|
||||||
|
Money feeChargedFromRemainingCharges = calculateTotalFeeChargedFromCharges(loan, date, currency);
|
||||||
|
|
||||||
|
// Include fees due at disbursement which are always included regardless of date
|
||||||
|
Money adjustedFeeCharged = feeChargedFromRemainingCharges.plus(summary.getTotalFeeChargesDueAtDisbursement(currency));
|
||||||
|
|
||||||
|
// Only proceed with adjustment if the fee charged differs from summary
|
||||||
|
if (adjustedFeeCharged.getAmount().compareTo(summary.getTotalFeeChargesCharged()) == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegate to domain to recalculate all derived totals consistently
|
||||||
|
summary.recalculateDerivedTotalsForAdjustedFeeCharged(adjustedFeeCharged.getAmount());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Money calculateTotalFeeChargedFromCharges(Loan loan, LocalDate date, MonetaryCurrency currency) {
|
||||||
|
Money total = Money.zero(currency);
|
||||||
|
for (LoanCharge charge : loan.getCharges()) {
|
||||||
|
if (charge.isActive() && !charge.isPenaltyCharge() && !charge.isDueAtDisbursement()) {
|
||||||
|
// For installment fees, calculate the portion up to the date
|
||||||
|
if (charge.isInstalmentFee()) {
|
||||||
|
Money installmentTotal = calculateInstallmentFeeUpToDate(charge, date, currency);
|
||||||
|
total = total.plus(installmentTotal);
|
||||||
|
} else {
|
||||||
|
// For one-time charges, include only if due on or before the date
|
||||||
|
LocalDate chargeDueDate = charge.getEffectiveDueDate();
|
||||||
|
if (chargeDueDate != null && !DateUtils.isAfter(chargeDueDate, date)) {
|
||||||
|
total = total.plus(charge.getAmount(currency));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Money calculateInstallmentFeeUpToDate(LoanCharge charge, LocalDate date, MonetaryCurrency currency) {
|
||||||
|
Money total = Money.zero(currency);
|
||||||
|
for (var installmentCharge : charge.installmentCharges()) {
|
||||||
|
var installment = installmentCharge.getInstallment();
|
||||||
|
if (installment != null && !DateUtils.isAfter(installment.getDueDate(), date)) {
|
||||||
|
total = total.plus(installmentCharge.getAmount(currency));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeAfterDateTransactions(Loan loan, LocalDate date) {
|
private void removeAfterDateTransactions(Loan loan, LocalDate date) {
|
||||||
|
|||||||
+592
@@ -19,10 +19,12 @@
|
|||||||
package org.apache.fineract.integrationtests.loan.pointintime;
|
package org.apache.fineract.integrationtests.loan.pointintime;
|
||||||
|
|
||||||
import static org.apache.fineract.integrationtests.BaseLoanIntegrationTest.TransactionProcessingStrategyCode.ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
|
import static org.apache.fineract.integrationtests.BaseLoanIntegrationTest.TransactionProcessingStrategyCode.ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||||
import org.apache.fineract.client.models.LoanPointInTimeData;
|
import org.apache.fineract.client.models.LoanPointInTimeData;
|
||||||
import org.apache.fineract.client.models.LoanProductChargeData;
|
import org.apache.fineract.client.models.LoanProductChargeData;
|
||||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||||
@@ -33,6 +35,7 @@ import org.apache.fineract.client.models.PostLoansRequestChargeData;
|
|||||||
import org.apache.fineract.client.models.PostLoansResponse;
|
import org.apache.fineract.client.models.PostLoansResponse;
|
||||||
import org.apache.fineract.integrationtests.BaseLoanIntegrationTest;
|
import org.apache.fineract.integrationtests.BaseLoanIntegrationTest;
|
||||||
import org.apache.fineract.integrationtests.common.ClientHelper;
|
import org.apache.fineract.integrationtests.common.ClientHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
public class LoanPointInTimeTest extends BaseLoanIntegrationTest {
|
public class LoanPointInTimeTest extends BaseLoanIntegrationTest {
|
||||||
@@ -858,4 +861,593 @@ public class LoanPointInTimeTest extends BaseLoanIntegrationTest {
|
|||||||
assertThat(pointInTimeData.getArrears().getTotalOverdue()).isZero();
|
assertThat(pointInTimeData.getArrears().getTotalOverdue()).isZero();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_LoanPointInTimeData_InstallmentFeeAllocation() {
|
||||||
|
AtomicReference<Long> aLoanId = new AtomicReference<>();
|
||||||
|
double installmentFeeAmount = 100.0;
|
||||||
|
|
||||||
|
runAt("01 October 2025", () -> {
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
|
||||||
|
int numberOfRepayments = 6;
|
||||||
|
int repaymentEvery = 1;
|
||||||
|
|
||||||
|
Long installmentFeeChargeId = createInstallmentFeeCharge(installmentFeeAmount);
|
||||||
|
|
||||||
|
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct()
|
||||||
|
.numberOfRepayments(numberOfRepayments).repaymentEvery(repaymentEvery).installmentAmountInMultiplesOf(null)
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()).interestType(InterestType.DECLINING_BALANCE)
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)
|
||||||
|
.interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)
|
||||||
|
.isInterestRecalculationEnabled(true).recalculationRestFrequencyInterval(1)
|
||||||
|
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)
|
||||||
|
.rescheduleStrategyMethod(RescheduleStrategyMethod.REDUCE_EMI_AMOUNT).allowPartialPeriodInterestCalculation(false)
|
||||||
|
.disallowExpectedDisbursements(false).allowApprovedDisbursedAmountsOverApplied(false).overAppliedNumber(null)
|
||||||
|
.overAppliedCalculationType(null).multiDisburseLoan(null)
|
||||||
|
.charges(List.of(new LoanProductChargeData().id(installmentFeeChargeId)));
|
||||||
|
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||||
|
Long loanProductId = loanProductResponse.getResourceId();
|
||||||
|
|
||||||
|
double amount = 6000.0;
|
||||||
|
|
||||||
|
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 October 2025", amount, numberOfRepayments)
|
||||||
|
.repaymentEvery(repaymentEvery).loanTermFrequency(numberOfRepayments)
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS).loanTermFrequencyType(RepaymentFrequencyType.MONTHS)
|
||||||
|
.interestType(InterestType.DECLINING_BALANCE).interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)
|
||||||
|
.charges(List.of(new PostLoansRequestChargeData().chargeId(installmentFeeChargeId)
|
||||||
|
.amount(BigDecimal.valueOf(installmentFeeAmount))));
|
||||||
|
|
||||||
|
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||||
|
|
||||||
|
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
||||||
|
approveLoanRequest(amount, "01 October 2025"));
|
||||||
|
|
||||||
|
aLoanId.getAndSet(approvedLoanResult.getLoanId());
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(amount), "01 October 2025");
|
||||||
|
|
||||||
|
verifyTransactions(loanId, transaction(6000.0, "Disbursement", "01 October 2025"));
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 November 2025", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
addRepaymentForLoan(loanId, 1100.0, "01 November 2025");
|
||||||
|
|
||||||
|
verifyTransactions(loanId, transaction(6000.0, "Disbursement", "01 October 2025"),
|
||||||
|
transaction(1100.0, "Repayment", "01 November 2025"));
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 December 2025", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
addRepaymentForLoan(loanId, 1100.0, "01 December 2025");
|
||||||
|
|
||||||
|
verifyTransactions(loanId, transaction(6000.0, "Disbursement", "01 October 2025"),
|
||||||
|
transaction(1100.0, "Repayment", "01 November 2025"), transaction(1100.0, "Repayment", "01 December 2025"));
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 January 2026", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
addRepaymentForLoan(loanId, 1100.0, "01 January 2026");
|
||||||
|
|
||||||
|
verifyTransactions(loanId, transaction(6000.0, "Disbursement", "01 October 2025"),
|
||||||
|
transaction(1100.0, "Repayment", "01 November 2025"), transaction(1100.0, "Repayment", "01 December 2025"),
|
||||||
|
transaction(1100.0, "Repayment", "01 January 2026"));
|
||||||
|
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
BigDecimal regularApiFeeChargesPaid = loanDetails.getSummary().getFeeChargesPaid();
|
||||||
|
BigDecimal regularApiFeeChargesCharged = loanDetails.getSummary().getFeeChargesCharged();
|
||||||
|
|
||||||
|
assertThat(regularApiFeeChargesCharged).isEqualByComparingTo(BigDecimal.valueOf(600.0));
|
||||||
|
assertThat(regularApiFeeChargesPaid).isEqualByComparingTo(BigDecimal.valueOf(300.0));
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("08 January 2026", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId, "08 January 2026");
|
||||||
|
|
||||||
|
assertThat(pointInTimeData.getFee().getFeeChargesCharged())
|
||||||
|
.as("Point-in-time feeChargesCharged should only include fees for installments due by the requested date")
|
||||||
|
.isEqualByComparingTo(BigDecimal.valueOf(300.0));
|
||||||
|
|
||||||
|
assertThat(pointInTimeData.getFee().getFeeChargesPaid())
|
||||||
|
.as("Point-in-time feeChargesPaid should reflect paid installment fees up to the requested date")
|
||||||
|
.isEqualByComparingTo(BigDecimal.valueOf(300.0));
|
||||||
|
|
||||||
|
assertThat(pointInTimeData.getFee().getFeeChargesOutstanding())
|
||||||
|
.as("Point-in-time feeChargesOutstanding should be 0 since all due fees are paid")
|
||||||
|
.isEqualByComparingTo(BigDecimal.ZERO);
|
||||||
|
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(3000.0, 0.0, 0.0, 0.0, 3000.0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_LoanPointInTimeData_ClosedLoanWithInstallmentFees() {
|
||||||
|
AtomicReference<Long> aLoanId = new AtomicReference<>();
|
||||||
|
double installmentFeeAmount = 25.0;
|
||||||
|
|
||||||
|
runAt("01 October 2023", () -> {
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
|
||||||
|
int numberOfRepayments = 2;
|
||||||
|
int repaymentEvery = 1;
|
||||||
|
|
||||||
|
Long installmentFeeChargeId = createInstallmentFeeCharge(installmentFeeAmount);
|
||||||
|
|
||||||
|
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct()
|
||||||
|
.numberOfRepayments(numberOfRepayments).repaymentEvery(repaymentEvery).installmentAmountInMultiplesOf(null)
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()).interestType(InterestType.FLAT)
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD)
|
||||||
|
.isInterestRecalculationEnabled(false).disallowExpectedDisbursements(false)
|
||||||
|
.allowApprovedDisbursedAmountsOverApplied(false).overAppliedNumber(null).overAppliedCalculationType(null)
|
||||||
|
.multiDisburseLoan(null).charges(List.of(new LoanProductChargeData().id(installmentFeeChargeId)));
|
||||||
|
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||||
|
Long loanProductId = loanProductResponse.getResourceId();
|
||||||
|
|
||||||
|
double amount = 2000.0;
|
||||||
|
|
||||||
|
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 October 2023", amount, numberOfRepayments)
|
||||||
|
.repaymentEvery(repaymentEvery).loanTermFrequency(numberOfRepayments)
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS).loanTermFrequencyType(RepaymentFrequencyType.MONTHS)
|
||||||
|
.interestType(InterestType.FLAT).interestCalculationPeriodType(InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD)
|
||||||
|
.charges(List.of(new PostLoansRequestChargeData().chargeId(installmentFeeChargeId)
|
||||||
|
.amount(BigDecimal.valueOf(installmentFeeAmount))));
|
||||||
|
|
||||||
|
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||||
|
|
||||||
|
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
||||||
|
approveLoanRequest(amount, "01 October 2023"));
|
||||||
|
|
||||||
|
aLoanId.getAndSet(approvedLoanResult.getLoanId());
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(amount), "01 October 2023");
|
||||||
|
|
||||||
|
verifyTransactions(loanId, transaction(2000.0, "Disbursement", "01 October 2023"));
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 November 2023", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
// First repayment: 1000 principal + 25 fee = 1025
|
||||||
|
addRepaymentForLoan(loanId, 1025.0, "01 November 2023");
|
||||||
|
|
||||||
|
verifyTransactions(loanId, transaction(2000.0, "Disbursement", "01 October 2023"),
|
||||||
|
transaction(1025.0, "Repayment", "01 November 2023"));
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 December 2023", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
// Second repayment: 1000 principal + 25 fee = 1025 (loan should close)
|
||||||
|
addRepaymentForLoan(loanId, 1025.0, "01 December 2023");
|
||||||
|
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
|
||||||
|
// Verify loan is closed
|
||||||
|
assertThat(loanDetails.getStatus().getCode()).isEqualTo("loanStatusType.closed.obligations.met");
|
||||||
|
|
||||||
|
// Verify fee charges
|
||||||
|
BigDecimal feeChargesCharged = loanDetails.getSummary().getFeeChargesCharged();
|
||||||
|
BigDecimal feeChargesPaid = loanDetails.getSummary().getFeeChargesPaid();
|
||||||
|
|
||||||
|
assertThat(feeChargesCharged).as("Total fee charges charged should be 50 (2 installments * 25)")
|
||||||
|
.isEqualByComparingTo(BigDecimal.valueOf(50.0));
|
||||||
|
assertThat(feeChargesPaid).as("Total fee charges paid should be 50").isEqualByComparingTo(BigDecimal.valueOf(50.0));
|
||||||
|
|
||||||
|
// With periodic accrual accounting, an accrual transaction is created for the installment fees
|
||||||
|
verifyTransactions(loanId, transaction(2000.0, "Disbursement", "01 October 2023"),
|
||||||
|
transaction(1025.0, "Repayment", "01 November 2023"), transaction(50.0, "Accrual", "01 December 2023"),
|
||||||
|
transaction(1025.0, "Repayment", "01 December 2023"));
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("15 December 2023", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
// Query point-in-time data for a date AFTER the loan was closed
|
||||||
|
LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId, "15 December 2023");
|
||||||
|
|
||||||
|
// For a closed loan, all installment fees should be included (50 total)
|
||||||
|
assertThat(pointInTimeData.getFee().getFeeChargesCharged())
|
||||||
|
.as("Point-in-time feeChargesCharged for closed loan should be 50.0 (2 installments * 25)")
|
||||||
|
.isEqualByComparingTo(BigDecimal.valueOf(50.0));
|
||||||
|
|
||||||
|
assertThat(pointInTimeData.getFee().getFeeChargesPaid()).as("Point-in-time feeChargesPaid for closed loan should be 50.0")
|
||||||
|
.isEqualByComparingTo(BigDecimal.valueOf(50.0));
|
||||||
|
|
||||||
|
assertThat(pointInTimeData.getFee().getFeeChargesOutstanding())
|
||||||
|
.as("Point-in-time feeChargesOutstanding for closed loan should be 0").isEqualByComparingTo(BigDecimal.ZERO);
|
||||||
|
|
||||||
|
// Total outstanding should be 0 for a closed loan
|
||||||
|
verifyOutstanding(pointInTimeData, outstanding(0.0, 0.0, 0.0, 0.0, 0.0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_LoanPointInTimeData_TotalExpectedAmountsConsistency() {
|
||||||
|
AtomicReference<Long> aLoanId = new AtomicReference<>();
|
||||||
|
double installmentFeeAmount = 100.0;
|
||||||
|
|
||||||
|
runAt("01 October 2025", () -> {
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
|
||||||
|
int numberOfRepayments = 6;
|
||||||
|
int repaymentEvery = 1;
|
||||||
|
|
||||||
|
Long installmentFeeChargeId = createInstallmentFeeCharge(installmentFeeAmount);
|
||||||
|
|
||||||
|
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct()
|
||||||
|
.numberOfRepayments(numberOfRepayments).repaymentEvery(repaymentEvery).installmentAmountInMultiplesOf(null)
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()).interestType(InterestType.DECLINING_BALANCE)
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)
|
||||||
|
.interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)
|
||||||
|
.isInterestRecalculationEnabled(true).recalculationRestFrequencyInterval(1)
|
||||||
|
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)
|
||||||
|
.rescheduleStrategyMethod(RescheduleStrategyMethod.REDUCE_EMI_AMOUNT).allowPartialPeriodInterestCalculation(false)
|
||||||
|
.disallowExpectedDisbursements(false).allowApprovedDisbursedAmountsOverApplied(false).overAppliedNumber(null)
|
||||||
|
.overAppliedCalculationType(null).multiDisburseLoan(null)
|
||||||
|
.charges(List.of(new LoanProductChargeData().id(installmentFeeChargeId)));
|
||||||
|
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||||
|
Long loanProductId = loanProductResponse.getResourceId();
|
||||||
|
|
||||||
|
double amount = 6000.0;
|
||||||
|
|
||||||
|
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 October 2025", amount, numberOfRepayments)
|
||||||
|
.repaymentEvery(repaymentEvery).loanTermFrequency(numberOfRepayments)
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS).loanTermFrequencyType(RepaymentFrequencyType.MONTHS)
|
||||||
|
.interestType(InterestType.DECLINING_BALANCE).interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)
|
||||||
|
.charges(List.of(new PostLoansRequestChargeData().chargeId(installmentFeeChargeId)
|
||||||
|
.amount(BigDecimal.valueOf(installmentFeeAmount))));
|
||||||
|
|
||||||
|
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||||
|
|
||||||
|
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
||||||
|
approveLoanRequest(amount, "01 October 2025"));
|
||||||
|
|
||||||
|
aLoanId.getAndSet(approvedLoanResult.getLoanId());
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(amount), "01 October 2025");
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 November 2025", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
addRepaymentForLoan(loanId, 1100.0, "01 November 2025");
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 December 2025", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
addRepaymentForLoan(loanId, 1100.0, "01 December 2025");
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 January 2026", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
addRepaymentForLoan(loanId, 1100.0, "01 January 2026");
|
||||||
|
|
||||||
|
// Verify regular API values (full schedule)
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
BigDecimal regularApiFeeChargesCharged = loanDetails.getSummary().getFeeChargesCharged();
|
||||||
|
assertThat(regularApiFeeChargesCharged).as("Regular API should show full fees (600)")
|
||||||
|
.isEqualByComparingTo(BigDecimal.valueOf(600.0));
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("08 January 2026", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId, "08 January 2026");
|
||||||
|
|
||||||
|
// Fee charges should only include fees for installments due by the requested date (3 installments * 100 =
|
||||||
|
// 300)
|
||||||
|
BigDecimal expectedFeeCharged = BigDecimal.valueOf(300.0);
|
||||||
|
assertThat(pointInTimeData.getFee().getFeeChargesCharged())
|
||||||
|
.as("Point-in-time feeChargesCharged should be 300 (3 installments * 100)").isEqualByComparingTo(expectedFeeCharged);
|
||||||
|
|
||||||
|
// Verify that totalExpectedRepayment is consistent with adjusted fees
|
||||||
|
// totalExpectedRepayment = principal + interest + fees + penalties
|
||||||
|
// For this loan: 6000 (principal) + 0 (interest) + 300 (adjusted fees) + 0 (penalties) = 6300
|
||||||
|
BigDecimal expectedTotalExpectedRepayment = BigDecimal.valueOf(6300.0);
|
||||||
|
assertThat(pointInTimeData.getTotal().getTotalExpectedRepayment())
|
||||||
|
.as("Point-in-time totalExpectedRepayment should be consistent with adjusted feeChargesCharged (6000 + 300 = 6300)")
|
||||||
|
.isEqualByComparingTo(expectedTotalExpectedRepayment);
|
||||||
|
|
||||||
|
// Verify that totalExpectedCostOfLoan is consistent with adjusted fees
|
||||||
|
// totalExpectedCostOfLoan = interest + fees + penalties
|
||||||
|
// For this loan: 0 (interest) + 300 (adjusted fees) + 0 (penalties) = 300
|
||||||
|
BigDecimal expectedTotalExpectedCostOfLoan = BigDecimal.valueOf(300.0);
|
||||||
|
assertThat(pointInTimeData.getTotal().getTotalExpectedCostOfLoan())
|
||||||
|
.as("Point-in-time totalExpectedCostOfLoan should be consistent with adjusted feeChargesCharged (0 + 300 = 300)")
|
||||||
|
.isEqualByComparingTo(expectedTotalExpectedCostOfLoan);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_LoanPointInTimeData_InterestFieldsRemainCoherent() {
|
||||||
|
AtomicReference<Long> aLoanId = new AtomicReference<>();
|
||||||
|
double installmentFeeAmount = 50.0;
|
||||||
|
double interestRatePerPeriod = 12.0;
|
||||||
|
|
||||||
|
runAt("01 October 2025", () -> {
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
|
||||||
|
int numberOfRepayments = 4;
|
||||||
|
int repaymentEvery = 1;
|
||||||
|
|
||||||
|
Long installmentFeeChargeId = createInstallmentFeeCharge(installmentFeeAmount);
|
||||||
|
|
||||||
|
PostLoanProductsRequest product = createOnePeriod30DaysPeriodicAccrualProduct(interestRatePerPeriod)
|
||||||
|
.numberOfRepayments(numberOfRepayments).repaymentEvery(repaymentEvery).installmentAmountInMultiplesOf(null)
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()).interestType(InterestType.FLAT)
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD)
|
||||||
|
.isInterestRecalculationEnabled(false).disallowExpectedDisbursements(false)
|
||||||
|
.allowApprovedDisbursedAmountsOverApplied(false).overAppliedNumber(null).overAppliedCalculationType(null)
|
||||||
|
.multiDisburseLoan(null).charges(List.of(new LoanProductChargeData().id(installmentFeeChargeId)));
|
||||||
|
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||||
|
Long loanProductId = loanProductResponse.getResourceId();
|
||||||
|
|
||||||
|
double amount = 4000.0;
|
||||||
|
|
||||||
|
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 October 2025", amount, numberOfRepayments)
|
||||||
|
.repaymentEvery(repaymentEvery).loanTermFrequency(numberOfRepayments)
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS).loanTermFrequencyType(RepaymentFrequencyType.MONTHS)
|
||||||
|
.interestType(InterestType.FLAT).interestCalculationPeriodType(InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD)
|
||||||
|
.interestRatePerPeriod(BigDecimal.valueOf(interestRatePerPeriod)).charges(List.of(new PostLoansRequestChargeData()
|
||||||
|
.chargeId(installmentFeeChargeId).amount(BigDecimal.valueOf(installmentFeeAmount))));
|
||||||
|
|
||||||
|
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||||
|
|
||||||
|
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
||||||
|
approveLoanRequest(amount, "01 October 2025"));
|
||||||
|
|
||||||
|
aLoanId.getAndSet(approvedLoanResult.getLoanId());
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(amount), "01 October 2025");
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 November 2025", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
addRepaymentForLoan(loanId, 1170.0, "01 November 2025");
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 December 2025", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
addRepaymentForLoan(loanId, 1170.0, "01 December 2025");
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("08 December 2025", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
BigDecimal regularApiInterestCharged = loanDetails.getSummary().getInterestCharged();
|
||||||
|
BigDecimal regularApiInterestPaid = loanDetails.getSummary().getInterestPaid();
|
||||||
|
BigDecimal regularApiInterestOutstanding = loanDetails.getSummary().getInterestOutstanding();
|
||||||
|
|
||||||
|
LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId, "08 December 2025");
|
||||||
|
|
||||||
|
assertThat(pointInTimeData.getInterest().getInterestCharged())
|
||||||
|
.as("Point-in-time interestCharged should remain unchanged from regular API")
|
||||||
|
.isEqualByComparingTo(regularApiInterestCharged);
|
||||||
|
|
||||||
|
assertThat(pointInTimeData.getInterest().getInterestPaid())
|
||||||
|
.as("Point-in-time interestPaid should remain unchanged from regular API").isEqualByComparingTo(regularApiInterestPaid);
|
||||||
|
|
||||||
|
assertThat(pointInTimeData.getInterest().getInterestOutstanding())
|
||||||
|
.as("Point-in-time interestOutstanding should remain unchanged from regular API")
|
||||||
|
.isEqualByComparingTo(regularApiInterestOutstanding);
|
||||||
|
|
||||||
|
assertThat(pointInTimeData.getFee().getFeeChargesCharged())
|
||||||
|
.as("Point-in-time feeChargesCharged should only include fees for 2 installments due by Dec 8")
|
||||||
|
.isEqualByComparingTo(BigDecimal.valueOf(100.0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_LoanPointInTimeData_PenaltyFieldsRemainCoherent() {
|
||||||
|
AtomicReference<Long> aLoanId = new AtomicReference<>();
|
||||||
|
double installmentFeeAmount = 50.0;
|
||||||
|
double installmentPenaltyAmount = 25.0;
|
||||||
|
|
||||||
|
runAt("01 October 2025", () -> {
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
|
||||||
|
int numberOfRepayments = 4;
|
||||||
|
int repaymentEvery = 1;
|
||||||
|
|
||||||
|
Long installmentFeeChargeId = createInstallmentFeeCharge(installmentFeeAmount);
|
||||||
|
Long installmentPenaltyChargeId = createInstallmentPenaltyCharge(installmentPenaltyAmount);
|
||||||
|
|
||||||
|
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct()
|
||||||
|
.numberOfRepayments(numberOfRepayments).repaymentEvery(repaymentEvery).installmentAmountInMultiplesOf(null)
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()).interestType(InterestType.FLAT)
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD)
|
||||||
|
.isInterestRecalculationEnabled(false).disallowExpectedDisbursements(false)
|
||||||
|
.allowApprovedDisbursedAmountsOverApplied(false).overAppliedNumber(null).overAppliedCalculationType(null)
|
||||||
|
.multiDisburseLoan(null).charges(List.of(new LoanProductChargeData().id(installmentFeeChargeId),
|
||||||
|
new LoanProductChargeData().id(installmentPenaltyChargeId)));
|
||||||
|
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||||
|
Long loanProductId = loanProductResponse.getResourceId();
|
||||||
|
|
||||||
|
double amount = 4000.0;
|
||||||
|
|
||||||
|
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 October 2025", amount, numberOfRepayments)
|
||||||
|
.repaymentEvery(repaymentEvery).loanTermFrequency(numberOfRepayments)
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS).loanTermFrequencyType(RepaymentFrequencyType.MONTHS)
|
||||||
|
.interestType(InterestType.FLAT).interestCalculationPeriodType(InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD)
|
||||||
|
.charges(List.of(
|
||||||
|
new PostLoansRequestChargeData().chargeId(installmentFeeChargeId)
|
||||||
|
.amount(BigDecimal.valueOf(installmentFeeAmount)),
|
||||||
|
new PostLoansRequestChargeData().chargeId(installmentPenaltyChargeId)
|
||||||
|
.amount(BigDecimal.valueOf(installmentPenaltyAmount))));
|
||||||
|
|
||||||
|
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||||
|
|
||||||
|
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
||||||
|
approveLoanRequest(amount, "01 October 2025"));
|
||||||
|
|
||||||
|
aLoanId.getAndSet(approvedLoanResult.getLoanId());
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(amount), "01 October 2025");
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 November 2025", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
addRepaymentForLoan(loanId, 1075.0, "01 November 2025");
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 December 2025", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
addRepaymentForLoan(loanId, 1075.0, "01 December 2025");
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("08 December 2025", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
BigDecimal regularApiPenaltyCharged = loanDetails.getSummary().getPenaltyChargesCharged();
|
||||||
|
BigDecimal regularApiPenaltyPaid = loanDetails.getSummary().getPenaltyChargesPaid();
|
||||||
|
BigDecimal regularApiPenaltyOutstanding = loanDetails.getSummary().getPenaltyChargesOutstanding();
|
||||||
|
|
||||||
|
LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId, "08 December 2025");
|
||||||
|
|
||||||
|
assertThat(pointInTimeData.getPenalty().getPenaltyChargesCharged())
|
||||||
|
.as("Point-in-time penaltyChargesCharged should remain unchanged from regular API")
|
||||||
|
.isEqualByComparingTo(regularApiPenaltyCharged);
|
||||||
|
|
||||||
|
assertThat(pointInTimeData.getPenalty().getPenaltyChargesPaid())
|
||||||
|
.as("Point-in-time penaltyChargesPaid should remain unchanged from regular API")
|
||||||
|
.isEqualByComparingTo(regularApiPenaltyPaid);
|
||||||
|
|
||||||
|
assertThat(pointInTimeData.getPenalty().getPenaltyChargesOutstanding())
|
||||||
|
.as("Point-in-time penaltyChargesOutstanding should remain unchanged from regular API")
|
||||||
|
.isEqualByComparingTo(regularApiPenaltyOutstanding);
|
||||||
|
|
||||||
|
assertThat(pointInTimeData.getFee().getFeeChargesCharged())
|
||||||
|
.as("Point-in-time feeChargesCharged should only include fees for 2 installments due by Dec 8")
|
||||||
|
.isEqualByComparingTo(BigDecimal.valueOf(100.0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_LoanPointInTimeData_MatchesRegularApiWhenNoFiltering() {
|
||||||
|
AtomicReference<Long> aLoanId = new AtomicReference<>();
|
||||||
|
double installmentFeeAmount = 100.0;
|
||||||
|
|
||||||
|
runAt("01 October 2025", () -> {
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
|
||||||
|
int numberOfRepayments = 3;
|
||||||
|
int repaymentEvery = 1;
|
||||||
|
|
||||||
|
Long installmentFeeChargeId = createInstallmentFeeCharge(installmentFeeAmount);
|
||||||
|
|
||||||
|
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct()
|
||||||
|
.numberOfRepayments(numberOfRepayments).repaymentEvery(repaymentEvery).installmentAmountInMultiplesOf(null)
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()).interestType(InterestType.FLAT)
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD)
|
||||||
|
.isInterestRecalculationEnabled(false).disallowExpectedDisbursements(false)
|
||||||
|
.allowApprovedDisbursedAmountsOverApplied(false).overAppliedNumber(null).overAppliedCalculationType(null)
|
||||||
|
.multiDisburseLoan(null).charges(List.of(new LoanProductChargeData().id(installmentFeeChargeId)));
|
||||||
|
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||||
|
Long loanProductId = loanProductResponse.getResourceId();
|
||||||
|
|
||||||
|
double amount = 3000.0;
|
||||||
|
|
||||||
|
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 October 2025", amount, numberOfRepayments)
|
||||||
|
.repaymentEvery(repaymentEvery).loanTermFrequency(numberOfRepayments)
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS).loanTermFrequencyType(RepaymentFrequencyType.MONTHS)
|
||||||
|
.interestType(InterestType.FLAT).interestCalculationPeriodType(InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD)
|
||||||
|
.charges(List.of(new PostLoansRequestChargeData().chargeId(installmentFeeChargeId)
|
||||||
|
.amount(BigDecimal.valueOf(installmentFeeAmount))));
|
||||||
|
|
||||||
|
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||||
|
|
||||||
|
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
||||||
|
approveLoanRequest(amount, "01 October 2025"));
|
||||||
|
|
||||||
|
aLoanId.getAndSet(approvedLoanResult.getLoanId());
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(amount), "01 October 2025");
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 November 2025", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
addRepaymentForLoan(loanId, 1100.0, "01 November 2025");
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 December 2025", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
addRepaymentForLoan(loanId, 1100.0, "01 December 2025");
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("01 January 2026", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
addRepaymentForLoan(loanId, 1100.0, "01 January 2026");
|
||||||
|
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
|
||||||
|
assertThat(loanDetails.getStatus().getCode()).isEqualTo("loanStatusType.closed.obligations.met");
|
||||||
|
});
|
||||||
|
|
||||||
|
runAt("15 January 2026", () -> {
|
||||||
|
Long loanId = aLoanId.get();
|
||||||
|
|
||||||
|
GetLoansLoanIdResponse regularApi = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
LoanPointInTimeData pointInTimeApi = getPointInTimeData(loanId, "15 January 2026");
|
||||||
|
|
||||||
|
assertThat(pointInTimeApi.getFee().getFeeChargesCharged())
|
||||||
|
.as("Closed loan: point-in-time feeChargesCharged should match regular API")
|
||||||
|
.isEqualByComparingTo(regularApi.getSummary().getFeeChargesCharged());
|
||||||
|
|
||||||
|
assertThat(pointInTimeApi.getFee().getFeeChargesPaid()).as("Closed loan: point-in-time feeChargesPaid should match regular API")
|
||||||
|
.isEqualByComparingTo(regularApi.getSummary().getFeeChargesPaid());
|
||||||
|
|
||||||
|
assertThat(pointInTimeApi.getFee().getFeeChargesOutstanding())
|
||||||
|
.as("Closed loan: point-in-time feeChargesOutstanding should match regular API")
|
||||||
|
.isEqualByComparingTo(regularApi.getSummary().getFeeChargesOutstanding());
|
||||||
|
|
||||||
|
assertThat(pointInTimeApi.getTotal().getTotalExpectedRepayment())
|
||||||
|
.as("Closed loan: point-in-time totalExpectedRepayment should match regular API")
|
||||||
|
.isEqualByComparingTo(regularApi.getSummary().getTotalExpectedRepayment());
|
||||||
|
|
||||||
|
assertThat(pointInTimeApi.getTotal().getTotalExpectedCostOfLoan())
|
||||||
|
.as("Closed loan: point-in-time totalExpectedCostOfLoan should match regular API")
|
||||||
|
.isEqualByComparingTo(regularApi.getSummary().getTotalExpectedCostOfLoan());
|
||||||
|
|
||||||
|
assertThat(pointInTimeApi.getPrincipal().getPrincipalOutstanding())
|
||||||
|
.as("Closed loan: point-in-time principalOutstanding should match regular API")
|
||||||
|
.isEqualByComparingTo(regularApi.getSummary().getPrincipalOutstanding());
|
||||||
|
|
||||||
|
assertThat(pointInTimeApi.getTotal().getTotalOutstanding())
|
||||||
|
.as("Closed loan: point-in-time totalOutstanding should match regular API")
|
||||||
|
.isEqualByComparingTo(regularApi.getSummary().getTotalOutstanding());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long createInstallmentFeeCharge(double amount) {
|
||||||
|
Integer chargeId = ChargesHelper.createCharges(requestSpec, responseSpec,
|
||||||
|
ChargesHelper.getLoanInstallmentJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, String.valueOf(amount), false));
|
||||||
|
assertNotNull(chargeId);
|
||||||
|
return chargeId.longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long createInstallmentPenaltyCharge(double amount) {
|
||||||
|
Integer chargeId = ChargesHelper.createCharges(requestSpec, responseSpec,
|
||||||
|
ChargesHelper.getLoanInstallmentJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, String.valueOf(amount), true));
|
||||||
|
assertNotNull(chargeId);
|
||||||
|
return chargeId.longValue();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user