FINERACT-2326: fix delinquent days & delinquency date after delinquency pause calculations
This commit is contained in:
+3
@@ -28,4 +28,7 @@ public interface DelinquencyEffectivePauseHelper {
|
||||
List<LoanDelinquencyActionData> calculateEffectiveDelinquencyList(List<LoanDelinquencyAction> savedDelinquencyActions);
|
||||
|
||||
Long getPausedDaysBeforeDate(List<LoanDelinquencyActionData> effectiveDelinquencyList, LocalDate date);
|
||||
|
||||
Long getPausedDaysWithinRange(List<LoanDelinquencyActionData> effectiveDelinquencyList, LocalDate startInclusive,
|
||||
LocalDate endExclusive);
|
||||
}
|
||||
|
||||
+24
@@ -66,6 +66,30 @@ public class DelinquencyEffectivePauseHelperImpl implements DelinquencyEffective
|
||||
return Long.sum(pausedDaysClosedPausePeriods, pausedDaysRunningPausePeriods);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getPausedDaysWithinRange(List<LoanDelinquencyActionData> effectiveDelinquencyList, LocalDate startInclusive,
|
||||
LocalDate endExclusive) {
|
||||
if (startInclusive == null || endExclusive == null || !startInclusive.isBefore(endExclusive)) {
|
||||
return 0L;
|
||||
}
|
||||
return effectiveDelinquencyList.stream().map(pausePeriod -> {
|
||||
LocalDate pauseStart = pausePeriod.getStartDate();
|
||||
LocalDate pauseEnd = Optional.ofNullable(pausePeriod.getEndDate()).orElse(endExclusive);
|
||||
if (pauseStart == null || !pauseStart.isBefore(endExclusive)) {
|
||||
return 0L;
|
||||
}
|
||||
if (!pauseEnd.isAfter(startInclusive)) {
|
||||
return 0L;
|
||||
}
|
||||
LocalDate overlapStart = pauseStart.isAfter(startInclusive) ? pauseStart : startInclusive;
|
||||
LocalDate overlapEnd = pauseEnd.isBefore(endExclusive) ? pauseEnd : endExclusive;
|
||||
if (!overlapStart.isBefore(overlapEnd)) {
|
||||
return 0L;
|
||||
}
|
||||
return DateUtils.getDifferenceInDays(overlapStart, overlapEnd);
|
||||
}).reduce(0L, Long::sum);
|
||||
}
|
||||
|
||||
private Optional<LoanDelinquencyAction> findMatchingResume(LoanDelinquencyAction pause, List<LoanDelinquencyAction> resumes) {
|
||||
if (resumes != null && resumes.size() > 0) {
|
||||
for (LoanDelinquencyAction resume : resumes) {
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 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.helper;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.portfolio.delinquency.data.LoanInstallmentDelinquencyTagData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.InstallmentLevelDelinquency;
|
||||
|
||||
/**
|
||||
* Static utility class for aggregating installment-level delinquency data.
|
||||
*
|
||||
* @see InstallmentLevelDelinquency
|
||||
* @see LoanInstallmentDelinquencyTagData
|
||||
*/
|
||||
public final class InstallmentDelinquencyAggregator {
|
||||
|
||||
private InstallmentDelinquencyAggregator() {}
|
||||
|
||||
/**
|
||||
* Aggregates installment-level delinquency data by rangeId and sorts by minimumAgeDays.
|
||||
*
|
||||
* This method performs two key operations: 1. Groups installments by delinquency rangeId and sums delinquentAmount
|
||||
* for installments with the same rangeId 2. Sorts the aggregated results by minimumAgeDays in ascending order
|
||||
*
|
||||
* @param installmentData
|
||||
* Collection of installment delinquency data to aggregate
|
||||
* @return Sorted list of aggregated delinquency data, empty list if input is null or empty
|
||||
*/
|
||||
public static List<InstallmentLevelDelinquency> aggregateAndSort(Collection<LoanInstallmentDelinquencyTagData> installmentData) {
|
||||
|
||||
if (installmentData == null || installmentData.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
Collection<InstallmentLevelDelinquency> aggregated = installmentData.stream().map(InstallmentLevelDelinquency::from)
|
||||
.collect(Collectors.groupingBy(InstallmentLevelDelinquency::getRangeId, delinquentAmountSummingCollector())).values()
|
||||
.stream().map(opt -> opt.orElseThrow(() -> new IllegalStateException("Unexpected empty Optional in aggregation"))).toList();
|
||||
|
||||
return aggregated.stream().sorted(Comparator.comparing(ild -> Optional.ofNullable(ild.getMinimumAgeDays()).orElse(0))).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a custom collector that sums delinquent amounts while preserving range metadata.
|
||||
*
|
||||
* This collector uses the reducing operation to combine multiple InstallmentLevelDelinquency objects with the same
|
||||
* rangeId. It preserves the range classification (rangeId, classification, minimumAgeDays, maximumAgeDays) while
|
||||
* summing the delinquentAmount fields.
|
||||
*
|
||||
* Note: This uses the 1-argument reducing() variant which returns Optional<T> to avoid the identity value bug that
|
||||
* would cause amounts to be incorrectly doubled when aggregating single installments.
|
||||
*
|
||||
* @return Collector that combines InstallmentLevelDelinquency objects by summing amounts
|
||||
*/
|
||||
private static Collector<InstallmentLevelDelinquency, ?, Optional<InstallmentLevelDelinquency>> delinquentAmountSummingCollector() {
|
||||
return Collectors.reducing((item1, item2) -> {
|
||||
final InstallmentLevelDelinquency result = new InstallmentLevelDelinquency();
|
||||
result.setRangeId(item1.getRangeId());
|
||||
result.setClassification(item1.getClassification());
|
||||
result.setMaximumAgeDays(item1.getMaximumAgeDays());
|
||||
result.setMinimumAgeDays(item1.getMinimumAgeDays());
|
||||
result.setDelinquentAmount(MathUtil.add(item1.getDelinquentAmount(), item2.getDelinquentAmount()));
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
||||
+4
-29
@@ -27,8 +27,6 @@ import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collector;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
@@ -49,6 +47,7 @@ import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistor
|
||||
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository;
|
||||
import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository;
|
||||
import org.apache.fineract.portfolio.delinquency.helper.DelinquencyEffectivePauseHelper;
|
||||
import org.apache.fineract.portfolio.delinquency.helper.InstallmentDelinquencyAggregator;
|
||||
import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyBucketMapper;
|
||||
import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyRangeMapper;
|
||||
import org.apache.fineract.portfolio.delinquency.mapper.LoanDelinquencyTagMapper;
|
||||
@@ -260,36 +259,12 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
|
||||
Collection<LoanInstallmentDelinquencyTagData> loanInstallmentDelinquencyTagData = retrieveLoanInstallmentsCurrentDelinquencyTag(
|
||||
loanId);
|
||||
if (loanInstallmentDelinquencyTagData != null && !loanInstallmentDelinquencyTagData.isEmpty()) {
|
||||
|
||||
// installment level delinquency grouped by rangeId, and summed up the delinquent amount
|
||||
Collection<InstallmentLevelDelinquency> installmentLevelDelinquencies = loanInstallmentDelinquencyTagData.stream()
|
||||
.map(InstallmentLevelDelinquency::from)
|
||||
.collect(Collectors.groupingBy(InstallmentLevelDelinquency::getRangeId, delinquentAmountSummingCollector())).values();
|
||||
|
||||
// sort this based on minimum days, so ranges will be delivered in ascending order
|
||||
List<InstallmentLevelDelinquency> sorted = installmentLevelDelinquencies.stream().sorted((o1, o2) -> {
|
||||
Integer first = Optional.ofNullable(o1.getMinimumAgeDays()).orElse(0);
|
||||
Integer second = Optional.ofNullable(o2.getMinimumAgeDays()).orElse(0);
|
||||
return first.compareTo(second);
|
||||
}).toList();
|
||||
|
||||
collectionData.setInstallmentLevelDelinquency(sorted);
|
||||
List<InstallmentLevelDelinquency> aggregated = InstallmentDelinquencyAggregator
|
||||
.aggregateAndSort(loanInstallmentDelinquencyTagData);
|
||||
collectionData.setInstallmentLevelDelinquency(aggregated);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static Collector<InstallmentLevelDelinquency, ?, InstallmentLevelDelinquency> delinquentAmountSummingCollector() {
|
||||
return Collectors.reducing(new InstallmentLevelDelinquency(), (item1, item2) -> {
|
||||
final InstallmentLevelDelinquency result = new InstallmentLevelDelinquency();
|
||||
result.setRangeId(Optional.ofNullable(item1.getRangeId()).orElse(item2.getRangeId()));
|
||||
result.setClassification(Optional.ofNullable(item1.getClassification()).orElse(item2.getClassification()));
|
||||
result.setMaximumAgeDays(Optional.ofNullable(item1.getMaximumAgeDays()).orElse(item2.getMaximumAgeDays()));
|
||||
result.setMinimumAgeDays(Optional.ofNullable(item1.getMinimumAgeDays()).orElse(item2.getMinimumAgeDays()));
|
||||
result.setDelinquentAmount(MathUtil.add(item1.getDelinquentAmount(), item2.getDelinquentAmount()));
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
void enrichWithDelinquencyPausePeriodInfo(CollectionData collectionData, Collection<LoanDelinquencyActionData> effectiveDelinquencyList,
|
||||
LocalDate businessDate) {
|
||||
List<DelinquencyPausePeriod> result = effectiveDelinquencyList.stream() //
|
||||
|
||||
+27
-26
@@ -119,14 +119,15 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
|
||||
log.debug("Loan id {} with overdue since date {} and outstanding amount {}", loan.getId(), overdueSinceDate, outstandingAmount);
|
||||
|
||||
long overdueDays = 0L;
|
||||
LocalDate overdueSinceDateForCalculation = overdueSinceDate;
|
||||
if (overdueSinceDate != null) {
|
||||
overdueDays = DateUtils.getDifferenceInDays(overdueSinceDate, businessDate);
|
||||
if (overdueDays < 0) {
|
||||
overdueDays = 0L;
|
||||
}
|
||||
collectionData.setPastDueDays(overdueDays);
|
||||
overdueSinceDate = overdueSinceDate.plusDays(graceDays.longValue());
|
||||
collectionData.setDelinquentDate(overdueSinceDate);
|
||||
LocalDate delinquentStartDate = overdueSinceDate.plusDays(graceDays.longValue());
|
||||
collectionData.setDelinquentDate(delinquentStartDate);
|
||||
}
|
||||
collectionData.setDelinquentAmount(outstandingAmount);
|
||||
collectionData.setDelinquentPrincipal(delinquentPrincipal);
|
||||
@@ -134,11 +135,8 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
|
||||
collectionData.setDelinquentFee(delinquentFee);
|
||||
collectionData.setDelinquentPenalty(delinquentPenalty);
|
||||
|
||||
collectionData.setDelinquentDays(0L);
|
||||
final long delinquentDays = overdueDays - graceDays;
|
||||
if (delinquentDays > 0) {
|
||||
calculateDelinquentDays(effectiveDelinquencyList, businessDate, collectionData, delinquentDays);
|
||||
}
|
||||
calculateAndSetDelinquentDays(collectionData, overdueDays, graceDays, effectiveDelinquencyList, businessDate,
|
||||
overdueSinceDateForCalculation);
|
||||
|
||||
log.debug("Result: {}", collectionData);
|
||||
return collectionData;
|
||||
@@ -200,31 +198,22 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
|
||||
log.debug("Loan id {} with overdue since date {} and outstanding amount {}", loan.getId(), overdueSinceDate, outstandingAmount);
|
||||
|
||||
long overdueDays = 0L;
|
||||
LocalDate overdueSinceDateForCalculation = overdueSinceDate;
|
||||
if (overdueSinceDate != null) {
|
||||
overdueDays = DateUtils.getDifferenceInDays(overdueSinceDate, businessDate);
|
||||
if (overdueDays < 0) {
|
||||
overdueDays = 0L;
|
||||
}
|
||||
collectionData.setPastDueDays(overdueDays);
|
||||
overdueSinceDate = overdueSinceDate.plusDays(graceDays.longValue());
|
||||
collectionData.setDelinquentDate(overdueSinceDate);
|
||||
LocalDate delinquentStartDate = overdueSinceDate.plusDays(graceDays.longValue());
|
||||
collectionData.setDelinquentDate(delinquentStartDate);
|
||||
}
|
||||
collectionData.setDelinquentAmount(outstandingAmount);
|
||||
collectionData.setDelinquentDays(0L);
|
||||
final long delinquentDays = overdueDays - graceDays;
|
||||
if (delinquentDays > 0) {
|
||||
calculateDelinquentDays(effectiveDelinquencyList, businessDate, collectionData, delinquentDays);
|
||||
}
|
||||
calculateAndSetDelinquentDays(collectionData, overdueDays, graceDays, effectiveDelinquencyList, businessDate,
|
||||
overdueSinceDateForCalculation);
|
||||
return new LoanDelinquencyData(collectionData, loanInstallmentsCollectionData);
|
||||
}
|
||||
|
||||
private void calculateDelinquentDays(List<LoanDelinquencyActionData> effectiveDelinquencyList, LocalDate businessDate,
|
||||
CollectionData collectionData, Long delinquentDays) {
|
||||
Long pausedDays = delinquencyEffectivePauseHelper.getPausedDaysBeforeDate(effectiveDelinquencyList, businessDate);
|
||||
Long calculatedDelinquentDays = delinquentDays - pausedDays;
|
||||
collectionData.setDelinquentDays(calculatedDelinquentDays > 0 ? calculatedDelinquentDays : 0L);
|
||||
}
|
||||
|
||||
private CollectionData getInstallmentOverdueCollectionData(final Loan loan, final LoanRepaymentScheduleInstallment installment,
|
||||
final List<LoanDelinquencyActionData> effectiveDelinquencyList, final List<LoanTransaction> chargebackTransactions) {
|
||||
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
|
||||
@@ -248,6 +237,7 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
|
||||
|
||||
// Grace days are not considered for installment level delinquency calculation currently.
|
||||
long overdueDays = 0L;
|
||||
LocalDate overdueSinceDateForCalculation = overdueSinceDate;
|
||||
if (overdueSinceDate != null) {
|
||||
overdueDays = DateUtils.getDifferenceInDays(overdueSinceDate, businessDate);
|
||||
if (overdueDays < 0) {
|
||||
@@ -257,11 +247,8 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
|
||||
collectionData.setDelinquentDate(overdueSinceDate);
|
||||
}
|
||||
collectionData.setDelinquentAmount(outstandingAmount);
|
||||
collectionData.setDelinquentDays(0L);
|
||||
final long delinquentDays = overdueDays;
|
||||
if (delinquentDays > 0) {
|
||||
calculateDelinquentDays(effectiveDelinquencyList, businessDate, collectionData, delinquentDays);
|
||||
}
|
||||
calculateAndSetDelinquentDays(collectionData, overdueDays, 0, effectiveDelinquencyList, businessDate,
|
||||
overdueSinceDateForCalculation);
|
||||
return collectionData;
|
||||
|
||||
}
|
||||
@@ -356,4 +343,18 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
|
||||
return collectionData;
|
||||
}
|
||||
|
||||
private void calculateAndSetDelinquentDays(CollectionData collectionData, long overdueDays, Integer graceDays,
|
||||
List<LoanDelinquencyActionData> effectiveDelinquencyList, LocalDate businessDate, LocalDate overdueSinceDate) {
|
||||
collectionData.setDelinquentDays(0L);
|
||||
if (overdueDays > 0) {
|
||||
Long pausedDays = delinquencyEffectivePauseHelper.getPausedDaysWithinRange(effectiveDelinquencyList, overdueSinceDate,
|
||||
businessDate);
|
||||
if (pausedDays == null) {
|
||||
pausedDays = 0L;
|
||||
}
|
||||
final long delinquentDays = overdueDays - pausedDays - graceDays;
|
||||
collectionData.setDelinquentDays(delinquentDays > 0 ? delinquentDays : 0L);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user