diff --git a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml
index 27e5aea91..56a18e16e 100644
--- a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml
+++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml
@@ -31,4 +31,5 @@
+
diff --git a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1009_refactor_loan_Installment_delinquency_tag.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1009_refactor_loan_Installment_delinquency_tag.xml
new file mode 100644
index 000000000..62df11440
--- /dev/null
+++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1009_refactor_loan_Installment_delinquency_tag.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/data/LoanInstallmentDelinquencyTagData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/data/LoanInstallmentDelinquencyTagData.java
new file mode 100644
index 000000000..860156951
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/data/LoanInstallmentDelinquencyTagData.java
@@ -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();
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanInstallmentDelinquencyTag.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanInstallmentDelinquencyTag.java
new file mode 100644
index 000000000..248c49d51
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanInstallmentDelinquencyTag.java
@@ -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;
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanInstallmentDelinquencyTagRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanInstallmentDelinquencyTagRepository.java
new file mode 100644
index 000000000..5729fdec6
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanInstallmentDelinquencyTagRepository.java
@@ -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, JpaSpecificationExecutor {
+
+ Optional findByLoanAndInstallment(Loan loan, LoanRepaymentScheduleInstallment installment);
+
+ @Query("select i from LoanInstallmentDelinquencyTag i where i.loan.id = :loanId")
+ List 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 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 tagIds);
+
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformService.java
index b1c2dad11..9f3510cce 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformService.java
@@ -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 retrieveLoanInstallmentsCurrentDelinquencyTag(Long loanId);
+
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java
index c98b49327..7e1d9c145 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java
@@ -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 retrieveAllDelinquencyRanges() {
@@ -126,4 +129,9 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
return collectionData;
}
+ @Override
+ public Collection retrieveLoanInstallmentsCurrentDelinquencyTag(Long loanId) {
+ return repositoryLoanInstallmentDelinquencyTag.findInstallmentDelinquencyTags(loanId);
+ }
+
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java
index 883a47808..0520c36d1 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java
@@ -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 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 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 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 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 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 currentLoanInstallmentDelinquencyTags = loanInstallmentDelinquencyTagRepository
+ .findByLoanId(loanId);
+ if (currentLoanInstallmentDelinquencyTags != null && currentLoanInstallmentDelinquencyTags.size() > 0) {
+ List loanInstallmentTagsForDelete = currentLoanInstallmentDelinquencyTags.stream()
+ .filter(tag -> tag.getInstallment() == null).map(tag -> tag.getId()).toList();
+ if (loanInstallmentTagsForDelete.size() > 0) {
+ loanInstallmentDelinquencyTagRepository.deleteAllLoanInstallmentsTagsByIds(loanInstallmentTagsForDelete);
+ }
+ }
+ }
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainService.java
index 5cd19cb5f..656ceb6b7 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainService.java
@@ -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);
+
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java
index 343eeabdb..67085cd65 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java
@@ -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 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 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 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 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;
+ }
+
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java
index 40131394b..88c27afa8 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java
@@ -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
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanDelinquencyData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanDelinquencyData.java
new file mode 100644
index 000000000..3efaea390
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanDelinquencyData.java
@@ -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 loanInstallmentsCollectionData;
+}
diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java
index 866c87d4a..ab85206e2 100644
--- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java
+++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java
@@ -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 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 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> 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 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 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 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 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> loanInstallmentDelinquencyTagsArgumentCaptor = ArgumentCaptor
+ .forClass(List.class);
+ ArgumentCaptor 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 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 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 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 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);
+
+ }
}
diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/LoanDelinquencyDomainServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/LoanDelinquencyDomainServiceTest.java
index c75580724..047f7a08d 100644
--- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/LoanDelinquencyDomainServiceTest.java
+++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/LoanDelinquencyDomainServiceTest.java
@@ -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 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 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()));
+
+ }
+
}