blob: 8b048e7af896bf19b9b0ece23ba4a50e04128fb6 [file] [log] [blame]
/**
* 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 java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.portfolio.delinquency.helper.DelinquencyEffectivePauseHelper;
import org.apache.fineract.portfolio.delinquency.validator.LoanDelinquencyActionData;
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
import org.apache.fineract.portfolio.loanaccount.data.LoanDelinquencyData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@RequiredArgsConstructor
public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainService {
private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper;
@Override
@Transactional(readOnly = true)
public CollectionData getOverdueCollectionData(final Loan loan, List<LoanDelinquencyActionData> effectiveDelinquencyList) {
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
final MonetaryCurrency loanCurrency = loan.getCurrency();
LocalDate overdueSinceDate = null;
CollectionData collectionData = CollectionData.template();
BigDecimal outstandingAmount = BigDecimal.ZERO;
boolean oldestOverdueInstallment = false;
boolean overdueSinceDateWasSet = false;
boolean firstNotYetDueInstallment = false;
log.debug("Loan id {} with {} installments", loan.getId(), loan.getRepaymentScheduleInstallments().size());
// Get the oldest overdue installment if exists one
for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
if (!installment.isObligationsMet()) {
if (DateUtils.isBefore(installment.getDueDate(), businessDate)) {
log.debug("Loan Id: {} with installment {} due date {}", loan.getId(), installment.getInstallmentNumber(),
installment.getDueDate());
outstandingAmount = outstandingAmount.add(installment.getTotalOutstanding(loanCurrency).getAmount());
if (!oldestOverdueInstallment) {
log.debug("Oldest installment {} {}", installment.getInstallmentNumber(), installment.getDueDate());
CollectionData overDueInstallmentDelinquentData = calculateDelinquencyDataForOverdueInstallment(loan, installment);
overdueSinceDate = overDueInstallmentDelinquentData.getDelinquentDate();
oldestOverdueInstallment = true;
overdueSinceDateWasSet = true;
}
} else if (!firstNotYetDueInstallment) {
log.debug("Loan Id: {} with installment {} due date {}", loan.getId(), installment.getInstallmentNumber(),
installment.getDueDate());
firstNotYetDueInstallment = true;
CollectionData nonOverDueInstallmentDelinquentData = calculateDelinquencyDataForNonOverdueInstallment(loan,
installment);
outstandingAmount = outstandingAmount.add(nonOverDueInstallmentDelinquentData.getDelinquentAmount());
if (!overdueSinceDateWasSet) {
overdueSinceDate = nonOverDueInstallmentDelinquentData.getDelinquentDate();
overdueSinceDateWasSet = true;
}
}
}
}
Integer graceDays = 0;
if (loan.getLoanProductRelatedDetail().getGraceOnArrearsAgeing() != null) {
graceDays = loan.getLoanProductRelatedDetail().getGraceOnArrearsAgeing();
}
log.debug("Loan id {} with overdue since date {} and outstanding amount {}", loan.getId(), overdueSinceDate, outstandingAmount);
Long overdueDays = 0L;
if (overdueSinceDate != null) {
overdueDays = DateUtils.getDifferenceInDays(overdueSinceDate, businessDate);
if (overdueDays < 0) {
overdueDays = 0L;
}
collectionData.setPastDueDays(overdueDays);
overdueSinceDate = overdueSinceDate.plusDays(graceDays.longValue());
collectionData.setDelinquentDate(overdueSinceDate);
}
collectionData.setDelinquentAmount(outstandingAmount);
collectionData.setDelinquentDays(0L);
Long delinquentDays = overdueDays - graceDays;
if (delinquentDays > 0) {
calculateDelinquentDays(effectiveDelinquencyList, businessDate, collectionData, delinquentDays);
}
log.debug("Result: {}", collectionData.toString());
return collectionData;
}
@Override
public LoanDelinquencyData getLoanDelinquencyData(final Loan loan, List<LoanDelinquencyActionData> effectiveDelinquencyList) {
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
LocalDate overdueSinceDate = null;
CollectionData collectionData = CollectionData.template();
Map<Long, CollectionData> loanInstallmentsCollectionData = new HashMap<>();
BigDecimal outstandingAmount = BigDecimal.ZERO;
boolean oldestOverdueInstallment = false;
boolean overdueSinceDateWasSet = false;
boolean firstNotYetDueInstallment = false;
log.debug("Loan id {} with {} installments", loan.getId(), loan.getRepaymentScheduleInstallments().size());
for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
CollectionData installmentCollectionData = CollectionData.template();
if (!installment.isObligationsMet()) {
installmentCollectionData = getInstallmentOverdueCollectionData(loan, installment, effectiveDelinquencyList);
outstandingAmount = outstandingAmount.add(installmentCollectionData.getDelinquentAmount());
// Get the oldest overdue installment if exists
if (DateUtils.isBefore(installment.getDueDate(), businessDate)) {
if (!oldestOverdueInstallment) {
overdueSinceDate = installmentCollectionData.getDelinquentDate();
oldestOverdueInstallment = true;
overdueSinceDateWasSet = true;
}
} else if (!firstNotYetDueInstallment) {
firstNotYetDueInstallment = true;
if (!overdueSinceDateWasSet) {
overdueSinceDate = installmentCollectionData.getDelinquentDate();
overdueSinceDateWasSet = true;
}
}
}
// if installment level delinquency enabled add delinquency data for installment
if (loan.isEnableInstallmentLevelDelinquency()) {
loanInstallmentsCollectionData.put(installment.getId(), installmentCollectionData);
}
}
Integer graceDays = 0;
if (loan.getLoanProductRelatedDetail().getGraceOnArrearsAgeing() != null) {
graceDays = loan.getLoanProductRelatedDetail().getGraceOnArrearsAgeing();
}
log.debug("Loan id {} with overdue since date {} and outstanding amount {}", loan.getId(), overdueSinceDate, outstandingAmount);
Long overdueDays = 0L;
if (overdueSinceDate != null) {
overdueDays = DateUtils.getDifferenceInDays(overdueSinceDate, businessDate);
if (overdueDays < 0) {
overdueDays = 0L;
}
collectionData.setPastDueDays(overdueDays);
overdueSinceDate = overdueSinceDate.plusDays(graceDays.longValue());
collectionData.setDelinquentDate(overdueSinceDate);
}
collectionData.setDelinquentAmount(outstandingAmount);
collectionData.setDelinquentDays(0L);
Long delinquentDays = overdueDays - graceDays;
if (delinquentDays > 0) {
calculateDelinquentDays(effectiveDelinquencyList, businessDate, collectionData, delinquentDays);
}
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,
List<LoanDelinquencyActionData> effectiveDelinquencyList) {
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
LocalDate overdueSinceDate = null;
CollectionData collectionData = CollectionData.template();
BigDecimal outstandingAmount = BigDecimal.ZERO;
if (DateUtils.isBefore(installment.getDueDate(), businessDate)) {
// checking overdue installment delinquency data
CollectionData overDueInstallmentDelinquentData = calculateDelinquencyDataForOverdueInstallment(loan, installment);
outstandingAmount = outstandingAmount.add(overDueInstallmentDelinquentData.getDelinquentAmount());
overdueSinceDate = overDueInstallmentDelinquentData.getDelinquentDate();
} else {
// checking non overdue installment for chargeback transactions before installment due date and before
// business date
CollectionData nonOverDueInstallmentDelinquentData = calculateDelinquencyDataForNonOverdueInstallment(loan, installment);
outstandingAmount = outstandingAmount.add(nonOverDueInstallmentDelinquentData.getDelinquentAmount());
overdueSinceDate = nonOverDueInstallmentDelinquentData.getDelinquentDate();
}
// Grace days are not considered for installment level delinquency calculation currently.
Long overdueDays = 0L;
if (overdueSinceDate != null) {
overdueDays = DateUtils.getDifferenceInDays(overdueSinceDate, businessDate);
if (overdueDays < 0) {
overdueDays = 0L;
}
collectionData.setPastDueDays(overdueDays);
collectionData.setDelinquentDate(overdueSinceDate);
}
collectionData.setDelinquentAmount(outstandingAmount);
collectionData.setDelinquentDays(0L);
Long delinquentDays = overdueDays;
if (delinquentDays > 0) {
calculateDelinquentDays(effectiveDelinquencyList, businessDate, collectionData, delinquentDays);
}
return collectionData;
}
private CollectionData calculateDelinquencyDataForOverdueInstallment(final Loan loan,
final LoanRepaymentScheduleInstallment installment) {
final MonetaryCurrency loanCurrency = loan.getCurrency();
LoanRepaymentScheduleInstallment latestInstallment = loan.getLastLoanRepaymentScheduleInstallment();
List<LoanTransaction> chargebackTransactions = loan.getLoanTransactions(LoanTransaction::isChargeback);
LocalDate overdueSinceDate = null;
CollectionData collectionData = CollectionData.template();
BigDecimal outstandingAmount = BigDecimal.ZERO;
outstandingAmount = outstandingAmount.add(installment.getTotalOutstanding(loanCurrency).getAmount());
overdueSinceDate = installment.getDueDate();
BigDecimal amountAvailable = installment.getTotalPaid(loanCurrency).getAmount();
boolean isLatestInstallment = Objects.equals(installment.getId(), latestInstallment.getId());
for (LoanTransaction loanTransaction : chargebackTransactions) {
boolean isLoanTransactionIsOnOrAfterInstallmentFromDate = DateUtils.isEqual(loanTransaction.getTransactionDate(),
installment.getFromDate()) || DateUtils.isAfter(loanTransaction.getTransactionDate(), installment.getFromDate());
boolean isLoanTransactionIsBeforeNotLastInstallmentDueDate = !isLatestInstallment
&& DateUtils.isBefore(loanTransaction.getTransactionDate(), installment.getDueDate());
boolean isLoanTransactionIsOnOrBeforeLastInstallmentDueDate = isLatestInstallment
&& (DateUtils.isEqual(loanTransaction.getTransactionDate(), installment.getDueDate())
|| DateUtils.isBefore(loanTransaction.getTransactionDate(), installment.getDueDate()));
if (isLoanTransactionIsOnOrAfterInstallmentFromDate
&& (isLoanTransactionIsBeforeNotLastInstallmentDueDate || isLoanTransactionIsOnOrBeforeLastInstallmentDueDate)) {
amountAvailable = amountAvailable.subtract(loanTransaction.getAmount());
if (amountAvailable.compareTo(BigDecimal.ZERO) < 0) {
overdueSinceDate = loanTransaction.getTransactionDate();
break;
}
}
}
collectionData.setDelinquentDate(overdueSinceDate);
collectionData.setDelinquentAmount(outstandingAmount);
return collectionData;
}
private CollectionData calculateDelinquencyDataForNonOverdueInstallment(final Loan loan,
final LoanRepaymentScheduleInstallment installment) {
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
final MonetaryCurrency loanCurrency = loan.getCurrency();
LocalDate overdueSinceDate = null;
CollectionData collectionData = CollectionData.template();
BigDecimal outstandingAmount = BigDecimal.ZERO;
List<LoanTransaction> chargebackTransactions = loan.getLoanTransactions(LoanTransaction::isChargeback);
BigDecimal amountAvailable = installment.getTotalPaid(loanCurrency).getAmount();
for (LoanTransaction loanTransaction : chargebackTransactions) {
boolean isLoanTransactionIsOnOrAfterInstallmentFromDate = DateUtils.isEqual(loanTransaction.getTransactionDate(),
installment.getFromDate()) || DateUtils.isAfter(loanTransaction.getTransactionDate(), installment.getFromDate());
boolean isLoanTransactionIsBeforeInstallmentDueDate = DateUtils.isBefore(loanTransaction.getTransactionDate(),
installment.getDueDate());
boolean isLoanTransactionIsBeforeBusinessDate = DateUtils.isBefore(loanTransaction.getTransactionDate(), businessDate);
if (isLoanTransactionIsOnOrAfterInstallmentFromDate && isLoanTransactionIsBeforeInstallmentDueDate
&& isLoanTransactionIsBeforeBusinessDate) {
amountAvailable = amountAvailable.subtract(loanTransaction.getAmount());
if (amountAvailable.compareTo(BigDecimal.ZERO) < 0) {
overdueSinceDate = loanTransaction.getTransactionDate();
}
}
}
if (amountAvailable.compareTo(BigDecimal.ZERO) < 0) {
outstandingAmount = outstandingAmount.add(amountAvailable.abs());
}
collectionData.setDelinquentDate(overdueSinceDate);
collectionData.setDelinquentAmount(outstandingAmount);
return collectionData;
}
}