Loan Delinquency Classification job

This commit is contained in:
Jose Alberto Hernandez
2022-08-22 11:52:40 +02:00
committed by Arnold Gálovics
parent 0af87a8766
commit 8b3975f8d7
29 changed files with 753 additions and 185 deletions
@@ -1155,6 +1155,15 @@ public class CommandWrapperBuilder {
return this;
}
public CommandWrapperBuilder assignDelinquency(final Long loanId) {
this.actionName = "UPDATEDELINQUENCY";
this.entityName = "LOAN";
this.entityId = loanId;
this.loanId = loanId;
this.href = "/loans/" + loanId;
return this;
}
public CommandWrapperBuilder createCodeValue(final Long codeId) {
this.actionName = "CREATE";
this.entityName = "CODEVALUE";
@@ -18,6 +18,8 @@
*/
package org.apache.fineract.infrastructure.core.service;
import static java.time.temporal.ChronoUnit.DAYS;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
@@ -76,4 +78,7 @@ public final class DateUtils {
return ThreadLocalContextUtil.getBusinessDate();
}
public static long getDifferenceInDays(final LocalDate localDateBefore, final LocalDate localDateAfter) {
return DAYS.between(localDateBefore, localDateAfter);
}
}
@@ -55,7 +55,8 @@ public enum JobName {
EXECUTE_DIRTY_JOBS("Execute All Dirty Jobs"), //
INCREASE_BUSINESS_DATE_BY_1_DAY("Increase Business Date by 1 day"), //
INCREASE_COB_DATE_BY_1_DAY("Increase COB Date by 1 day"), //
LOAN_COB("Loan COB");
LOAN_COB("Loan COB"), //
LOAN_DELINQUENCY_CLASSIFICATION("Loan Delinquency Classification");
private final String name;
@@ -0,0 +1,40 @@
/**
* 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.io.Serializable;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@ToString
@AllArgsConstructor
@Getter
@Setter
public class LoanDelinquencyTagHistoryData implements Serializable {
private Long id;
private Long loanId;
private String delinquencyRange;
private LocalDate addedOnDate;
private LocalDate liftedOnDate;
}
@@ -18,7 +18,7 @@
*/
package org.apache.fineract.portfolio.delinquency.domain;
import java.util.Set;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
@@ -46,7 +46,7 @@ public class DelinquencyBucket extends AbstractAuditableWithUTCDateTimeCustom {
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "m_delinquency_bucket_mappings", joinColumns = @JoinColumn(name = "delinquency_bucket_id"), inverseJoinColumns = @JoinColumn(name = "delinquency_range_id"))
private Set<DelinquencyRange> ranges;
private List<DelinquencyRange> ranges;
@Version
private Long version;
@@ -25,4 +25,5 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface DelinquencyRangeRepository extends JpaRepository<DelinquencyRange, Long>, JpaSpecificationExecutor<DelinquencyRange> {
Optional<DelinquencyRange> findByClassification(String classification);
}
@@ -28,6 +28,8 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface LoanDelinquencyTagHistoryRepository
extends JpaRepository<LoanDelinquencyTagHistory, Long>, JpaSpecificationExecutor<LoanDelinquencyTagHistory> {
LoanDelinquencyTagHistory findFirstByLoanOrderByAddedOnDateDesc(Loan loan);
List<LoanDelinquencyTagHistory> findByLoanOrderByAddedOnDateDesc(Loan loan);
Optional<LoanDelinquencyTagHistory> findByLoanAndLiftedOnDate(Loan loan, LocalDate liftedOnDate);
@@ -0,0 +1,42 @@
/**
* 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.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@CommandType(entity = "LOAN", action = "UPDATEDELINQUENCY")
public class UpdateLoanDelinquencyBucketCommandHandler implements NewCommandSourceHandler {
private final DelinquencyWritePlatformService writePlatformService;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.applyDelinquencyTagToLoan(command.entityId(), command);
}
}
@@ -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.mapper;
import java.util.List;
import org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryData;
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistory;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
@Mapper(componentModel = "spring")
public interface LoanDelinquencyTagMapper {
@Mappings({ @Mapping(target = "id", source = "source.id"), @Mapping(target = "loanId", source = "source.loan.id"),
@Mapping(target = "delinquencyRange", source = "source.delinquencyRange.classification"),
@Mapping(target = "addedOnDate", source = "source.addedOnDate"),
@Mapping(target = "liftedOnDate", source = "source.liftedOnDate") })
LoanDelinquencyTagHistoryData map(LoanDelinquencyTagHistory source);
List<LoanDelinquencyTagHistoryData> map(List<LoanDelinquencyTagHistory> sources);
}
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.delinquency.service;
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;
public interface DelinquencyReadPlatformService {
@@ -33,4 +34,7 @@ public interface DelinquencyReadPlatformService {
DelinquencyBucketData retrieveDelinquencyBucket(Long delinquencyBucketId);
DelinquencyRangeData retrieveCurrentDelinquencyTag(Long loanId);
Collection<LoanDelinquencyTagHistoryData> retrieveDelinquencyRangeHistory(Long loanId);
}
@@ -18,28 +18,25 @@
*/
package org.apache.fineract.portfolio.delinquency.service;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
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.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.exception.DelinquencyBucketNotFoundException;
import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyBucketMapper;
import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyRangeMapper;
import org.apache.fineract.portfolio.delinquency.mapper.LoanDelinquencyTagMapper;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -54,6 +51,7 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
private final LoanDelinquencyTagHistoryRepository repositoryLoanDelinquencyTagHistory;
private final DelinquencyRangeMapper mapperRange;
private final DelinquencyBucketMapper mapperBucket;
private final LoanDelinquencyTagMapper mapperLoanDelinquencyTagHistory;
private final LoanAssembler loanAssembler;
@Override
@@ -76,19 +74,10 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
@Override
public DelinquencyBucketData retrieveDelinquencyBucket(Long delinquencyBucketId) {
try {
final DelinquencyBucket delinquencyBucket = repositoryBucket.getReferenceById(delinquencyBucketId);
final DelinquencyBucketData delinquencyBucketData = mapperBucket.map(delinquencyBucket);
final DelinquencyBucketMappingMapper rmMappings = new DelinquencyBucketMappingMapper();
final String sql = "select " + rmMappings.schema() + " where buc.id = ? order by dran.min_age_days";
List<DelinquencyRangeData> ranges = this.jdbcTemplate.query(sql, rmMappings, delinquencyBucketId); // NOSONAR
delinquencyBucketData.setRanges(ranges);
return delinquencyBucketData;
} catch (final EmptyResultDataAccessException e) {
throw DelinquencyBucketNotFoundException.notFound(delinquencyBucketId, e);
}
final DelinquencyBucket delinquencyBucket = repositoryBucket.getReferenceById(delinquencyBucketId);
final DelinquencyBucketData delinquencyBucketData = mapperBucket.map(delinquencyBucket);
delinquencyBucketData.setRanges(mapperRange.map(delinquencyBucket.getRanges()));
return delinquencyBucketData;
}
@Override
@@ -102,24 +91,12 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
return null;
}
private static final class DelinquencyBucketMappingMapper implements RowMapper<DelinquencyRangeData> {
public String schema() {
return " dran.id as id, dran.classification as classification, dran.min_age_days as minimumAgeDays, dran.max_age_days as maximumAgeDays from m_delinquency_bucket buc "
+ " inner join m_delinquency_bucket_mappings dmap on dmap.delinquency_bucket_id = buc.id "
+ " inner join m_delinquency_range dran on dran.id = dmap.delinquency_range_id ";
}
@Override
public DelinquencyRangeData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
final Long id = rs.getLong("id");
final String classification = rs.getString("classification");
final Integer minimumAgeDays = rs.getInt("minimumAgeDays");
final Integer maximumAgeDays = rs.getInt("maximumAgeDays");
return new DelinquencyRangeData(id, classification, minimumAgeDays, maximumAgeDays);
}
@Override
public Collection<LoanDelinquencyTagHistoryData> retrieveDelinquencyRangeHistory(Long loanId) {
final Loan loan = this.loanAssembler.assembleFrom(loanId);
final List<LoanDelinquencyTagHistory> loanDelinquencyTagData = this.repositoryLoanDelinquencyTagHistory
.findByLoanOrderByAddedOnDateDesc(loan);
return mapperLoanDelinquencyTagHistory.map(loanDelinquencyTagData);
}
}
@@ -37,4 +37,8 @@ public interface DelinquencyWritePlatformService {
CommandProcessingResult deleteDelinquencyBucket(Long delinquencyBucketId, JsonCommand command);
CommandProcessingResult applyDelinquencyTagToLoan(Long loanId, JsonCommand command);
void applyDelinquencyTagToLoan(Long loanId, Long ageDays);
}
@@ -27,6 +27,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
@@ -48,12 +49,15 @@ import org.apache.fineract.portfolio.delinquency.exception.DelinquencyRangeInval
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyBucketParseAndValidator;
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlatformService {
private final DelinquencyBucketParseAndValidator dataValidatorBucket;
@@ -65,6 +69,7 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
private final LoanDelinquencyTagHistoryRepository loanDelinquencyTagRepository;
private final LoanRepositoryWrapper loanRepository;
private final LoanProductRepository loanProductRepository;
private final LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository;
@Override
public CommandProcessingResult createDelinquencyRange(JsonCommand command) {
@@ -139,11 +144,40 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(delinquencyBucket.getId()).build();
}
@Override
public CommandProcessingResult applyDelinquencyTagToLoan(Long loanId, JsonCommand command) {
Map<String, Object> changes = new HashMap<>();
final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId);
final DelinquencyBucket delinquencyBucket = loan.getLoanProduct().getDelinquencyBucket();
if (delinquencyBucket != null) {
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
LoanRepaymentScheduleInstallment loanRepaymentSchedule = this.loanRepaymentScheduleInstallmentRepository
.findFirstByLoanAndDueDateLessThanEqualAndObligationsMetOnDateOrderByDueDate(loan, businessDate, null);
Long overdueDays = 0L;
if (loanRepaymentSchedule != null) {
overdueDays = DateUtils.getDifferenceInDays(loanRepaymentSchedule.getDueDate(), businessDate);
}
changes = lookUpDelinquencyRange(loan, delinquencyBucket, overdueDays);
}
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(loan.getId()).with(changes).build();
}
@Override
public void applyDelinquencyTagToLoan(Long loanId, Long ageDays) {
final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId);
final DelinquencyBucket delinquencyBucket = loan.getLoanProduct().getDelinquencyBucket();
if (delinquencyBucket != null) {
lookUpDelinquencyRange(loan, delinquencyBucket, ageDays);
}
}
private DelinquencyRange createDelinquencyRange(DelinquencyRangeData data, Map<String, Object> changes) {
Optional<DelinquencyRange> delinquencyRange = repositoryRange.findByClassification(data.getClassification());
if (delinquencyRange.isEmpty()) {
if (data.getMinimumAgeDays() > data.getMaximumAgeDays()) {
if (data.getMaximumAgeDays() != null && data.getMinimumAgeDays() > data.getMaximumAgeDays()) {
final String errorMessage = "The age days values are invalid, the maximum age days can't be lower than minimum age days";
throw new DelinquencyRangeInvalidAgesException(errorMessage, data.getMinimumAgeDays(), data.getMaximumAgeDays());
}
@@ -222,18 +256,12 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
private void validateDelinquencyRanges(List<DelinquencyRange> ranges) {
// Sort the ranges based on the minAgeDays
final Comparator<DelinquencyRange> orderByMinAge = new Comparator<DelinquencyRange>() {
ranges = sortDelinquencyRangesByMinAge(ranges);
@Override
public int compare(DelinquencyRange o1, DelinquencyRange o2) {
return o1.getMinimumAgeDays().compareTo(o2.getMinimumAgeDays());
}
};
Collections.sort(ranges, orderByMinAge);
DelinquencyRange prevDelinquencyRange = null;
for (DelinquencyRange delinquencyRange : ranges) {
if (prevDelinquencyRange != null) {
if (isOverlaped(prevDelinquencyRange, delinquencyRange)) {
if (isOverlapped(prevDelinquencyRange, delinquencyRange)) {
final String errorMessage = "The delinquency ranges age days values are overlaped";
throw new DelinquencyBucketAgesOverlapedException(errorMessage, prevDelinquencyRange, delinquencyRange);
}
@@ -242,37 +270,104 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
}
}
private boolean isOverlaped(DelinquencyRange o1, DelinquencyRange o2) {
return Math.max(o1.getMinimumAgeDays(), o2.getMinimumAgeDays()) <= Math.min(o1.getMaximumAgeDays(), o2.getMaximumAgeDays());
}
public void setLoanDelinquencyTag(Long loanId, Long delinquencyRangeId) {
final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId);
final LocalDate transactionDate = DateUtils.getLocalDateOfTenant();
List<LoanDelinquencyTagHistory> loanDelinquencyTagHistory = this.loanDelinquencyTagRepository
.findByLoanOrderByAddedOnDateDesc(loan);
// The delinquencyRangeId in null means just goes out from Delinquency
if (delinquencyRangeId == null) {
// The Loan will go out from Delinquency
if (!loanDelinquencyTagHistory.isEmpty()) {
LoanDelinquencyTagHistory loanDelinquencyTagPrev = loanDelinquencyTagHistory.get(0);
loanDelinquencyTagPrev.setLiftedOnDate(transactionDate);
this.loanDelinquencyTagRepository.saveAndFlush(loanDelinquencyTagPrev);
}
private boolean isOverlapped(DelinquencyRange o1, DelinquencyRange o2) {
if (o2.getMaximumAgeDays() != null) { // Max Age undefined - Last one
return Math.max(o1.getMinimumAgeDays(), o2.getMinimumAgeDays()) <= Math.min(o1.getMaximumAgeDays(), o2.getMaximumAgeDays());
} else {
// The previous Loan Delinquency Tag will set as Lifted
if (!loanDelinquencyTagHistory.isEmpty()) {
LoanDelinquencyTagHistory loanDelinquencyTagPrev = loanDelinquencyTagHistory.get(0);
loanDelinquencyTagPrev.setLiftedOnDate(transactionDate);
this.loanDelinquencyTagRepository.save(loanDelinquencyTagPrev);
}
final DelinquencyRange delinquencyRange = repositoryRange.getReferenceById(delinquencyRangeId);
LoanDelinquencyTagHistory loanDelinquencyTag = new LoanDelinquencyTagHistory(delinquencyRange, loan, transactionDate, null);
loanDelinquencyTagHistory.add(loanDelinquencyTag);
this.loanDelinquencyTagRepository.saveAndFlush(loanDelinquencyTag);
return Math.max(o1.getMinimumAgeDays(), o2.getMinimumAgeDays()) <= Math.min(o1.getMaximumAgeDays(), o2.getMinimumAgeDays());
}
}
private Map<String, Object> lookUpDelinquencyRange(final Loan loan, final DelinquencyBucket delinquencyBucket, long ageDays) {
Map<String, Object> changes = new HashMap<>();
if (ageDays <= 0) { // No Delinquency
log.debug("Loan {} without delinquency range with {} days", loan.getId(), ageDays);
changes = setLoanDelinquencyTag(loan, null);
} else {
// Sort the ranges based on the minAgeDays
List<DelinquencyRange> ranges = sortDelinquencyRangesByMinAge(delinquencyBucket.getRanges());
for (DelinquencyRange delinquencyRange : ranges) {
if (delinquencyRange.getMaximumAgeDays() == null) {
if (delinquencyRange.getMinimumAgeDays() <= ageDays) {
log.debug("Loan {} with delinquency range {} with {} days", loan.getId(), delinquencyRange.getClassification(),
ageDays);
changes = setLoanDelinquencyTag(loan, delinquencyRange.getId());
break;
}
} else {
if (delinquencyRange.getMinimumAgeDays() <= ageDays && delinquencyRange.getMaximumAgeDays() >= ageDays) {
log.debug("Loan {} with delinquency range {} with {} days", loan.getId(), delinquencyRange.getClassification(),
ageDays);
changes = setLoanDelinquencyTag(loan, delinquencyRange.getId());
break;
}
}
}
}
changes.put("ageDays", ageDays);
return changes;
}
public Map<String, Object> setLoanDelinquencyTag(Long loanId, Long delinquencyRangeId) {
final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId);
return setLoanDelinquencyTag(loan, delinquencyRangeId);
}
public Map<String, Object> setLoanDelinquencyTag(Loan loan, Long delinquencyRangeId) {
Map<String, Object> changes = new HashMap<>();
List<LoanDelinquencyTagHistory> loanDelinquencyTagHistory = new ArrayList<>();
final LocalDate transactionDate = DateUtils.getBusinessLocalDate();
Optional<LoanDelinquencyTagHistory> optLoanDelinquencyTag = this.loanDelinquencyTagRepository.findByLoanAndLiftedOnDate(loan, null);
// The delinquencyRangeId in null means just goes out from Delinquency
LoanDelinquencyTagHistory loanDelinquencyTagPrev = null;
if (delinquencyRangeId == null) {
// The Loan will go out from Delinquency
if (optLoanDelinquencyTag.isPresent()) {
loanDelinquencyTagPrev = optLoanDelinquencyTag.get();
loanDelinquencyTagPrev.setLiftedOnDate(transactionDate);
loanDelinquencyTagHistory.add(loanDelinquencyTagPrev);
changes.put("previous", loanDelinquencyTagPrev.getDelinquencyRange());
}
} else {
if (optLoanDelinquencyTag.isPresent()) {
loanDelinquencyTagPrev = optLoanDelinquencyTag.get();
}
// If the Delinquency Tag has not changed
if (loanDelinquencyTagPrev != null && loanDelinquencyTagPrev.getDelinquencyRange().getId().equals(delinquencyRangeId)) {
changes.put("current", loanDelinquencyTagPrev.getDelinquencyRange());
} else {
// The previous Loan Delinquency Tag will set as Lifted
if (loanDelinquencyTagPrev != null) {
loanDelinquencyTagPrev.setLiftedOnDate(transactionDate);
loanDelinquencyTagHistory.add(loanDelinquencyTagPrev);
changes.put("previous", loanDelinquencyTagPrev.getDelinquencyRange());
}
final DelinquencyRange delinquencyRange = repositoryRange.getReferenceById(delinquencyRangeId);
LoanDelinquencyTagHistory loanDelinquencyTag = new LoanDelinquencyTagHistory(delinquencyRange, loan, transactionDate, null);
loanDelinquencyTagHistory.add(loanDelinquencyTag);
changes.put("current", loanDelinquencyTag.getDelinquencyRange());
}
}
if (loanDelinquencyTagHistory.size() > 0) {
this.loanDelinquencyTagRepository.saveAllAndFlush(loanDelinquencyTagHistory);
}
return changes;
}
private List<DelinquencyRange> sortDelinquencyRangesByMinAge(List<DelinquencyRange> ranges) {
final Comparator<DelinquencyRange> orderByMinAge = new Comparator<DelinquencyRange>() {
@Override
public int compare(DelinquencyRange o1, DelinquencyRange o2) {
return o1.getMinimumAgeDays().compareTo(o2.getMinimumAgeDays());
}
};
Collections.sort(ranges, orderByMinAge);
return ranges;
}
}
@@ -54,6 +54,7 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
@@ -100,6 +101,9 @@ import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService;
import org.apache.fineract.portfolio.client.data.ClientData;
import org.apache.fineract.portfolio.collateralmanagement.data.LoanCollateralResponseData;
import org.apache.fineract.portfolio.collateralmanagement.service.LoanCollateralManagementReadPlatformService;
import org.apache.fineract.portfolio.delinquency.api.DelinquencyApiResourceSwagger;
import org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryData;
import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService;
import org.apache.fineract.portfolio.floatingrates.data.InterestRatePeriodData;
import org.apache.fineract.portfolio.fund.data.FundData;
import org.apache.fineract.portfolio.fund.service.FundReadPlatformService;
@@ -201,6 +205,7 @@ import org.springframework.util.CollectionUtils;
+ "Specifies rest frequency start date for interest recalculation. This date must be before or equal to disbursement date\n"
+ "recalculationCompoundingFrequencyDate\n"
+ "Specifies compounding frequency start date for interest recalculation. This date must be equal to disbursement date")
@RequiredArgsConstructor
public class LoansApiResource {
private final Set<String> loanDataParameters = new HashSet<>(Arrays.asList("id", "accountNo", "status", "externalId", "clientId",
@@ -259,63 +264,8 @@ public class LoansApiResource {
private final DefaultToApiJsonSerializer<GlimRepaymentTemplate> glimTemplateToApiJsonSerializer;
private final GLIMAccountInfoReadPlatformService glimAccountInfoReadPlatformService;
private final LoanCollateralManagementReadPlatformService loanCollateralManagementReadPlatformService;
public LoansApiResource(final PlatformSecurityContext context, final LoanReadPlatformService loanReadPlatformService,
final LoanProductReadPlatformService loanProductReadPlatformService,
final LoanDropdownReadPlatformService dropdownReadPlatformService, final FundReadPlatformService fundReadPlatformService,
final ChargeReadPlatformService chargeReadPlatformService, final LoanChargeReadPlatformService loanChargeReadPlatformService,
final LoanScheduleCalculationPlatformService calculationPlatformService,
final GuarantorReadPlatformService guarantorReadPlatformService,
final CodeValueReadPlatformService codeValueReadPlatformService, final GroupReadPlatformService groupReadPlatformService,
final DefaultToApiJsonSerializer<LoanAccountData> toApiJsonSerializer,
final DefaultToApiJsonSerializer<LoanApprovalData> loanApprovalDataToApiJsonSerializer,
final DefaultToApiJsonSerializer<LoanScheduleData> loanScheduleToApiJsonSerializer,
final ApiRequestParameterHelper apiRequestParameterHelper, final FromJsonHelper fromJsonHelper,
final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService,
final CalendarReadPlatformService calendarReadPlatformService, final NoteReadPlatformService noteReadPlatformService,
final PortfolioAccountReadPlatformService portfolioAccountReadPlatformServiceImpl,
final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService,
final LoanScheduleHistoryReadPlatformService loanScheduleHistoryReadPlatformService,
final AccountDetailsReadPlatformService accountDetailsReadPlatformService,
final EntityDatatableChecksReadService entityDatatableChecksReadService,
final BulkImportWorkbookService bulkImportWorkbookService,
final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService, final RateReadService rateReadService,
final ConfigurationDomainService configurationDomainService,
final DefaultToApiJsonSerializer<GlimRepaymentTemplate> glimTemplateToApiJsonSerializer,
final GLIMAccountInfoReadPlatformService glimAccountInfoReadPlatformService,
final LoanCollateralManagementReadPlatformService loanCollateralManagementReadPlatformService) {
this.context = context;
this.loanReadPlatformService = loanReadPlatformService;
this.loanProductReadPlatformService = loanProductReadPlatformService;
this.dropdownReadPlatformService = dropdownReadPlatformService;
this.fundReadPlatformService = fundReadPlatformService;
this.chargeReadPlatformService = chargeReadPlatformService;
this.loanChargeReadPlatformService = loanChargeReadPlatformService;
this.calculationPlatformService = calculationPlatformService;
this.guarantorReadPlatformService = guarantorReadPlatformService;
this.codeValueReadPlatformService = codeValueReadPlatformService;
this.groupReadPlatformService = groupReadPlatformService;
this.toApiJsonSerializer = toApiJsonSerializer;
this.loanApprovalDataToApiJsonSerializer = loanApprovalDataToApiJsonSerializer;
this.loanScheduleToApiJsonSerializer = loanScheduleToApiJsonSerializer;
this.apiRequestParameterHelper = apiRequestParameterHelper;
this.fromJsonHelper = fromJsonHelper;
this.commandsSourceWritePlatformService = commandsSourceWritePlatformService;
this.calendarReadPlatformService = calendarReadPlatformService;
this.noteReadPlatformService = noteReadPlatformService;
this.portfolioAccountReadPlatformService = portfolioAccountReadPlatformServiceImpl;
this.accountAssociationsReadPlatformService = accountAssociationsReadPlatformService;
this.loanScheduleHistoryReadPlatformService = loanScheduleHistoryReadPlatformService;
this.accountDetailsReadPlatformService = accountDetailsReadPlatformService;
this.entityDatatableChecksReadService = entityDatatableChecksReadService;
this.rateReadService = rateReadService;
this.bulkImportWorkbookService = bulkImportWorkbookService;
this.bulkImportWorkbookPopulatorService = bulkImportWorkbookPopulatorService;
this.configurationDomainService = configurationDomainService;
this.glimTemplateToApiJsonSerializer = glimTemplateToApiJsonSerializer;
this.glimAccountInfoReadPlatformService = glimAccountInfoReadPlatformService;
this.loanCollateralManagementReadPlatformService = loanCollateralManagementReadPlatformService;
}
private final DefaultToApiJsonSerializer<LoanDelinquencyTagHistoryData> jsonSerializerTagHistory;
private final DelinquencyReadPlatformService delinquencyReadPlatformService;
/*
* This template API is used for loan approval, ideally this should be invoked on loan that are pending for
@@ -886,51 +836,37 @@ public class LoansApiResource {
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
CommandProcessingResult result = null;
CommandWrapper commandRequest = null;
if (is(commandParam, "reject")) {
final CommandWrapper commandRequest = builder.rejectLoanApplication(loanId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
commandRequest = builder.rejectLoanApplication(loanId).build();
} else if (is(commandParam, "withdrawnByApplicant")) {
final CommandWrapper commandRequest = builder.withdrawLoanApplication(loanId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
commandRequest = builder.withdrawLoanApplication(loanId).build();
} else if (is(commandParam, "approve")) {
final CommandWrapper commandRequest = builder.approveLoanApplication(loanId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
commandRequest = builder.approveLoanApplication(loanId).build();
} else if (is(commandParam, "disburse")) {
final CommandWrapper commandRequest = builder.disburseLoanApplication(loanId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
commandRequest = builder.disburseLoanApplication(loanId).build();
} else if (is(commandParam, "disburseToSavings")) {
final CommandWrapper commandRequest = builder.disburseLoanToSavingsApplication(loanId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
if (is(commandParam, "undoapproval")) {
final CommandWrapper commandRequest = builder.undoLoanApplicationApproval(loanId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
commandRequest = builder.disburseLoanToSavingsApplication(loanId).build();
} else if (is(commandParam, "undoapproval")) {
commandRequest = builder.undoLoanApplicationApproval(loanId).build();
} else if (is(commandParam, "undodisbursal")) {
final CommandWrapper commandRequest = builder.undoLoanApplicationDisbursal(loanId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
commandRequest = builder.undoLoanApplicationDisbursal(loanId).build();
} else if (is(commandParam, "undolastdisbursal")) {
final CommandWrapper commandRequest = builder.undoLastDisbursalLoanApplication(loanId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
if (is(commandParam, "assignloanofficer")) {
final CommandWrapper commandRequest = builder.assignLoanOfficer(loanId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
commandRequest = builder.undoLastDisbursalLoanApplication(loanId).build();
} else if (is(commandParam, "assignloanofficer")) {
commandRequest = builder.assignLoanOfficer(loanId).build();
} else if (is(commandParam, "unassignloanofficer")) {
final CommandWrapper commandRequest = builder.unassignLoanOfficer(loanId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
commandRequest = builder.unassignLoanOfficer(loanId).build();
} else if (is(commandParam, "recoverGuarantees")) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().recoverFromGuarantor(loanId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
commandRequest = new CommandWrapperBuilder().recoverFromGuarantor(loanId).build();
} else if (is(commandParam, "assigndelinquency")) {
commandRequest = builder.assignDelinquency(loanId).build();
}
if (result == null) {
if (commandRequest == null) {
throw new UnrecognizedQueryParamException("command", commandParam);
}
CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
}
@@ -978,31 +914,25 @@ public class LoansApiResource {
final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(apiRequestBodyAsJson);
CommandProcessingResult result = null;
CommandWrapper commandRequest = null;
if (is(commandParam, "reject")) {
final CommandWrapper commandRequest = builder.rejectGLIMApplication(glimId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
commandRequest = builder.rejectGLIMApplication(glimId).build();
} else if (is(commandParam, "approve")) {
final CommandWrapper commandRequest = builder.approveGLIMLoanApplication(glimId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
commandRequest = builder.approveGLIMLoanApplication(glimId).build();
} else if (is(commandParam, "disburse")) {
final CommandWrapper commandRequest = builder.disburseGlimLoanApplication(glimId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
commandRequest = builder.disburseGlimLoanApplication(glimId).build();
} else if (is(commandParam, "glimrepayment")) {
final CommandWrapper commandRequest = builder.repaymentGlimLoanApplication(glimId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
commandRequest = builder.repaymentGlimLoanApplication(glimId).build();
} else if (is(commandParam, "undodisbursal")) {
final CommandWrapper commandRequest = builder.undoGLIMLoanDisbursal(glimId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
commandRequest = builder.undoGLIMLoanDisbursal(glimId).build();
} else if (is(commandParam, "undoapproval")) {
final CommandWrapper commandRequest = builder.undoGLIMLoanApproval(glimId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
commandRequest = builder.undoGLIMLoanApproval(glimId).build();
}
if (result == null) {
if (commandRequest == null) {
throw new UnrecognizedQueryParamException("command", commandParam);
}
CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
}
@@ -1040,4 +970,21 @@ public class LoansApiResource {
uploadedInputStream, fileDetail, locale, dateFormat);
return this.toApiJsonSerializer.serialize(importDocumentId);
}
@GET
@Path("{loanId}/delinquencytags")
@Consumes({ MediaType.TEXT_HTML, MediaType.APPLICATION_JSON })
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Retrieve the Loan Delinquency Tag history using the Loan Id", description = "")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = DelinquencyApiResourceSwagger.GetDelinquencyTagHistoryResponse.class))) })
public String getDelinquencyTagHistory(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId,
@Context final UriInfo uriInfo) {
context.authenticatedUser().validateHasReadPermission("DELINQUENCY_TAGS");
final Collection<LoanDelinquencyTagHistoryData> loanDelinquencyTagHistoryData = this.delinquencyReadPlatformService
.retrieveDelinquencyRangeHistory(loanId);
ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters());
return this.jsonSerializerTagHistory.serialize(settings, loanDelinquencyTagHistoryData);
}
}
@@ -0,0 +1,36 @@
/**
* 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.io.Serializable;
import java.time.LocalDate;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public class LoanScheduleDelinquencyData implements Serializable {
private final Long loanId;
private final Long productId;
private final LocalDate dueDate;
private final LocalDate fromDate;
private final Long ageDays;
}
@@ -18,10 +18,17 @@
*/
package org.apache.fineract.portfolio.loanaccount.domain;
import java.time.LocalDate;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface LoanRepaymentScheduleInstallmentRepository
extends JpaRepository<LoanRepaymentScheduleInstallment, Long>, JpaSpecificationExecutor<LoanRepaymentScheduleInstallment> {
LoanRepaymentScheduleInstallment findFirstByLoanAndDueDateLessThanEqualAndObligationsMetOnDateOrderByDueDate(Loan loan,
LocalDate businnessDate, LocalDate obligationsMetOnDate);
List<LoanRepaymentScheduleInstallment> findByLoanAndDueDateLessThanEqualAndObligationsMetOnDateOrderByDueDate(Loan loan,
LocalDate businnessDate, LocalDate obligationsMetOnDate);
}
@@ -0,0 +1,61 @@
/**
* 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.jobs.setloandelinquencytags;
import lombok.AllArgsConstructor;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@AllArgsConstructor
public class SetLoanDelinquencyTagsConfig {
private JobBuilderFactory jobs;
private StepBuilderFactory steps;
private LoanProductRepository loanProductRepository;
private DelinquencyWritePlatformService delinquencyWritePlatformService;
private LoanReadPlatformService loanReadPlatformService;
@Bean
public Step setLoanDelinquencyTagsStep() {
return steps.get(JobName.LOAN_DELINQUENCY_CLASSIFICATION.name()).tasklet(setLoanDelinquencyTagsTasklet()).build();
}
@Bean
public Job setLoanDelinquencyTagsJob() {
return jobs.get(JobName.LOAN_DELINQUENCY_CLASSIFICATION.name()).start(setLoanDelinquencyTagsStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public SetLoanDelinquencyTagsTasklet setLoanDelinquencyTagsTasklet() {
return new SetLoanDelinquencyTagsTasklet(loanProductRepository, delinquencyWritePlatformService, loanReadPlatformService);
}
}
@@ -0,0 +1,58 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.jobs.setloandelinquencytags;
import java.util.Collection;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
@Slf4j
@RequiredArgsConstructor
public class SetLoanDelinquencyTagsTasklet implements Tasklet {
private final LoanProductRepository loanProductRepository;
private final DelinquencyWritePlatformService delinquencyWritePlatformService;
private final LoanReadPlatformService loanReadPlatformService;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
Collection<LoanScheduleDelinquencyData> loanScheduleDelinquencyData = this.loanReadPlatformService
.retrieveScheduleDelinquencyData(DateUtils.getBusinessLocalDate());
log.debug("Were found {} items", loanScheduleDelinquencyData.size());
for (LoanScheduleDelinquencyData loanDelinquencyData : loanScheduleDelinquencyData) {
this.delinquencyWritePlatformService.applyDelinquencyTagToLoan(loanDelinquencyData.getLoanId(),
loanDelinquencyData.getAgeDays());
}
log.debug("{}: Records affected by setLoanDelinquencyTags: {}", ThreadLocalContextUtil.getTenant().getName(),
loanScheduleDelinquencyData.size());
return RepeatStatus.FINISHED;
}
}
@@ -40,7 +40,8 @@ public class LoanAccrualPlatformServiceImpl implements LoanAccrualPlatformServic
@Override
public void addPeriodicAccruals(final LocalDate tillDate) throws JobExecutionException {
Collection<LoanScheduleAccrualData> loanScheduleAccrualDataList = this.loanReadPlatformService.retrivePeriodicAccrualData(tillDate);
Collection<LoanScheduleAccrualData> loanScheduleAccrualDataList = this.loanReadPlatformService
.retrievePeriodicAccrualData(tillDate);
addPeriodicAccruals(tillDate, loanScheduleAccrualDataList);
}
@@ -33,6 +33,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData;
import org.apache.fineract.portfolio.loanaccount.data.LoanApprovalData;
import org.apache.fineract.portfolio.loanaccount.data.LoanRepaymentScheduleInstallmentData;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
import org.apache.fineract.portfolio.loanaccount.data.PaidInAdvanceData;
@@ -104,11 +105,13 @@ public interface LoanReadPlatformService {
Collection<LoanScheduleAccrualData> retriveScheduleAccrualData();
Collection<LoanScheduleDelinquencyData> retrieveScheduleDelinquencyData(LocalDate businessLocalDate);
LoanTransactionData retrieveRecoveryPaymentTemplate(Long loanId);
LoanTransactionData retrieveLoanWriteoffTemplate(Long loanId);
Collection<LoanScheduleAccrualData> retrivePeriodicAccrualData(LocalDate tillDate);
Collection<LoanScheduleAccrualData> retrievePeriodicAccrualData(LocalDate tillDate);
Collection<Long> fetchLoansForInterestRecalculation();
@@ -88,6 +88,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanApprovalData;
import org.apache.fineract.portfolio.loanaccount.data.LoanInterestRecalculationData;
import org.apache.fineract.portfolio.loanaccount.data.LoanRepaymentScheduleInstallmentData;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData;
import org.apache.fineract.portfolio.loanaccount.data.LoanStatusEnumData;
import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
@@ -1647,6 +1648,22 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
}
@Override
public Collection<LoanScheduleDelinquencyData> retrieveScheduleDelinquencyData(LocalDate businessLocalDate) {
LoanScheduleDelinquencyMapper mapper = new LoanScheduleDelinquencyMapper(DateUtils.getBusinessLocalDate());
final StringBuilder sqlBuilder = new StringBuilder(400);
sqlBuilder.append("select ").append(mapper.schema())
.append(" where loan.loan_status_id=:active and ls.duedate <= :businessLocalDate and ")
.append(" mpl.delinquency_bucket_id is not null "); // Jusr the Loan Product linked to delinquency
// bucket
sqlBuilder.append(" order by loan.id, ls.duedate ");
Map<String, Object> paramMap = new HashMap<>(3);
paramMap.put("active", LoanStatus.ACTIVE.getValue());
paramMap.put("businessLocalDate", businessLocalDate);
return this.namedParameterJdbcTemplate.query(sqlBuilder.toString(), paramMap, mapper);
}
@Override
public Collection<LoanScheduleAccrualData> retriveScheduleAccrualData() {
@@ -1673,7 +1690,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
}
@Override
public Collection<LoanScheduleAccrualData> retrivePeriodicAccrualData(final LocalDate tillDate) {
public Collection<LoanScheduleAccrualData> retrievePeriodicAccrualData(final LocalDate tillDate) {
LoanSchedulePeriodicAccrualMapper mapper = new LoanSchedulePeriodicAccrualMapper();
LocalDate organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate();
@@ -1816,6 +1833,34 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
}
}
private static final class LoanScheduleDelinquencyMapper implements RowMapper<LoanScheduleDelinquencyData> {
private LocalDate businessDate;
LoanScheduleDelinquencyMapper(LocalDate businessDate) {
this.businessDate = businessDate;
}
public String schema() {
final StringBuilder sqlBuilder = new StringBuilder(400);
sqlBuilder.append(" loan.id as loanId, ls.duedate as duedate, ls.fromdate as fromdate, loan.product_id as productId ")
.append(" from m_loan_repayment_schedule ls left join m_loan loan on loan.id=ls.loan_id ")
.append(" left join m_product_loan mpl on mpl.id = loan.product_id ");
return sqlBuilder.toString();
}
@Override
public LoanScheduleDelinquencyData mapRow(ResultSet rs, @SuppressWarnings("unused") int rowNum) throws SQLException {
final Long loanId = rs.getLong("loanId");
final Long productId = rs.getLong("productId");
final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "duedate");
final LocalDate fromDate = JdbcSupport.getLocalDate(rs, "fromdate");
final long ageDays = DateUtils.getDifferenceInDays(dueDate, businessDate);
return new LoanScheduleDelinquencyData(loanId, productId, dueDate, fromDate, ageDays);
}
}
@Override
public LoanTransactionData retrieveRecoveryPaymentTemplate(Long loanId) {
final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
@@ -31,4 +31,6 @@ public interface LoanProductRepository extends JpaRepository<LoanProduct, Long>,
List<LoanProduct> retrieveLoanProductsByChargeId(@Param("chargeId") Long chargeId);
Long countByDelinquencyBucket(DelinquencyBucket delinquencyBucket);
List<LoanProduct> findByDelinquencyBucketNotNull();
}
@@ -143,6 +143,12 @@ public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanPro
this.aprCalculator, floatingRate, rates);
loanProduct.updateLoanProductInRelatedClasses();
if (command.parameterExists("delinquencyBucketId")) {
DelinquencyBucket delinquencyBucket = this.delinquencyBucketRepository
.getReferenceById(command.longValueOfParameterNamed("delinquencyBucketId"));
loanProduct.setDelinquencyBucket(delinquencyBucket);
}
this.loanProductRepository.saveAndFlush(loanProduct);
// save accounting mappings
@@ -59,4 +59,5 @@
<include file="parts/0037_add_loan_cob_job_data.xml" relativeToChangelogFile="true"/>
<include file="parts/0038_add_reversal_data_to_loan_transaction.xml" relativeToChangelogFile="true"/>
<include file="parts/0039_add_loan_account_locks.xml" relativeToChangelogFile="true"/>
<include file="parts/0040_add_delinquency_tags_job.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>
@@ -0,0 +1,68 @@
<?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.3.xsd">
<changeSet author="fineract" id="delinquency_tags_job-1">
<insert tableName="m_permission">
<column name="grouping" value="organisation"/>
<column name="code" value="READ_DELINQUENCY_TAGS"/>
<column name="entity_name" value="DELINQUENCY_TAGS"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="organisation"/>
<column name="code" value="UPDATE_DELINQUENCY_TAGS"/>
<column name="entity_name" value="DELINQUENCY_TAGS"/>
<column name="action_name" value="UPDATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
<insert tableName="m_permission">
<column name="grouping" value="organisation"/>
<column name="code" value="UPDATEDELINQUENCY_LOAN"/>
<column name="entity_name" value="LOAN"/>
<column name="action_name" value="UPDATEDELINQUENCY"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<changeSet author="fineract" id="delinquency_tags_job-2">
<insert tableName="job">
<column name="name" value="Loan Delinquency Classification"/>
<column name="display_name" value="Loan Delinquency Classification"/>
<column name="cron_expression" value="0 0 22 1/1 * ? *"/>
<column name="create_time" valueDate="${current_datetime}"/>
<column name="task_priority" valueNumeric="5"/>
<column name="group_name"/>
<column name="previous_run_start_time"/>
<column name="job_key" value="Loan Delinquency ClassificationDetail1 _ DEFAULT"/>
<column name="initializing_errorlog"/>
<column name="is_active" valueBoolean="true"/>
<column name="currently_running" valueBoolean="false"/>
<column name="updates_allowed" valueBoolean="true"/>
<column name="scheduler_group" valueNumeric="0"/>
<column name="is_misfired" valueBoolean="false"/>
<column name="node_id" valueNumeric="1"/>
<column name="is_mismatched_job" valueBoolean="true"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -27,21 +27,32 @@ import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.http.ContentType;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashMap;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.DeleteDelinquencyBucketResponse;
import org.apache.fineract.client.models.DeleteDelinquencyRangeResponse;
import org.apache.fineract.client.models.GetDelinquencyBucketsResponse;
import org.apache.fineract.client.models.GetDelinquencyRangesResponse;
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.PostDelinquencyBucketResponse;
import org.apache.fineract.client.models.PostDelinquencyRangeResponse;
import org.apache.fineract.client.models.PutDelinquencyBucketResponse;
import org.apache.fineract.client.models.PutDelinquencyRangeResponse;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.SchedulerJobHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper;
import org.apache.fineract.integrationtests.common.products.DelinquencyRangesHelper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@Slf4j
public class DelinquencyBucketsIntegrationTest {
private ResponseSpecification responseSpec;
@@ -208,4 +219,71 @@ public class DelinquencyBucketsIntegrationTest {
DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, response403Spec, jsonBucket);
}
@Test
public void testLoanClassificationJob() {
// Given
final LoanTransactionHelper loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
final SchedulerJobHelper schedulerJobHelper = new SchedulerJobHelper(requestSpec);
ArrayList<Integer> rangeIds = new ArrayList<>();
String jsonRange = DelinquencyRangesHelper.getAsJSON(1, 3);
PostDelinquencyRangeResponse delinquencyRangeResponse = DelinquencyRangesHelper.createDelinquencyRange(requestSpec, responseSpec,
jsonRange);
rangeIds.add(delinquencyRangeResponse.getResourceId());
jsonRange = DelinquencyRangesHelper.getAsJSON(4, 60);
// Create
delinquencyRangeResponse = DelinquencyRangesHelper.createDelinquencyRange(requestSpec, responseSpec, jsonRange);
rangeIds.add(delinquencyRangeResponse.getResourceId());
final GetDelinquencyRangesResponse range = DelinquencyRangesHelper.getDelinquencyRange(requestSpec, responseSpec,
delinquencyRangeResponse.getResourceId());
final String classificationExpected = range.getClassification();
log.info("Expected Delinquency Range classification {}", classificationExpected);
String jsonBucket = DelinquencyBucketsHelper.getAsJSON(rangeIds);
PostDelinquencyBucketResponse delinquencyBucketResponse = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec,
responseSpec, jsonBucket);
final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec,
delinquencyBucketResponse.getResourceId());
final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2012");
final HashMap<String, Object> loanProductMap = new LoanProductTestBuilder().build(null, delinquencyBucket.getId());
final Integer loanProductId = loanTransactionHelper.getLoanProductId(Utils.convertToJson(loanProductMap));
final GetLoanProductsProductIdResponse getLoanProductsProductResponse = loanTransactionHelper.getLoanProduct(loanProductId);
log.info("Loan Product Bucket Name: {}", getLoanProductsProductResponse.getDelinquencyBucket().getName());
LocalDate todaysDate = Utils.getLocalDateOfTenant();
todaysDate = todaysDate.minusDays(40);
final String operationDate = Utils.dateFormatter.format(todaysDate);
final String principalAmount = "10000";
final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(principalAmount).withLoanTermFrequency("12")
.withLoanTermFrequencyAsMonths().withNumberOfRepayments("12").withRepaymentEveryAfter("1")
.withRepaymentFrequencyTypeAsMonths() //
.withInterestRatePerPeriod("2") //
.withExpectedDisbursementDate(operationDate) //
.withInterestTypeAsDecliningBalance() //
.withSubmittedOnDate(operationDate) //
.build(clientId.toString(), loanProductId.toString(), null);
final Integer loanId = loanTransactionHelper.getLoanId(loanApplicationJSON);
loanTransactionHelper.approveLoan(operationDate, principalAmount, loanId, null);
loanTransactionHelper.disburseLoanWithNetDisbursalAmount(operationDate, loanId, principalAmount);
// When
final String jobName = "Loan Delinquency Classification";
schedulerJobHelper.executeAndAwaitJob(jobName);
final GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
log.info("Loan Delinquency Range {}", getLoansLoanIdResponse.getDelinquencyRange().getClassification());
// Then
assertNotNull(delinquencyBucketResponse);
assertNotNull(getLoanProductsProductResponse);
assertEquals(getLoanProductsProductResponse.getDelinquencyBucket().getName(), delinquencyBucket.getName());
assertNotNull(getLoansLoanIdResponse);
assertEquals(getLoansLoanIdResponse.getDelinquencyRange().getClassification(), classificationExpected);
}
}
@@ -47,6 +47,7 @@ import java.time.temporal.ChronoField;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -421,4 +422,9 @@ public final class Utils {
}
return stringBuilder.toString();
}
public static String convertToJson(HashMap<String, Object> map) {
return new Gson().toJson(map);
}
}
@@ -132,6 +132,11 @@ public class LoanProductTestBuilder {
private String installmentAmountInMultiplesOf;
public String build(final String chargeId) {
final HashMap<String, Object> map = build(chargeId, null);
return new Gson().toJson(map);
}
public HashMap<String, Object> build(final String chargeId, final Integer delinquencyBucketId) {
final HashMap<String, Object> map = new HashMap<>();
if (chargeId != null) {
@@ -234,7 +239,13 @@ public class LoanProductTestBuilder {
if (installmentAmountInMultiplesOf != null) {
map.put("installmentAmountInMultiplesOf", this.installmentAmountInMultiplesOf);
}
return new Gson().toJson(map);
// Delinquency Bucket
if (delinquencyBucketId != null) {
map.put("delinquencyBucketId", delinquencyBucketId);
}
return map;
}
public LoanProductTestBuilder withInstallmentAmountInMultiplesOf(String installmentAmountInMultiplesOf) {
@@ -38,6 +38,9 @@ import java.util.Map;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.util.JSON;
import org.apache.fineract.integrationtests.common.CommonConstants;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
@@ -73,11 +76,20 @@ public class LoanTransactionHelper {
public static final String DATE_TIME_FORMAT = "dd MMMM yyyy HH:mm";
private static final Gson GSON = new JSON().getGson();
public LoanTransactionHelper(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) {
this.requestSpec = requestSpec;
this.responseSpec = responseSpec;
}
public GetLoanProductsProductIdResponse getLoanProduct(final Integer loanProductId) {
final String GET_LOANPRODUCT_URL = "/fineract-provider/api/v1/loanproducts/" + loanProductId + "?" + Utils.TENANT_IDENTIFIER;
final String response = Utils.performServerGet(this.requestSpec, this.responseSpec, GET_LOANPRODUCT_URL);
log.info("Response {}", response);
return GSON.fromJson(response, GetLoanProductsProductIdResponse.class);
}
public Integer getLoanProductId(final String loanProductJSON) {
return Utils.performServerPost(this.requestSpec, this.responseSpec, CREATE_LOAN_PRODUCT_URL, loanProductJSON, "resourceId");
}
@@ -157,6 +169,13 @@ public class LoanTransactionHelper {
return Utils.performServerGet(requestSpec, responseSpec, URL, param);
}
public GetLoansLoanIdResponse getLoan(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
final Integer loanId) {
final String URL = "/fineract-provider/api/v1/loans/" + loanId + "?associations=all&" + Utils.TENANT_IDENTIFIER;
final String response = Utils.performServerGet(requestSpec, responseSpec, URL);
return GSON.fromJson(response, GetLoansLoanIdResponse.class);
}
public Object getLoanDetailExcludeFutureSchedule(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
final Integer loanID, final String param) {
final String URL = "/fineract-provider/api/v1/loans/" + loanID + "?associations=all&exclude=guarantors,futureSchedule&"