From cbe8f029e2095640da9ae022c87870304543fac9 Mon Sep 17 00:00:00 2001 From: Arnold Galovics Date: Mon, 15 Jan 2024 11:11:27 +0100 Subject: [PATCH] FINERACT-1971: Fix for not properly resolving delinquency range data for loan in case there's an installment delinquency --- ...DelinquencyWritePlatformServiceHelper.java | 268 ++++++++++++++++++ .../DelinquencyWritePlatformServiceImpl.java | 265 ++--------------- .../starter/DelinquencyConfiguration.java | 11 +- ...tePlatformServiceRangeChangeEventTest.java | 78 ++++- 4 files changed, 380 insertions(+), 242 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceHelper.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceHelper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceHelper.java new file mode 100644 index 00000000000..29aa610867b --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceHelper.java @@ -0,0 +1,268 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.delinquency.service; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.event.business.domain.loan.LoanDelinquencyRangeChangeBusinessEvent; +import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository; +import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistory; +import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository; +import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTag; +import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository; +import org.apache.fineract.portfolio.loanaccount.data.CollectionData; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +@Slf4j +public class DelinquencyWritePlatformServiceHelper { + + private final BusinessEventNotifierService businessEventNotifierService; + private final LoanDelinquencyTagHistoryRepository loanDelinquencyTagRepository; + private final DelinquencyRangeRepository repositoryRange; + private final LoanInstallmentDelinquencyTagRepository loanInstallmentDelinquencyTagRepository; + + public Map applyDelinquencyForLoan(final Loan loan, final DelinquencyBucket delinquencyBucket, long overdueDays) { + Map changes = new HashMap<>(); + + if (overdueDays <= 0) { // No Delinquency + log.debug("Loan {} without delinquency range with {} days", loan.getId(), overdueDays); + changes = setLoanDelinquencyTag(loan, null); + + } else { + // Sort the ranges based on the minAgeDays + final List ranges = sortDelinquencyRangesByMinAge(delinquencyBucket.getRanges()); + + for (final DelinquencyRange delinquencyRange : ranges) { + if (delinquencyRange.getMaximumAgeDays() == null) { // Last Range in the Bucket + if (delinquencyRange.getMinimumAgeDays() <= overdueDays) { + log.debug("Loan {} with delinquency range {} with {} days", loan.getId(), delinquencyRange.getClassification(), + overdueDays); + changes = setLoanDelinquencyTag(loan, delinquencyRange.getId()); + break; + } + } else { + if (delinquencyRange.getMinimumAgeDays() <= overdueDays && delinquencyRange.getMaximumAgeDays() >= overdueDays) { + log.debug("Loan {} with delinquency range {} with {} days", loan.getId(), delinquencyRange.getClassification(), + overdueDays); + changes = setLoanDelinquencyTag(loan, delinquencyRange.getId()); + break; + } + } + } + } + changes.put("overdueDays", overdueDays); + return changes; + } + + public Map setLoanDelinquencyTag(Loan loan, Long delinquencyRangeId) { + Map changes = new HashMap<>(); + List loanDelinquencyTagHistory = new ArrayList<>(); + final LocalDate transactionDate = DateUtils.getBusinessLocalDate(); + Optional optLoanDelinquencyTag = this.loanDelinquencyTagRepository.findByLoanAndLiftedOnDate(loan, null); + // The delinquencyRangeId in null means just goes out from Delinquency + LoanDelinquencyTagHistory loanDelinquencyTagPrev = null; + if (delinquencyRangeId == null) { + // The Loan will go out from Delinquency + if (optLoanDelinquencyTag.isPresent()) { + loanDelinquencyTagPrev = optLoanDelinquencyTag.get(); + loanDelinquencyTagPrev.setLiftedOnDate(transactionDate); + loanDelinquencyTagHistory.add(loanDelinquencyTagPrev); + changes.put("previous", loanDelinquencyTagPrev.getDelinquencyRange()); + // event when loan goes out of delinquency we do not calculate at + // installment level and remove all installment tags, so event needs to raised here. + if (loan.isEnableInstallmentLevelDelinquency()) { + businessEventNotifierService.notifyPostBusinessEvent(new LoanDelinquencyRangeChangeBusinessEvent(loan)); + } + } + } else { + if (optLoanDelinquencyTag.isPresent()) { + loanDelinquencyTagPrev = optLoanDelinquencyTag.get(); + } + // If the Delinquency Tag has not changed + if (loanDelinquencyTagPrev != null && loanDelinquencyTagPrev.getDelinquencyRange().getId().equals(delinquencyRangeId)) { + changes.put("current", loanDelinquencyTagPrev.getDelinquencyRange()); + } else { + // The previous Loan Delinquency Tag will set as Lifted + if (loanDelinquencyTagPrev != null) { + loanDelinquencyTagPrev.setLiftedOnDate(transactionDate); + loanDelinquencyTagHistory.add(loanDelinquencyTagPrev); + changes.put("previous", loanDelinquencyTagPrev.getDelinquencyRange()); + } + + final DelinquencyRange delinquencyRange = repositoryRange.getReferenceById(delinquencyRangeId); + LoanDelinquencyTagHistory loanDelinquencyTag = new LoanDelinquencyTagHistory(delinquencyRange, loan, transactionDate, null); + loanDelinquencyTagHistory.add(loanDelinquencyTag); + changes.put("current", loanDelinquencyTag.getDelinquencyRange()); + } + } + if (loanDelinquencyTagHistory.size() > 0) { + this.loanDelinquencyTagRepository.saveAllAndFlush(loanDelinquencyTagHistory); + // if installment level delinquency is enabled event will be raised at installment level calculation, no + // need to raise the event here + if (!loan.isEnableInstallmentLevelDelinquency()) { + businessEventNotifierService.notifyPostBusinessEvent(new LoanDelinquencyRangeChangeBusinessEvent(loan)); + } + } + return changes; + } + + public List sortDelinquencyRangesByMinAge(List ranges) { + final Comparator orderByMinAge = new Comparator() { + + @Override + public int compare(DelinquencyRange o1, DelinquencyRange o2) { + return o1.getMinimumAgeDays().compareTo(o2.getMinimumAgeDays()); + } + }; + Collections.sort(ranges, orderByMinAge); + return ranges; + } + + public void applyDelinquencyForLoanInstallments(final Loan loan, final DelinquencyBucket delinquencyBucket, + final Map installmentsCollectionData) { + boolean isDelinquencyRangeChangedForAnyOfInstallment = false; + for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) { + if (installmentsCollectionData.containsKey(installment.getId())) { + boolean isDelinquencySetForInstallment = setInstallmentDelinquencyDetails(loan, installment, delinquencyBucket, + installmentsCollectionData.get(installment.getId())); + isDelinquencyRangeChangedForAnyOfInstallment = isDelinquencyRangeChangedForAnyOfInstallment + || isDelinquencySetForInstallment; + } + } + // remove tags for non-existing installments that got deleted due to re-schedule + removeDelinquencyTagsForNonExistingInstallments(loan.getId()); + // raise event if there is any change at installment level delinquency + if (isDelinquencyRangeChangedForAnyOfInstallment) { + businessEventNotifierService.notifyPostBusinessEvent(new LoanDelinquencyRangeChangeBusinessEvent(loan)); + } + + } + + private void removeDelinquencyTagsForNonExistingInstallments(Long loanId) { + List currentLoanInstallmentDelinquencyTags = loanInstallmentDelinquencyTagRepository + .findByLoanId(loanId); + if (currentLoanInstallmentDelinquencyTags != null && currentLoanInstallmentDelinquencyTags.size() > 0) { + List loanInstallmentTagsForDelete = currentLoanInstallmentDelinquencyTags.stream() + .filter(tag -> tag.getInstallment() == null).map(tag -> tag.getId()).toList(); + if (loanInstallmentTagsForDelete.size() > 0) { + loanInstallmentDelinquencyTagRepository.deleteAllLoanInstallmentsTagsByIds(loanInstallmentTagsForDelete); + } + } + } + + private boolean setInstallmentDelinquencyDetails(final Loan loan, final LoanRepaymentScheduleInstallment installment, + final DelinquencyBucket delinquencyBucket, final CollectionData installmentDelinquencyData) { + DelinquencyRange delinquencyRangeForInstallment = getInstallmentDelinquencyRange(delinquencyBucket, + installmentDelinquencyData.getDelinquentDays()); + return setDelinquencyDetailsForInstallment(loan, installment, installmentDelinquencyData, delinquencyRangeForInstallment); + } + + private DelinquencyRange getInstallmentDelinquencyRange(final DelinquencyBucket delinquencyBucket, Long overDueDays) { + DelinquencyRange delinquencyRangeForInstallment = null; + if (overDueDays > 0) { + // Sort the ranges based on the minAgeDays + final List ranges = sortDelinquencyRangesByMinAge(delinquencyBucket.getRanges()); + for (final DelinquencyRange delinquencyRange : ranges) { + if (delinquencyRange.getMaximumAgeDays() == null) { // Last Range in the Bucket + if (delinquencyRange.getMinimumAgeDays() <= overDueDays) { + delinquencyRangeForInstallment = delinquencyRange; + break; + } + } else { + if (delinquencyRange.getMinimumAgeDays() <= overDueDays && delinquencyRange.getMaximumAgeDays() >= overDueDays) { + delinquencyRangeForInstallment = delinquencyRange; + break; + } + } + } + + } + return delinquencyRangeForInstallment; + } + + private boolean setDelinquencyDetailsForInstallment(final Loan loan, final LoanRepaymentScheduleInstallment installment, + CollectionData installmentDelinquencyData, final DelinquencyRange delinquencyRangeForInstallment) { + List installmentDelinquencyTags = new ArrayList<>(); + LocalDate delinquencyCalculationDate = DateUtils.getBusinessLocalDate(); + boolean isDelinquencyRangeChanged = false; + + LoanInstallmentDelinquencyTag previousInstallmentDelinquencyTag = loanInstallmentDelinquencyTagRepository + .findByLoanAndInstallment(loan, installment).orElse(null); + + if (delinquencyRangeForInstallment == null) { + // if currentInstallmentDelinquencyTag exists and range is null, installment is out of delinquency, delete + // delinquency details + if (previousInstallmentDelinquencyTag != null) { + // event installment out of delinquency + loanInstallmentDelinquencyTagRepository.delete(previousInstallmentDelinquencyTag); + isDelinquencyRangeChanged = true; + } + } else { + LoanInstallmentDelinquencyTag installmentDelinquency = null; + if (previousInstallmentDelinquencyTag != null) { + if (!previousInstallmentDelinquencyTag.getDelinquencyRange().getId().equals(delinquencyRangeForInstallment.getId())) { + // if current delinquency range exists and there is range change, delete previous delinquency + // details and add new range details + installmentDelinquency = new LoanInstallmentDelinquencyTag(delinquencyRangeForInstallment, loan, installment, + delinquencyCalculationDate, null, previousInstallmentDelinquencyTag.getFirstOverdueDate(), + installmentDelinquencyData.getDelinquentAmount()); + loanInstallmentDelinquencyTagRepository.delete(previousInstallmentDelinquencyTag); + // event installment delinquency range change + isDelinquencyRangeChanged = true; + } else { + previousInstallmentDelinquencyTag.setOutstandingAmount(installmentDelinquencyData.getDelinquentAmount()); + installmentDelinquency = previousInstallmentDelinquencyTag; + } + } else { + // add new range, first time delinquent + installmentDelinquency = new LoanInstallmentDelinquencyTag(delinquencyRangeForInstallment, loan, installment, + delinquencyCalculationDate, null, installmentDelinquencyData.getDelinquentDate(), + installmentDelinquencyData.getDelinquentAmount()); + // event installment delinquent + isDelinquencyRangeChanged = true; + } + + if (installmentDelinquency != null) { + installmentDelinquencyTags.add(installmentDelinquency); + } + + } + + if (installmentDelinquencyTags.size() > 0) { + loanInstallmentDelinquencyTagRepository.saveAllAndFlush(installmentDelinquencyTags); + } + return isDelinquencyRangeChanged; + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java index 20402997778..29451c169dd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java @@ -20,8 +20,6 @@ import java.time.LocalDate; import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -50,7 +48,6 @@ import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyActionRepository; import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistory; import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository; -import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTag; import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository; import org.apache.fineract.portfolio.delinquency.exception.DelinquencyBucketAgesOverlapedException; import org.apache.fineract.portfolio.delinquency.exception.DelinquencyRangeInvalidAgesException; @@ -63,7 +60,6 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanDelinquencyData; import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData; import org.apache.fineract.portfolio.loanaccount.domain.Loan; -import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository; import org.springframework.transaction.annotation.Transactional; @@ -74,20 +70,20 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat private final DelinquencyBucketParseAndValidator dataValidatorBucket; private final DelinquencyRangeParseAndValidator dataValidatorRange; - private final DelinquencyRangeRepository repositoryRange; private final DelinquencyBucketRepository repositoryBucket; private final DelinquencyBucketMappingsRepository repositoryBucketMappings; private final LoanDelinquencyTagHistoryRepository loanDelinquencyTagRepository; private final LoanRepositoryWrapper loanRepository; private final LoanProductRepository loanProductRepository; - private final BusinessEventNotifierService businessEventNotifierService; private final LoanDelinquencyDomainService loanDelinquencyDomainService; private final LoanInstallmentDelinquencyTagRepository loanInstallmentDelinquencyTagRepository; private final DelinquencyReadPlatformService delinquencyReadPlatformService; private final LoanDelinquencyActionRepository loanDelinquencyActionRepository; private final DelinquencyActionParseAndValidator delinquencyActionParseAndValidator; private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper; + private final BusinessEventNotifierService businessEventNotifierService; + private final DelinquencyWritePlatformServiceHelper delinquencyHelper; @Override public CommandProcessingResult createDelinquencyRange(JsonCommand command) { @@ -186,16 +182,17 @@ public CommandProcessingResult applyDelinquencyTagToLoan(Long loanId, JsonComman final CollectionData collectionData = loanDelinquencyData.getLoanCollectionData(); // loan installments delinquent data final Map installmentsCollectionData = loanDelinquencyData.getLoanInstallmentsCollectionData(); - // delinquency for installments - if (installmentsCollectionData.size() > 0) { - applyDelinquencyDetailsForLoanInstallments(loan, delinquencyBucket, installmentsCollectionData); - } - // delinquency for loan - changes = lookUpDelinquencyRange(loan, delinquencyBucket, collectionData.getDelinquentDays()); + log.debug("Delinquency {}", collectionData); + + changes = applyDelinquencyToLoanAndInstallments(loan, delinquencyBucket, collectionData, installmentsCollectionData); } - return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(loan.getId()) - .withEntityExternalId(loan.getExternalId()).with(changes).build(); + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // + .withEntityId(loan.getId()) // + .withEntityExternalId(loan.getExternalId()) // + .with(changes) // + .build(); // } @Override @@ -210,16 +207,24 @@ public void applyDelinquencyTagToLoan(LoanScheduleDelinquencyData loanDelinquenc final CollectionData collectionData = loanDelinquentData.getLoanCollectionData(); // loan installments delinquent data final Map installmentsCollectionData = loanDelinquentData.getLoanInstallmentsCollectionData(); - // delinquency for installments - if (installmentsCollectionData.size() > 0) { - applyDelinquencyDetailsForLoanInstallments(loan, delinquencyBucket, installmentsCollectionData); - } log.debug("Delinquency {}", collectionData); - // delinquency for loan - lookUpDelinquencyRange(loan, delinquencyBucket, collectionData.getDelinquentDays()); + + applyDelinquencyToLoanAndInstallments(loan, delinquencyBucket, collectionData, installmentsCollectionData); } } + private Map applyDelinquencyToLoanAndInstallments(Loan loan, DelinquencyBucket delinquencyBucket, + CollectionData collectionData, Map installmentsCollectionData) { + // Order is important: first calculate loan level delinquency, then the installment level + // delinquency for loan + Map result = delinquencyHelper.applyDelinquencyForLoan(loan, delinquencyBucket, collectionData.getDelinquentDays()); + // delinquency for installments + if (!installmentsCollectionData.isEmpty()) { + delinquencyHelper.applyDelinquencyForLoanInstallments(loan, delinquencyBucket, installmentsCollectionData); + } + return result; + } + @Override @Transactional public CommandProcessingResult createDelinquencyAction(Long loanId, JsonCommand command) { @@ -242,7 +247,8 @@ public CommandProcessingResult createDelinquencyAction(Long loanId, JsonCommand } } businessEventNotifierService.notifyPostBusinessEvent(new LoanAccountDelinquencyPauseChangedBusinessEvent(loan)); - return new CommandProcessingResultBuilder().withCommandId(command.commandId()) // + return new CommandProcessingResultBuilder() // + .withCommandId(command.commandId()) // .withEntityId(saved.getId()) // .withOfficeId(loan.getOfficeId()) // .withClientId(loan.getClientId()) // @@ -271,7 +277,7 @@ public void removeDelinquencyTagToLoan(final Loan loan) { if (loan.isEnableInstallmentLevelDelinquency()) { cleanLoanInstallmentsDelinquencyTags(loan); } - setLoanDelinquencyTag(loan, null); + delinquencyHelper.setLoanDelinquencyTag(loan, null); } @Override @@ -365,7 +371,7 @@ private void setDelinquencyBucketMappings(DelinquencyBucket delinquencyBucket, D private void validateDelinquencyRanges(List ranges) { // Sort the ranges based on the minAgeDays - ranges = sortDelinquencyRangesByMinAge(ranges); + ranges = delinquencyHelper.sortDelinquencyRangesByMinAge(ranges); DelinquencyRange prevDelinquencyRange = null; for (DelinquencyRange delinquencyRange : ranges) { @@ -387,220 +393,7 @@ private boolean isOverlapped(DelinquencyRange o1, DelinquencyRange o2) { } } - private Map lookUpDelinquencyRange(final Loan loan, final DelinquencyBucket delinquencyBucket, long overdueDays) { - Map changes = new HashMap<>(); - - if (overdueDays <= 0) { // No Delinquency - log.debug("Loan {} without delinquency range with {} days", loan.getId(), overdueDays); - changes = setLoanDelinquencyTag(loan, null); - - } else { - // Sort the ranges based on the minAgeDays - final List ranges = sortDelinquencyRangesByMinAge(delinquencyBucket.getRanges()); - - for (final DelinquencyRange delinquencyRange : ranges) { - if (delinquencyRange.getMaximumAgeDays() == null) { // Last Range in the Bucket - if (delinquencyRange.getMinimumAgeDays() <= overdueDays) { - log.debug("Loan {} with delinquency range {} with {} days", loan.getId(), delinquencyRange.getClassification(), - overdueDays); - changes = setLoanDelinquencyTag(loan, delinquencyRange.getId()); - break; - } - } else { - if (delinquencyRange.getMinimumAgeDays() <= overdueDays && delinquencyRange.getMaximumAgeDays() >= overdueDays) { - log.debug("Loan {} with delinquency range {} with {} days", loan.getId(), delinquencyRange.getClassification(), - overdueDays); - changes = setLoanDelinquencyTag(loan, delinquencyRange.getId()); - break; - } - } - } - } - changes.put("overdueDays", overdueDays); - return changes; - } - - private Map setLoanDelinquencyTag(Loan loan, Long delinquencyRangeId) { - Map changes = new HashMap<>(); - List loanDelinquencyTagHistory = new ArrayList<>(); - final LocalDate transactionDate = DateUtils.getBusinessLocalDate(); - Optional optLoanDelinquencyTag = this.loanDelinquencyTagRepository.findByLoanAndLiftedOnDate(loan, null); - // The delinquencyRangeId in null means just goes out from Delinquency - LoanDelinquencyTagHistory loanDelinquencyTagPrev = null; - if (delinquencyRangeId == null) { - // The Loan will go out from Delinquency - if (optLoanDelinquencyTag.isPresent()) { - loanDelinquencyTagPrev = optLoanDelinquencyTag.get(); - loanDelinquencyTagPrev.setLiftedOnDate(transactionDate); - loanDelinquencyTagHistory.add(loanDelinquencyTagPrev); - changes.put("previous", loanDelinquencyTagPrev.getDelinquencyRange()); - // event when loan goes out of delinquency we do not calculate at - // installment level and remove all installment tags, so event needs to raised here. - if (loan.isEnableInstallmentLevelDelinquency()) { - businessEventNotifierService.notifyPostBusinessEvent(new LoanDelinquencyRangeChangeBusinessEvent(loan)); - } - } - } else { - if (optLoanDelinquencyTag.isPresent()) { - loanDelinquencyTagPrev = optLoanDelinquencyTag.get(); - } - // If the Delinquency Tag has not changed - if (loanDelinquencyTagPrev != null && loanDelinquencyTagPrev.getDelinquencyRange().getId().equals(delinquencyRangeId)) { - changes.put("current", loanDelinquencyTagPrev.getDelinquencyRange()); - } else { - // The previous Loan Delinquency Tag will set as Lifted - if (loanDelinquencyTagPrev != null) { - loanDelinquencyTagPrev.setLiftedOnDate(transactionDate); - loanDelinquencyTagHistory.add(loanDelinquencyTagPrev); - changes.put("previous", loanDelinquencyTagPrev.getDelinquencyRange()); - } - - final DelinquencyRange delinquencyRange = repositoryRange.getReferenceById(delinquencyRangeId); - LoanDelinquencyTagHistory loanDelinquencyTag = new LoanDelinquencyTagHistory(delinquencyRange, loan, transactionDate, null); - loanDelinquencyTagHistory.add(loanDelinquencyTag); - changes.put("current", loanDelinquencyTag.getDelinquencyRange()); - } - } - if (loanDelinquencyTagHistory.size() > 0) { - this.loanDelinquencyTagRepository.saveAllAndFlush(loanDelinquencyTagHistory); - // if installment level delinquency is enabled event will be raised at installment level calculation, no - // need to raise the event here - if (!loan.isEnableInstallmentLevelDelinquency()) { - businessEventNotifierService.notifyPostBusinessEvent(new LoanDelinquencyRangeChangeBusinessEvent(loan)); - } - } - return changes; - } - - private List sortDelinquencyRangesByMinAge(List ranges) { - final Comparator orderByMinAge = new Comparator() { - - @Override - public int compare(DelinquencyRange o1, DelinquencyRange o2) { - return o1.getMinimumAgeDays().compareTo(o2.getMinimumAgeDays()); - } - }; - Collections.sort(ranges, orderByMinAge); - return ranges; - } - - private void applyDelinquencyDetailsForLoanInstallments(final Loan loan, final DelinquencyBucket delinquencyBucket, - final Map installmentsCollectionData) { - boolean isDelinquencyRangeChangedForAnyOfInstallment = false; - for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) { - if (installmentsCollectionData.containsKey(installment.getId())) { - boolean isDelinquencySetForInstallment = setInstallmentDelinquencyDetails(loan, installment, delinquencyBucket, - installmentsCollectionData.get(installment.getId())); - isDelinquencyRangeChangedForAnyOfInstallment = isDelinquencyRangeChangedForAnyOfInstallment - || isDelinquencySetForInstallment; - } - } - // remove tags for non existing installments that got deleted due to re-schedule - removeDelinquencyTagsForNonExistingInstallments(loan.getId()); - // raise event if there is any change at installment level delinquency - if (isDelinquencyRangeChangedForAnyOfInstallment) { - businessEventNotifierService.notifyPostBusinessEvent(new LoanDelinquencyRangeChangeBusinessEvent(loan)); - } - - } - - private boolean setInstallmentDelinquencyDetails(final Loan loan, final LoanRepaymentScheduleInstallment installment, - final DelinquencyBucket delinquencyBucket, final CollectionData installmentDelinquencyData) { - DelinquencyRange delinquencyRangeForInstallment = getInstallmentDelinquencyRange(delinquencyBucket, - installmentDelinquencyData.getDelinquentDays()); - return setDelinquencyDetailsForInstallment(loan, installment, installmentDelinquencyData, delinquencyRangeForInstallment); - } - - private DelinquencyRange getInstallmentDelinquencyRange(final DelinquencyBucket delinquencyBucket, Long overDueDays) { - DelinquencyRange delinquencyRangeForInstallment = null; - if (overDueDays > 0) { - // Sort the ranges based on the minAgeDays - final List ranges = sortDelinquencyRangesByMinAge(delinquencyBucket.getRanges()); - for (final DelinquencyRange delinquencyRange : ranges) { - if (delinquencyRange.getMaximumAgeDays() == null) { // Last Range in the Bucket - if (delinquencyRange.getMinimumAgeDays() <= overDueDays) { - delinquencyRangeForInstallment = delinquencyRange; - break; - } - } else { - if (delinquencyRange.getMinimumAgeDays() <= overDueDays && delinquencyRange.getMaximumAgeDays() >= overDueDays) { - delinquencyRangeForInstallment = delinquencyRange; - break; - } - } - } - - } - return delinquencyRangeForInstallment; - } - - private boolean setDelinquencyDetailsForInstallment(final Loan loan, final LoanRepaymentScheduleInstallment installment, - CollectionData installmentDelinquencyData, final DelinquencyRange delinquencyRangeForInstallment) { - List installmentDelinquencyTags = new ArrayList<>(); - LocalDate delinquencyCalculationDate = DateUtils.getBusinessLocalDate(); - boolean isDelinquencyRangeChanged = false; - - LoanInstallmentDelinquencyTag previousInstallmentDelinquencyTag = loanInstallmentDelinquencyTagRepository - .findByLoanAndInstallment(loan, installment).orElse(null); - - if (delinquencyRangeForInstallment == null) { - // if currentInstallmentDelinquencyTag exists and range is null, installment is out of delinquency, delete - // delinquency details - if (previousInstallmentDelinquencyTag != null) { - // event installment out of delinquency - loanInstallmentDelinquencyTagRepository.delete(previousInstallmentDelinquencyTag); - isDelinquencyRangeChanged = true; - } - } else { - LoanInstallmentDelinquencyTag installmentDelinquency = null; - if (previousInstallmentDelinquencyTag != null) { - if (!previousInstallmentDelinquencyTag.getDelinquencyRange().getId().equals(delinquencyRangeForInstallment.getId())) { - // if current delinquency range exists and there is range change, delete previous delinquency - // details and add new range details - installmentDelinquency = new LoanInstallmentDelinquencyTag(delinquencyRangeForInstallment, loan, installment, - delinquencyCalculationDate, null, previousInstallmentDelinquencyTag.getFirstOverdueDate(), - installmentDelinquencyData.getDelinquentAmount()); - loanInstallmentDelinquencyTagRepository.delete(previousInstallmentDelinquencyTag); - // event installment delinquency range change - isDelinquencyRangeChanged = true; - } else { - previousInstallmentDelinquencyTag.setOutstandingAmount(installmentDelinquencyData.getDelinquentAmount()); - installmentDelinquency = previousInstallmentDelinquencyTag; - } - } else { - // add new range, first time delinquent - installmentDelinquency = new LoanInstallmentDelinquencyTag(delinquencyRangeForInstallment, loan, installment, - delinquencyCalculationDate, null, installmentDelinquencyData.getDelinquentDate(), - installmentDelinquencyData.getDelinquentAmount()); - // event installment delinquent - isDelinquencyRangeChanged = true; - } - - if (installmentDelinquency != null) { - installmentDelinquencyTags.add(installmentDelinquency); - } - - } - - if (installmentDelinquencyTags.size() > 0) { - loanInstallmentDelinquencyTagRepository.saveAllAndFlush(installmentDelinquencyTags); - } - return isDelinquencyRangeChanged; - } - private void cleanLoanInstallmentsDelinquencyTags(Loan loan) { loanInstallmentDelinquencyTagRepository.deleteAllLoanInstallmentsTags(loan.getId()); } - - private void removeDelinquencyTagsForNonExistingInstallments(Long loanId) { - List currentLoanInstallmentDelinquencyTags = loanInstallmentDelinquencyTagRepository - .findByLoanId(loanId); - if (currentLoanInstallmentDelinquencyTags != null && currentLoanInstallmentDelinquencyTags.size() > 0) { - List loanInstallmentTagsForDelete = currentLoanInstallmentDelinquencyTags.stream() - .filter(tag -> tag.getInstallment() == null).map(tag -> tag.getId()).toList(); - if (loanInstallmentTagsForDelete.size() > 0) { - loanInstallmentDelinquencyTagRepository.deleteAllLoanInstallmentsTagsByIds(loanInstallmentTagsForDelete); - } - } - } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java index 897e62b9a9b..7848ec730bb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java @@ -33,6 +33,7 @@ import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformServiceImpl; import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformService; +import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformServiceHelper; import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformServiceImpl; import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainService; import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainServiceImpl; @@ -76,11 +77,13 @@ public DelinquencyWritePlatformService delinquencyWritePlatformService(Delinquen LoanInstallmentDelinquencyTagRepository loanInstallmentDelinquencyTagRepository, DelinquencyReadPlatformService delinquencyReadPlatformService, LoanDelinquencyActionRepository loanDelinquencyActionRepository, DelinquencyActionParseAndValidator delinquencyActionParseAndValidator, - DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper) { + DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper, + DelinquencyWritePlatformServiceHelper delinquencyWritePlatformServiceHelper) { return new DelinquencyWritePlatformServiceImpl(dataValidatorBucket, dataValidatorRange, repositoryRange, repositoryBucket, - repositoryBucketMappings, loanDelinquencyTagRepository, loanRepository, loanProductRepository, businessEventNotifierService, - loanDelinquencyDomainService, loanInstallmentDelinquencyTagRepository, delinquencyReadPlatformService, - loanDelinquencyActionRepository, delinquencyActionParseAndValidator, delinquencyEffectivePauseHelper); + repositoryBucketMappings, loanDelinquencyTagRepository, loanRepository, loanProductRepository, loanDelinquencyDomainService, + loanInstallmentDelinquencyTagRepository, delinquencyReadPlatformService, loanDelinquencyActionRepository, + delinquencyActionParseAndValidator, delinquencyEffectivePauseHelper, businessEventNotifierService, + delinquencyWritePlatformServiceHelper); } @Bean diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java index 7bd0c22c562..83fc0410393 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java @@ -24,6 +24,8 @@ import static org.mockito.ArgumentMatchers.anyIterable; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -62,6 +64,7 @@ import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository; import org.apache.fineract.portfolio.delinquency.helper.DelinquencyEffectivePauseHelper; import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; +import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformServiceHelper; import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformServiceImpl; import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainService; import org.apache.fineract.portfolio.delinquency.validator.DelinquencyActionParseAndValidator; @@ -81,7 +84,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; -import org.mockito.InjectMocks; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @@ -120,7 +123,8 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest { @Mock private DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper; - @InjectMocks + private DelinquencyWritePlatformServiceHelper delinquencyWritePlatformServiceHelper; + private DelinquencyWritePlatformServiceImpl underTest; @BeforeEach @@ -129,6 +133,14 @@ public void setUp() { ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); ThreadLocalContextUtil .setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, LocalDate.now(ZoneId.systemDefault())))); + + delinquencyWritePlatformServiceHelper = Mockito.spy(new DelinquencyWritePlatformServiceHelper(businessEventNotifierService, + loanDelinquencyTagRepository, repositoryRange, loanInstallmentDelinquencyTagRepository)); + underTest = new DelinquencyWritePlatformServiceImpl(dataValidatorBucket, dataValidatorRange, repositoryRange, repositoryBucket, + repositoryBucketMappings, loanDelinquencyTagRepository, loanRepository, loanProductRepository, loanDelinquencyDomainService, + loanInstallmentDelinquencyTagRepository, delinquencyReadPlatformService, loanDelinquencyActionRepository, + delinquencyActionParseAndValidator, delinquencyEffectivePauseHelper, businessEventNotifierService, + delinquencyWritePlatformServiceHelper); } @AfterEach @@ -180,6 +192,68 @@ public void givenLoanAccountWithDelinquencyBucketWhenRangeChangeThenEventIsRaise assertEquals(loanForProcessing, loanPayloadForEvent); } + @Test + public void test_ApplyDelinquencyTagToLoan_ExecutesDelinquencyApplication_InTheRightOrder() { + // given + final List effectiveDelinquencyList = Collections.emptyList(); + Loan loanForProcessing = Mockito.mock(Loan.class); + LoanProduct loanProduct = Mockito.mock(LoanProduct.class); + DelinquencyRange range1 = DelinquencyRange.instance("Range1", 1, 2); + range1.setId(1L); + DelinquencyRange range2 = DelinquencyRange.instance("Range30", 3, 30); + range2.setId(2L); + List listDelinquencyRanges = Arrays.asList(range1, range2); + DelinquencyBucket delinquencyBucket = new DelinquencyBucket("test Bucket"); + delinquencyBucket.setRanges(listDelinquencyRanges); + + final Long daysDiff = 2L; + final LocalDate fromDate = DateUtils.getBusinessLocalDate().minusMonths(1).minusDays(daysDiff); + final LocalDate dueDate = DateUtils.getBusinessLocalDate().minusDays(daysDiff); + final BigDecimal installmentPrincipalAmount = BigDecimal.valueOf(100); + final BigDecimal zeroAmount = BigDecimal.ZERO; + + LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loanForProcessing, 1, fromDate, dueDate, + installmentPrincipalAmount, zeroAmount, zeroAmount, zeroAmount, false, new HashSet<>(), zeroAmount); + installment.setId(1L); + + List repaymentScheduleInstallments = Arrays.asList(installment); + + LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(2); + LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L, + loanForProcessing); + CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 2L, null, 2L, overDueSinceDate, BigDecimal.ZERO, null, null, + null, null, null, null); + + CollectionData installmentCollectionData = new CollectionData(BigDecimal.ZERO, 2L, null, 2L, overDueSinceDate, + installmentPrincipalAmount, null, null, null, null, null, null); + + Map installmentsCollection = new HashMap<>(); + installmentsCollection.put(1L, installmentCollectionData); + + LoanDelinquencyData loanDelinquencyData = new LoanDelinquencyData(collectionData, installmentsCollection); + + when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct); + when(loanProduct.getDelinquencyBucket()).thenReturn(delinquencyBucket); + when(loanForProcessing.hasDelinquencyBucket()).thenReturn(true); + when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(repaymentScheduleInstallments); + when(loanForProcessing.isEnableInstallmentLevelDelinquency()).thenReturn(true); + when(loanDelinquencyTagRepository.findByLoanAndLiftedOnDate(any(), any())).thenReturn(Optional.empty()); + when(loanDelinquencyDomainService.getLoanDelinquencyData(loanForProcessing, effectiveDelinquencyList)) + .thenReturn(loanDelinquencyData); + when(loanInstallmentDelinquencyTagRepository.findByLoanAndInstallment(loanForProcessing, repaymentScheduleInstallments.get(0))) + .thenReturn(Optional.empty()); + + // when + underTest.applyDelinquencyTagToLoan(loanScheduleDelinquencyData, effectiveDelinquencyList); + + // then + InOrder inOrder = inOrder(delinquencyWritePlatformServiceHelper); + inOrder.verify(delinquencyWritePlatformServiceHelper).applyDelinquencyForLoan(eq(loanForProcessing), eq(delinquencyBucket), + anyLong()); + inOrder.verify(delinquencyWritePlatformServiceHelper).applyDelinquencyForLoanInstallments(eq(loanForProcessing), + eq(delinquencyBucket), eq(installmentsCollection)); + } + @Test public void givenLoanAccountWithDelinquencyBucketWhenNoRangeChangeThenNoEventIsRaised() { // given