FINERACT-2322: Extend loan collection data with next due payment amount field

This commit is contained in:
Soma Sörös
2025-08-06 13:52:43 +02:00
committed by Adam Saghy
parent 514a684281
commit 28043ce438
21 changed files with 807 additions and 92 deletions
@@ -27,6 +27,14 @@
"string" "string"
] ]
}, },
{
"default": null,
"name": "nextPaymentAmount",
"type": [
"null",
"bigdecimal"
]
},
{ {
"default": null, "default": null,
"name": "delinquentDays", "name": "delinquentDays",
@@ -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);
}
@@ -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) {
@@ -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);
}
@@ -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);
}
}
@@ -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
@@ -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()));
} }
@@ -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();
@@ -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();
}
}
@@ -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) {
@@ -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);
} }
@@ -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);
@@ -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) //
@@ -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);
} }
@@ -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())
@@ -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]")
@@ -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);
@@ -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);
@@ -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;
} }
@@ -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);
}
}