diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index 06dff8f3b..2612ae931 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -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"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java index f6c9b5f33..a18a60f77 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java @@ -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); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java index 021bbafbe..5c02a6a21 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobName.java @@ -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; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/data/LoanDelinquencyTagHistoryData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/data/LoanDelinquencyTagHistoryData.java new file mode 100644 index 000000000..ed85ba3d9 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/data/LoanDelinquencyTagHistoryData.java @@ -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; + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucket.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucket.java index 8f7d4968f..65af80cd3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucket.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyBucket.java @@ -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 ranges; + private List ranges; @Version private Long version; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRangeRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRangeRepository.java index 9333f08ff..da8085600 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRangeRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/DelinquencyRangeRepository.java @@ -25,4 +25,5 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor; public interface DelinquencyRangeRepository extends JpaRepository, JpaSpecificationExecutor { Optional findByClassification(String classification); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanDelinquencyTagHistoryRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanDelinquencyTagHistoryRepository.java index fe13827f2..ef57dc10f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanDelinquencyTagHistoryRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanDelinquencyTagHistoryRepository.java @@ -28,6 +28,8 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor; public interface LoanDelinquencyTagHistoryRepository extends JpaRepository, JpaSpecificationExecutor { + LoanDelinquencyTagHistory findFirstByLoanOrderByAddedOnDateDesc(Loan loan); + List findByLoanOrderByAddedOnDateDesc(Loan loan); Optional findByLoanAndLiftedOnDate(Loan loan, LocalDate liftedOnDate); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/UpdateLoanDelinquencyBucketCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/UpdateLoanDelinquencyBucketCommandHandler.java new file mode 100644 index 000000000..d36eddd2d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/UpdateLoanDelinquencyBucketCommandHandler.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/mapper/LoanDelinquencyTagMapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/mapper/LoanDelinquencyTagMapper.java new file mode 100644 index 000000000..b95ff4e92 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/mapper/LoanDelinquencyTagMapper.java @@ -0,0 +1,39 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.delinquency.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 map(List sources); + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformService.java index f05e06b64..9206c615f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformService.java @@ -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 retrieveDelinquencyRangeHistory(Long loanId); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java index 8f1f64984..a7d61d50a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java @@ -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 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 { - - 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 retrieveDelinquencyRangeHistory(Long loanId) { + final Loan loan = this.loanAssembler.assembleFrom(loanId); + final List loanDelinquencyTagData = this.repositoryLoanDelinquencyTagHistory + .findByLoanOrderByAddedOnDateDesc(loan); + return mapperLoanDelinquencyTagHistory.map(loanDelinquencyTagData); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformService.java index e72bc38f8..77c571c62 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformService.java @@ -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); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java index ed981a5a5..c97d467e2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java @@ -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 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 changes) { Optional 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 ranges) { // Sort the ranges based on the minAgeDays - final Comparator orderByMinAge = new Comparator() { + 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 = 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 lookUpDelinquencyRange(final Loan loan, final DelinquencyBucket delinquencyBucket, long ageDays) { + Map 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 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 setLoanDelinquencyTag(Long loanId, Long delinquencyRangeId) { + final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId); + return setLoanDelinquencyTag(loan, delinquencyRangeId); + } + + public Map setLoanDelinquencyTag(Loan loan, Long delinquencyRangeId) { + Map changes = new HashMap<>(); + List loanDelinquencyTagHistory = new ArrayList<>(); + final LocalDate transactionDate = DateUtils.getBusinessLocalDate(); + Optional 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 sortDelinquencyRangesByMinAge(List ranges) { + final Comparator orderByMinAge = new Comparator() { + + @Override + public int compare(DelinquencyRange o1, DelinquencyRange o2) { + return o1.getMinimumAgeDays().compareTo(o2.getMinimumAgeDays()); + } + }; + Collections.sort(ranges, orderByMinAge); + return ranges; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java index 58e08f18b..f1ba37001 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java @@ -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 loanDataParameters = new HashSet<>(Arrays.asList("id", "accountNo", "status", "externalId", "clientId", @@ -259,63 +264,8 @@ public class LoansApiResource { private final DefaultToApiJsonSerializer 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 toApiJsonSerializer, - final DefaultToApiJsonSerializer loanApprovalDataToApiJsonSerializer, - final DefaultToApiJsonSerializer 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 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 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 = this.delinquencyReadPlatformService + .retrieveDelinquencyRangeHistory(loanId); + ApiRequestJsonSerializationSettings settings = apiRequestParameterHelper.process(uriInfo.getQueryParameters()); + return this.jsonSerializerTagHistory.serialize(settings, loanDelinquencyTagHistoryData); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanScheduleDelinquencyData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanScheduleDelinquencyData.java new file mode 100644 index 000000000..23964fdda --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanScheduleDelinquencyData.java @@ -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; + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallmentRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallmentRepository.java index 32e14f605..b6da7b843 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallmentRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallmentRepository.java @@ -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, JpaSpecificationExecutor { + LoanRepaymentScheduleInstallment findFirstByLoanAndDueDateLessThanEqualAndObligationsMetOnDateOrderByDueDate(Loan loan, + LocalDate businnessDate, LocalDate obligationsMetOnDate); + + List findByLoanAndDueDateLessThanEqualAndObligationsMetOnDateOrderByDueDate(Loan loan, + LocalDate businnessDate, LocalDate obligationsMetOnDate); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/setloandelinquencytags/SetLoanDelinquencyTagsConfig.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/setloandelinquencytags/SetLoanDelinquencyTagsConfig.java new file mode 100644 index 000000000..86ccf2cbb --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/setloandelinquencytags/SetLoanDelinquencyTagsConfig.java @@ -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); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/setloandelinquencytags/SetLoanDelinquencyTagsTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/setloandelinquencytags/SetLoanDelinquencyTagsTasklet.java new file mode 100644 index 000000000..caebad057 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/setloandelinquencytags/SetLoanDelinquencyTagsTasklet.java @@ -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 = 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; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualPlatformServiceImpl.java index f181bd81f..590fbb56b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualPlatformServiceImpl.java @@ -40,7 +40,8 @@ public class LoanAccrualPlatformServiceImpl implements LoanAccrualPlatformServic @Override public void addPeriodicAccruals(final LocalDate tillDate) throws JobExecutionException { - Collection loanScheduleAccrualDataList = this.loanReadPlatformService.retrivePeriodicAccrualData(tillDate); + Collection loanScheduleAccrualDataList = this.loanReadPlatformService + .retrievePeriodicAccrualData(tillDate); addPeriodicAccruals(tillDate, loanScheduleAccrualDataList); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java index 549ffee28..f8402066f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java @@ -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 retriveScheduleAccrualData(); + Collection retrieveScheduleDelinquencyData(LocalDate businessLocalDate); + LoanTransactionData retrieveRecoveryPaymentTemplate(Long loanId); LoanTransactionData retrieveLoanWriteoffTemplate(Long loanId); - Collection retrivePeriodicAccrualData(LocalDate tillDate); + Collection retrievePeriodicAccrualData(LocalDate tillDate); Collection fetchLoansForInterestRecalculation(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index 2b38734a2..4fbb9bb2a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -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 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 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 retriveScheduleAccrualData() { @@ -1673,7 +1690,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService { } @Override - public Collection retrivePeriodicAccrualData(final LocalDate tillDate) { + public Collection 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 { + + 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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRepository.java index 071fa4524..64c93583d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRepository.java @@ -31,4 +31,6 @@ public interface LoanProductRepository extends JpaRepository, List retrieveLoanProductsByChargeId(@Param("chargeId") Long chargeId); Long countByDelinquencyBucket(DelinquencyBucket delinquencyBucket); + + List findByDelinquencyBucketNotNull(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java index 7f6377cb5..7af4e1174 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductWritePlatformServiceJpaRepositoryImpl.java @@ -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 diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 35cccc090..206e4d4bf 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -59,4 +59,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0040_add_delinquency_tags_job.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0040_add_delinquency_tags_job.xml new file mode 100644 index 000000000..ee8ba6754 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0040_add_delinquency_tags_job.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java index d08542194..f7e51cec6 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java @@ -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 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 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); + + } + } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java index 04d33843c..6ba031238 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java @@ -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 map) { + return new Gson().toJson(map); + } + } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java index 3271c04cc..4d9ff06bd 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java @@ -132,6 +132,11 @@ public class LoanProductTestBuilder { private String installmentAmountInMultiplesOf; public String build(final String chargeId) { + final HashMap map = build(chargeId, null); + return new Gson().toJson(map); + } + + public HashMap build(final String chargeId, final Integer delinquencyBucketId) { final HashMap 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) { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index 4b888e464..15c45722d 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -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&"