FINERACT-2322: Extend loan collection data with next due payment amount field
This commit is contained in:
@@ -27,6 +27,14 @@
|
|||||||
"string"
|
"string"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"default": null,
|
||||||
|
"name": "nextPaymentAmount",
|
||||||
|
"type": [
|
||||||
|
"null",
|
||||||
|
"bigdecimal"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"default": null,
|
"default": null,
|
||||||
"name": "delinquentDays",
|
"name": "delinquentDays",
|
||||||
|
|||||||
+58
@@ -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.delinquency.service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public abstract class AbstractPossibleNextRepaymentCalculationService implements PossibleNextRepaymentCalculationService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BigDecimal possibleNextRepaymentAmount(Loan loan, LocalDate nextPaymentDueDate) {
|
||||||
|
LoanRepaymentScheduleInstallment nextInstallment = loan.getRelatedRepaymentScheduleInstallment(nextPaymentDueDate);
|
||||||
|
if (nextInstallment == null || nextInstallment.isObligationsMet()) {
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
if (loan.isInterestRecalculationEnabled()
|
||||||
|
// if rest frequency type is same as repayment, then interest values should be on the repayment schedule
|
||||||
|
// correctly.
|
||||||
|
&& !loan.getLoanInterestRecalculationDetails().getRestFrequencyType().isSameAsRepayment()
|
||||||
|
// if charge off, installments already shows correct values, no further calculation is required.
|
||||||
|
&& !loan.isChargeOffOnDate(nextPaymentDueDate)
|
||||||
|
// all strategy works like same as repayment on installment due date.
|
||||||
|
&& nextInstallment.getDueDate().isAfter(ThreadLocalContextUtil.getBusinessDate())
|
||||||
|
// there is no overdue / overdue related to that installment is calculated.
|
||||||
|
&& !nextInstallment.getFromDate().isEqual(ThreadLocalContextUtil.getBusinessDate())
|
||||||
|
&& MathUtil.isGreaterThanZero(loan.getDisbursedAmount())) {
|
||||||
|
// try to predict future outstanding balances with interest recalculation
|
||||||
|
return calculateInterestRecalculationFutureOutstandingValue(loan, nextPaymentDueDate, nextInstallment);
|
||||||
|
}
|
||||||
|
return nextInstallment.getTotalOutstanding(loan.getCurrency()).getAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract BigDecimal calculateInterestRecalculationFutureOutstandingValue(Loan loan, LocalDate nextPaymentDueDate,
|
||||||
|
LoanRepaymentScheduleInstallment nextInstallment);
|
||||||
|
|
||||||
|
}
|
||||||
+7
@@ -81,6 +81,7 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
|
|||||||
private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper;
|
private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper;
|
||||||
private final ConfigurationDomainService configurationDomainService;
|
private final ConfigurationDomainService configurationDomainService;
|
||||||
private final LoanTransactionRepository loanTransactionRepository;
|
private final LoanTransactionRepository loanTransactionRepository;
|
||||||
|
private final PossibleNextRepaymentCalculationServiceDiscovery possibleNextRepaymentCalculationServiceDiscovery;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<DelinquencyRangeData> retrieveAllDelinquencyRanges() {
|
public List<DelinquencyRangeData> retrieveAllDelinquencyRanges() {
|
||||||
@@ -152,6 +153,12 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
|
|||||||
collectionData = loanDelinquencyDomainService.getOverdueCollectionData(loan, effectiveDelinquencyList);
|
collectionData = loanDelinquencyDomainService.getOverdueCollectionData(loan, effectiveDelinquencyList);
|
||||||
collectionData.setAvailableDisbursementAmount(calculateAvailableDisbursementAmount(loan));
|
collectionData.setAvailableDisbursementAmount(calculateAvailableDisbursementAmount(loan));
|
||||||
collectionData.setNextPaymentDueDate(possibleNextRepaymentDate(nextPaymentDueDateConfig, loan));
|
collectionData.setNextPaymentDueDate(possibleNextRepaymentDate(nextPaymentDueDateConfig, loan));
|
||||||
|
PossibleNextRepaymentCalculationService possibleNextRepaymentCalculationService = possibleNextRepaymentCalculationServiceDiscovery
|
||||||
|
.getService(loan);
|
||||||
|
if (possibleNextRepaymentCalculationService != null) {
|
||||||
|
collectionData.setNextPaymentAmount(
|
||||||
|
possibleNextRepaymentCalculationService.possibleNextRepaymentAmount(loan, collectionData.getNextPaymentDueDate()));
|
||||||
|
}
|
||||||
|
|
||||||
final LoanTransaction lastPayment = loan.getLastPaymentTransaction();
|
final LoanTransaction lastPayment = loan.getLastPaymentTransaction();
|
||||||
if (lastPayment != null) {
|
if (lastPayment != null) {
|
||||||
|
|||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* 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.service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
|
||||||
|
public interface PossibleNextRepaymentCalculationService {
|
||||||
|
|
||||||
|
boolean canAccept(Loan loan);
|
||||||
|
|
||||||
|
BigDecimal possibleNextRepaymentAmount(Loan loan, LocalDate nextPaymentDueDate);
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.portfolio.delinquency.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Service
|
||||||
|
public class PossibleNextRepaymentCalculationServiceDiscovery {
|
||||||
|
|
||||||
|
private final List<PossibleNextRepaymentCalculationService> services;
|
||||||
|
|
||||||
|
public PossibleNextRepaymentCalculationService getService(final Loan loan) {
|
||||||
|
return services.stream().filter(s -> s.canAccept(loan)).findAny().orElse(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-2
@@ -37,6 +37,7 @@ import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatfor
|
|||||||
import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformServiceImpl;
|
import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformServiceImpl;
|
||||||
import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainService;
|
import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainService;
|
||||||
import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainServiceImpl;
|
import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainServiceImpl;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.service.PossibleNextRepaymentCalculationServiceDiscovery;
|
||||||
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyActionParseAndValidator;
|
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyActionParseAndValidator;
|
||||||
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyBucketParseAndValidator;
|
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyBucketParseAndValidator;
|
||||||
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator;
|
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator;
|
||||||
@@ -62,11 +63,12 @@ public class DelinquencyConfiguration {
|
|||||||
LoanInstallmentDelinquencyTagRepository repositoryLoanInstallmentDelinquencyTag,
|
LoanInstallmentDelinquencyTagRepository repositoryLoanInstallmentDelinquencyTag,
|
||||||
LoanDelinquencyActionRepository loanDelinquencyActionRepository,
|
LoanDelinquencyActionRepository loanDelinquencyActionRepository,
|
||||||
DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper, ConfigurationDomainService configurationDomainService,
|
DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper, ConfigurationDomainService configurationDomainService,
|
||||||
LoanTransactionRepository loanTransactionRepository) {
|
LoanTransactionRepository loanTransactionRepository,
|
||||||
|
PossibleNextRepaymentCalculationServiceDiscovery possibleNextRepaymentCalculationService) {
|
||||||
return new DelinquencyReadPlatformServiceImpl(repositoryRange, repositoryBucket, repositoryLoanDelinquencyTagHistory, mapperRange,
|
return new DelinquencyReadPlatformServiceImpl(repositoryRange, repositoryBucket, repositoryLoanDelinquencyTagHistory, mapperRange,
|
||||||
mapperBucket, mapperLoanDelinquencyTagHistory, loanRepository, loanDelinquencyDomainService,
|
mapperBucket, mapperLoanDelinquencyTagHistory, loanRepository, loanDelinquencyDomainService,
|
||||||
repositoryLoanInstallmentDelinquencyTag, loanDelinquencyActionRepository, delinquencyEffectivePauseHelper,
|
repositoryLoanInstallmentDelinquencyTag, loanDelinquencyActionRepository, delinquencyEffectivePauseHelper,
|
||||||
configurationDomainService, loanTransactionRepository);
|
configurationDomainService, loanTransactionRepository, possibleNextRepaymentCalculationService);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
+2
-1
@@ -31,6 +31,7 @@ public final class CollectionData {
|
|||||||
private BigDecimal availableDisbursementAmount;
|
private BigDecimal availableDisbursementAmount;
|
||||||
private Long pastDueDays;
|
private Long pastDueDays;
|
||||||
private LocalDate nextPaymentDueDate;
|
private LocalDate nextPaymentDueDate;
|
||||||
|
private BigDecimal nextPaymentAmount;
|
||||||
private Long delinquentDays;
|
private Long delinquentDays;
|
||||||
private LocalDate delinquentDate;
|
private LocalDate delinquentDate;
|
||||||
private BigDecimal delinquentAmount;
|
private BigDecimal delinquentAmount;
|
||||||
@@ -50,7 +51,7 @@ public final class CollectionData {
|
|||||||
|
|
||||||
public static CollectionData template() {
|
public static CollectionData template() {
|
||||||
final BigDecimal zero = BigDecimal.ZERO;
|
final BigDecimal zero = BigDecimal.ZERO;
|
||||||
return new CollectionData(zero, 0L, null, 0L, null, zero, null, zero, null, zero, null, null, zero, zero, zero, zero);
|
return new CollectionData(zero, 0L, null, zero, 0L, null, zero, null, zero, null, zero, null, null, zero, zero, zero, zero);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1403,7 +1403,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
|||||||
* @return a schedule installment is related to the provided date
|
* @return a schedule installment is related to the provided date
|
||||||
**/
|
**/
|
||||||
public LoanRepaymentScheduleInstallment getRelatedRepaymentScheduleInstallment(LocalDate date) {
|
public LoanRepaymentScheduleInstallment getRelatedRepaymentScheduleInstallment(LocalDate date) {
|
||||||
// TODO first installment should be fromInclusive
|
|
||||||
return getRepaymentScheduleInstallment(e -> DateUtils.isDateInRangeFromExclusiveToInclusive(date, e.getFromDate(), e.getDueDate()));
|
return getRepaymentScheduleInstallment(e -> DateUtils.isDateInRangeFromExclusiveToInclusive(date, e.getFromDate(), e.getDueDate()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
@@ -55,6 +55,11 @@ public class EmbeddableProgressiveLoanScheduleGenerator {
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ProgressiveLoanModel> findOneByLoan(Loan loan) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<ProgressiveLoanInterestScheduleModel> extractModel(Optional<ProgressiveLoanModel> progressiveLoanModel) {
|
public Optional<ProgressiveLoanInterestScheduleModel> extractModel(Optional<ProgressiveLoanModel> progressiveLoanModel) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
|
|||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* 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.service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.ProgressiveLoanModel;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ProgressiveTransactionCtx;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.service.InterestScheduleModelRepositoryWrapper;
|
||||||
|
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
|
||||||
|
import org.apache.fineract.portfolio.loanproduct.calc.data.RepaymentPeriod;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class ProgressivePossibleNextRepaymentCalculationServiceImpl extends AbstractPossibleNextRepaymentCalculationService {
|
||||||
|
|
||||||
|
private final InterestScheduleModelRepositoryWrapper interestScheduleModelRepository;
|
||||||
|
private final AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BigDecimal calculateInterestRecalculationFutureOutstandingValue(Loan loan, LocalDate nextPaymentDueDate,
|
||||||
|
LoanRepaymentScheduleInstallment nextInstallment) {
|
||||||
|
MonetaryCurrency currency = loan.getCurrency();
|
||||||
|
Optional<ProgressiveLoanModel> progressiveLoanModel = interestScheduleModelRepository.findOneByLoan(loan);
|
||||||
|
Optional<ProgressiveLoanInterestScheduleModel> optionalScheduleModel = interestScheduleModelRepository
|
||||||
|
.extractModel(progressiveLoanModel);
|
||||||
|
ProgressiveLoanInterestScheduleModel scheduleModel = optionalScheduleModel.orElseGet(
|
||||||
|
() -> advancedPaymentScheduleTransactionProcessor.calculateInterestScheduleModel(loan.getId(), nextPaymentDueDate));
|
||||||
|
if (scheduleModel == null) {
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments = loan.getRepaymentScheduleInstallments();
|
||||||
|
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(loan.getCurrency(), repaymentScheduleInstallments, Set.of(),
|
||||||
|
new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail(), scheduleModel);
|
||||||
|
ctx.setChargedOff(loan.isChargedOff());
|
||||||
|
ctx.setContractTerminated(loan.isContractTermination());
|
||||||
|
advancedPaymentScheduleTransactionProcessor.recalculateInterestForDate(nextPaymentDueDate, ctx, false);
|
||||||
|
RepaymentPeriod repaymentPeriod = scheduleModel.findRepaymentPeriodByDueDate(nextPaymentDueDate)
|
||||||
|
.orElseGet(scheduleModel::getLastRepaymentPeriod);
|
||||||
|
|
||||||
|
return repaymentPeriod.getOutstandingPrincipal().add(repaymentPeriod.getOutstandingInterest())
|
||||||
|
.add(nextInstallment.getFeeChargesOutstanding(currency)).add(nextInstallment.getPenaltyChargesOutstanding(currency))
|
||||||
|
.getAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canAccept(Loan loan) {
|
||||||
|
return loan.isProgressiveSchedule();
|
||||||
|
}
|
||||||
|
}
|
||||||
+64
-71
@@ -1498,104 +1498,97 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
super.handleWriteOff(transaction, ctx.getCurrency(), ctx.getInstallments());
|
super.handleWriteOff(transaction, ctx.getCurrency(), ctx.getInstallments());
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<LoanRepaymentScheduleInstallment> findOverdueInstallmentsBeforeDateSortedByInstallmentNumber(LocalDate targetDate,
|
private List<RepaymentPeriod> findPossiblyOverdueRepaymentPeriods(LocalDate targetDate, ProgressiveLoanInterestScheduleModel model) {
|
||||||
ProgressiveTransactionCtx transactionCtx) {
|
return model.repaymentPeriods().stream() //
|
||||||
return transactionCtx.getInstallments().stream() //
|
.filter(repaymentPeriod -> DateUtils.isAfter(targetDate, repaymentPeriod.getDueDate())).toList();
|
||||||
.filter(installment -> !installment.isDownPayment() && !installment.isAdditional())
|
}
|
||||||
.filter(installment -> installment.isOverdueOn(targetDate))
|
|
||||||
.sorted(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).toList();
|
public boolean rework(LocalDate targetDate, ProgressiveTransactionCtx ctx) {
|
||||||
|
boolean hasChange = false;
|
||||||
|
ProgressiveLoanInterestScheduleModel model = ctx.getModel();
|
||||||
|
List<RepaymentPeriod> overdueInstallmentsSortedByInstallmentNumber = findPossiblyOverdueRepaymentPeriods(targetDate, model);
|
||||||
|
if (!overdueInstallmentsSortedByInstallmentNumber.isEmpty()) {
|
||||||
|
RepaymentPeriod lastPeriod = model.getLastRepaymentPeriod();
|
||||||
|
RepaymentPeriod currentPeriod = model.findRepaymentPeriod(targetDate).orElse(lastPeriod);
|
||||||
|
MonetaryCurrency currency = model.zero().getCurrency();
|
||||||
|
Money overDuePrincipal = Money.zero(currency);
|
||||||
|
Money aggregatedOverDuePrincipal = Money.zero(currency);
|
||||||
|
for (RepaymentPeriod processingPeriod : overdueInstallmentsSortedByInstallmentNumber) {
|
||||||
|
// add and subtract outstanding principal
|
||||||
|
if (!overDuePrincipal.isZero()) {
|
||||||
|
boolean currentChanges = adjustOverduePrincipal(targetDate, processingPeriod, overDuePrincipal,
|
||||||
|
aggregatedOverDuePrincipal, ctx);
|
||||||
|
|
||||||
|
hasChange = hasChange || currentChanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
overDuePrincipal = processingPeriod.getOutstandingPrincipal();
|
||||||
|
aggregatedOverDuePrincipal = aggregatedOverDuePrincipal.add(overDuePrincipal);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentPeriod.equals(lastPeriod) || !targetDate.isAfter(lastPeriod.getDueDate())) {
|
||||||
|
boolean currentChanges = adjustOverduePrincipal(targetDate, currentPeriod, overDuePrincipal, aggregatedOverDuePrincipal,
|
||||||
|
ctx);
|
||||||
|
hasChange = hasChange || currentChanges;
|
||||||
|
|
||||||
|
}
|
||||||
|
if (aggregatedOverDuePrincipal.isGreaterThanZero()
|
||||||
|
&& (model.lastOverdueBalanceChange() == null || model.lastOverdueBalanceChange().isBefore(targetDate))) {
|
||||||
|
model.lastOverdueBalanceChange(targetDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void recalculateInterestForDate(LocalDate targetDate, ProgressiveTransactionCtx ctx) {
|
public void recalculateInterestForDate(LocalDate targetDate, ProgressiveTransactionCtx ctx) {
|
||||||
|
recalculateInterestForDate(targetDate, ctx, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recalculateInterestForDate(LocalDate targetDate, ProgressiveTransactionCtx ctx, boolean updateInstallments) {
|
||||||
if (ctx.getInstallments() != null && !ctx.getInstallments().isEmpty()) {
|
if (ctx.getInstallments() != null && !ctx.getInstallments().isEmpty()) {
|
||||||
Loan loan = ctx.getInstallments().getFirst().getLoan();
|
Loan loan = ctx.getInstallments().getFirst().getLoan();
|
||||||
if (loan.isInterestBearingAndInterestRecalculationEnabled() && !loan.isNpa() && !ctx.isChargedOff()
|
if (loan.isInterestBearingAndInterestRecalculationEnabled() && !loan.isNpa() && !ctx.isChargedOff()
|
||||||
&& !ctx.isContractTerminated()) {
|
&& !ctx.isContractTerminated() && !loan.getLoanInterestRecalculationDetails().disallowInterestCalculationOnPastDue()) {
|
||||||
|
|
||||||
List<LoanRepaymentScheduleInstallment> overdueInstallmentsSortedByInstallmentNumber = findOverdueInstallmentsBeforeDateSortedByInstallmentNumber(
|
boolean modelHasUpdates = rework(targetDate, ctx);
|
||||||
targetDate, ctx);
|
if (modelHasUpdates && updateInstallments) {
|
||||||
if (!overdueInstallmentsSortedByInstallmentNumber.isEmpty()) {
|
updateInstallmentsPrincipalAndInterestByModel(ctx);
|
||||||
List<LoanRepaymentScheduleInstallment> normalInstallments = ctx.getInstallments().stream() //
|
|
||||||
.filter(installment -> !installment.isAdditional() && !installment.isDownPayment()).toList();
|
|
||||||
|
|
||||||
Optional<LoanRepaymentScheduleInstallment> currentInstallmentOptional = normalInstallments.stream().filter(
|
|
||||||
installment -> installment.getFromDate().isBefore(targetDate) && !installment.getDueDate().isBefore(targetDate))
|
|
||||||
.findAny();
|
|
||||||
|
|
||||||
// get DUE installment or last installment
|
|
||||||
LoanRepaymentScheduleInstallment lastInstallment = normalInstallments.stream()
|
|
||||||
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).get();
|
|
||||||
LoanRepaymentScheduleInstallment currentInstallment = currentInstallmentOptional.orElse(lastInstallment);
|
|
||||||
|
|
||||||
Money overDuePrincipal = Money.zero(ctx.getCurrency());
|
|
||||||
Money aggregatedOverDuePrincipal = Money.zero(ctx.getCurrency());
|
|
||||||
for (LoanRepaymentScheduleInstallment processingInstallment : overdueInstallmentsSortedByInstallmentNumber) {
|
|
||||||
// add and subtract outstanding principal
|
|
||||||
if (!overDuePrincipal.isZero()) {
|
|
||||||
adjustOverduePrincipalForInstallment(targetDate, processingInstallment, overDuePrincipal,
|
|
||||||
aggregatedOverDuePrincipal, ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
overDuePrincipal = processingInstallment.getPrincipalOutstanding(ctx.getCurrency());
|
|
||||||
aggregatedOverDuePrincipal = aggregatedOverDuePrincipal.add(overDuePrincipal);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean adjustNeeded = !currentInstallment.equals(lastInstallment) || !lastInstallment.isOverdueOn(targetDate);
|
|
||||||
if (adjustNeeded) {
|
|
||||||
adjustOverduePrincipalForInstallment(targetDate, currentInstallment, overDuePrincipal, aggregatedOverDuePrincipal,
|
|
||||||
ctx);
|
|
||||||
}
|
|
||||||
if (aggregatedOverDuePrincipal.isGreaterThanZero() && (ctx.getModel().lastOverdueBalanceChange() == null
|
|
||||||
|| ctx.getModel().lastOverdueBalanceChange().isBefore(targetDate))) {
|
|
||||||
ctx.getModel().lastOverdueBalanceChange(targetDate);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void adjustOverduePrincipalForInstallment(LocalDate currentDate, LoanRepaymentScheduleInstallment currentInstallment,
|
private boolean adjustOverduePrincipal(LocalDate currentDate, RepaymentPeriod currentInstallment, Money overduePrincipal,
|
||||||
Money overduePrincipal, Money aggregatedOverDuePrincipal, ProgressiveTransactionCtx ctx) {
|
Money aggregatedOverDuePrincipal, ProgressiveTransactionCtx ctx) {
|
||||||
if (currentInstallment.getLoan().getLoanInterestRecalculationDetails().disallowInterestCalculationOnPastDue()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalDate fromDate = currentInstallment.getFromDate();
|
LocalDate fromDate = currentInstallment.getFromDate();
|
||||||
LocalDate toDate = currentInstallment.getDueDate();
|
LocalDate toDate = currentInstallment.getDueDate();
|
||||||
boolean hasUpdate = false;
|
ProgressiveLoanInterestScheduleModel model = ctx.getModel();
|
||||||
|
boolean prepayAttempt = ctx.isPrepayAttempt();
|
||||||
|
LoanInterestRecalculationDetails loanInterestRecalculationDetails = ctx.getInstallments().getFirst().getLoan()
|
||||||
|
.getLoanInterestRecalculationDetails();
|
||||||
|
|
||||||
if (!currentDate.equals(ctx.getModel().lastOverdueBalanceChange())) {
|
if (!currentDate.equals(model.lastOverdueBalanceChange())) {
|
||||||
// if we have same date for fromDate & last overdue balance change then it means we have the up-to-date
|
if (model.lastOverdueBalanceChange() == null || currentInstallment.getFromDate().isAfter(model.lastOverdueBalanceChange())) {
|
||||||
// model.
|
emiCalculator.addBalanceCorrection(model, fromDate, overduePrincipal);
|
||||||
if (ctx.getModel().lastOverdueBalanceChange() == null
|
|
||||||
|| currentInstallment.getFromDate().isAfter(ctx.getModel().lastOverdueBalanceChange())) {
|
|
||||||
// first overdue hit for installment. setting overdue balance correction from instalment from date.
|
|
||||||
emiCalculator.addBalanceCorrection(ctx.getModel(), fromDate, overduePrincipal);
|
|
||||||
} else {
|
} else {
|
||||||
// not the first balance correction on installment period, then setting overdue balance correction from
|
emiCalculator.addBalanceCorrection(model, model.lastOverdueBalanceChange(), overduePrincipal);
|
||||||
// last balance change's current date. previous interest period already has the correct balance
|
|
||||||
// correction.
|
|
||||||
emiCalculator.addBalanceCorrection(ctx.getModel(), ctx.getModel().lastOverdueBalanceChange(), overduePrincipal);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hasUpdate = true;
|
|
||||||
|
|
||||||
if (currentDate.isAfter(fromDate) && !currentDate.isAfter(toDate)) {
|
if (currentDate.isAfter(fromDate) && !currentDate.isAfter(toDate)) {
|
||||||
LocalDate lastOverdueBalanceChange;
|
LocalDate lastOverdueBalanceChange;
|
||||||
if (shouldRecalculateTillInstallmentDueDate(currentInstallment.getLoan().getLoanInterestRecalculationDetails(),
|
if (shouldRecalculateTillInstallmentDueDate(loanInterestRecalculationDetails, prepayAttempt)) {
|
||||||
ctx.isPrepayAttempt())) {
|
|
||||||
lastOverdueBalanceChange = toDate;
|
lastOverdueBalanceChange = toDate;
|
||||||
} else {
|
} else {
|
||||||
lastOverdueBalanceChange = currentDate;
|
lastOverdueBalanceChange = currentDate;
|
||||||
}
|
}
|
||||||
emiCalculator.addBalanceCorrection(ctx.getModel(), lastOverdueBalanceChange, aggregatedOverDuePrincipal.negated());
|
emiCalculator.addBalanceCorrection(model, lastOverdueBalanceChange, aggregatedOverDuePrincipal.negated());
|
||||||
ctx.getModel().lastOverdueBalanceChange(lastOverdueBalanceChange);
|
model.lastOverdueBalanceChange(lastOverdueBalanceChange);
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
if (hasUpdate) {
|
|
||||||
updateInstallmentsPrincipalAndInterestByModel(ctx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateInstallmentsPrincipalAndInterestByModel(ProgressiveTransactionCtx ctx) {
|
private void updateInstallmentsPrincipalAndInterestByModel(ProgressiveTransactionCtx ctx) {
|
||||||
|
|||||||
+3
@@ -19,6 +19,7 @@
|
|||||||
package org.apache.fineract.portfolio.loanaccount.repository;
|
package org.apache.fineract.portfolio.loanaccount.repository;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.ProgressiveLoanModel;
|
import org.apache.fineract.portfolio.loanaccount.domain.ProgressiveLoanModel;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
@@ -27,4 +28,6 @@ public interface ProgressiveLoanModelRepository
|
|||||||
extends JpaSpecificationExecutor<ProgressiveLoanModel>, JpaRepository<ProgressiveLoanModel, Long> {
|
extends JpaSpecificationExecutor<ProgressiveLoanModel>, JpaRepository<ProgressiveLoanModel, Long> {
|
||||||
|
|
||||||
Optional<ProgressiveLoanModel> findOneByLoanId(Long loanId);
|
Optional<ProgressiveLoanModel> findOneByLoanId(Long loanId);
|
||||||
|
|
||||||
|
Optional<ProgressiveLoanModel> findOneByLoan(Loan loan);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -29,6 +29,8 @@ public interface InterestScheduleModelRepositoryWrapper {
|
|||||||
|
|
||||||
Optional<ProgressiveLoanModel> findOneByLoanId(Long loanId);
|
Optional<ProgressiveLoanModel> findOneByLoanId(Long loanId);
|
||||||
|
|
||||||
|
Optional<ProgressiveLoanModel> findOneByLoan(Loan loan);
|
||||||
|
|
||||||
Optional<ProgressiveLoanInterestScheduleModel> extractModel(Optional<ProgressiveLoanModel> progressiveLoanModel);
|
Optional<ProgressiveLoanInterestScheduleModel> extractModel(Optional<ProgressiveLoanModel> progressiveLoanModel);
|
||||||
|
|
||||||
ProgressiveLoanInterestScheduleModel writeInterestScheduleModel(Loan loan, ProgressiveLoanInterestScheduleModel model);
|
ProgressiveLoanInterestScheduleModel writeInterestScheduleModel(Loan loan, ProgressiveLoanInterestScheduleModel model);
|
||||||
|
|||||||
+10
@@ -23,6 +23,7 @@ import jakarta.transaction.Transactional;
|
|||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.infrastructure.core.persistence.FlushModeHandler;
|
import org.apache.fineract.infrastructure.core.persistence.FlushModeHandler;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
@@ -78,6 +79,15 @@ public class InterestScheduleModelRepositoryWrapperImpl implements InterestSched
|
|||||||
return progressiveLoanModel[0];
|
return progressiveLoanModel[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ProgressiveLoanModel> findOneByLoan(Loan loan) {
|
||||||
|
AtomicReference<Optional<ProgressiveLoanModel>> progressiveLoanModelRef = new AtomicReference<>();
|
||||||
|
flushModeHandler.withFlushMode(FlushModeType.COMMIT, () -> {
|
||||||
|
progressiveLoanModelRef.set(loanModelRepository.findOneByLoan(loan));
|
||||||
|
});
|
||||||
|
return progressiveLoanModelRef.get();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<ProgressiveLoanInterestScheduleModel> extractModel(Optional<ProgressiveLoanModel> progressiveLoanModel) {
|
public Optional<ProgressiveLoanInterestScheduleModel> extractModel(Optional<ProgressiveLoanModel> progressiveLoanModel) {
|
||||||
return progressiveLoanModel.map(ProgressiveLoanModel::getJsonModel) //
|
return progressiveLoanModel.map(ProgressiveLoanModel::getJsonModel) //
|
||||||
|
|||||||
+9
@@ -381,6 +381,15 @@ public final class RepaymentPeriod {
|
|||||||
return previous == null;
|
return previous == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gives back getDueInterest minus paid interest
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public Money getOutstandingInterest() {
|
||||||
|
return MathUtil.negativeToZero(getDueInterest().minus(getPaidInterest()), mc);
|
||||||
|
}
|
||||||
|
|
||||||
public Money getOutstandingPrincipal() {
|
public Money getOutstandingPrincipal() {
|
||||||
return MathUtil.negativeToZero(getDuePrincipal().minus(getPaidPrincipal()), mc);
|
return MathUtil.negativeToZero(getDuePrincipal().minus(getPaidPrincipal()), mc);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -61,6 +61,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
|||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalculationDetails;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
|
||||||
@@ -467,6 +468,9 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
|||||||
when(loanTransaction.getLoan()).thenReturn(loan);
|
when(loanTransaction.getLoan()).thenReturn(loan);
|
||||||
when(loan.getCurrency()).thenReturn(currency);
|
when(loan.getCurrency()).thenReturn(currency);
|
||||||
when(loan.getPaymentAllocationRules()).thenReturn(List.of(loanPaymentAllocationRule));
|
when(loan.getPaymentAllocationRules()).thenReturn(List.of(loanPaymentAllocationRule));
|
||||||
|
LoanInterestRecalculationDetails loanInterestRecalculationDetails = mock(LoanInterestRecalculationDetails.class);
|
||||||
|
when(loanInterestRecalculationDetails.disallowInterestCalculationOnPastDue()).thenReturn(false);
|
||||||
|
when(loan.getLoanInterestRecalculationDetails()).thenReturn(loanInterestRecalculationDetails);
|
||||||
|
|
||||||
when(loanPaymentAllocationRule.getTransactionType()).thenReturn(PaymentAllocationTransactionType.DEFAULT);
|
when(loanPaymentAllocationRule.getTransactionType()).thenReturn(PaymentAllocationTransactionType.DEFAULT);
|
||||||
when(loanPaymentAllocationRule.getAllocationTypes())
|
when(loanPaymentAllocationRule.getAllocationTypes())
|
||||||
|
|||||||
+2
@@ -1022,6 +1022,8 @@ final class LoansApiResourceSwagger {
|
|||||||
public Integer pastDueDays;
|
public Integer pastDueDays;
|
||||||
@Schema(example = "[2022, 07, 01]")
|
@Schema(example = "[2022, 07, 01]")
|
||||||
public LocalDate nextPaymentDueDate;
|
public LocalDate nextPaymentDueDate;
|
||||||
|
@Schema(example = "123.23")
|
||||||
|
public BigDecimal nextPaymentAmount;
|
||||||
@Schema(example = "4")
|
@Schema(example = "4")
|
||||||
public Integer delinquentDays;
|
public Integer delinquentDays;
|
||||||
@Schema(example = "[2022, 07, 01]")
|
@Schema(example = "[2022, 07, 01]")
|
||||||
|
|||||||
+3
-1
@@ -82,6 +82,7 @@ import org.apache.fineract.portfolio.delinquency.mapper.LoanDelinquencyTagMapper
|
|||||||
import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService;
|
import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformServiceImpl;
|
import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformServiceImpl;
|
||||||
import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainService;
|
import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainService;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.service.PossibleNextRepaymentCalculationServiceDiscovery;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
|
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData;
|
import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
@@ -364,7 +365,8 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
|
|||||||
DelinquencyReadPlatformService delinquencyReadPlatformService = new DelinquencyReadPlatformServiceImpl(repositoryRange,
|
DelinquencyReadPlatformService delinquencyReadPlatformService = new DelinquencyReadPlatformServiceImpl(repositoryRange,
|
||||||
repositoryBucket, repositoryLoanDelinquencyTagHistory, mapperRange, mapperBucket, mapperLoanDelinquencyTagHistory,
|
repositoryBucket, repositoryLoanDelinquencyTagHistory, mapperRange, mapperBucket, mapperLoanDelinquencyTagHistory,
|
||||||
loanRepository, loanDelinquencyDomainService, repositoryLoanInstallmentDelinquencyTag, loanDelinquencyActionRepository,
|
loanRepository, loanDelinquencyDomainService, repositoryLoanInstallmentDelinquencyTag, loanDelinquencyActionRepository,
|
||||||
delinquencyEffectivePauseHelper, configurationDomainService, Mockito.mock(LoanTransactionRepository.class));
|
delinquencyEffectivePauseHelper, configurationDomainService, Mockito.mock(LoanTransactionRepository.class),
|
||||||
|
Mockito.mock(PossibleNextRepaymentCalculationServiceDiscovery.class));
|
||||||
|
|
||||||
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
|
||||||
when(loanProduct.isMultiDisburseLoan()).thenReturn(false);
|
when(loanProduct.isMultiDisburseLoan()).thenReturn(false);
|
||||||
|
|||||||
+16
-16
@@ -186,8 +186,8 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
|||||||
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
||||||
loanForProcessing);
|
loanForProcessing);
|
||||||
final BigDecimal zero = BigDecimal.ZERO;
|
final BigDecimal zero = BigDecimal.ZERO;
|
||||||
CollectionData collectionData = new CollectionData(zero, 2L, null, 2L, overDueSinceDate, zero, null, null, null, null, null, null,
|
CollectionData collectionData = new CollectionData(zero, 2L, null, zero, 2L, overDueSinceDate, zero, null, null, null, null, null,
|
||||||
zero, zero, zero, zero);
|
null, zero, zero, zero, zero);
|
||||||
|
|
||||||
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
||||||
|
|
||||||
@@ -240,10 +240,10 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
|||||||
LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(2);
|
LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(2);
|
||||||
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
||||||
loanForProcessing);
|
loanForProcessing);
|
||||||
CollectionData collectionData = new CollectionData(zeroAmount, 2L, null, 2L, overDueSinceDate, zeroAmount, null, null, null, null,
|
CollectionData collectionData = new CollectionData(zeroAmount, 2L, null, zeroAmount, 2L, overDueSinceDate, zeroAmount, null, null,
|
||||||
null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
||||||
|
|
||||||
CollectionData installmentCollectionData = new CollectionData(zeroAmount, 2L, null, 2L, overDueSinceDate,
|
CollectionData installmentCollectionData = new CollectionData(zeroAmount, 2L, null, zeroAmount, 2L, overDueSinceDate,
|
||||||
installmentPrincipalAmount, null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
installmentPrincipalAmount, null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
||||||
|
|
||||||
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
||||||
@@ -369,10 +369,10 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
|||||||
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
||||||
loanForProcessing);
|
loanForProcessing);
|
||||||
|
|
||||||
CollectionData collectionData = new CollectionData(zeroAmount, 2L, null, 2L, overDueSinceDate, zeroAmount, null, null, null, null,
|
CollectionData collectionData = new CollectionData(zeroAmount, 2L, null, zeroAmount, 2L, overDueSinceDate, zeroAmount, null, null,
|
||||||
null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
||||||
|
|
||||||
CollectionData installmentCollectionData = new CollectionData(zeroAmount, 2L, null, 2L, overDueSinceDate,
|
CollectionData installmentCollectionData = new CollectionData(zeroAmount, 2L, null, zeroAmount, 2L, overDueSinceDate,
|
||||||
installmentPrincipalAmount, null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
installmentPrincipalAmount, null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
||||||
|
|
||||||
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
||||||
@@ -446,10 +446,10 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
|||||||
LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(29);
|
LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(29);
|
||||||
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
||||||
loanForProcessing);
|
loanForProcessing);
|
||||||
CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 29L, null, 29L, overDueSinceDate, zeroAmount, null, null, null,
|
CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 29L, null, zeroAmount, 29L, overDueSinceDate, zeroAmount, null,
|
||||||
null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
||||||
|
|
||||||
CollectionData installmentCollectionData = new CollectionData(zeroAmount, 29L, null, 29L, overDueSinceDate,
|
CollectionData installmentCollectionData = new CollectionData(zeroAmount, 29L, null, zeroAmount, 29L, overDueSinceDate,
|
||||||
installmentPrincipalAmount, null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
installmentPrincipalAmount, null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
||||||
|
|
||||||
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
||||||
@@ -534,14 +534,14 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
|||||||
LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(29);
|
LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(29);
|
||||||
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
||||||
loanForProcessing);
|
loanForProcessing);
|
||||||
CollectionData collectionData = new CollectionData(zeroAmount, 29L, null, 29L, overDueSinceDate, zeroAmount, null, null, null, null,
|
CollectionData collectionData = new CollectionData(zeroAmount, 29L, null, zeroAmount, 29L, overDueSinceDate, zeroAmount, null, null,
|
||||||
null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
||||||
|
|
||||||
CollectionData installmentCollectionData_1 = new CollectionData(zeroAmount, 29L, null, 29L, overDueSinceDate,
|
CollectionData installmentCollectionData_1 = new CollectionData(zeroAmount, 29L, null, zeroAmount, 29L, overDueSinceDate,
|
||||||
installmentPrincipalAmount, null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
installmentPrincipalAmount, null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
||||||
|
|
||||||
CollectionData installmentCollectionData_2 = new CollectionData(zeroAmount, 0L, null, 0L, null, installmentPrincipalAmount, null,
|
CollectionData installmentCollectionData_2 = new CollectionData(zeroAmount, 0L, null, zeroAmount, 0L, null,
|
||||||
null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
installmentPrincipalAmount, null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
||||||
|
|
||||||
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
||||||
installmentsCollection.put(1L, installmentCollectionData_1);
|
installmentsCollection.put(1L, installmentCollectionData_1);
|
||||||
|
|||||||
+131
@@ -20,6 +20,7 @@ package org.apache.fineract.integrationtests;
|
|||||||
|
|
||||||
import static java.lang.System.lineSeparator;
|
import static java.lang.System.lineSeparator;
|
||||||
import static org.apache.fineract.integrationtests.BaseLoanIntegrationTest.TransactionProcessingStrategyCode.ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
|
import static org.apache.fineract.integrationtests.BaseLoanIntegrationTest.TransactionProcessingStrategyCode.ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
|
||||||
|
import static org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY;
|
||||||
import static org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY;
|
import static org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||||
@@ -318,6 +319,107 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
|||||||
return createOnePeriod30DaysPeriodicAccrualProduct((double) 0);
|
return createOnePeriod30DaysPeriodicAccrualProduct((double) 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected PostLoanProductsRequest create4ICumulative() {
|
||||||
|
final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec);
|
||||||
|
Assertions.assertNotNull(delinquencyBucketId);
|
||||||
|
|
||||||
|
return new PostLoanProductsRequest().name(Utils.uniqueRandomStringGenerator("4I_PROGRESSIVE_", 6))//
|
||||||
|
.shortName(Utils.uniqueRandomStringGenerator("", 4))//
|
||||||
|
.description("4 installment product - progressive")//
|
||||||
|
.includeInBorrowerCycle(false)//
|
||||||
|
.useBorrowerCycle(false)//
|
||||||
|
.currencyCode("EUR")//
|
||||||
|
.digitsAfterDecimal(2)//
|
||||||
|
.principal(1000.0)//
|
||||||
|
.minPrincipal(100.0)//
|
||||||
|
.maxPrincipal(10000.0)//
|
||||||
|
.numberOfRepayments(4)//
|
||||||
|
.repaymentEvery(1)//
|
||||||
|
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS_L)//
|
||||||
|
.interestRatePerPeriod(10D)//
|
||||||
|
.minInterestRatePerPeriod(0D)//
|
||||||
|
.maxInterestRatePerPeriod(120D)//
|
||||||
|
.interestRateFrequencyType(InterestRateFrequencyType.YEARS)//
|
||||||
|
.isLinkedToFloatingInterestRates(false)//
|
||||||
|
.isLinkedToFloatingInterestRates(false)//
|
||||||
|
.allowVariableInstallments(false)//
|
||||||
|
.amortizationType(AmortizationType.EQUAL_INSTALLMENTS)//
|
||||||
|
.interestType(InterestType.DECLINING_BALANCE)//
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
|
||||||
|
.allowPartialPeriodInterestCalcualtion(false)//
|
||||||
|
.creditAllocation(List.of())//
|
||||||
|
.overdueDaysForNPA(179)//
|
||||||
|
.daysInMonthType(30)//
|
||||||
|
.daysInYearType(360)//
|
||||||
|
.isInterestRecalculationEnabled(true)//
|
||||||
|
.interestRecalculationCompoundingMethod(0)//
|
||||||
|
.rescheduleStrategyMethod(RescheduleStrategyMethod.RESCHEDULE_NEXT_REPAYMENTS)//
|
||||||
|
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)//
|
||||||
|
.recalculationRestFrequencyInterval(1)//
|
||||||
|
.isArrearsBasedOnOriginalSchedule(false)//
|
||||||
|
.isCompoundingToBePostedAsTransaction(false)//
|
||||||
|
.preClosureInterestCalculationStrategy(1)//
|
||||||
|
.allowCompoundingOnEod(false)//
|
||||||
|
.canDefineInstallmentAmount(true)//
|
||||||
|
.repaymentStartDateType(1)//
|
||||||
|
.charges(List.of())//
|
||||||
|
.principalVariationsForBorrowerCycle(List.of())//
|
||||||
|
.interestRateVariationsForBorrowerCycle(List.of())//
|
||||||
|
.numberOfRepaymentVariationsForBorrowerCycle(List.of())//
|
||||||
|
.accountingRule(3)//
|
||||||
|
.canUseForTopup(false)//
|
||||||
|
.fundSourceAccountId(fundSource.getAccountID().longValue())//
|
||||||
|
.loanPortfolioAccountId(loansReceivableAccount.getAccountID().longValue())//
|
||||||
|
.transfersInSuspenseAccountId(suspenseAccount.getAccountID().longValue())//
|
||||||
|
.interestOnLoanAccountId(interestIncomeAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromFeeAccountId(feeIncomeAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromPenaltyAccountId(penaltyIncomeAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromRecoveryAccountId(recoveriesAccount.getAccountID().longValue())//
|
||||||
|
.writeOffAccountId(writtenOffAccount.getAccountID().longValue())//
|
||||||
|
.overpaymentLiabilityAccountId(overpaymentAccount.getAccountID().longValue())//
|
||||||
|
.receivableInterestAccountId(interestReceivableAccount.getAccountID().longValue())//
|
||||||
|
.receivableFeeAccountId(feeReceivableAccount.getAccountID().longValue())//
|
||||||
|
.receivablePenaltyAccountId(penaltyReceivableAccount.getAccountID().longValue())//
|
||||||
|
.goodwillCreditAccountId(goodwillExpenseAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromGoodwillCreditInterestAccountId(interestIncomeChargeOffAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromGoodwillCreditFeesAccountId(feeChargeOffAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromGoodwillCreditPenaltyAccountId(feeChargeOffAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromChargeOffInterestAccountId(interestIncomeChargeOffAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromChargeOffFeesAccountId(feeChargeOffAccount.getAccountID().longValue())//
|
||||||
|
.incomeFromChargeOffPenaltyAccountId(penaltyChargeOffAccount.getAccountID().longValue())//
|
||||||
|
.chargeOffExpenseAccountId(chargeOffExpenseAccount.getAccountID().longValue())//
|
||||||
|
.chargeOffFraudExpenseAccountId(chargeOffFraudExpenseAccount.getAccountID().longValue())//
|
||||||
|
.dateFormat(DATETIME_PATTERN)//
|
||||||
|
.locale("en")//
|
||||||
|
.enableAccrualActivityPosting(false)//
|
||||||
|
.multiDisburseLoan(true)//
|
||||||
|
.maxTrancheCount(10)//
|
||||||
|
.outstandingLoanBalance(10000.0)//
|
||||||
|
.disallowExpectedDisbursements(true)//
|
||||||
|
.allowApprovedDisbursedAmountsOverApplied(true)//
|
||||||
|
.overAppliedCalculationType("percentage")//
|
||||||
|
.overAppliedNumber(50)//
|
||||||
|
.principalThresholdForLastInstallment(50)//
|
||||||
|
.holdGuaranteeFunds(false)//
|
||||||
|
.accountMovesOutOfNPAOnlyOnArrearsCompletion(false)//
|
||||||
|
.allowAttributeOverrides(new AllowAttributeOverrides()//
|
||||||
|
.amortizationType(true)//
|
||||||
|
.interestType(true)//
|
||||||
|
.transactionProcessingStrategyCode(true)//
|
||||||
|
.interestCalculationPeriodType(true)//
|
||||||
|
.inArrearsTolerance(true)//
|
||||||
|
.repaymentEvery(true)//
|
||||||
|
.graceOnPrincipalAndInterestPayment(true)//
|
||||||
|
.graceOnArrearsAgeing(true)//
|
||||||
|
).isEqualAmortization(false)//
|
||||||
|
.delinquencyBucketId(delinquencyBucketId.longValue())//
|
||||||
|
.enableDownPayment(false)//
|
||||||
|
.enableInstallmentLevelDelinquency(false)//
|
||||||
|
.transactionProcessingStrategyCode(
|
||||||
|
LoanProductTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY)//
|
||||||
|
.loanScheduleType(LoanScheduleType.CUMULATIVE.toString());//
|
||||||
|
}
|
||||||
|
|
||||||
protected PostLoanProductsRequest create4IProgressive() {
|
protected PostLoanProductsRequest create4IProgressive() {
|
||||||
final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec);
|
final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec);
|
||||||
Assertions.assertNotNull(delinquencyBucketId);
|
Assertions.assertNotNull(delinquencyBucketId);
|
||||||
@@ -1076,6 +1178,23 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
|||||||
return postLoansRequest;
|
return postLoansRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected PostLoansRequest applyCumulativeLoanRequest(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount,
|
||||||
|
Double interestRate, int numberOfRepayments, Consumer<PostLoansRequest> customizer) {
|
||||||
|
|
||||||
|
PostLoansRequest postLoansRequest = new PostLoansRequest().clientId(clientId)
|
||||||
|
.transactionProcessingStrategyCode(DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY)
|
||||||
|
.productId(loanProductId).expectedDisbursementDate(loanDisbursementDate).dateFormat(DATETIME_PATTERN).locale("en")
|
||||||
|
.submittedOnDate(loanDisbursementDate).amortizationType(1).interestRatePerPeriod(BigDecimal.valueOf(interestRate))
|
||||||
|
.numberOfRepayments(numberOfRepayments).principal(BigDecimal.valueOf(amount)).loanTermFrequency(numberOfRepayments)
|
||||||
|
.repaymentEvery(1).repaymentFrequencyType(RepaymentFrequencyType.MONTHS)
|
||||||
|
.loanTermFrequencyType(RepaymentFrequencyType.MONTHS).interestType(InterestType.DECLINING_BALANCE)
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY).loanType("individual");
|
||||||
|
if (customizer != null) {
|
||||||
|
customizer.accept(postLoansRequest);
|
||||||
|
}
|
||||||
|
return postLoansRequest;
|
||||||
|
}
|
||||||
|
|
||||||
protected PostLoansRequest applyLP2ProgressiveLoanRequest(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount,
|
protected PostLoansRequest applyLP2ProgressiveLoanRequest(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount,
|
||||||
Double interestRate, int numberOfRepayments, Consumer<PostLoansRequest> customizer) {
|
Double interestRate, int numberOfRepayments, Consumer<PostLoansRequest> customizer) {
|
||||||
|
|
||||||
@@ -1119,6 +1238,17 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
|||||||
return approvedLoanResult.getLoanId();
|
return approvedLoanResult.getLoanId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Long applyAndApproveCumulativeLoan(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount,
|
||||||
|
Double interestRate, int numberOfRepayments, Consumer<PostLoansRequest> customizer) {
|
||||||
|
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyCumulativeLoanRequest(clientId, loanProductId,
|
||||||
|
loanDisbursementDate, amount, interestRate, numberOfRepayments, customizer));
|
||||||
|
|
||||||
|
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
||||||
|
approveLoanRequest(amount, loanDisbursementDate));
|
||||||
|
|
||||||
|
return approvedLoanResult.getLoanId();
|
||||||
|
}
|
||||||
|
|
||||||
protected Long applyAndApproveProgressiveLoan(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount,
|
protected Long applyAndApproveProgressiveLoan(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount,
|
||||||
Double interestRate, int numberOfRepayments, Consumer<PostLoansRequest> customizer) {
|
Double interestRate, int numberOfRepayments, Consumer<PostLoansRequest> customizer) {
|
||||||
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(clientId, loanProductId,
|
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(clientId, loanProductId,
|
||||||
@@ -1548,6 +1678,7 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
|||||||
|
|
||||||
public static class RescheduleStrategyMethod {
|
public static class RescheduleStrategyMethod {
|
||||||
|
|
||||||
|
public static final Integer RESCHEDULE_NEXT_REPAYMENTS = 1;
|
||||||
public static final Integer REDUCE_EMI_AMOUNT = 3;
|
public static final Integer REDUCE_EMI_AMOUNT = 3;
|
||||||
public static final Integer ADJUST_LAST_UNPAID_PERIOD = 4;
|
public static final Integer ADJUST_LAST_UNPAID_PERIOD = 4;
|
||||||
}
|
}
|
||||||
|
|||||||
+335
@@ -0,0 +1,335 @@
|
|||||||
|
/**
|
||||||
|
* 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.integrationtests;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.fineract.avro.loan.v1.LoanAccountDataV1;
|
||||||
|
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||||
|
import org.apache.fineract.infrastructure.event.external.data.ExternalEventResponse;
|
||||||
|
import org.apache.fineract.integrationtests.common.ClientHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.externalevents.ExternalEventHelper;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class LoanNextPaymentDueAmountIntegrationTest extends BaseLoanIntegrationTest {
|
||||||
|
|
||||||
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
private static final String LOAN_ACCOUNT_DATA_V_1 = "org.apache.fineract.avro.loan.v1.LoanAccountDataV1";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_progressive_interest_noRecalculation() {
|
||||||
|
externalEventHelper.enableBusinessEvent("LoanApprovedBusinessEvent");
|
||||||
|
externalEventHelper.enableBusinessEvent("LoanBalanceChangedBusinessEvent");
|
||||||
|
externalEventHelper.enableBusinessEvent("LoanDisbursalBusinessEvent");
|
||||||
|
AtomicReference<Long> loanIdRef = new AtomicReference<>();
|
||||||
|
runAt("15 January 2023", () -> {
|
||||||
|
|
||||||
|
Long clientId = ClientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
|
||||||
|
Long loanProductId = loanProductHelper.createLoanProduct(create4IProgressive().isInterestRecalculationEnabled(false))
|
||||||
|
.getResourceId();
|
||||||
|
|
||||||
|
Long loanId = applyAndApproveProgressiveLoan(clientId, loanProductId, "01 January 2023", 100.0, 9.9, 4, null);
|
||||||
|
loanIdRef.set(loanId);
|
||||||
|
|
||||||
|
deleteAllExternalEvents();
|
||||||
|
loanTransactionHelper.disburseLoan(loanId, "01 January 2023", 100.0);
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(100.000000, null, "01 January 2023"), //
|
||||||
|
installment(24.700000, 0.820000, 25.520000, false, "01 February 2023"), //
|
||||||
|
installment(24.900000, 0.620000, 25.520000, false, "01 March 2023"), //
|
||||||
|
installment(25.100000, 0.420000, 25.520000, false, "01 April 2023"), //
|
||||||
|
installment(25.300000, 0.210000, 25.510000, false, "01 May 2023") //
|
||||||
|
);
|
||||||
|
verifyAllLoanAccountTypedExternalEventHasNextPaymentDueAmount("2023-02-01", 25.52d);
|
||||||
|
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
verifyNextPayment(loanDetails, LocalDate.of(2023, 2, 1), BigDecimal.valueOf(25.52d));
|
||||||
|
});
|
||||||
|
runAt("31 January 2023", () -> {
|
||||||
|
Long loanId = loanIdRef.get();
|
||||||
|
inlineLoanCOBHelper.executeInlineCOB(loanId);
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
verifyNextPayment(loanDetails, LocalDate.of(2023, 2, 1), BigDecimal.valueOf(25.52d));
|
||||||
|
});
|
||||||
|
runAt("1 February 2023", () -> {
|
||||||
|
Long loanId = loanIdRef.get();
|
||||||
|
inlineLoanCOBHelper.executeInlineCOB(loanId);
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
verifyNextPayment(loanDetails, LocalDate.of(2023, 2, 1), BigDecimal.valueOf(25.52d));
|
||||||
|
});
|
||||||
|
runAt("2 February 2023", () -> {
|
||||||
|
Long loanId = loanIdRef.get();
|
||||||
|
inlineLoanCOBHelper.executeInlineCOB(loanId);
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
verifyNextPayment(loanDetails, LocalDate.of(2023, 2, 1), BigDecimal.valueOf(25.52d));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_progressive_interest_noRecalculation_prepay() {
|
||||||
|
externalEventHelper.enableBusinessEvent("LoanApprovedBusinessEvent");
|
||||||
|
externalEventHelper.enableBusinessEvent("LoanBalanceChangedBusinessEvent");
|
||||||
|
externalEventHelper.enableBusinessEvent("LoanDisbursalBusinessEvent");
|
||||||
|
AtomicReference<Long> loanIdRef = new AtomicReference<>();
|
||||||
|
runAt("15 January 2023", () -> {
|
||||||
|
|
||||||
|
Long clientId = ClientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
|
||||||
|
Long loanProductId = loanProductHelper.createLoanProduct(create4IProgressive().isInterestRecalculationEnabled(false))
|
||||||
|
.getResourceId();
|
||||||
|
|
||||||
|
Long loanId = applyAndApproveProgressiveLoan(clientId, loanProductId, "01 January 2023", 100.0, 9.9, 4, null);
|
||||||
|
loanIdRef.set(loanId);
|
||||||
|
|
||||||
|
deleteAllExternalEvents();
|
||||||
|
loanTransactionHelper.disburseLoan(loanId, "01 January 2023", 100.0);
|
||||||
|
|
||||||
|
verifyAllLoanAccountTypedExternalEventHasNextPaymentDueAmount("2023-02-01", 25.52d);
|
||||||
|
|
||||||
|
deleteAllExternalEvents();
|
||||||
|
addRepaymentForLoan(loanId, 25.52d, "15 January 2023");
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(100.000000, null, "01 January 2023"), //
|
||||||
|
installment(24.700000, 0.820000, 0.0, true, "01 February 2023"), //
|
||||||
|
installment(24.900000, 0.620000, 25.520000, false, "01 March 2023"), //
|
||||||
|
installment(25.100000, 0.420000, 25.520000, false, "01 April 2023"), //
|
||||||
|
installment(25.300000, 0.210000, 25.510000, false, "01 May 2023") //
|
||||||
|
);
|
||||||
|
|
||||||
|
verifyAllLoanAccountTypedExternalEventHasNextPaymentDueAmount("2023-03-01", 25.52d);
|
||||||
|
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
verifyNextPayment(loanDetails, LocalDate.of(2023, 3, 1), BigDecimal.valueOf(25.52d));
|
||||||
|
});
|
||||||
|
runAt("31 January 2023", () -> {
|
||||||
|
Long loanId = loanIdRef.get();
|
||||||
|
inlineLoanCOBHelper.executeInlineCOB(loanId);
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
verifyNextPayment(loanDetails, LocalDate.of(2023, 3, 1), BigDecimal.valueOf(25.52d));
|
||||||
|
});
|
||||||
|
runAt("1 February 2023", () -> {
|
||||||
|
Long loanId = loanIdRef.get();
|
||||||
|
inlineLoanCOBHelper.executeInlineCOB(loanId);
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
verifyNextPayment(loanDetails, LocalDate.of(2023, 3, 1), BigDecimal.valueOf(25.52d));
|
||||||
|
});
|
||||||
|
runAt("2 February 2023", () -> {
|
||||||
|
Long loanId = loanIdRef.get();
|
||||||
|
inlineLoanCOBHelper.executeInlineCOB(loanId);
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
verifyNextPayment(loanDetails, LocalDate.of(2023, 3, 1), BigDecimal.valueOf(25.52d));
|
||||||
|
});
|
||||||
|
runAt("2 March 2023", () -> {
|
||||||
|
Long loanId = loanIdRef.get();
|
||||||
|
inlineLoanCOBHelper.executeInlineCOB(loanId);
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
verifyNextPayment(loanDetails, LocalDate.of(2023, 3, 1), BigDecimal.valueOf(25.52d));
|
||||||
|
deleteAllExternalEvents();
|
||||||
|
addRepaymentForLoan(loanId, 25.52d, "02 March 2023");
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(100.000000, null, "01 January 2023"), //
|
||||||
|
installment(24.700000, 0.820000, 0.0, true, "01 February 2023"), //
|
||||||
|
installment(24.900000, 0.620000, 0.0, true, "01 March 2023"), //
|
||||||
|
installment(25.100000, 0.420000, 25.520000, false, "01 April 2023"), //
|
||||||
|
installment(25.300000, 0.210000, 25.510000, false, "01 May 2023") //
|
||||||
|
);
|
||||||
|
|
||||||
|
verifyAllLoanAccountTypedExternalEventHasNextPaymentDueAmount("2023-04-01", 25.52d);
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_progressive_interest_recalculation_sameAsRepaymentPeriod() {
|
||||||
|
externalEventHelper.enableBusinessEvent("LoanApprovedBusinessEvent");
|
||||||
|
externalEventHelper.enableBusinessEvent("LoanBalanceChangedBusinessEvent");
|
||||||
|
externalEventHelper.enableBusinessEvent("LoanDisbursalBusinessEvent");
|
||||||
|
AtomicReference<Long> loanIdRef = new AtomicReference<>();
|
||||||
|
runAt("15 February 2023", () -> {
|
||||||
|
|
||||||
|
Long clientId = ClientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
|
||||||
|
Long loanProductId = loanProductHelper
|
||||||
|
.createLoanProduct(create4IProgressive().isInterestRecalculationEnabled(true).recalculationRestFrequencyInterval(1)
|
||||||
|
.recalculationRestFrequencyType(RecalculationRestFrequencyType.SAME_AS_REPAYMENT_PERIOD))
|
||||||
|
.getResourceId();
|
||||||
|
|
||||||
|
Long loanId = applyAndApproveProgressiveLoan(clientId, loanProductId, "01 January 2023", 100.0, 9.9, 4, null);
|
||||||
|
loanIdRef.set(loanId);
|
||||||
|
|
||||||
|
deleteAllExternalEvents();
|
||||||
|
loanTransactionHelper.disburseLoan(loanId, "01 January 2023", 100.0);
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(100.000000, null, "01 January 2023"), //
|
||||||
|
installment(24.700000, 0.820000, 25.520000, false, "01 February 2023"), //
|
||||||
|
installment(24.800000, 0.720000, 25.520000, false, "01 March 2023"), //
|
||||||
|
installment(25.100000, 0.420000, 25.520000, false, "01 April 2023"), //
|
||||||
|
installment(25.400000, 0.210000, 25.610000, false, "01 May 2023") //
|
||||||
|
);
|
||||||
|
|
||||||
|
verifyAllLoanAccountTypedExternalEventHasNextPaymentDueAmount("2023-02-01", 25.52d);
|
||||||
|
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
verifyNextPayment(loanDetails, LocalDate.of(2023, 2, 1), BigDecimal.valueOf(25.52d));
|
||||||
|
});
|
||||||
|
runAt("31 January 2023", () -> {
|
||||||
|
Long loanId = loanIdRef.get();
|
||||||
|
inlineLoanCOBHelper.executeInlineCOB(loanId);
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
verifyNextPayment(loanDetails, LocalDate.of(2023, 2, 1), BigDecimal.valueOf(25.52d));
|
||||||
|
});
|
||||||
|
runAt("1 February 2023", () -> {
|
||||||
|
Long loanId = loanIdRef.get();
|
||||||
|
inlineLoanCOBHelper.executeInlineCOB(loanId);
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
verifyNextPayment(loanDetails, LocalDate.of(2023, 2, 1), BigDecimal.valueOf(25.52d));
|
||||||
|
});
|
||||||
|
runAt("2 February 2023", () -> {
|
||||||
|
Long loanId = loanIdRef.get();
|
||||||
|
inlineLoanCOBHelper.executeInlineCOB(loanId);
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
verifyNextPayment(loanDetails, LocalDate.of(2023, 2, 1), BigDecimal.valueOf(25.52d));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test_progressive_interest_recalculation_daily() {
|
||||||
|
externalEventHelper.enableBusinessEvent("LoanApprovedBusinessEvent");
|
||||||
|
externalEventHelper.enableBusinessEvent("LoanBalanceChangedBusinessEvent");
|
||||||
|
externalEventHelper.enableBusinessEvent("LoanDisbursalBusinessEvent");
|
||||||
|
AtomicReference<Long> loanIdRef = new AtomicReference<>();
|
||||||
|
runAt("15 February 2023", () -> {
|
||||||
|
|
||||||
|
Long clientId = ClientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
|
||||||
|
Long loanProductId = loanProductHelper.createLoanProduct(create4IProgressive().isInterestRecalculationEnabled(true)
|
||||||
|
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY).recalculationRestFrequencyInterval(1)
|
||||||
|
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)).getResourceId();
|
||||||
|
|
||||||
|
Long loanId = applyAndApproveProgressiveLoan(clientId, loanProductId, "01 January 2023", 100.0, 9.9, 4, null);
|
||||||
|
|
||||||
|
loanIdRef.set(loanId);
|
||||||
|
|
||||||
|
deleteAllExternalEvents();
|
||||||
|
loanTransactionHelper.disburseLoan(loanId, "01 January 2023", 100.0);
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(100.000000, null, "01 January 2023"), //
|
||||||
|
installment(24.700000, 0.820000, 25.520000, false, "01 February 2023"), //
|
||||||
|
installment(24.800000, 0.720000, 25.520000, false, "01 March 2023"), //
|
||||||
|
installment(25.100000, 0.420000, 25.520000, false, "01 April 2023"), //
|
||||||
|
installment(25.400000, 0.210000, 25.610000, false, "01 May 2023") //
|
||||||
|
);
|
||||||
|
verifyAllLoanAccountTypedExternalEventHasNextPaymentDueAmount("2023-02-01", 25.52d);
|
||||||
|
|
||||||
|
deleteAllExternalEvents();
|
||||||
|
addRepaymentForLoan(loanId, 25.52d, "15 January 2023");
|
||||||
|
|
||||||
|
verifyAllLoanAccountTypedExternalEventHasNextPaymentDueAmount("2023-03-01", 25.52d);
|
||||||
|
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
|
||||||
|
verifyNextPayment(loanDetails, LocalDate.of(2023, 3, 1), BigDecimal.valueOf(25.52d));
|
||||||
|
|
||||||
|
});
|
||||||
|
runAt("31 January 2023", () -> {
|
||||||
|
Long loanId = loanIdRef.get();
|
||||||
|
inlineLoanCOBHelper.executeInlineCOB(loanId);
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
verifyNextPayment(loanDetails, LocalDate.of(2023, 3, 1), BigDecimal.valueOf(25.52d));
|
||||||
|
});
|
||||||
|
runAt("1 February 2023", () -> {
|
||||||
|
Long loanId = loanIdRef.get();
|
||||||
|
inlineLoanCOBHelper.executeInlineCOB(loanId);
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
verifyNextPayment(loanDetails, LocalDate.of(2023, 3, 1), BigDecimal.valueOf(25.52d));
|
||||||
|
});
|
||||||
|
runAt("2 February 2023", () -> {
|
||||||
|
Long loanId = loanIdRef.get();
|
||||||
|
inlineLoanCOBHelper.executeInlineCOB(loanId);
|
||||||
|
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||||
|
verifyNextPayment(loanDetails, LocalDate.of(2023, 3, 1), BigDecimal.valueOf(25.52d));
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* verifies that all the external events which has org.apache.fineract.avro.loan.v1.LoanAccountDataV1 types payload
|
||||||
|
* has the correct nextPaymentDueDate and nextPaymentDueAmount
|
||||||
|
*
|
||||||
|
* @param dueDate
|
||||||
|
* expected due date formatted yyyy-MM-dd format (ie 2023-12-31 )
|
||||||
|
* @param amount
|
||||||
|
* expected amount
|
||||||
|
*/
|
||||||
|
private void verifyAllLoanAccountTypedExternalEventHasNextPaymentDueAmount(String dueDate, Double amount) {
|
||||||
|
List<ExternalEventResponse> allExternalEvents = ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
|
||||||
|
allExternalEvents.stream().filter(e -> LOAN_ACCOUNT_DATA_V_1.equals(e.getSchema()))
|
||||||
|
.forEach(event -> verifyLoanEvent(event, data -> verifyNextPayment(data, dueDate, BigDecimal.valueOf(amount))));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// utility
|
||||||
|
LoanAccountDataV1 convertLoan(ExternalEventResponse externalEventDTO) {
|
||||||
|
if (LOAN_ACCOUNT_DATA_V_1.equals(externalEventDTO.getSchema())) {
|
||||||
|
return objectMapper.convertValue(externalEventDTO.getPayLoad(), LoanAccountDataV1.class);
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Unexpected schema: " + externalEventDTO.getSchema());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void verifyLoanEvent(ExternalEventResponse externalEventDTO, Consumer<LoanAccountDataV1> validator) {
|
||||||
|
validator.accept(convertLoan(externalEventDTO));
|
||||||
|
}
|
||||||
|
|
||||||
|
void verifyNextPayment(LoanAccountDataV1 loanAccountData, String nextPaymentDueDate, BigDecimal nextPaymentAmount) {
|
||||||
|
Assertions.assertNotNull(loanAccountData, "loanDetails should not be null");
|
||||||
|
Assertions.assertNotNull(loanAccountData.getDelinquent(), "loanDetails.delinquent should not be null");
|
||||||
|
String nextPaymentDueDateActual = loanAccountData.getDelinquent().getNextPaymentDueDate();
|
||||||
|
BigDecimal nextPaymentAmountActual = loanAccountData.getDelinquent().getNextPaymentAmount();
|
||||||
|
log.info("Verify ExternalEventResponse nextPaymentDueDate. Expected: {}, Actual: {}", nextPaymentDueDate, nextPaymentDueDateActual);
|
||||||
|
Assertions.assertEquals(nextPaymentDueDate, nextPaymentDueDateActual);
|
||||||
|
log.info("Verify ExternalEventResponse nextPaymentAmount. Expected: {}, Actual: {}", nextPaymentAmount, nextPaymentAmountActual);
|
||||||
|
Assertions.assertEquals(nextPaymentAmount, nextPaymentAmountActual);
|
||||||
|
}
|
||||||
|
|
||||||
|
void verifyNextPayment(GetLoansLoanIdResponse loanDetails, LocalDate nextPaymentDueDate, BigDecimal nextPaymentAmount) {
|
||||||
|
Assertions.assertNotNull(loanDetails, "loanDetails should not be null");
|
||||||
|
Assertions.assertNotNull(loanDetails.getDelinquent(), "loanDetails.delinquent should not be null");
|
||||||
|
LocalDate nextPaymentDueDateActual = loanDetails.getDelinquent().getNextPaymentDueDate();
|
||||||
|
BigDecimal nextPaymentAmountActual = loanDetails.getDelinquent().getNextPaymentAmount();
|
||||||
|
log.info("Verify GetLoansLoanIdResponse nextPaymentDueDate. Expected: {}, Actual: {}", nextPaymentDueDate,
|
||||||
|
nextPaymentDueDateActual);
|
||||||
|
Assertions.assertEquals(nextPaymentDueDate, nextPaymentDueDateActual);
|
||||||
|
log.info("Verify GetLoansLoanIdResponse nextPaymentAmount. Expected: {}, Actual: {}", nextPaymentAmount, nextPaymentAmountActual);
|
||||||
|
Assertions.assertEquals(nextPaymentAmount, nextPaymentAmountActual);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user