FINERACT-1992-FINERACT-Installment-level-delinquency-calculation

This commit is contained in:
Ruchi Dhamankar
2023-10-27 14:31:59 +02:00
committed by Adam Saghy
parent 585d200c67
commit 8cea837a5b
14 changed files with 828 additions and 58 deletions
@@ -31,4 +31,5 @@
<include relativeToChangelogFile="true" file="parts/1006_add_downpayment_transaction_enum_permissions.xml"/>
<include relativeToChangelogFile="true" file="parts/1007_add_loan_product_schedule_extension_for_down_payment_configuration.xml"/>
<include relativeToChangelogFile="true" file="parts/1008_add_loan_installment_delinquency_tag.xml"/>
<include relativeToChangelogFile="true" file="parts/1009_refactor_loan_Installment_delinquency_tag.xml"/>
</databaseChangeLog>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet author="fineract" id="1">
<renameTable oldTableName="m_loan_installment_delinquency_tag_history" newTableName="m_loan_installment_delinquency_tag"/>
</changeSet>
<changeSet author="fineract" id="2">
<dropForeignKeyConstraint baseTableName="m_loan_installment_delinquency_tag" constraintName="FK_m_delinquency_installment_tags_installment" />
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,39 @@
/**
* 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.data;
import java.math.BigDecimal;
public interface LoanInstallmentDelinquencyTagData {
InstallmentDelinquencyRange getDelinquencyRange();
BigDecimal getOutstandingAmount();
interface InstallmentDelinquencyRange {
Long getId();
String getClassification();
Integer getMinimumAgeDays();
Integer getMaximumAgeDays();
}
}
@@ -0,0 +1,80 @@
/**
* 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.domain;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "m_loan_installment_delinquency_tag")
public class LoanInstallmentDelinquencyTag extends AbstractAuditableWithUTCDateTimeCustom {
@ManyToOne
@JoinColumn(name = "delinquency_range_id", nullable = false)
private DelinquencyRange delinquencyRange;
@ManyToOne
@JoinColumn(name = "loan_id", nullable = false)
private Loan loan;
@ManyToOne
@JoinColumn(name = "installment_id", nullable = false)
private LoanRepaymentScheduleInstallment installment;
@Column(name = "addedon_date", nullable = false)
private LocalDate addedOnDate;
@Column(name = "liftedon_date", nullable = true)
private LocalDate liftedOnDate;
@Column(name = "first_overdue_date", nullable = false)
private LocalDate firstOverdueDate;
@Column(name = "outstanding_amount", scale = 6, precision = 19)
private BigDecimal outstandingAmount;
@Version
private Long version;
public LoanInstallmentDelinquencyTag(DelinquencyRange delinquencyRange, Loan loan, LoanRepaymentScheduleInstallment installment,
LocalDate addedOnDate, LocalDate liftedOnDate, LocalDate firstOverdueDate, BigDecimal outstandingAmount) {
this.delinquencyRange = delinquencyRange;
this.loan = loan;
this.installment = installment;
this.addedOnDate = addedOnDate;
this.liftedOnDate = liftedOnDate;
this.firstOverdueDate = firstOverdueDate;
this.outstandingAmount = outstandingAmount;
}
}
@@ -0,0 +1,52 @@
/**
* 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.domain;
import java.util.List;
import java.util.Optional;
import org.apache.fineract.portfolio.delinquency.data.LoanInstallmentDelinquencyTagData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface LoanInstallmentDelinquencyTagRepository
extends JpaRepository<LoanInstallmentDelinquencyTag, Long>, JpaSpecificationExecutor<LoanInstallmentDelinquencyTag> {
Optional<LoanInstallmentDelinquencyTag> findByLoanAndInstallment(Loan loan, LoanRepaymentScheduleInstallment installment);
@Query("select i from LoanInstallmentDelinquencyTag i where i.loan.id = :loanId")
List<LoanInstallmentDelinquencyTag> findByLoanId(@Param("loanId") Long loanId);
// Fetching Installment Delinquency range and outstanding amount
@Query("select i.delinquencyRange, i.outstandingAmount from LoanInstallmentDelinquencyTag i where i.loan.id = :loanId")
List<LoanInstallmentDelinquencyTagData> findInstallmentDelinquencyTags(@Param("loanId") Long loanId);
@Modifying(flushAutomatically = true)
@Query("delete from LoanInstallmentDelinquencyTag i where i.loan.id = :loanId")
void deleteAllLoanInstallmentsTags(@Param("loanId") Long loanId);
@Modifying(flushAutomatically = true)
@Query("delete from LoanInstallmentDelinquencyTag i where i.id IN :tagIds")
void deleteAllLoanInstallmentsTagsByIds(@Param("tagIds") List<Long> tagIds);
}
@@ -22,6 +22,7 @@ import java.util.Collection;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData;
import org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryData;
import org.apache.fineract.portfolio.delinquency.data.LoanInstallmentDelinquencyTagData;
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
public interface DelinquencyReadPlatformService {
@@ -40,4 +41,6 @@ public interface DelinquencyReadPlatformService {
CollectionData calculateLoanCollectionData(Long loanId);
Collection<LoanInstallmentDelinquencyTagData> retrieveLoanInstallmentsCurrentDelinquencyTag(Long loanId);
}
@@ -25,12 +25,14 @@ import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData;
import org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryData;
import org.apache.fineract.portfolio.delinquency.data.LoanInstallmentDelinquencyTagData;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository;
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.LoanInstallmentDelinquencyTagRepository;
import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyBucketMapper;
import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyRangeMapper;
import org.apache.fineract.portfolio.delinquency.mapper.LoanDelinquencyTagMapper;
@@ -52,6 +54,7 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
private final LoanDelinquencyTagMapper mapperLoanDelinquencyTagHistory;
private final LoanRepository loanRepository;
private final LoanDelinquencyDomainService loanDelinquencyDomainService;
private final LoanInstallmentDelinquencyTagRepository repositoryLoanInstallmentDelinquencyTag;
@Override
public Collection<DelinquencyRangeData> retrieveAllDelinquencyRanges() {
@@ -126,4 +129,9 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
return collectionData;
}
@Override
public Collection<LoanInstallmentDelinquencyTagData> retrieveLoanInstallmentsCurrentDelinquencyTag(Long loanId) {
return repositoryLoanInstallmentDelinquencyTag.findInstallmentDelinquencyTags(loanId);
}
}
@@ -46,13 +46,17 @@ 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.delinquency.exception.DelinquencyBucketAgesOverlapedException;
import org.apache.fineract.portfolio.delinquency.exception.DelinquencyRangeInvalidAgesException;
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyBucketParseAndValidator;
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator;
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;
@@ -71,6 +75,7 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
private final LoanProductRepository loanProductRepository;
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanDelinquencyDomainService loanDelinquencyDomainService;
private final LoanInstallmentDelinquencyTagRepository loanInstallmentDelinquencyTagRepository;
@Override
public CommandProcessingResult createDelinquencyRange(JsonCommand command) {
@@ -158,8 +163,17 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId);
final DelinquencyBucket delinquencyBucket = loan.getLoanProduct().getDelinquencyBucket();
if (delinquencyBucket != null) {
final CollectionData collectionData = loanDelinquencyDomainService.getOverdueCollectionData(loan);
final LoanDelinquencyData loanDelinquencyData = loanDelinquencyDomainService.getLoanDelinquencyData(loan);
// loan delinquent data
final CollectionData collectionData = loanDelinquencyData.getLoanCollectionData();
// loan installments delinquent data
final Map<Long, CollectionData> installmentsCollectionData = loanDelinquencyData.getLoanInstallmentsCollectionData();
// delinquency for loan
changes = lookUpDelinquencyRange(loan, delinquencyBucket, collectionData.getDelinquentDays());
// delinquency for installments
if (installmentsCollectionData.size() > 0) {
applyDelinquencyDetailsForLoanInstallments(loan, delinquencyBucket, installmentsCollectionData);
}
}
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(loan.getId())
.withEntityExternalId(loan.getExternalId()).with(changes).build();
@@ -170,15 +184,27 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
final Loan loan = loanDelinquencyData.getLoan();
if (loan.hasDelinquencyBucket()) {
final DelinquencyBucket delinquencyBucket = loan.getLoanProduct().getDelinquencyBucket();
final CollectionData collectionData = loanDelinquencyDomainService.getOverdueCollectionData(loan);
final LoanDelinquencyData loanDelinquentData = loanDelinquencyDomainService.getLoanDelinquencyData(loan);
// loan delinquent data
final CollectionData collectionData = loanDelinquentData.getLoanCollectionData();
// loan installments delinquent data
final Map<Long, CollectionData> installmentsCollectionData = loanDelinquentData.getLoanInstallmentsCollectionData();
log.debug("Delinquency {}", collectionData);
// delinquency for loan
lookUpDelinquencyRange(loan, delinquencyBucket, collectionData.getDelinquentDays());
// delinquency for installments
if (installmentsCollectionData.size() > 0) {
applyDelinquencyDetailsForLoanInstallments(loan, delinquencyBucket, installmentsCollectionData);
}
}
}
@Override
public void removeDelinquencyTagToLoan(final Loan loan) {
setLoanDelinquencyTag(loan, null);
if (loan.isEnableInstallmentLevelDelinquency()) {
cleanLoanInstallmentsDelinquencyTags(loan);
}
}
@Override
@@ -382,4 +408,107 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
return ranges;
}
private void applyDelinquencyDetailsForLoanInstallments(final Loan loan, final DelinquencyBucket delinquencyBucket,
final Map<Long, CollectionData> installmentsCollectionData) {
for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
if (installmentsCollectionData.containsKey(installment.getId())) {
setInstallmentDelinquencyDetails(loan, installment, delinquencyBucket, installmentsCollectionData.get(installment.getId()));
}
}
// remove tags for non existing installments that got deleted due to re-schedule
removeDelinquencyTagsForNonExistingInstallments(loan.getId());
}
private void setInstallmentDelinquencyDetails(final Loan loan, final LoanRepaymentScheduleInstallment installment,
final DelinquencyBucket delinquencyBucket, final CollectionData installmentDelinquencyData) {
DelinquencyRange delinquencyRangeForInstallment = getInstallmentDelinquencyRange(delinquencyBucket,
installmentDelinquencyData.getDelinquentDays());
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 void setDelinquencyDetailsForInstallment(final Loan loan, final LoanRepaymentScheduleInstallment installment,
CollectionData installmentDelinquencyData, final DelinquencyRange delinquencyRangeForInstallment) {
List<LoanInstallmentDelinquencyTag> installmentDelinquencyTags = new ArrayList<>();
LocalDate delinquencyCalculationDate = DateUtils.getBusinessLocalDate();
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);
}
} 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
}
} else {
// add new range, first time delinquent
installmentDelinquency = new LoanInstallmentDelinquencyTag(delinquencyRangeForInstallment, loan, installment,
delinquencyCalculationDate, null, installmentDelinquencyData.getDelinquentDate(),
installmentDelinquencyData.getDelinquentAmount());
// event installment delinquent
}
if (installmentDelinquency != null) {
installmentDelinquencyTags.add(installmentDelinquency);
}
}
if (installmentDelinquencyTags.size() > 0) {
loanInstallmentDelinquencyTagRepository.saveAllAndFlush(installmentDelinquencyTags);
}
}
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);
}
}
}
}
@@ -19,6 +19,7 @@
package org.apache.fineract.portfolio.delinquency.service;
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
import org.apache.fineract.portfolio.loanaccount.data.LoanDelinquencyData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
public interface LoanDelinquencyDomainService {
@@ -31,4 +32,6 @@ public interface LoanDelinquencyDomainService {
*/
CollectionData getOverdueCollectionData(Loan loan);
LoanDelinquencyData getLoanDelinquencyData(Loan loan);
}
@@ -20,12 +20,15 @@ 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.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
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;
@@ -42,15 +45,10 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
final MonetaryCurrency loanCurrency = loan.getCurrency();
LocalDate overdueSinceDate = null;
CollectionData collectionData = CollectionData.template();
BigDecimal amountAvailable;
BigDecimal outstandingAmount = BigDecimal.ZERO;
boolean oldestOverdueInstallment = false;
boolean overdueSinceDateWasSet = false;
boolean firstNotYetDueInstallment = false;
LoanRepaymentScheduleInstallment latestInstallment = loan.getLastLoanRepaymentScheduleInstallment();
List<LoanTransaction> chargebackTransactions = loan.getLoanTransactions(LoanTransaction::isChargeback);
log.debug("Loan id {} with {} installments", loan.getId(), loan.getRepaymentScheduleInstallments().size());
// Get the oldest overdue installment if exists one
for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
@@ -61,58 +59,21 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
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;
overdueSinceDate = installment.getDueDate();
overdueSinceDateWasSet = true;
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.isAfter(loanTransaction.getTransactionDate(), installment.getDueDate());
if (isLoanTransactionIsOnOrAfterInstallmentFromDate && (isLoanTransactionIsBeforeNotLastInstallmentDueDate
|| isLoanTransactionIsOnOrBeforeLastInstallmentDueDate)) {
amountAvailable = amountAvailable.subtract(loanTransaction.getAmount());
if (amountAvailable.compareTo(BigDecimal.ZERO) < 0) {
overdueSinceDate = loanTransaction.getTransactionDate();
break;
}
}
}
}
} else if (!firstNotYetDueInstallment) {
log.debug("Loan Id: {} with installment {} due date {}", loan.getId(), installment.getInstallmentNumber(),
installment.getDueDate());
firstNotYetDueInstallment = true;
amountAvailable = installment.getTotalPaid(loanCurrency).getAmount();
log.debug("Amount available {}", amountAvailable);
for (LoanTransaction loanTransaction : chargebackTransactions) {
boolean isLoanTransactionIsOnOrAfterInstallmentFromDate = !DateUtils.isBefore(loanTransaction.getTransactionDate(),
installment.getFromDate());
boolean isLoanTransactionIsBeforeInstallmentDueDate = DateUtils.isBefore(loanTransaction.getTransactionDate(),
installment.getDueDate());
boolean isLoanTransactionIsBeforeBusinessDate = DateUtils.isBefore(loanTransaction.getTransactionDate(),
businessDate);
if (isLoanTransactionIsOnOrAfterInstallmentFromDate && isLoanTransactionIsBeforeInstallmentDueDate
&& isLoanTransactionIsBeforeBusinessDate) {
log.debug("Loan CB Transaction: {} {} {}", loanTransaction.getId(), loanTransaction.getTransactionDate(),
loanTransaction.getAmount());
amountAvailable = amountAvailable.subtract(loanTransaction.getAmount());
if (amountAvailable.compareTo(BigDecimal.ZERO) < 0 && !overdueSinceDateWasSet) {
overdueSinceDate = loanTransaction.getTransactionDate();
overdueSinceDateWasSet = true;
}
}
}
if (amountAvailable.compareTo(BigDecimal.ZERO) < 0) {
outstandingAmount = outstandingAmount.add(amountAvailable.abs());
CollectionData nonOverDueInstallmentDelinquentData = calculateDelinquencyDataForNonOverdueInstallment(loan,
installment);
outstandingAmount = outstandingAmount.add(nonOverDueInstallmentDelinquentData.getDelinquentAmount());
if (!overdueSinceDateWasSet) {
overdueSinceDate = nonOverDueInstallmentDelinquentData.getDelinquentDate();
overdueSinceDateWasSet = true;
}
}
}
@@ -145,4 +106,178 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
return collectionData;
}
@Override
public LoanDelinquencyData getLoanDelinquencyData(final Loan loan) {
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);
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) {
collectionData.setDelinquentDays(delinquentDays);
}
return new LoanDelinquencyData(collectionData, loanInstallmentsCollectionData);
}
private CollectionData getInstallmentOverdueCollectionData(final Loan loan, final LoanRepaymentScheduleInstallment installment) {
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) {
// TODO : Changes for considering paused delinquency days for overdue days calculation
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) {
collectionData.setDelinquentDays(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;
}
}
@@ -23,6 +23,7 @@ import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketMapping
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository;
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository;
import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository;
import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyBucketMapper;
import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyRangeMapper;
import org.apache.fineract.portfolio.delinquency.mapper.LoanDelinquencyTagMapper;
@@ -50,9 +51,11 @@ public class DelinquencyConfiguration {
DelinquencyBucketRepository repositoryBucket, LoanDelinquencyTagHistoryRepository repositoryLoanDelinquencyTagHistory,
DelinquencyRangeMapper mapperRange, DelinquencyBucketMapper mapperBucket,
LoanDelinquencyTagMapper mapperLoanDelinquencyTagHistory, LoanRepository loanRepository,
LoanDelinquencyDomainService loanDelinquencyDomainService) {
LoanDelinquencyDomainService loanDelinquencyDomainService,
LoanInstallmentDelinquencyTagRepository repositoryLoanInstallmentDelinquencyTag) {
return new DelinquencyReadPlatformServiceImpl(repositoryRange, repositoryBucket, repositoryLoanDelinquencyTagHistory, mapperRange,
mapperBucket, mapperLoanDelinquencyTagHistory, loanRepository, loanDelinquencyDomainService);
mapperBucket, mapperLoanDelinquencyTagHistory, loanRepository, loanDelinquencyDomainService,
repositoryLoanInstallmentDelinquencyTag);
}
@Bean
@@ -62,10 +65,11 @@ public class DelinquencyConfiguration {
DelinquencyBucketRepository repositoryBucket, DelinquencyBucketMappingsRepository repositoryBucketMappings,
LoanDelinquencyTagHistoryRepository loanDelinquencyTagRepository, LoanRepositoryWrapper loanRepository,
LoanProductRepository loanProductRepository, BusinessEventNotifierService businessEventNotifierService,
LoanDelinquencyDomainService loanDelinquencyDomainService) {
LoanDelinquencyDomainService loanDelinquencyDomainService,
LoanInstallmentDelinquencyTagRepository loanInstallmentDelinquencyTagRepository) {
return new DelinquencyWritePlatformServiceImpl(dataValidatorBucket, dataValidatorRange, repositoryRange, repositoryBucket,
repositoryBucketMappings, loanDelinquencyTagRepository, loanRepository, loanProductRepository, businessEventNotifierService,
loanDelinquencyDomainService);
loanDelinquencyDomainService, loanInstallmentDelinquencyTagRepository);
}
@Bean
@@ -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.loanaccount.data;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@AllArgsConstructor
@ToString
@Getter
@Setter
public class LoanDelinquencyData {
private CollectionData loanCollectionData;
private Map<Long, CollectionData> loanInstallmentsCollectionData;
}
@@ -19,6 +19,7 @@
package org.apache.fineract.portfolio.deliquency;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyIterable;
import static org.mockito.Mockito.times;
@@ -30,6 +31,7 @@ import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -46,13 +48,17 @@ import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketReposit
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository;
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.service.DelinquencyWritePlatformServiceImpl;
import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainService;
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyBucketParseAndValidator;
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator;
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.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
@@ -88,6 +94,9 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
private BusinessEventNotifierService businessEventNotifierService;
@Mock
private LoanDelinquencyDomainService loanDelinquencyDomainService;
@Mock
private LoanInstallmentDelinquencyTagRepository loanInstallmentDelinquencyTagRepository;
@InjectMocks
private DelinquencyWritePlatformServiceImpl underTest;
@@ -120,11 +129,15 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 2L, null, 2L, overDueSinceDate, BigDecimal.ZERO, null, null,
null, null);
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
LoanDelinquencyData loanDelinquencyData = new LoanDelinquencyData(collectionData, installmentsCollection);
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
when(loanProduct.getDelinquencyBucket()).thenReturn(delinquencyBucket);
when(loanForProcessing.hasDelinquencyBucket()).thenReturn(true);
when(loanDelinquencyTagRepository.findByLoanAndLiftedOnDate(any(), any())).thenReturn(Optional.empty());
when(loanDelinquencyDomainService.getOverdueCollectionData(loanForProcessing)).thenReturn(collectionData);
when(loanDelinquencyDomainService.getLoanDelinquencyData(loanForProcessing)).thenReturn(loanDelinquencyData);
// when
underTest.applyDelinquencyTagToLoan(loanScheduleDelinquencyData);
@@ -155,11 +168,17 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 2L,
loanForProcessing);
CollectionData collectionData = CollectionData.template();
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
LoanDelinquencyData loanDelinquencyData = new LoanDelinquencyData(collectionData, installmentsCollection);
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
when(loanProduct.getDelinquencyBucket()).thenReturn(delinquencyBucket);
when(loanForProcessing.hasDelinquencyBucket()).thenReturn(true);
when(loanDelinquencyTagRepository.findByLoanAndLiftedOnDate(any(), any())).thenReturn(Optional.empty());
when(loanDelinquencyDomainService.getOverdueCollectionData(loanForProcessing)).thenReturn(CollectionData.template());
when(loanDelinquencyDomainService.getLoanDelinquencyData(loanForProcessing)).thenReturn(loanDelinquencyData);
// when
underTest.applyDelinquencyTagToLoan(loanScheduleDelinquencyData);
@@ -189,4 +208,142 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
}
@Test
public void givenLoanAccountWithOverdueInstallmentAndEnableInstallmentThenDelinquencyRangeIsSetForInstallmentTest() {
ArgumentCaptor<List<LoanInstallmentDelinquencyTag>> loanInstallmentDelinquencyTagsArgumentCaptor = ArgumentCaptor
.forClass(List.class);
// given
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);
CollectionData installmentCollectionData = new CollectionData(BigDecimal.ZERO, 2L, null, 2L, overDueSinceDate,
installmentPrincipalAmount, 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(loanDelinquencyTagRepository.findByLoanAndLiftedOnDate(any(), any())).thenReturn(Optional.empty());
when(loanDelinquencyDomainService.getLoanDelinquencyData(loanForProcessing)).thenReturn(loanDelinquencyData);
when(loanInstallmentDelinquencyTagRepository.findByLoanAndInstallment(loanForProcessing, repaymentScheduleInstallments.get(0)))
.thenReturn(Optional.empty());
// when
underTest.applyDelinquencyTagToLoan(loanScheduleDelinquencyData);
// then
verify(loanDelinquencyTagRepository, times(1)).saveAllAndFlush(anyIterable());
verify(loanInstallmentDelinquencyTagRepository, times(1)).saveAllAndFlush(loanInstallmentDelinquencyTagsArgumentCaptor.capture());
List<LoanInstallmentDelinquencyTag> installmentDelinquencyTags = loanInstallmentDelinquencyTagsArgumentCaptor.getValue();
assertEquals(1, installmentDelinquencyTags.size());
assertEquals(1, installmentDelinquencyTags.get(0).getInstallment().getInstallmentNumber());
assertEquals(1, installmentDelinquencyTags.get(0).getDelinquencyRange().getId());
assertEquals(installmentPrincipalAmount, installmentDelinquencyTags.get(0).getOutstandingAmount());
}
@Test
public void givenLoanAccountWithOverdueInstallmentAndEnableInstallmentThenDelinquencyRangeChangesForInstallmentTest() {
ArgumentCaptor<List<LoanInstallmentDelinquencyTag>> loanInstallmentDelinquencyTagsArgumentCaptor = ArgumentCaptor
.forClass(List.class);
ArgumentCaptor<LoanInstallmentDelinquencyTag> loanInstallmentDelinquencyTagArgumentCaptorForDelete = ArgumentCaptor
.forClass(LoanInstallmentDelinquencyTag.class);
// given
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(29);
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
loanForProcessing);
CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 29L, null, 29L, overDueSinceDate, BigDecimal.ZERO, null, null,
null, null);
CollectionData installmentCollectionData = new CollectionData(BigDecimal.ZERO, 29L, null, 29L, overDueSinceDate,
installmentPrincipalAmount, null, null, null, null);
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
installmentsCollection.put(1L, installmentCollectionData);
LoanDelinquencyData loanDelinquencyData = new LoanDelinquencyData(collectionData, installmentsCollection);
LoanInstallmentDelinquencyTag previousInstallmentTag = new LoanInstallmentDelinquencyTag();
previousInstallmentTag.setDelinquencyRange(range1);
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
when(loanProduct.getDelinquencyBucket()).thenReturn(delinquencyBucket);
when(loanForProcessing.hasDelinquencyBucket()).thenReturn(true);
when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(repaymentScheduleInstallments);
when(loanDelinquencyTagRepository.findByLoanAndLiftedOnDate(any(), any())).thenReturn(Optional.empty());
when(loanDelinquencyDomainService.getLoanDelinquencyData(loanForProcessing)).thenReturn(loanDelinquencyData);
when(loanInstallmentDelinquencyTagRepository.findByLoanAndInstallment(loanForProcessing, repaymentScheduleInstallments.get(0)))
.thenReturn(Optional.of(previousInstallmentTag));
// when
underTest.applyDelinquencyTagToLoan(loanScheduleDelinquencyData);
// then
verify(loanDelinquencyTagRepository, times(1)).saveAllAndFlush(anyIterable());
verify(loanInstallmentDelinquencyTagRepository, times(1)).saveAllAndFlush(loanInstallmentDelinquencyTagsArgumentCaptor.capture());
verify(loanInstallmentDelinquencyTagRepository, times(1)).delete(loanInstallmentDelinquencyTagArgumentCaptorForDelete.capture());
List<LoanInstallmentDelinquencyTag> installmentDelinquencyTags = loanInstallmentDelinquencyTagsArgumentCaptor.getValue();
assertEquals(1, installmentDelinquencyTags.size());
assertEquals(1, installmentDelinquencyTags.get(0).getInstallment().getInstallmentNumber());
assertEquals(2, installmentDelinquencyTags.get(0).getDelinquencyRange().getId());
assertEquals(installmentPrincipalAmount, installmentDelinquencyTags.get(0).getOutstandingAmount());
LoanInstallmentDelinquencyTag deletedInstallmentDelinquencyTag = loanInstallmentDelinquencyTagArgumentCaptorForDelete.getValue();
assertNotNull(deletedInstallmentDelinquencyTag);
assertEquals(previousInstallmentTag, deletedInstallmentDelinquencyTag);
}
}
@@ -19,6 +19,7 @@
package org.apache.fineract.portfolio.deliquency;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.when;
import java.math.BigDecimal;
@@ -43,6 +44,7 @@ import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainServiceImpl;
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;
@@ -185,4 +187,95 @@ public class LoanDelinquencyDomainServiceTest {
}
@Test
public void givenLoanInstallmentWithOverdueEnableInstallmentDelinquencyThenCalculateDelinquentData() {
// given
final Long daysDiff = 2L;
final LocalDate fromDate = businessDate.minusMonths(1).minusDays(daysDiff);
final LocalDate dueDate = businessDate.minusDays(daysDiff);
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, 1, fromDate, dueDate, principal,
zeroAmount, zeroAmount, zeroAmount, false, new HashSet<>(), zeroAmount);
installment.setId(1L);
List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments = Arrays.asList(installment);
// when
when(loanProductRelatedDetail.getGraceOnArrearsAgeing()).thenReturn(0);
when(loan.getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
when(loan.getRepaymentScheduleInstallments()).thenReturn(repaymentScheduleInstallments);
when(loan.getLoanTransactions(Mockito.any(Predicate.class))).thenReturn(Collections.emptyList());
when(loan.getLastLoanRepaymentScheduleInstallment()).thenReturn(repaymentScheduleInstallments.get(0));
when(loan.getCurrency()).thenReturn(currency);
when(loan.isEnableInstallmentLevelDelinquency()).thenReturn(true);
LoanDelinquencyData collectionData = underTest.getLoanDelinquencyData(loan);
// then
assertNotNull(collectionData);
assertNotNull(collectionData.getLoanInstallmentsCollectionData());
assertEquals(1L, collectionData.getLoanInstallmentsCollectionData().size());
CollectionData loanCollectionData = collectionData.getLoanCollectionData();
CollectionData installmentCollectionData = collectionData.getLoanInstallmentsCollectionData().get(1L);
assertEquals(daysDiff, loanCollectionData.getDelinquentDays());
assertEquals(dueDate, loanCollectionData.getDelinquentDate());
assertEquals(loanCollectionData.getDelinquentDays(), loanCollectionData.getPastDueDays());
assertEquals(daysDiff, installmentCollectionData.getDelinquentDays());
assertEquals(dueDate, installmentCollectionData.getDelinquentDate());
assertEquals(installmentCollectionData.getDelinquentDays(), installmentCollectionData.getPastDueDays());
}
@Test
public void givenLoanInstallmentWithoutOverdueWithChargebackAndEnableInstallmentDelinquencyThenCalculateDelinquentData() {
// given
PaymentDetail paymentDetail = Mockito.mock(PaymentDetail.class);
Long daysDiff = 2L;
final LocalDate fromDate = businessDate.minusMonths(1).plusDays(daysDiff);
final LocalDate dueDate = businessDate.plusDays(daysDiff);
final LocalDate transactionDate = businessDate.minusDays(daysDiff);
final Money zeroMoney = Money.zero(currency);
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, 1, fromDate, dueDate, principal,
zeroAmount, zeroAmount, zeroAmount, false, new HashSet<>(), zeroAmount);
installment.setId(1L);
LoanTransaction loanTransaction = LoanTransaction.chargeback(loan, Money.of(currency, principal), paymentDetail, transactionDate,
null);
installment.getLoanTransactionToRepaymentScheduleMappings().add(LoanTransactionToRepaymentScheduleMapping
.createFrom(loanTransaction, installment, zeroMoney, zeroMoney, zeroMoney, zeroMoney));
List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments = Arrays.asList(installment);
// when
when(loanProductRelatedDetail.getGraceOnArrearsAgeing()).thenReturn(0);
when(loan.getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
when(loan.getRepaymentScheduleInstallments()).thenReturn(repaymentScheduleInstallments);
when(loan.isEnableInstallmentLevelDelinquency()).thenReturn(true);
when(loan.getCurrency()).thenReturn(currency);
when(loan.getLoanTransactions(Mockito.any(Predicate.class))).thenReturn(Arrays.asList(loanTransaction));
LoanDelinquencyData collectionData = underTest.getLoanDelinquencyData(loan);
// then
assertNotNull(collectionData);
assertNotNull(collectionData.getLoanInstallmentsCollectionData());
assertEquals(1L, collectionData.getLoanInstallmentsCollectionData().size());
CollectionData loanCollectionData = collectionData.getLoanCollectionData();
CollectionData installmentCollectionData = collectionData.getLoanInstallmentsCollectionData().get(1L);
assertEquals(daysDiff, loanCollectionData.getDelinquentDays());
assertEquals(transactionDate, loanCollectionData.getDelinquentDate());
assertEquals(loanCollectionData.getDelinquentDays(), loanCollectionData.getPastDueDays());
// then
assertEquals(daysDiff, installmentCollectionData.getDelinquentDays());
assertEquals(transactionDate, installmentCollectionData.getDelinquentDate());
assertEquals(installmentCollectionData.getDelinquentDays(), installmentCollectionData.getPastDueDays());
assertEquals(0, principal.compareTo(installmentCollectionData.getDelinquentAmount()));
}
}