FINERACT-2308: fix duplicates for multiple tranches
This commit is contained in:
-7
@@ -1696,7 +1696,6 @@ public final class LoanApplicationValidator {
|
||||
BigDecimal tatalDisbursement = BigDecimal.ZERO;
|
||||
final JsonArray variationArray = this.fromApiJsonHelper.extractJsonArrayNamed(LoanApiConstants.disbursementDataParameterName,
|
||||
element);
|
||||
List<LocalDate> expectedDisbursementDates = new ArrayList<>();
|
||||
if (variationArray != null && !variationArray.isEmpty()) {
|
||||
if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isEqualAmortizationParam, element)) {
|
||||
boolean isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isEqualAmortizationParam,
|
||||
@@ -1722,12 +1721,6 @@ public final class LoanApplicationValidator {
|
||||
.failWithCode(LoanApiConstants.DISBURSEMENT_DATE_BEFORE_ERROR);
|
||||
}
|
||||
|
||||
if (expectedDisbursementDate != null && expectedDisbursementDates.contains(expectedDisbursementDate)) {
|
||||
baseDataValidator.reset().parameter(LoanApiConstants.expectedDisbursementDateParameterName)
|
||||
.failWithCode(LoanApiConstants.DISBURSEMENT_DATE_UNIQUE_ERROR);
|
||||
}
|
||||
expectedDisbursementDates.add(expectedDisbursementDate);
|
||||
|
||||
BigDecimal principal = this.fromApiJsonHelper
|
||||
.extractBigDecimalNamed(LoanApiConstants.disbursementPrincipalParameterName, jsonObject, locale);
|
||||
baseDataValidator.reset().parameter(LoanApiConstants.disbursementDataParameterName)
|
||||
|
||||
+107
-4
@@ -28,12 +28,15 @@ import com.google.gson.JsonPrimitive;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
@@ -57,6 +60,7 @@ import org.apache.fineract.portfolio.loanaccount.serialization.LoanDisbursementV
|
||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||
import org.springframework.lang.NonNull;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class LoanDisbursementService {
|
||||
|
||||
@@ -125,10 +129,13 @@ public class LoanDisbursementService {
|
||||
loan.setActualDisbursementDate(actualDisbursementDate);
|
||||
}
|
||||
BigDecimal diff = BigDecimal.ZERO;
|
||||
final Collection<LoanDisbursementDetails> details = loan.fetchUndisbursedDetail();
|
||||
final Collection<LoanDisbursementDetails> rawDetails = loan.fetchUndisbursedDetail();
|
||||
final Collection<LoanDisbursementDetails> details = hasMultipleTranchesOnSameDateWithSameExpectedDate(rawDetails,
|
||||
actualDisbursementDate) ? sortDisbursementDetailsByBusinessRules(rawDetails) : rawDetails;
|
||||
if (principalDisbursed == null) {
|
||||
disburseAmount = loan.getLoanRepaymentScheduleDetail().getPrincipal();
|
||||
if (!details.isEmpty()) {
|
||||
// When no specific amount provided, disburse ALL undisbursed tranches for the date
|
||||
disburseAmount = disburseAmount.zero();
|
||||
for (LoanDisbursementDetails disbursementDetails : details) {
|
||||
disbursementDetails.updateActualDisbursementDate(actualDisbursementDate);
|
||||
@@ -145,9 +152,45 @@ public class LoanDisbursementService {
|
||||
if (details.isEmpty()) {
|
||||
diff = loan.getLoanRepaymentScheduleDetail().getPrincipal().minus(principalDisbursed).getAmount();
|
||||
} else {
|
||||
for (LoanDisbursementDetails disbursementDetails : details) {
|
||||
disbursementDetails.updateActualDisbursementDate(actualDisbursementDate);
|
||||
disbursementDetails.updatePrincipal(principalDisbursed);
|
||||
// Check if this is a tranche-based loan (has multiple predefined disbursement details)
|
||||
// versus a non-tranche multi-disbursal loan (creates disbursement details on-the-fly)
|
||||
boolean isTrancheBasedLoan = hasMultipleOrPreDefinedDisbursementDetails(loan, details);
|
||||
|
||||
if (isTrancheBasedLoan && details.size() >= 1) {
|
||||
// For tranche-based loans, find the matching tranche by amount first, then by order
|
||||
LoanDisbursementDetails selectedTranche = null;
|
||||
|
||||
// First try to find a tranche that exactly matches the requested disbursement amount
|
||||
for (LoanDisbursementDetails disbursementDetails : details) {
|
||||
if (disbursementDetails.actualDisbursementDate() == null
|
||||
&& disbursementDetails.principal().compareTo(principalDisbursed) == 0) {
|
||||
selectedTranche = disbursementDetails;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no exact match found, take the first available tranche (next in line)
|
||||
if (selectedTranche == null) {
|
||||
for (LoanDisbursementDetails disbursementDetails : details) {
|
||||
if (disbursementDetails.actualDisbursementDate() == null) {
|
||||
selectedTranche = disbursementDetails;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedTranche != null) {
|
||||
// Update the selected tranche with the actual disbursement
|
||||
selectedTranche.updateActualDisbursementDate(actualDisbursementDate);
|
||||
selectedTranche.updatePrincipal(principalDisbursed);
|
||||
}
|
||||
} else {
|
||||
// For non-tranche multi-disbursal loans: preserve original behavior
|
||||
// Update all available disbursement details with the actual disbursement date and amount
|
||||
for (LoanDisbursementDetails disbursementDetails : details) {
|
||||
disbursementDetails.updateActualDisbursementDate(actualDisbursementDate);
|
||||
disbursementDetails.updatePrincipal(principalDisbursed);
|
||||
}
|
||||
}
|
||||
}
|
||||
BigDecimal totalAmount = BigDecimal.ZERO;
|
||||
@@ -377,4 +420,64 @@ public class LoanDisbursementService {
|
||||
return returnObject;
|
||||
}
|
||||
|
||||
private boolean hasMultipleOrPreDefinedDisbursementDetails(final Loan loan,
|
||||
final Collection<LoanDisbursementDetails> undisbursedDetails) {
|
||||
Collection<LoanDisbursementDetails> allDisbursementDetails = loan.getDisbursementDetails();
|
||||
|
||||
if (undisbursedDetails.size() > 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (allDisbursementDetails.size() > 1 && !undisbursedDetails.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (undisbursedDetails.size() == 1) {
|
||||
LoanDisbursementDetails singleDetail = undisbursedDetails.iterator().next();
|
||||
BigDecimal loanPrincipal = loan.getLoanRepaymentScheduleDetail().getPrincipal().getAmount();
|
||||
|
||||
if (singleDetail.principal().compareTo(loanPrincipal) == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Default to tranche behavior for safety in ambiguous cases
|
||||
return true;
|
||||
}
|
||||
|
||||
public static List<LoanDisbursementDetails> sortDisbursementDetailsByBusinessRules(
|
||||
Collection<LoanDisbursementDetails> disbursementDetails) {
|
||||
if (disbursementDetails == null || disbursementDetails.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return disbursementDetails.stream()
|
||||
.sorted(Comparator.comparing(LoanDisbursementDetails::expectedDisbursementDate)
|
||||
.thenComparing((LoanDisbursementDetails d1, LoanDisbursementDetails d2) -> d2.principal().compareTo(d1.principal()))
|
||||
.thenComparing(LoanDisbursementDetails::getId))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static boolean hasMultipleTranchesOnSameDate(Collection<LoanDisbursementDetails> disbursementDetails) {
|
||||
if (disbursementDetails == null || disbursementDetails.size() <= 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return disbursementDetails.stream()
|
||||
.collect(Collectors.groupingBy(LoanDisbursementDetails::expectedDisbursementDate, Collectors.counting())).values().stream()
|
||||
.anyMatch(count -> count > 1);
|
||||
}
|
||||
|
||||
public static boolean hasMultipleTranchesOnSameDateWithSameExpectedDate(Collection<LoanDisbursementDetails> disbursementDetails,
|
||||
LocalDate actualDisbursementDate) {
|
||||
if (disbursementDetails == null || disbursementDetails.size() <= 1 || actualDisbursementDate == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long tranchesForActualDate = disbursementDetails.stream()
|
||||
.filter(detail -> actualDisbursementDate.equals(detail.expectedDisbursementDate())).count();
|
||||
|
||||
return tranchesForActualDate > 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+63
-10
@@ -31,6 +31,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -1550,22 +1551,74 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
private List<LoanSchedulePeriodDataWrapper> collectEligibleDisbursementData(LoanScheduleType loanScheduleType,
|
||||
Collection<DisbursementData> disbursementData, LocalDate fromDate, LocalDate dueDate, Set<Long> disbursementPeriodIds) {
|
||||
List<LoanSchedulePeriodDataWrapper> disbursementDataList = new ArrayList<>();
|
||||
// Collect eligible disbursement data
|
||||
for (final DisbursementData data : disbursementData) {
|
||||
boolean isDueForDisbursement = data.isDueForDisbursement(loanScheduleType, fromDate, dueDate);
|
||||
boolean isEligible = ((fromDate.equals(this.disbursement.disbursementDate()) && data.disbursementDate().equals(fromDate))
|
||||
|| (fromDate.equals(dueDate) && data.disbursementDate().equals(fromDate))
|
||||
|| canAddDisbursementData(data, isDueForDisbursement, excludePastUnDisbursed))
|
||||
&& !disbursementPeriodIds.contains(data.getId());
|
||||
|
||||
if (isEligible) {
|
||||
disbursementDataList.add(new LoanSchedulePeriodDataWrapper(data, data.disbursementDate(), true));
|
||||
disbursementPeriodIds.add(data.getId());
|
||||
boolean hasMultipleTranchesOnSameDate = hasMultipleTranchesOnSameDate(disbursementData);
|
||||
|
||||
if (hasMultipleTranchesOnSameDate) {
|
||||
Map<LocalDate, List<DisbursementData>> disbursementsByDate = new HashMap<>();
|
||||
|
||||
for (final DisbursementData data : disbursementData) {
|
||||
boolean isDueForDisbursement = data.isDueForDisbursement(loanScheduleType, fromDate, dueDate);
|
||||
boolean isEligible = ((fromDate.equals(this.disbursement.disbursementDate())
|
||||
&& data.disbursementDate().equals(fromDate))
|
||||
|| (fromDate.equals(dueDate) && data.disbursementDate().equals(fromDate))
|
||||
|| canAddDisbursementData(data, isDueForDisbursement, excludePastUnDisbursed))
|
||||
&& !disbursementPeriodIds.contains(data.getId());
|
||||
|
||||
if (isEligible) {
|
||||
disbursementsByDate.computeIfAbsent(data.disbursementDate(), k -> new ArrayList<>()).add(data);
|
||||
disbursementPeriodIds.add(data.getId());
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<LocalDate, List<DisbursementData>> entry : disbursementsByDate.entrySet()) {
|
||||
List<DisbursementData> sameDateDisbursements = entry.getValue();
|
||||
|
||||
if (sameDateDisbursements.size() > 1) {
|
||||
List<DisbursementData> disbursedTranches = sameDateDisbursements.stream().filter(DisbursementData::isDisbursed)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!disbursedTranches.isEmpty()) {
|
||||
for (DisbursementData data : disbursedTranches) {
|
||||
disbursementDataList.add(new LoanSchedulePeriodDataWrapper(data, data.disbursementDate(), true));
|
||||
}
|
||||
} else {
|
||||
for (DisbursementData data : sameDateDisbursements) {
|
||||
disbursementDataList.add(new LoanSchedulePeriodDataWrapper(data, data.disbursementDate(), true));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DisbursementData data = sameDateDisbursements.get(0);
|
||||
disbursementDataList.add(new LoanSchedulePeriodDataWrapper(data, data.disbursementDate(), true));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (final DisbursementData data : disbursementData) {
|
||||
boolean isDueForDisbursement = data.isDueForDisbursement(loanScheduleType, fromDate, dueDate);
|
||||
boolean isEligible = ((fromDate.equals(this.disbursement.disbursementDate())
|
||||
&& data.disbursementDate().equals(fromDate))
|
||||
|| (fromDate.equals(dueDate) && data.disbursementDate().equals(fromDate))
|
||||
|| canAddDisbursementData(data, isDueForDisbursement, excludePastUnDisbursed))
|
||||
&& !disbursementPeriodIds.contains(data.getId());
|
||||
|
||||
if (isEligible) {
|
||||
disbursementDataList.add(new LoanSchedulePeriodDataWrapper(data, data.disbursementDate(), true));
|
||||
disbursementPeriodIds.add(data.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return disbursementDataList;
|
||||
}
|
||||
|
||||
private boolean hasMultipleTranchesOnSameDate(Collection<DisbursementData> disbursementData) {
|
||||
if (disbursementData == null || disbursementData.size() <= 1) {
|
||||
return false;
|
||||
}
|
||||
return disbursementData.stream().collect(Collectors.groupingBy(DisbursementData::disbursementDate, Collectors.counting()))
|
||||
.values().stream().anyMatch(count -> count > 1);
|
||||
}
|
||||
|
||||
private List<LoanSchedulePeriodDataWrapper> collectEligibleCapitalizedIncomeData(LocalDate fromDate, LocalDate dueDate,
|
||||
Set<Long> disbursementPeriodIds) {
|
||||
List<LoanSchedulePeriodDataWrapper> capitalizedIncomeDataList = new ArrayList<>();
|
||||
|
||||
+2
-14
@@ -202,7 +202,6 @@ public class LoanApplicationApprovalTest {
|
||||
private void trancheLoansApprovalValidation(Integer clientID, Integer loanProductID, List<HashMap> createTranches) {
|
||||
final String proposedAmount = "5000";
|
||||
final String approvalAmount1 = "10000";
|
||||
final String approvalAmount2 = "3000";
|
||||
final String approvalAmount3 = "400";
|
||||
final String approvalAmount4 = "200";
|
||||
|
||||
@@ -213,11 +212,6 @@ public class LoanApplicationApprovalTest {
|
||||
approveTranche1.add(createTrancheDetail("01 March 2014", "5000"));
|
||||
approveTranche1.add(createTrancheDetail("23 March 2014", "5000"));
|
||||
|
||||
List<HashMap> approveTranche2 = new ArrayList<>();
|
||||
approveTranche2.add(createTrancheDetail("01 March 2014", "1000"));
|
||||
approveTranche2.add(createTrancheDetail("23 March 2014", "1000"));
|
||||
approveTranche2.add(createTrancheDetail("23 March 2014", "1000"));
|
||||
|
||||
List<HashMap> approveTranche3 = new ArrayList<>();
|
||||
approveTranche3.add(createTrancheDetail("01 March 2014", "100"));
|
||||
approveTranche3.add(createTrancheDetail("23 March 2014", "100"));
|
||||
@@ -238,15 +232,9 @@ public class LoanApplicationApprovalTest {
|
||||
log.info("-----------------------------------APPROVE LOAN-----------------------------------------------------------");
|
||||
this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpecForStatusCode400);
|
||||
|
||||
/* Tranches with same expected disbursement date */
|
||||
List<HashMap<String, Object>> error = this.loanTransactionHelper.approveLoanForTranches(approveDate, expectedDisbursementDate,
|
||||
approvalAmount2, loanID, approveTranche2, CommonConstants.RESPONSE_ERROR);
|
||||
assertEquals("validation.msg.loan.expectedDisbursementDate.disbursement.date.must.be.unique.for.tranches",
|
||||
error.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE));
|
||||
|
||||
/* Sum of tranches is greater than approved amount */
|
||||
error = this.loanTransactionHelper.approveLoanForTranches(approveDate, expectedDisbursementDate, approvalAmount4, loanID,
|
||||
approveTranche4, CommonConstants.RESPONSE_ERROR);
|
||||
List<HashMap<String, Object>> error = this.loanTransactionHelper.approveLoanForTranches(approveDate, expectedDisbursementDate,
|
||||
approvalAmount4, loanID, approveTranche4, CommonConstants.RESPONSE_ERROR);
|
||||
assertEquals("validation.msg.loan.principal.sum.of.multi.disburse.amounts.must.be.equal.to.or.lesser.than.approved.principal",
|
||||
error.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user