FINERACT-1971: Fix for not properly resolving delinquency range data for loan in case there's an installment delinquency
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 0000000..29aa610
--- /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<String, Object> applyDelinquencyForLoan(final Loan loan, final DelinquencyBucket delinquencyBucket, long overdueDays) {
+ Map<String, Object> 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<DelinquencyRange> 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<String, Object> setLoanDelinquencyTag(Loan loan, Long delinquencyRangeId) {
+ Map<String, Object> changes = new HashMap<>();
+ List<LoanDelinquencyTagHistory> loanDelinquencyTagHistory = new ArrayList<>();
+ final LocalDate transactionDate = DateUtils.getBusinessLocalDate();
+ Optional<LoanDelinquencyTagHistory> 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<DelinquencyRange> sortDelinquencyRangesByMinAge(List<DelinquencyRange> ranges) {
+ final Comparator<DelinquencyRange> orderByMinAge = new Comparator<DelinquencyRange>() {
+
+ @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<Long, CollectionData> 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<LoanInstallmentDelinquencyTag> currentLoanInstallmentDelinquencyTags = loanInstallmentDelinquencyTagRepository
+ .findByLoanId(loanId);
+ if (currentLoanInstallmentDelinquencyTags != null && currentLoanInstallmentDelinquencyTags.size() > 0) {
+ List<Long> 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<DelinquencyRange> 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<LoanInstallmentDelinquencyTag> 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 2040299..29451c1 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 @@
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 @@
final CollectionData collectionData = loanDelinquencyData.getLoanCollectionData();
// loan installments delinquent data
final Map<Long, CollectionData> 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 @@
final CollectionData collectionData = loanDelinquentData.getLoanCollectionData();
// loan installments delinquent data
final Map<Long, CollectionData> 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<String, Object> applyDelinquencyToLoanAndInstallments(Loan loan, DelinquencyBucket delinquencyBucket,
+ CollectionData collectionData, Map<Long, CollectionData> installmentsCollectionData) {
+ // Order is important: first calculate loan level delinquency, then the installment level
+ // delinquency for loan
+ Map<String, Object> 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 @@
}
}
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 @@
if (loan.isEnableInstallmentLevelDelinquency()) {
cleanLoanInstallmentsDelinquencyTags(loan);
}
- setLoanDelinquencyTag(loan, null);
+ delinquencyHelper.setLoanDelinquencyTag(loan, null);
}
@Override
@@ -365,7 +371,7 @@
private void validateDelinquencyRanges(List<DelinquencyRange> 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 Map<String, Object> lookUpDelinquencyRange(final Loan loan, final DelinquencyBucket delinquencyBucket, long overdueDays) {
- Map<String, Object> 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<DelinquencyRange> 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<String, Object> setLoanDelinquencyTag(Loan loan, Long delinquencyRangeId) {
- Map<String, Object> changes = new HashMap<>();
- List<LoanDelinquencyTagHistory> loanDelinquencyTagHistory = new ArrayList<>();
- final LocalDate transactionDate = DateUtils.getBusinessLocalDate();
- Optional<LoanDelinquencyTagHistory> 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<DelinquencyRange> sortDelinquencyRangesByMinAge(List<DelinquencyRange> ranges) {
- final Comparator<DelinquencyRange> orderByMinAge = new Comparator<DelinquencyRange>() {
-
- @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<Long, CollectionData> 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<DelinquencyRange> 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<LoanInstallmentDelinquencyTag> 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<LoanInstallmentDelinquencyTag> currentLoanInstallmentDelinquencyTags = loanInstallmentDelinquencyTagRepository
- .findByLoanId(loanId);
- if (currentLoanInstallmentDelinquencyTags != null && currentLoanInstallmentDelinquencyTags.size() > 0) {
- List<Long> 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 897e62b..7848ec7 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 @@
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 7bd0c22..83fc041 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 @@
@Mock
private DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper;
- @InjectMocks
+ private DelinquencyWritePlatformServiceHelper delinquencyWritePlatformServiceHelper;
+
private DelinquencyWritePlatformServiceImpl underTest;
@BeforeEach
@@ -129,6 +133,14 @@
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
@@ -181,6 +193,68 @@
}
@Test
+ public void test_ApplyDelinquencyTagToLoan_ExecutesDelinquencyApplication_InTheRightOrder() {
+ // given
+ final List<LoanDelinquencyActionData> 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<DelinquencyRange> 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<LoanRepaymentScheduleInstallment> 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<Long, CollectionData> 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
final List<LoanDelinquencyActionData> effectiveDelinquencyList = Collections.emptyList();