FINERACT-1981: Progressive Loan schedule handling - Installment charges
This commit is contained in:
committed by
Adam Saghy
parent
f76a65ed99
commit
d165a3feda
+48
-8
@@ -76,6 +76,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeOffBehaviour;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalculationDetails;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
@@ -97,6 +98,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
|
||||
@@ -125,17 +127,20 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
private final InterestRefundService interestRefundService;
|
||||
private final LoanScheduleComponent loanSchedule;
|
||||
private final LoanTransactionRepository loanTransactionRepository;
|
||||
private final LoanChargeService loanChargeService;
|
||||
|
||||
public AdvancedPaymentScheduleTransactionProcessor(final EMICalculator emiCalculator, final LoanRepositoryWrapper loanRepositoryWrapper,
|
||||
final InterestRefundService interestRefundService, final ExternalIdFactory externalIdFactory,
|
||||
final LoanScheduleComponent loanSchedule, final LoanTransactionRepository loanTransactionRepository,
|
||||
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService) {
|
||||
final LoanChargeValidator loanChargeValidator, final LoanBalanceService loanBalanceService,
|
||||
final LoanChargeService loanChargeService) {
|
||||
super(externalIdFactory, loanChargeValidator, loanBalanceService);
|
||||
this.emiCalculator = emiCalculator;
|
||||
this.loanRepositoryWrapper = loanRepositoryWrapper;
|
||||
this.interestRefundService = interestRefundService;
|
||||
this.loanTransactionRepository = loanTransactionRepository;
|
||||
this.loanSchedule = loanSchedule;
|
||||
this.loanChargeService = loanChargeService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -372,6 +377,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
createMissingAccrualTransactionDuringChargeOffIfNeeded(newInterest, loanTransaction, loanTransaction.getTransactionDate(),
|
||||
transactionCtx);
|
||||
|
||||
if (!loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
|
||||
recalculateInstallmentFeeCharges(loanTransaction);
|
||||
}
|
||||
|
||||
loanTransaction.resetDerivedComponents();
|
||||
// determine how much is outstanding total and breakdown for principal, interest and charges
|
||||
for (final LoanRepaymentScheduleInstallment currentInstallment : transactionCtx.getInstallments()) {
|
||||
@@ -1215,11 +1224,37 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
private void handleDisbursement(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
|
||||
// TODO: Fix this and enhance EMICalculator to support reamortization and reaging
|
||||
if (disbursementTransaction.getLoan().isInterestBearing()) {
|
||||
final Loan loan = disbursementTransaction.getLoan();
|
||||
if (loan.isInterestBearing()) {
|
||||
handleDisbursementWithEMICalculator(disbursementTransaction, transactionCtx);
|
||||
} else {
|
||||
handleDisbursementWithoutEMICalculator(disbursementTransaction, transactionCtx);
|
||||
}
|
||||
if (!disbursementTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
|
||||
recalculateInstallmentFeeCharges(disbursementTransaction);
|
||||
}
|
||||
}
|
||||
|
||||
private void recalculateInstallmentFeeCharges(final LoanTransaction loanTransaction) {
|
||||
final Loan loan = loanTransaction.getLoan();
|
||||
final List<LoanCharge> loanInstallmentFeeCharges = loan.getActiveCharges().stream()
|
||||
.filter(c -> c.isInstalmentFee() && c.getSubmittedOnDate().isBefore(loanTransaction.getTransactionDate())).toList();
|
||||
loanChargeService.recalculateParticularChargesAfterTransactionOccurs(loan, loanInstallmentFeeCharges,
|
||||
loanTransaction.getTransactionDate());
|
||||
if (!loanInstallmentFeeCharges.isEmpty()) {
|
||||
final List<LoanRepaymentScheduleInstallment> installmentsToUpdate = loan.getRepaymentScheduleInstallments().stream()
|
||||
.filter(i -> i.isNotFullyPaidOff() && !i.isAdditional()).toList();
|
||||
for (LoanRepaymentScheduleInstallment installment : installmentsToUpdate) {
|
||||
if (installment.isDownPayment()) {
|
||||
continue;
|
||||
}
|
||||
final BigDecimal newFee = installment.getInstallmentCharges().stream().map(LoanInstallmentCharge::getAmount)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
if (newFee.compareTo(BigDecimal.ZERO) != 0) {
|
||||
installment.setFeeChargesCharged(newFee);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDisbursementWithEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
|
||||
@@ -1670,6 +1705,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
createMissingAccrualTransactionDuringChargeOffIfNeeded(newInterest, loanTransaction, loanTransaction.getTransactionDate(),
|
||||
transactionCtx);
|
||||
|
||||
if (!loanTransaction.getLoan().isInterestBearingAndInterestRecalculationEnabled()) {
|
||||
recalculateInstallmentFeeCharges(loanTransaction);
|
||||
}
|
||||
|
||||
loanTransaction.resetDerivedComponents();
|
||||
// determine how much is outstanding total and breakdown for principal, interest and charges
|
||||
for (final LoanRepaymentScheduleInstallment currentInstallment : transactionCtx.getInstallments()) {
|
||||
@@ -1765,8 +1804,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
if (futureFee.compareTo(BigDecimal.ZERO) > 0 || futurePenalty.compareTo(BigDecimal.ZERO) > 0) {
|
||||
final Optional<LocalDate> latestDueDate = loan.getCharges().stream()
|
||||
.filter(loanCharge -> loanCharge.isActive() && loanCharge.isNotFullyPaid()).map(LoanCharge::getDueDate)
|
||||
.max(LocalDate::compareTo);
|
||||
.filter(loanCharge -> loanCharge.isActive() && loanCharge.isNotFullyPaid() && loanCharge.getDueDate() != null)
|
||||
.map(LoanCharge::getDueDate).max(LocalDate::compareTo);
|
||||
|
||||
if (latestDueDate.isPresent()) {
|
||||
final LoanRepaymentScheduleInstallment lastInstallment = installmentsUpToTransactionDate.getLast();
|
||||
@@ -2526,11 +2565,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Set<LoanCharge> getLoanChargesOfInstallment(Set<LoanCharge> charges, LoanRepaymentScheduleInstallment installment,
|
||||
int firstInstallmentNumber) {
|
||||
boolean isFirstInstallment = installment.getInstallmentNumber().equals(firstInstallmentNumber);
|
||||
private Set<LoanCharge> getLoanChargesOfInstallment(final Set<LoanCharge> charges, final LoanRepaymentScheduleInstallment installment,
|
||||
final int firstInstallmentNumber) {
|
||||
final boolean isFirstInstallment = installment.getInstallmentNumber().equals(firstInstallmentNumber);
|
||||
return charges.stream()
|
||||
.filter(loanCharge -> loanCharge.isDueInPeriod(installment.getFromDate(), installment.getDueDate(), isFirstInstallment))
|
||||
.filter(loanCharge -> (loanCharge.isInstalmentFee() && loanCharge.hasInstallmentFor(installment))
|
||||
|| loanCharge.isDueInPeriod(installment.getFromDate(), installment.getDueDate(), isFirstInstallment))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
|
||||
+19
-1
@@ -66,12 +66,17 @@ public class ChangeOperation implements Comparable<ChangeOperation> {
|
||||
return loanCharge.isPresent();
|
||||
}
|
||||
|
||||
public boolean isInstallmentFee() {
|
||||
return loanCharge.map(LoanCharge::isInstalmentFee).orElse(false);
|
||||
}
|
||||
|
||||
private boolean isAccrualActivity() {
|
||||
return isTransaction() && loanTransaction.get().isAccrualActivity();
|
||||
}
|
||||
|
||||
private boolean isBackdatedCharge() {
|
||||
return isCharge() && DateUtils.isBefore(loanCharge.get().getDueDate(), loanCharge.get().getSubmittedOnDate());
|
||||
return isCharge() && loanCharge.isPresent() && loanCharge.get().getDueDate() != null
|
||||
&& DateUtils.isBefore(loanCharge.get().getDueDate(), loanCharge.get().getSubmittedOnDate());
|
||||
}
|
||||
|
||||
private LocalDate getEffectiveDate() {
|
||||
@@ -80,6 +85,8 @@ public class ChangeOperation implements Comparable<ChangeOperation> {
|
||||
} else if (loanCharge.isPresent()) {
|
||||
if (isBackdatedCharge()) {
|
||||
return loanCharge.get().getDueDate();
|
||||
} else if (isInstallmentFee()) {
|
||||
return loanCharge.get().getLoan().isDisbursed() ? loanCharge.get().getSubmittedOnDate() : loanCharge.get().getDueDate();
|
||||
} else {
|
||||
return loanCharge.get().getSubmittedOnDate();
|
||||
}
|
||||
@@ -119,6 +126,17 @@ public class ChangeOperation implements Comparable<ChangeOperation> {
|
||||
public int compareTo(@NonNull ChangeOperation o) {
|
||||
int datePortion = DateUtils.compareWithNullsLast(this.getEffectiveDate(), o.getEffectiveDate());
|
||||
if (datePortion == 0) {
|
||||
boolean thisIsDisb = this.isTransaction() && this.getLoanTransaction().isPresent()
|
||||
&& this.getLoanTransaction().get().isDisbursement();
|
||||
boolean otherIsDisb = o.isTransaction() && o.getLoanTransaction().isPresent() && o.getLoanTransaction().get().isDisbursement();
|
||||
|
||||
if (thisIsDisb && o.isCharge() && o.isInstallmentFee()) {
|
||||
return -1;
|
||||
}
|
||||
if (this.isCharge() && this.isInstallmentFee() && otherIsDisb) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
final boolean isAccrual = isAccrualActivity();
|
||||
if (isAccrual != o.isAccrualActivity()) {
|
||||
return isAccrual ? 1 : -1;
|
||||
|
||||
+2
-1
@@ -75,6 +75,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
|
||||
@@ -121,7 +122,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, Mockito.mock(LoanRepositoryWrapper.class),
|
||||
Mockito.mock(InterestRefundService.class), Mockito.mock(ExternalIdFactory.class), Mockito.mock(LoanScheduleComponent.class),
|
||||
Mockito.mock(LoanTransactionRepository.class), Mockito.mock(LoanChargeValidator.class),
|
||||
Mockito.mock(LoanBalanceService.class));
|
||||
Mockito.mock(LoanBalanceService.class), Mockito.mock(LoanChargeService.class));
|
||||
|
||||
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
|
||||
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
|
||||
|
||||
Reference in New Issue
Block a user