This is an automated email from the ASF dual-hosted git repository.
arnold pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new cbe8f029e FINERACT-1971: Fix for not properly resolving delinquency
range data for loan in case there's an installment delinquency
cbe8f029e is described below
commit cbe8f029e2095640da9ae022c87870304543fac9
Author: Arnold Galovics <[email protected]>
AuthorDate: Mon Jan 15 11:11:27 2024 +0100
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 +-
...cyWritePlatformServiceRangeChangeEventTest.java | 78 +++++-
4 files changed, 380 insertions(+), 242 deletions(-)
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 000000000..29aa61086
--- /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 204029977..29451c169 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 @@ 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;
@@ -50,7 +48,6 @@ import
org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction;
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.CollectionData;
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 class DelinquencyWritePlatformServiceImpl
implements DelinquencyWritePlat
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 @@ public class DelinquencyWritePlatformServiceImpl
implements DelinquencyWritePlat
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 @@ public class DelinquencyWritePlatformServiceImpl implements
DelinquencyWritePlat
}
}
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 class DelinquencyWritePlatformServiceImpl implements
DelinquencyWritePlat
if (loan.isEnableInstallmentLevelDelinquency()) {
cleanLoanInstallmentsDelinquencyTags(loan);
}
- setLoanDelinquencyTag(loan, null);
+ delinquencyHelper.setLoanDelinquencyTag(loan, null);
}
@Override
@@ -365,7 +371,7 @@ public class DelinquencyWritePlatformServiceImpl implements
DelinquencyWritePlat
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 @@ public class DelinquencyWritePlatformServiceImpl
implements DelinquencyWritePlat
}
}
- 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 897e62b9a..7848ec730 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.mapper.LoanDelinquencyTagMapper
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 class DelinquencyConfiguration {
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 7bd0c22c5..83fc04103 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.any;
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.LoanInstallmentDelinquen
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.BeforeEach;
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 class
DelinquencyWritePlatformServiceRangeChangeEventTest {
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 class
DelinquencyWritePlatformServiceRangeChangeEventTest {
assertEquals(loanForProcessing, loanPayloadForEvent);
}
+ @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