FINERACT-2322: Extend loan collection data with next due payment amount field

This commit is contained in:
Soma Sörös
2025-08-06 13:52:43 +02:00
committed by Adam Saghy
parent 514a684281
commit 28043ce438
21 changed files with 807 additions and 92 deletions
@@ -0,0 +1,58 @@
/**
* 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.math.BigDecimal;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
@RequiredArgsConstructor
public abstract class AbstractPossibleNextRepaymentCalculationService implements PossibleNextRepaymentCalculationService {
@Override
public BigDecimal possibleNextRepaymentAmount(Loan loan, LocalDate nextPaymentDueDate) {
LoanRepaymentScheduleInstallment nextInstallment = loan.getRelatedRepaymentScheduleInstallment(nextPaymentDueDate);
if (nextInstallment == null || nextInstallment.isObligationsMet()) {
return BigDecimal.ZERO;
}
if (loan.isInterestRecalculationEnabled()
// if rest frequency type is same as repayment, then interest values should be on the repayment schedule
// correctly.
&& !loan.getLoanInterestRecalculationDetails().getRestFrequencyType().isSameAsRepayment()
// if charge off, installments already shows correct values, no further calculation is required.
&& !loan.isChargeOffOnDate(nextPaymentDueDate)
// all strategy works like same as repayment on installment due date.
&& nextInstallment.getDueDate().isAfter(ThreadLocalContextUtil.getBusinessDate())
// there is no overdue / overdue related to that installment is calculated.
&& !nextInstallment.getFromDate().isEqual(ThreadLocalContextUtil.getBusinessDate())
&& MathUtil.isGreaterThanZero(loan.getDisbursedAmount())) {
// try to predict future outstanding balances with interest recalculation
return calculateInterestRecalculationFutureOutstandingValue(loan, nextPaymentDueDate, nextInstallment);
}
return nextInstallment.getTotalOutstanding(loan.getCurrency()).getAmount();
}
public abstract BigDecimal calculateInterestRecalculationFutureOutstandingValue(Loan loan, LocalDate nextPaymentDueDate,
LoanRepaymentScheduleInstallment nextInstallment);
}
@@ -81,6 +81,7 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper;
private final ConfigurationDomainService configurationDomainService;
private final LoanTransactionRepository loanTransactionRepository;
private final PossibleNextRepaymentCalculationServiceDiscovery possibleNextRepaymentCalculationServiceDiscovery;
@Override
public List<DelinquencyRangeData> retrieveAllDelinquencyRanges() {
@@ -152,6 +153,12 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
collectionData = loanDelinquencyDomainService.getOverdueCollectionData(loan, effectiveDelinquencyList);
collectionData.setAvailableDisbursementAmount(calculateAvailableDisbursementAmount(loan));
collectionData.setNextPaymentDueDate(possibleNextRepaymentDate(nextPaymentDueDateConfig, loan));
PossibleNextRepaymentCalculationService possibleNextRepaymentCalculationService = possibleNextRepaymentCalculationServiceDiscovery
.getService(loan);
if (possibleNextRepaymentCalculationService != null) {
collectionData.setNextPaymentAmount(
possibleNextRepaymentCalculationService.possibleNextRepaymentAmount(loan, collectionData.getNextPaymentDueDate()));
}
final LoanTransaction lastPayment = loan.getLastPaymentTransaction();
if (lastPayment != null) {
@@ -0,0 +1,30 @@
/**
* 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.math.BigDecimal;
import java.time.LocalDate;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
public interface PossibleNextRepaymentCalculationService {
boolean canAccept(Loan loan);
BigDecimal possibleNextRepaymentAmount(Loan loan, LocalDate nextPaymentDueDate);
}
@@ -0,0 +1,35 @@
/**
* 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.util.List;
import lombok.AllArgsConstructor;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.springframework.stereotype.Service;
@AllArgsConstructor
@Service
public class PossibleNextRepaymentCalculationServiceDiscovery {
private final List<PossibleNextRepaymentCalculationService> services;
public PossibleNextRepaymentCalculationService getService(final Loan loan) {
return services.stream().filter(s -> s.canAccept(loan)).findAny().orElse(null);
}
}
@@ -37,6 +37,7 @@ import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatfor
import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformServiceImpl;
import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainService;
import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainServiceImpl;
import org.apache.fineract.portfolio.delinquency.service.PossibleNextRepaymentCalculationServiceDiscovery;
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyActionParseAndValidator;
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyBucketParseAndValidator;
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator;
@@ -62,11 +63,12 @@ public class DelinquencyConfiguration {
LoanInstallmentDelinquencyTagRepository repositoryLoanInstallmentDelinquencyTag,
LoanDelinquencyActionRepository loanDelinquencyActionRepository,
DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper, ConfigurationDomainService configurationDomainService,
LoanTransactionRepository loanTransactionRepository) {
LoanTransactionRepository loanTransactionRepository,
PossibleNextRepaymentCalculationServiceDiscovery possibleNextRepaymentCalculationService) {
return new DelinquencyReadPlatformServiceImpl(repositoryRange, repositoryBucket, repositoryLoanDelinquencyTagHistory, mapperRange,
mapperBucket, mapperLoanDelinquencyTagHistory, loanRepository, loanDelinquencyDomainService,
repositoryLoanInstallmentDelinquencyTag, loanDelinquencyActionRepository, delinquencyEffectivePauseHelper,
configurationDomainService, loanTransactionRepository);
configurationDomainService, loanTransactionRepository, possibleNextRepaymentCalculationService);
}
@Bean
@@ -31,6 +31,7 @@ public final class CollectionData {
private BigDecimal availableDisbursementAmount;
private Long pastDueDays;
private LocalDate nextPaymentDueDate;
private BigDecimal nextPaymentAmount;
private Long delinquentDays;
private LocalDate delinquentDate;
private BigDecimal delinquentAmount;
@@ -50,7 +51,7 @@ public final class CollectionData {
public static CollectionData template() {
final BigDecimal zero = BigDecimal.ZERO;
return new CollectionData(zero, 0L, null, 0L, null, zero, null, zero, null, zero, null, null, zero, zero, zero, zero);
return new CollectionData(zero, 0L, null, zero, 0L, null, zero, null, zero, null, zero, null, null, zero, zero, zero, zero);
}
}
@@ -1403,7 +1403,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
* @return a schedule installment is related to the provided date
**/
public LoanRepaymentScheduleInstallment getRelatedRepaymentScheduleInstallment(LocalDate date) {
// TODO first installment should be fromInclusive
return getRepaymentScheduleInstallment(e -> DateUtils.isDateInRangeFromExclusiveToInclusive(date, e.getFromDate(), e.getDueDate()));
}