FINERACT-1992-FINERACT-Installment-level-delinquency-calculation
This commit is contained in:
committed by
Adam Saghy
parent
585d200c67
commit
8cea837a5b
+1
@@ -31,4 +31,5 @@
|
|||||||
<include relativeToChangelogFile="true" file="parts/1006_add_downpayment_transaction_enum_permissions.xml"/>
|
<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/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/1008_add_loan_installment_delinquency_tag.xml"/>
|
||||||
|
<include relativeToChangelogFile="true" file="parts/1009_refactor_loan_Installment_delinquency_tag.xml"/>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+31
@@ -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>
|
||||||
+39
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+80
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+52
@@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
+3
@@ -22,6 +22,7 @@ import java.util.Collection;
|
|||||||
import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData;
|
import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData;
|
||||||
import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData;
|
import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData;
|
||||||
import org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryData;
|
import org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryData;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.data.LoanInstallmentDelinquencyTagData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
|
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
|
||||||
|
|
||||||
public interface DelinquencyReadPlatformService {
|
public interface DelinquencyReadPlatformService {
|
||||||
@@ -40,4 +41,6 @@ public interface DelinquencyReadPlatformService {
|
|||||||
|
|
||||||
CollectionData calculateLoanCollectionData(Long loanId);
|
CollectionData calculateLoanCollectionData(Long loanId);
|
||||||
|
|
||||||
|
Collection<LoanInstallmentDelinquencyTagData> retrieveLoanInstallmentsCurrentDelinquencyTag(Long loanId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+8
@@ -25,12 +25,14 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData;
|
import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData;
|
||||||
import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData;
|
import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData;
|
||||||
import org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryData;
|
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.DelinquencyBucket;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository;
|
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange;
|
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository;
|
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistory;
|
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistory;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository;
|
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.DelinquencyBucketMapper;
|
||||||
import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyRangeMapper;
|
import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyRangeMapper;
|
||||||
import org.apache.fineract.portfolio.delinquency.mapper.LoanDelinquencyTagMapper;
|
import org.apache.fineract.portfolio.delinquency.mapper.LoanDelinquencyTagMapper;
|
||||||
@@ -52,6 +54,7 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
|
|||||||
private final LoanDelinquencyTagMapper mapperLoanDelinquencyTagHistory;
|
private final LoanDelinquencyTagMapper mapperLoanDelinquencyTagHistory;
|
||||||
private final LoanRepository loanRepository;
|
private final LoanRepository loanRepository;
|
||||||
private final LoanDelinquencyDomainService loanDelinquencyDomainService;
|
private final LoanDelinquencyDomainService loanDelinquencyDomainService;
|
||||||
|
private final LoanInstallmentDelinquencyTagRepository repositoryLoanInstallmentDelinquencyTag;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<DelinquencyRangeData> retrieveAllDelinquencyRanges() {
|
public Collection<DelinquencyRangeData> retrieveAllDelinquencyRanges() {
|
||||||
@@ -126,4 +129,9 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
|
|||||||
return collectionData;
|
return collectionData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<LoanInstallmentDelinquencyTagData> retrieveLoanInstallmentsCurrentDelinquencyTag(Long loanId) {
|
||||||
|
return repositoryLoanInstallmentDelinquencyTag.findInstallmentDelinquencyTags(loanId);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+131
-2
@@ -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.DelinquencyRangeRepository;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistory;
|
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistory;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository;
|
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.DelinquencyBucketAgesOverlapedException;
|
||||||
import org.apache.fineract.portfolio.delinquency.exception.DelinquencyRangeInvalidAgesException;
|
import org.apache.fineract.portfolio.delinquency.exception.DelinquencyRangeInvalidAgesException;
|
||||||
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyBucketParseAndValidator;
|
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyBucketParseAndValidator;
|
||||||
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator;
|
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
|
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.data.LoanScheduleDelinquencyData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
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.loanaccount.domain.LoanRepositoryWrapper;
|
||||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
|
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
|
||||||
|
|
||||||
@@ -71,6 +75,7 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
|
|||||||
private final LoanProductRepository loanProductRepository;
|
private final LoanProductRepository loanProductRepository;
|
||||||
private final BusinessEventNotifierService businessEventNotifierService;
|
private final BusinessEventNotifierService businessEventNotifierService;
|
||||||
private final LoanDelinquencyDomainService loanDelinquencyDomainService;
|
private final LoanDelinquencyDomainService loanDelinquencyDomainService;
|
||||||
|
private final LoanInstallmentDelinquencyTagRepository loanInstallmentDelinquencyTagRepository;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandProcessingResult createDelinquencyRange(JsonCommand command) {
|
public CommandProcessingResult createDelinquencyRange(JsonCommand command) {
|
||||||
@@ -158,8 +163,17 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
|
|||||||
final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId);
|
final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId);
|
||||||
final DelinquencyBucket delinquencyBucket = loan.getLoanProduct().getDelinquencyBucket();
|
final DelinquencyBucket delinquencyBucket = loan.getLoanProduct().getDelinquencyBucket();
|
||||||
if (delinquencyBucket != null) {
|
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());
|
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())
|
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(loan.getId())
|
||||||
.withEntityExternalId(loan.getExternalId()).with(changes).build();
|
.withEntityExternalId(loan.getExternalId()).with(changes).build();
|
||||||
@@ -170,15 +184,27 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
|
|||||||
final Loan loan = loanDelinquencyData.getLoan();
|
final Loan loan = loanDelinquencyData.getLoan();
|
||||||
if (loan.hasDelinquencyBucket()) {
|
if (loan.hasDelinquencyBucket()) {
|
||||||
final DelinquencyBucket delinquencyBucket = loan.getLoanProduct().getDelinquencyBucket();
|
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);
|
log.debug("Delinquency {}", collectionData);
|
||||||
|
// delinquency for loan
|
||||||
lookUpDelinquencyRange(loan, delinquencyBucket, collectionData.getDelinquentDays());
|
lookUpDelinquencyRange(loan, delinquencyBucket, collectionData.getDelinquentDays());
|
||||||
|
// delinquency for installments
|
||||||
|
if (installmentsCollectionData.size() > 0) {
|
||||||
|
applyDelinquencyDetailsForLoanInstallments(loan, delinquencyBucket, installmentsCollectionData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeDelinquencyTagToLoan(final Loan loan) {
|
public void removeDelinquencyTagToLoan(final Loan loan) {
|
||||||
setLoanDelinquencyTag(loan, null);
|
setLoanDelinquencyTag(loan, null);
|
||||||
|
if (loan.isEnableInstallmentLevelDelinquency()) {
|
||||||
|
cleanLoanInstallmentsDelinquencyTags(loan);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -382,4 +408,107 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
|
|||||||
return ranges;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -19,6 +19,7 @@
|
|||||||
package org.apache.fineract.portfolio.delinquency.service;
|
package org.apache.fineract.portfolio.delinquency.service;
|
||||||
|
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
|
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.Loan;
|
||||||
|
|
||||||
public interface LoanDelinquencyDomainService {
|
public interface LoanDelinquencyDomainService {
|
||||||
@@ -31,4 +32,6 @@ public interface LoanDelinquencyDomainService {
|
|||||||
*/
|
*/
|
||||||
CollectionData getOverdueCollectionData(Loan loan);
|
CollectionData getOverdueCollectionData(Loan loan);
|
||||||
|
|
||||||
|
LoanDelinquencyData getLoanDelinquencyData(Loan loan);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+185
-50
@@ -20,12 +20,15 @@ package org.apache.fineract.portfolio.delinquency.service;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
|
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.Loan;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||||
@@ -42,15 +45,10 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
|
|||||||
final MonetaryCurrency loanCurrency = loan.getCurrency();
|
final MonetaryCurrency loanCurrency = loan.getCurrency();
|
||||||
LocalDate overdueSinceDate = null;
|
LocalDate overdueSinceDate = null;
|
||||||
CollectionData collectionData = CollectionData.template();
|
CollectionData collectionData = CollectionData.template();
|
||||||
BigDecimal amountAvailable;
|
|
||||||
BigDecimal outstandingAmount = BigDecimal.ZERO;
|
BigDecimal outstandingAmount = BigDecimal.ZERO;
|
||||||
boolean oldestOverdueInstallment = false;
|
boolean oldestOverdueInstallment = false;
|
||||||
boolean overdueSinceDateWasSet = false;
|
boolean overdueSinceDateWasSet = false;
|
||||||
boolean firstNotYetDueInstallment = 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());
|
log.debug("Loan id {} with {} installments", loan.getId(), loan.getRepaymentScheduleInstallments().size());
|
||||||
// Get the oldest overdue installment if exists one
|
// Get the oldest overdue installment if exists one
|
||||||
for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
|
for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
|
||||||
@@ -61,58 +59,21 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
|
|||||||
outstandingAmount = outstandingAmount.add(installment.getTotalOutstanding(loanCurrency).getAmount());
|
outstandingAmount = outstandingAmount.add(installment.getTotalOutstanding(loanCurrency).getAmount());
|
||||||
if (!oldestOverdueInstallment) {
|
if (!oldestOverdueInstallment) {
|
||||||
log.debug("Oldest installment {} {}", installment.getInstallmentNumber(), installment.getDueDate());
|
log.debug("Oldest installment {} {}", installment.getInstallmentNumber(), installment.getDueDate());
|
||||||
|
CollectionData overDueInstallmentDelinquentData = calculateDelinquencyDataForOverdueInstallment(loan, installment);
|
||||||
|
overdueSinceDate = overDueInstallmentDelinquentData.getDelinquentDate();
|
||||||
oldestOverdueInstallment = true;
|
oldestOverdueInstallment = true;
|
||||||
overdueSinceDate = installment.getDueDate();
|
|
||||||
overdueSinceDateWasSet = true;
|
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) {
|
} else if (!firstNotYetDueInstallment) {
|
||||||
log.debug("Loan Id: {} with installment {} due date {}", loan.getId(), installment.getInstallmentNumber(),
|
log.debug("Loan Id: {} with installment {} due date {}", loan.getId(), installment.getInstallmentNumber(),
|
||||||
installment.getDueDate());
|
installment.getDueDate());
|
||||||
firstNotYetDueInstallment = true;
|
firstNotYetDueInstallment = true;
|
||||||
amountAvailable = installment.getTotalPaid(loanCurrency).getAmount();
|
CollectionData nonOverDueInstallmentDelinquentData = calculateDelinquencyDataForNonOverdueInstallment(loan,
|
||||||
log.debug("Amount available {}", amountAvailable);
|
installment);
|
||||||
for (LoanTransaction loanTransaction : chargebackTransactions) {
|
outstandingAmount = outstandingAmount.add(nonOverDueInstallmentDelinquentData.getDelinquentAmount());
|
||||||
boolean isLoanTransactionIsOnOrAfterInstallmentFromDate = !DateUtils.isBefore(loanTransaction.getTransactionDate(),
|
if (!overdueSinceDateWasSet) {
|
||||||
installment.getFromDate());
|
overdueSinceDate = nonOverDueInstallmentDelinquentData.getDelinquentDate();
|
||||||
boolean isLoanTransactionIsBeforeInstallmentDueDate = DateUtils.isBefore(loanTransaction.getTransactionDate(),
|
overdueSinceDateWasSet = true;
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,4 +106,178 @@ public class LoanDelinquencyDomainServiceImpl implements LoanDelinquencyDomainSe
|
|||||||
return collectionData;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-4
@@ -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.DelinquencyBucketRepository;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository;
|
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository;
|
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.DelinquencyBucketMapper;
|
||||||
import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyRangeMapper;
|
import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyRangeMapper;
|
||||||
import org.apache.fineract.portfolio.delinquency.mapper.LoanDelinquencyTagMapper;
|
import org.apache.fineract.portfolio.delinquency.mapper.LoanDelinquencyTagMapper;
|
||||||
@@ -50,9 +51,11 @@ public class DelinquencyConfiguration {
|
|||||||
DelinquencyBucketRepository repositoryBucket, LoanDelinquencyTagHistoryRepository repositoryLoanDelinquencyTagHistory,
|
DelinquencyBucketRepository repositoryBucket, LoanDelinquencyTagHistoryRepository repositoryLoanDelinquencyTagHistory,
|
||||||
DelinquencyRangeMapper mapperRange, DelinquencyBucketMapper mapperBucket,
|
DelinquencyRangeMapper mapperRange, DelinquencyBucketMapper mapperBucket,
|
||||||
LoanDelinquencyTagMapper mapperLoanDelinquencyTagHistory, LoanRepository loanRepository,
|
LoanDelinquencyTagMapper mapperLoanDelinquencyTagHistory, LoanRepository loanRepository,
|
||||||
LoanDelinquencyDomainService loanDelinquencyDomainService) {
|
LoanDelinquencyDomainService loanDelinquencyDomainService,
|
||||||
|
LoanInstallmentDelinquencyTagRepository repositoryLoanInstallmentDelinquencyTag) {
|
||||||
return new DelinquencyReadPlatformServiceImpl(repositoryRange, repositoryBucket, repositoryLoanDelinquencyTagHistory, mapperRange,
|
return new DelinquencyReadPlatformServiceImpl(repositoryRange, repositoryBucket, repositoryLoanDelinquencyTagHistory, mapperRange,
|
||||||
mapperBucket, mapperLoanDelinquencyTagHistory, loanRepository, loanDelinquencyDomainService);
|
mapperBucket, mapperLoanDelinquencyTagHistory, loanRepository, loanDelinquencyDomainService,
|
||||||
|
repositoryLoanInstallmentDelinquencyTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@@ -62,10 +65,11 @@ public class DelinquencyConfiguration {
|
|||||||
DelinquencyBucketRepository repositoryBucket, DelinquencyBucketMappingsRepository repositoryBucketMappings,
|
DelinquencyBucketRepository repositoryBucket, DelinquencyBucketMappingsRepository repositoryBucketMappings,
|
||||||
LoanDelinquencyTagHistoryRepository loanDelinquencyTagRepository, LoanRepositoryWrapper loanRepository,
|
LoanDelinquencyTagHistoryRepository loanDelinquencyTagRepository, LoanRepositoryWrapper loanRepository,
|
||||||
LoanProductRepository loanProductRepository, BusinessEventNotifierService businessEventNotifierService,
|
LoanProductRepository loanProductRepository, BusinessEventNotifierService businessEventNotifierService,
|
||||||
LoanDelinquencyDomainService loanDelinquencyDomainService) {
|
LoanDelinquencyDomainService loanDelinquencyDomainService,
|
||||||
|
LoanInstallmentDelinquencyTagRepository loanInstallmentDelinquencyTagRepository) {
|
||||||
return new DelinquencyWritePlatformServiceImpl(dataValidatorBucket, dataValidatorRange, repositoryRange, repositoryBucket,
|
return new DelinquencyWritePlatformServiceImpl(dataValidatorBucket, dataValidatorRange, repositoryRange, repositoryBucket,
|
||||||
repositoryBucketMappings, loanDelinquencyTagRepository, loanRepository, loanProductRepository, businessEventNotifierService,
|
repositoryBucketMappings, loanDelinquencyTagRepository, loanRepository, loanProductRepository, businessEventNotifierService,
|
||||||
loanDelinquencyDomainService);
|
loanDelinquencyDomainService, loanInstallmentDelinquencyTagRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
+35
@@ -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;
|
||||||
|
}
|
||||||
+159
-2
@@ -19,6 +19,7 @@
|
|||||||
package org.apache.fineract.portfolio.deliquency;
|
package org.apache.fineract.portfolio.deliquency;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
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.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyIterable;
|
import static org.mockito.ArgumentMatchers.anyIterable;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
@@ -30,6 +31,7 @@ import java.time.LocalDate;
|
|||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
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.DelinquencyRange;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository;
|
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository;
|
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.DelinquencyWritePlatformServiceImpl;
|
||||||
import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainService;
|
import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainService;
|
||||||
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyBucketParseAndValidator;
|
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyBucketParseAndValidator;
|
||||||
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator;
|
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
|
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.data.LoanScheduleDelinquencyData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
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.loanaccount.domain.LoanRepositoryWrapper;
|
||||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
|
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
|
||||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
|
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
|
||||||
@@ -88,6 +94,9 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
|||||||
private BusinessEventNotifierService businessEventNotifierService;
|
private BusinessEventNotifierService businessEventNotifierService;
|
||||||
@Mock
|
@Mock
|
||||||
private LoanDelinquencyDomainService loanDelinquencyDomainService;
|
private LoanDelinquencyDomainService loanDelinquencyDomainService;
|
||||||
|
@Mock
|
||||||
|
private LoanInstallmentDelinquencyTagRepository loanInstallmentDelinquencyTagRepository;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private DelinquencyWritePlatformServiceImpl underTest;
|
private DelinquencyWritePlatformServiceImpl underTest;
|
||||||
|
|
||||||
@@ -120,11 +129,15 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
|||||||
CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 2L, null, 2L, overDueSinceDate, BigDecimal.ZERO, null, null,
|
CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 2L, null, 2L, overDueSinceDate, BigDecimal.ZERO, null, null,
|
||||||
null, null);
|
null, null);
|
||||||
|
|
||||||
|
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
||||||
|
|
||||||
|
LoanDelinquencyData loanDelinquencyData = new LoanDelinquencyData(collectionData, installmentsCollection);
|
||||||
|
|
||||||
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
|
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
|
||||||
when(loanProduct.getDelinquencyBucket()).thenReturn(delinquencyBucket);
|
when(loanProduct.getDelinquencyBucket()).thenReturn(delinquencyBucket);
|
||||||
when(loanForProcessing.hasDelinquencyBucket()).thenReturn(true);
|
when(loanForProcessing.hasDelinquencyBucket()).thenReturn(true);
|
||||||
when(loanDelinquencyTagRepository.findByLoanAndLiftedOnDate(any(), any())).thenReturn(Optional.empty());
|
when(loanDelinquencyTagRepository.findByLoanAndLiftedOnDate(any(), any())).thenReturn(Optional.empty());
|
||||||
when(loanDelinquencyDomainService.getOverdueCollectionData(loanForProcessing)).thenReturn(collectionData);
|
when(loanDelinquencyDomainService.getLoanDelinquencyData(loanForProcessing)).thenReturn(loanDelinquencyData);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
underTest.applyDelinquencyTagToLoan(loanScheduleDelinquencyData);
|
underTest.applyDelinquencyTagToLoan(loanScheduleDelinquencyData);
|
||||||
@@ -155,11 +168,17 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
|||||||
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 2L,
|
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 2L,
|
||||||
loanForProcessing);
|
loanForProcessing);
|
||||||
|
|
||||||
|
CollectionData collectionData = CollectionData.template();
|
||||||
|
|
||||||
|
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
||||||
|
|
||||||
|
LoanDelinquencyData loanDelinquencyData = new LoanDelinquencyData(collectionData, installmentsCollection);
|
||||||
|
|
||||||
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
|
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
|
||||||
when(loanProduct.getDelinquencyBucket()).thenReturn(delinquencyBucket);
|
when(loanProduct.getDelinquencyBucket()).thenReturn(delinquencyBucket);
|
||||||
when(loanForProcessing.hasDelinquencyBucket()).thenReturn(true);
|
when(loanForProcessing.hasDelinquencyBucket()).thenReturn(true);
|
||||||
when(loanDelinquencyTagRepository.findByLoanAndLiftedOnDate(any(), any())).thenReturn(Optional.empty());
|
when(loanDelinquencyTagRepository.findByLoanAndLiftedOnDate(any(), any())).thenReturn(Optional.empty());
|
||||||
when(loanDelinquencyDomainService.getOverdueCollectionData(loanForProcessing)).thenReturn(CollectionData.template());
|
when(loanDelinquencyDomainService.getLoanDelinquencyData(loanForProcessing)).thenReturn(loanDelinquencyData);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
underTest.applyDelinquencyTagToLoan(loanScheduleDelinquencyData);
|
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);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+93
@@ -19,6 +19,7 @@
|
|||||||
package org.apache.fineract.portfolio.deliquency;
|
package org.apache.fineract.portfolio.deliquency;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
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.organisation.monetary.domain.MoneyHelper;
|
||||||
import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainServiceImpl;
|
import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainServiceImpl;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
|
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.Loan;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
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()));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user