FINERACT-2322: Extend loan collection data with next due payment amount field
This commit is contained in:
@@ -27,6 +27,14 @@
|
||||
"string"
|
||||
]
|
||||
},
|
||||
{
|
||||
"default": null,
|
||||
"name": "nextPaymentAmount",
|
||||
"type": [
|
||||
"null",
|
||||
"bigdecimal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"default": null,
|
||||
"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 ConfigurationDomainService configurationDomainService;
|
||||
private final LoanTransactionRepository loanTransactionRepository;
|
||||
private final PossibleNextRepaymentCalculationServiceDiscovery possibleNextRepaymentCalculationServiceDiscovery;
|
||||
|
||||
@Override
|
||||
public List<DelinquencyRangeData> retrieveAllDelinquencyRanges() {
|
||||
@@ -152,6 +153,12 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
|
||||
collectionData = loanDelinquencyDomainService.getOverdueCollectionData(loan, effectiveDelinquencyList);
|
||||
collectionData.setAvailableDisbursementAmount(calculateAvailableDisbursementAmount(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();
|
||||
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.LoanDelinquencyDomainService;
|
||||
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.DelinquencyBucketParseAndValidator;
|
||||
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator;
|
||||
@@ -62,11 +63,12 @@ public class DelinquencyConfiguration {
|
||||
LoanInstallmentDelinquencyTagRepository repositoryLoanInstallmentDelinquencyTag,
|
||||
LoanDelinquencyActionRepository loanDelinquencyActionRepository,
|
||||
DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper, ConfigurationDomainService configurationDomainService,
|
||||
LoanTransactionRepository loanTransactionRepository) {
|
||||
LoanTransactionRepository loanTransactionRepository,
|
||||
PossibleNextRepaymentCalculationServiceDiscovery possibleNextRepaymentCalculationService) {
|
||||
return new DelinquencyReadPlatformServiceImpl(repositoryRange, repositoryBucket, repositoryLoanDelinquencyTagHistory, mapperRange,
|
||||
mapperBucket, mapperLoanDelinquencyTagHistory, loanRepository, loanDelinquencyDomainService,
|
||||
repositoryLoanInstallmentDelinquencyTag, loanDelinquencyActionRepository, delinquencyEffectivePauseHelper,
|
||||
configurationDomainService, loanTransactionRepository);
|
||||
configurationDomainService, loanTransactionRepository, possibleNextRepaymentCalculationService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
+2
-1
@@ -31,6 +31,7 @@ public final class CollectionData {
|
||||
private BigDecimal availableDisbursementAmount;
|
||||
private Long pastDueDays;
|
||||
private LocalDate nextPaymentDueDate;
|
||||
private BigDecimal nextPaymentAmount;
|
||||
private Long delinquentDays;
|
||||
private LocalDate delinquentDate;
|
||||
private BigDecimal delinquentAmount;
|
||||
@@ -50,7 +51,7 @@ public final class CollectionData {
|
||||
|
||||
public static CollectionData template() {
|
||||
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
|
||||
**/
|
||||
public LoanRepaymentScheduleInstallment getRelatedRepaymentScheduleInstallment(LocalDate date) {
|
||||
// TODO first installment should be fromInclusive
|
||||
return getRepaymentScheduleInstallment(e -> DateUtils.isDateInRangeFromExclusiveToInclusive(date, e.getFromDate(), e.getDueDate()));
|
||||
}
|
||||
|
||||
|
||||
+5
@@ -55,6 +55,11 @@ public class EmbeddableProgressiveLoanScheduleGenerator {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ProgressiveLoanModel> findOneByLoan(Loan loan) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<ProgressiveLoanInterestScheduleModel> extractModel(Optional<ProgressiveLoanModel> progressiveLoanModel) {
|
||||
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());
|
||||
}
|
||||
|
||||
private List<LoanRepaymentScheduleInstallment> findOverdueInstallmentsBeforeDateSortedByInstallmentNumber(LocalDate targetDate,
|
||||
ProgressiveTransactionCtx transactionCtx) {
|
||||
return transactionCtx.getInstallments().stream() //
|
||||
.filter(installment -> !installment.isDownPayment() && !installment.isAdditional())
|
||||
.filter(installment -> installment.isOverdueOn(targetDate))
|
||||
.sorted(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).toList();
|
||||
private List<RepaymentPeriod> findPossiblyOverdueRepaymentPeriods(LocalDate targetDate, ProgressiveLoanInterestScheduleModel model) {
|
||||
return model.repaymentPeriods().stream() //
|
||||
.filter(repaymentPeriod -> DateUtils.isAfter(targetDate, repaymentPeriod.getDueDate())).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) {
|
||||
recalculateInterestForDate(targetDate, ctx, true);
|
||||
}
|
||||
|
||||
public void recalculateInterestForDate(LocalDate targetDate, ProgressiveTransactionCtx ctx, boolean updateInstallments) {
|
||||
if (ctx.getInstallments() != null && !ctx.getInstallments().isEmpty()) {
|
||||
Loan loan = ctx.getInstallments().getFirst().getLoan();
|
||||
if (loan.isInterestBearingAndInterestRecalculationEnabled() && !loan.isNpa() && !ctx.isChargedOff()
|
||||
&& !ctx.isContractTerminated()) {
|
||||
&& !ctx.isContractTerminated() && !loan.getLoanInterestRecalculationDetails().disallowInterestCalculationOnPastDue()) {
|
||||
|
||||
List<LoanRepaymentScheduleInstallment> overdueInstallmentsSortedByInstallmentNumber = findOverdueInstallmentsBeforeDateSortedByInstallmentNumber(
|
||||
targetDate, ctx);
|
||||
if (!overdueInstallmentsSortedByInstallmentNumber.isEmpty()) {
|
||||
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);
|
||||
}
|
||||
boolean modelHasUpdates = rework(targetDate, ctx);
|
||||
if (modelHasUpdates && updateInstallments) {
|
||||
updateInstallmentsPrincipalAndInterestByModel(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustOverduePrincipalForInstallment(LocalDate currentDate, LoanRepaymentScheduleInstallment currentInstallment,
|
||||
Money overduePrincipal, Money aggregatedOverDuePrincipal, ProgressiveTransactionCtx ctx) {
|
||||
if (currentInstallment.getLoan().getLoanInterestRecalculationDetails().disallowInterestCalculationOnPastDue()) {
|
||||
return;
|
||||
}
|
||||
private boolean adjustOverduePrincipal(LocalDate currentDate, RepaymentPeriod currentInstallment, Money overduePrincipal,
|
||||
Money aggregatedOverDuePrincipal, ProgressiveTransactionCtx ctx) {
|
||||
|
||||
LocalDate fromDate = currentInstallment.getFromDate();
|
||||
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 we have same date for fromDate & last overdue balance change then it means we have the up-to-date
|
||||
// model.
|
||||
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);
|
||||
if (!currentDate.equals(model.lastOverdueBalanceChange())) {
|
||||
if (model.lastOverdueBalanceChange() == null || currentInstallment.getFromDate().isAfter(model.lastOverdueBalanceChange())) {
|
||||
emiCalculator.addBalanceCorrection(model, fromDate, overduePrincipal);
|
||||
} else {
|
||||
// not the first balance correction on installment period, then setting overdue balance correction from
|
||||
// last balance change's current date. previous interest period already has the correct balance
|
||||
// correction.
|
||||
emiCalculator.addBalanceCorrection(ctx.getModel(), ctx.getModel().lastOverdueBalanceChange(), overduePrincipal);
|
||||
emiCalculator.addBalanceCorrection(model, model.lastOverdueBalanceChange(), overduePrincipal);
|
||||
}
|
||||
|
||||
hasUpdate = true;
|
||||
|
||||
if (currentDate.isAfter(fromDate) && !currentDate.isAfter(toDate)) {
|
||||
LocalDate lastOverdueBalanceChange;
|
||||
if (shouldRecalculateTillInstallmentDueDate(currentInstallment.getLoan().getLoanInterestRecalculationDetails(),
|
||||
ctx.isPrepayAttempt())) {
|
||||
if (shouldRecalculateTillInstallmentDueDate(loanInterestRecalculationDetails, prepayAttempt)) {
|
||||
lastOverdueBalanceChange = toDate;
|
||||
} else {
|
||||
lastOverdueBalanceChange = currentDate;
|
||||
}
|
||||
emiCalculator.addBalanceCorrection(ctx.getModel(), lastOverdueBalanceChange, aggregatedOverDuePrincipal.negated());
|
||||
ctx.getModel().lastOverdueBalanceChange(lastOverdueBalanceChange);
|
||||
emiCalculator.addBalanceCorrection(model, lastOverdueBalanceChange, aggregatedOverDuePrincipal.negated());
|
||||
model.lastOverdueBalanceChange(lastOverdueBalanceChange);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasUpdate) {
|
||||
updateInstallmentsPrincipalAndInterestByModel(ctx);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void updateInstallmentsPrincipalAndInterestByModel(ProgressiveTransactionCtx ctx) {
|
||||
|
||||
+3
@@ -19,6 +19,7 @@
|
||||
package org.apache.fineract.portfolio.loanaccount.repository;
|
||||
|
||||
import java.util.Optional;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.ProgressiveLoanModel;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
@@ -27,4 +28,6 @@ public interface ProgressiveLoanModelRepository
|
||||
extends JpaSpecificationExecutor<ProgressiveLoanModel>, JpaRepository<ProgressiveLoanModel, Long> {
|
||||
|
||||
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> findOneByLoan(Loan loan);
|
||||
|
||||
Optional<ProgressiveLoanInterestScheduleModel> extractModel(Optional<ProgressiveLoanModel> progressiveLoanModel);
|
||||
|
||||
ProgressiveLoanInterestScheduleModel writeInterestScheduleModel(Loan loan, ProgressiveLoanInterestScheduleModel model);
|
||||
|
||||
+10
@@ -23,6 +23,7 @@ import jakarta.transaction.Transactional;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.persistence.FlushModeHandler;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
@@ -78,6 +79,15 @@ public class InterestScheduleModelRepositoryWrapperImpl implements InterestSched
|
||||
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
|
||||
public Optional<ProgressiveLoanInterestScheduleModel> extractModel(Optional<ProgressiveLoanModel> progressiveLoanModel) {
|
||||
return progressiveLoanModel.map(ProgressiveLoanModel::getJsonModel) //
|
||||
|
||||
+9
@@ -381,6 +381,15 @@ public final class RepaymentPeriod {
|
||||
return previous == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives back getDueInterest minus paid interest
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Money getOutstandingInterest() {
|
||||
return MathUtil.negativeToZero(getDueInterest().minus(getPaidInterest()), mc);
|
||||
}
|
||||
|
||||
public Money getOutstandingPrincipal() {
|
||||
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.LoanChargePaidBy;
|
||||
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.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
|
||||
@@ -467,6 +468,9 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
when(loanTransaction.getLoan()).thenReturn(loan);
|
||||
when(loan.getCurrency()).thenReturn(currency);
|
||||
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.getAllocationTypes())
|
||||
|
||||
+2
@@ -1022,6 +1022,8 @@ final class LoansApiResourceSwagger {
|
||||
public Integer pastDueDays;
|
||||
@Schema(example = "[2022, 07, 01]")
|
||||
public LocalDate nextPaymentDueDate;
|
||||
@Schema(example = "123.23")
|
||||
public BigDecimal nextPaymentAmount;
|
||||
@Schema(example = "4")
|
||||
public Integer delinquentDays;
|
||||
@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.DelinquencyReadPlatformServiceImpl;
|
||||
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.LoanAccountData;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
@@ -364,7 +365,8 @@ public class LoanAccountDelinquencyRangeEventSerializerTest {
|
||||
DelinquencyReadPlatformService delinquencyReadPlatformService = new DelinquencyReadPlatformServiceImpl(repositoryRange,
|
||||
repositoryBucket, repositoryLoanDelinquencyTagHistory, mapperRange, mapperBucket, mapperLoanDelinquencyTagHistory,
|
||||
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);
|
||||
when(loanProduct.isMultiDisburseLoan()).thenReturn(false);
|
||||
|
||||
+16
-16
@@ -186,8 +186,8 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
||||
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
||||
loanForProcessing);
|
||||
final BigDecimal zero = BigDecimal.ZERO;
|
||||
CollectionData collectionData = new CollectionData(zero, 2L, null, 2L, overDueSinceDate, zero, null, null, null, null, null, null,
|
||||
zero, zero, zero, zero);
|
||||
CollectionData collectionData = new CollectionData(zero, 2L, null, zero, 2L, overDueSinceDate, zero, null, null, null, null, null,
|
||||
null, zero, zero, zero, zero);
|
||||
|
||||
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
||||
|
||||
@@ -240,10 +240,10 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
||||
LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(2);
|
||||
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
||||
loanForProcessing);
|
||||
CollectionData collectionData = new CollectionData(zeroAmount, 2L, null, 2L, overDueSinceDate, zeroAmount, null, null, null, null,
|
||||
null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
||||
CollectionData collectionData = new CollectionData(zeroAmount, 2L, null, zeroAmount, 2L, overDueSinceDate, zeroAmount, null, null,
|
||||
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);
|
||||
|
||||
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
||||
@@ -369,10 +369,10 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
||||
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
||||
loanForProcessing);
|
||||
|
||||
CollectionData collectionData = new CollectionData(zeroAmount, 2L, null, 2L, overDueSinceDate, zeroAmount, null, null, null, null,
|
||||
null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
||||
CollectionData collectionData = new CollectionData(zeroAmount, 2L, null, zeroAmount, 2L, overDueSinceDate, zeroAmount, null, null,
|
||||
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);
|
||||
|
||||
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
||||
@@ -446,10 +446,10 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
||||
LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(29);
|
||||
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
||||
loanForProcessing);
|
||||
CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 29L, null, 29L, overDueSinceDate, zeroAmount, null, null, null,
|
||||
null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
||||
CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 29L, null, zeroAmount, 29L, overDueSinceDate, zeroAmount, null,
|
||||
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);
|
||||
|
||||
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
||||
@@ -534,14 +534,14 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
||||
LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(29);
|
||||
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
||||
loanForProcessing);
|
||||
CollectionData collectionData = new CollectionData(zeroAmount, 29L, null, 29L, overDueSinceDate, zeroAmount, null, null, null, null,
|
||||
null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
||||
CollectionData collectionData = new CollectionData(zeroAmount, 29L, null, zeroAmount, 29L, overDueSinceDate, zeroAmount, null, null,
|
||||
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);
|
||||
|
||||
CollectionData installmentCollectionData_2 = new CollectionData(zeroAmount, 0L, null, 0L, null, installmentPrincipalAmount, null,
|
||||
null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
||||
CollectionData installmentCollectionData_2 = new CollectionData(zeroAmount, 0L, null, zeroAmount, 0L, null,
|
||||
installmentPrincipalAmount, null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
|
||||
|
||||
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
||||
installmentsCollection.put(1L, installmentCollectionData_1);
|
||||
|
||||
+131
@@ -20,6 +20,7 @@ package org.apache.fineract.integrationtests;
|
||||
|
||||
import static java.lang.System.lineSeparator;
|
||||
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.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
@@ -318,6 +319,107 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
||||
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() {
|
||||
final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec);
|
||||
Assertions.assertNotNull(delinquencyBucketId);
|
||||
@@ -1076,6 +1178,23 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
||||
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,
|
||||
Double interestRate, int numberOfRepayments, Consumer<PostLoansRequest> customizer) {
|
||||
|
||||
@@ -1119,6 +1238,17 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
||||
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,
|
||||
Double interestRate, int numberOfRepayments, Consumer<PostLoansRequest> customizer) {
|
||||
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(clientId, loanProductId,
|
||||
@@ -1548,6 +1678,7 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
||||
|
||||
public static class RescheduleStrategyMethod {
|
||||
|
||||
public static final Integer RESCHEDULE_NEXT_REPAYMENTS = 1;
|
||||
public static final Integer REDUCE_EMI_AMOUNT = 3;
|
||||
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