FINERACT-2190: Avoid recreation of loan repayment schedule periods

This commit is contained in:
Jose Alberto Hernandez
2025-04-04 14:29:10 +02:00
committed by Adam Saghy
parent b9cfa470ff
commit 52c41b1471
13 changed files with 229 additions and 117 deletions
@@ -57,7 +57,6 @@ import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
@@ -112,7 +111,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Loa
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod;
import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType;
import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod;
@@ -610,8 +608,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.interestChargedFromDate = loanApplicationTerms.getInterestChargedFromDate();
this.submittedOnDate = submittedOnDate != null ? submittedOnDate : DateUtils.getBusinessLocalDate();
updateLoanSchedule(loanScheduleModel);
updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
// Copy interest recalculation settings if interest recalculation is enabled
@@ -1005,53 +1001,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.rates.addAll(loanRates);
}
public void updateLoanSchedule(final LoanScheduleModel modifiedLoanSchedule) {
this.repaymentScheduleInstallments.clear();
for (final LoanScheduleModelPeriod scheduledLoanInstallment : modifiedLoanSchedule.getPeriods()) {
if (scheduledLoanInstallment.isRepaymentPeriod() || scheduledLoanInstallment.isDownPaymentPeriod()) {
final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(this,
scheduledLoanInstallment.periodNumber(), scheduledLoanInstallment.periodFromDate(),
scheduledLoanInstallment.periodDueDate(), scheduledLoanInstallment.principalDue(),
scheduledLoanInstallment.interestDue(), scheduledLoanInstallment.feeChargesDue(),
scheduledLoanInstallment.penaltyChargesDue(), scheduledLoanInstallment.isRecalculatedInterestComponent(),
scheduledLoanInstallment.getLoanCompoundingDetails(), scheduledLoanInstallment.rescheduleInterestPortion(),
scheduledLoanInstallment.isDownPaymentPeriod());
addLoanRepaymentScheduleInstallment(installment);
}
}
updateLoanScheduleDependentDerivedFields();
updateLoanSummaryDerivedFields();
}
public void updateLoanSchedule(final Collection<LoanRepaymentScheduleInstallment> installments) {
List<LoanRepaymentScheduleInstallment> existingInstallments = new ArrayList<>(this.repaymentScheduleInstallments);
repaymentScheduleInstallments.clear();
for (final LoanRepaymentScheduleInstallment installment : installments) {
LoanRepaymentScheduleInstallment existingInstallment = findByInstallmentNumber(existingInstallments,
installment.getInstallmentNumber());
if (existingInstallment != null) {
Set<LoanInstallmentCharge> existingCharges = existingInstallment.getInstallmentCharges();
installment.getInstallmentCharges().addAll(existingCharges);
existingCharges.forEach(c -> c.setInstallment(installment));
existingInstallment.getInstallmentCharges().clear();
}
addLoanRepaymentScheduleInstallment(installment);
}
updateLoanScheduleDependentDerivedFields();
updateLoanSummaryDerivedFields();
}
private LoanRepaymentScheduleInstallment findByInstallmentNumber(Collection<LoanRepaymentScheduleInstallment> installments,
Integer installmentNumber) {
for (LoanRepaymentScheduleInstallment installment : installments) {
if (Objects.equals(installment.getInstallmentNumber(), installmentNumber)) {
return installment;
}
}
return null;
}
public void updateLoanScheduleDependentDerivedFields() {
if (this.getLoanRepaymentScheduleInstallmentsSize() > 0) {
this.expectedMaturityDate = determineExpectedMaturityDate();
@@ -38,10 +38,12 @@ import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks;
@Getter
@Setter
@Entity
@Table(name = "m_loan_repayment_schedule")
public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDateTimeCustom<Long>
@@ -60,67 +62,67 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
@Column(name = "duedate", nullable = false)
private LocalDate dueDate;
@Column(name = "principal_amount", scale = 6, precision = 19, nullable = true)
@Column(name = "principal_amount", scale = 6, precision = 19)
private BigDecimal principal;
@Column(name = "principal_completed_derived", scale = 6, precision = 19, nullable = true)
@Column(name = "principal_completed_derived", scale = 6, precision = 19)
private BigDecimal principalCompleted;
@Column(name = "principal_writtenoff_derived", scale = 6, precision = 19, nullable = true)
@Column(name = "principal_writtenoff_derived", scale = 6, precision = 19)
private BigDecimal principalWrittenOff;
@Column(name = "interest_amount", scale = 6, precision = 19, nullable = true)
@Column(name = "interest_amount", scale = 6, precision = 19)
private BigDecimal interestCharged;
@Column(name = "interest_completed_derived", scale = 6, precision = 19, nullable = true)
@Column(name = "interest_completed_derived", scale = 6, precision = 19)
private BigDecimal interestPaid;
@Column(name = "interest_waived_derived", scale = 6, precision = 19, nullable = true)
@Column(name = "interest_waived_derived", scale = 6, precision = 19)
private BigDecimal interestWaived;
@Column(name = "interest_writtenoff_derived", scale = 6, precision = 19, nullable = true)
@Column(name = "interest_writtenoff_derived", scale = 6, precision = 19)
private BigDecimal interestWrittenOff;
@Column(name = "accrual_interest_derived", scale = 6, precision = 19, nullable = true)
@Column(name = "accrual_interest_derived", scale = 6, precision = 19)
private BigDecimal interestAccrued;
@Column(name = "reschedule_interest_portion", scale = 6, precision = 19, nullable = true)
@Column(name = "reschedule_interest_portion", scale = 6, precision = 19)
private BigDecimal rescheduleInterestPortion;
@Column(name = "fee_charges_amount", scale = 6, precision = 19, nullable = true)
@Column(name = "fee_charges_amount", scale = 6, precision = 19)
private BigDecimal feeChargesCharged;
@Column(name = "fee_charges_completed_derived", scale = 6, precision = 19, nullable = true)
@Column(name = "fee_charges_completed_derived", scale = 6, precision = 19)
private BigDecimal feeChargesPaid;
@Column(name = "fee_charges_writtenoff_derived", scale = 6, precision = 19, nullable = true)
@Column(name = "fee_charges_writtenoff_derived", scale = 6, precision = 19)
private BigDecimal feeChargesWrittenOff;
@Column(name = "fee_charges_waived_derived", scale = 6, precision = 19, nullable = true)
@Column(name = "fee_charges_waived_derived", scale = 6, precision = 19)
private BigDecimal feeChargesWaived;
@Column(name = "accrual_fee_charges_derived", scale = 6, precision = 19, nullable = true)
@Column(name = "accrual_fee_charges_derived", scale = 6, precision = 19)
private BigDecimal feeAccrued;
@Column(name = "penalty_charges_amount", scale = 6, precision = 19, nullable = true)
@Column(name = "penalty_charges_amount", scale = 6, precision = 19)
private BigDecimal penaltyCharges;
@Column(name = "penalty_charges_completed_derived", scale = 6, precision = 19, nullable = true)
@Column(name = "penalty_charges_completed_derived", scale = 6, precision = 19)
private BigDecimal penaltyChargesPaid;
@Column(name = "penalty_charges_writtenoff_derived", scale = 6, precision = 19, nullable = true)
@Column(name = "penalty_charges_writtenoff_derived", scale = 6, precision = 19)
private BigDecimal penaltyChargesWrittenOff;
@Column(name = "penalty_charges_waived_derived", scale = 6, precision = 19, nullable = true)
@Column(name = "penalty_charges_waived_derived", scale = 6, precision = 19)
private BigDecimal penaltyChargesWaived;
@Column(name = "accrual_penalty_charges_derived", scale = 6, precision = 19, nullable = true)
@Column(name = "accrual_penalty_charges_derived", scale = 6, precision = 19)
private BigDecimal penaltyAccrued;
@Column(name = "total_paid_in_advance_derived", scale = 6, precision = 19, nullable = true)
@Column(name = "total_paid_in_advance_derived", scale = 6, precision = 19)
private BigDecimal totalPaidInAdvance;
@Column(name = "total_paid_late_derived", scale = 6, precision = 19, nullable = true)
@Column(name = "total_paid_late_derived", scale = 6, precision = 19)
private BigDecimal totalPaidLate;
@Column(name = "completed_derived", nullable = false)
@@ -139,17 +141,17 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
// approach
// https://blog.thepete.net/blog/2023/12/05/expand/contract-making-a-breaking-change-without-a-big-bang/
@Setter
@Column(name = "credits_amount", scale = 6, precision = 19, nullable = true)
@Column(name = "credits_amount", scale = 6, precision = 19)
private BigDecimal creditedPrincipal;
@Setter
@Column(name = "credited_interest", scale = 6, precision = 19, nullable = true)
@Column(name = "credited_interest", scale = 6, precision = 19)
private BigDecimal creditedInterest;
@Column(name = "credited_fee", scale = 6, precision = 19, nullable = true)
@Column(name = "credited_fee", scale = 6, precision = 19)
private BigDecimal creditedFee;
@Column(name = "credited_penalty", scale = 6, precision = 19, nullable = true)
@Column(name = "credited_penalty", scale = 6, precision = 19)
private BigDecimal creditedPenalty;
@Column(name = "is_down_payment", nullable = false)
@@ -1068,4 +1070,58 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
MathUtil.nullToZero(MathUtil.add(getPrincipal(), getInterestCharged(), getFeeChargesCharged(), getPenaltyCharges())));
}
public void copyFrom(final LoanScheduleModelPeriod period) {
resetBalances();
setFromDate(period.periodFromDate());
setDueDate(period.periodDueDate());
setPrincipal(period.principalDue());
setInterestCharged(period.interestDue());
setFeeChargesCharged(period.feeChargesDue());
setPenaltyCharges(period.penaltyChargesDue());
setRecalculatedInterestComponent(period.isRecalculatedInterestComponent());
if (getLoanCompoundingDetails() != null) {
getLoanCompoundingDetails().clear();
}
if (period.getLoanCompoundingDetails() != null) {
period.getLoanCompoundingDetails().forEach(lcd -> {
lcd.setLoanRepaymentScheduleInstallment(this);
getLoanCompoundingDetails().add(lcd);
});
}
setRescheduleInterestPortion(period.rescheduleInterestPortion());
setDownPayment(period.isDownPaymentPeriod());
setAdditional(false);
setReAged(false);
}
public void copyFrom(final LoanRepaymentScheduleInstallment installment) {
setFromDate(installment.getFromDate());
setDueDate(installment.getDueDate());
setPrincipal(installment.getPrincipal());
setInterestCharged(installment.getInterestCharged());
setFeeChargesCharged(installment.getFeeChargesCharged());
setPenaltyCharges(installment.getPenaltyCharges());
setRecalculatedInterestComponent(installment.isRecalculatedInterestComponent());
if (getLoanCompoundingDetails() != null) {
getLoanCompoundingDetails().clear();
}
if (installment.getLoanCompoundingDetails() != null) {
installment.getLoanCompoundingDetails().forEach(lcd -> {
lcd.setLoanRepaymentScheduleInstallment(this);
getLoanCompoundingDetails().add(lcd);
});
}
Set<LoanInstallmentCharge> installmentCharges = installment.getInstallmentCharges();
installmentCharges.forEach(c -> c.setInstallment(this));
getInstallmentCharges().addAll(installmentCharges);
setAdditional(installment.isAdditional());
setReAged(installment.isReAged());
setDownPayment(installment.isDownPayment());
}
}
@@ -29,6 +29,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
@RequiredArgsConstructor
public class LoanScheduleService {
@@ -36,16 +37,17 @@ public class LoanScheduleService {
private final LoanChargeService loanChargeService;
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
private final LoanTransactionProcessingService loadTransactionProcessingService;
private final LoanScheduleComponent loanSchedule;
/**
* Ability to regenerate the repayment schedule based on the loans current details/state.
*/
public void regenerateRepaymentSchedule(final Loan loan, final ScheduleGeneratorDTO scheduleGeneratorDTO) {
final LoanScheduleModel loanSchedule = loan.regenerateScheduleModel(scheduleGeneratorDTO);
if (loanSchedule == null) {
final LoanScheduleModel loanScheduleModel = loan.regenerateScheduleModel(scheduleGeneratorDTO);
if (loanScheduleModel == null) {
return;
}
loan.updateLoanSchedule(loanSchedule);
loanSchedule.updateLoanSchedule(loan, loanScheduleModel);
final Set<LoanCharge> charges = loan.getActiveCharges();
for (final LoanCharge loanCharge : charges) {
if (!loanCharge.isWaived()) {
@@ -80,15 +82,15 @@ public class LoanScheduleService {
public void regenerateRepaymentScheduleWithInterestRecalculation(final Loan loan, final ScheduleGeneratorDTO generatorDTO) {
final LocalDate lastTransactionDate = loan.getLastUserTransactionDate();
final LoanScheduleDTO loanSchedule = loadTransactionProcessingService.getRecalculatedSchedule(generatorDTO, loan);
if (loanSchedule == null) {
final LoanScheduleDTO loanScheduleDTO = loadTransactionProcessingService.getRecalculatedSchedule(generatorDTO, loan);
if (loanScheduleDTO == null) {
return;
}
// Either the installments got recalculated or the model
if (loanSchedule.getInstallments() != null) {
loan.updateLoanSchedule(loanSchedule.getInstallments());
if (loanScheduleDTO.getInstallments() != null) {
loanSchedule.updateLoanSchedule(loan, loanScheduleDTO.getInstallments());
} else {
loan.updateLoanSchedule(loanSchedule.getLoanScheduleModel());
loanSchedule.updateLoanSchedule(loan, loanScheduleDTO.getLoanScheduleModel());
}
loan.setInterestRecalculatedOn(DateUtils.getBusinessLocalDate());
final LocalDate lastRepaymentDate = loan.getLastRepaymentPeriodDueDate(true);
@@ -0,0 +1,91 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.service.schedule;
import java.util.Collection;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class LoanScheduleComponent {
public void updateLoanSchedule(Loan loan, final LoanScheduleModel modifiedLoanSchedule) {
final List<LoanScheduleModelPeriod> periods = modifiedLoanSchedule.getPeriods();
for (final LoanScheduleModelPeriod scheduledLoanInstallment : modifiedLoanSchedule.getPeriods()) {
if (scheduledLoanInstallment.isRepaymentPeriod() || scheduledLoanInstallment.isDownPaymentPeriod()) {
LoanRepaymentScheduleInstallment existingInstallment = findByInstallmentNumber(loan.getRepaymentScheduleInstallments(),
scheduledLoanInstallment.periodNumber());
if (existingInstallment == null) {
final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan,
scheduledLoanInstallment.periodNumber(), scheduledLoanInstallment.periodFromDate(),
scheduledLoanInstallment.periodDueDate(), scheduledLoanInstallment.principalDue(),
scheduledLoanInstallment.interestDue(), scheduledLoanInstallment.feeChargesDue(),
scheduledLoanInstallment.penaltyChargesDue(), scheduledLoanInstallment.isRecalculatedInterestComponent(),
scheduledLoanInstallment.getLoanCompoundingDetails(), scheduledLoanInstallment.rescheduleInterestPortion(),
scheduledLoanInstallment.isDownPaymentPeriod());
loan.addLoanRepaymentScheduleInstallment(installment);
} else {
existingInstallment.copyFrom(scheduledLoanInstallment);
}
}
}
// Review Installments removed
loan.getRepaymentScheduleInstallments().removeIf(i -> !existInstallment(periods, i.getInstallmentNumber()));
loan.updateLoanScheduleDependentDerivedFields();
loan.updateLoanSummaryDerivedFields();
}
public void updateLoanSchedule(Loan loan, final List<LoanRepaymentScheduleInstallment> installments) {
for (final LoanRepaymentScheduleInstallment installment : installments) {
LoanRepaymentScheduleInstallment existingInstallment = findByInstallmentNumber(loan.getRepaymentScheduleInstallments(),
installment.getInstallmentNumber());
if (existingInstallment != null) {
existingInstallment.copyFrom(installment);
} else {
loan.addLoanRepaymentScheduleInstallment(installment);
}
}
// Review Installments removed
loan.getRepaymentScheduleInstallments().removeIf(i -> !existInstallment(installments, i.getInstallmentNumber()));
loan.updateLoanScheduleDependentDerivedFields();
loan.updateLoanSummaryDerivedFields();
}
private LoanRepaymentScheduleInstallment findByInstallmentNumber(final Collection<LoanRepaymentScheduleInstallment> installments,
final Integer installmentNumber) {
return installments.stream().filter(i -> installmentNumber.compareTo(i.getInstallmentNumber()) == 0).findFirst().orElse(null);
}
private boolean existInstallment(final Collection<LoanRepaymentScheduleInstallment> installments, final Integer installmentNumber) {
return installments.stream().anyMatch(i -> installmentNumber.compareTo(i.getInstallmentNumber()) == 0);
}
private boolean existInstallment(final List<LoanScheduleModelPeriod> periods, final Integer installmentNumber) {
return periods.stream().anyMatch(p -> p.periodNumber() != null && installmentNumber.compareTo(p.periodNumber()) == 0);
}
}
@@ -91,6 +91,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Mon
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService;
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.PeriodDueDetails;
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
@@ -114,13 +115,15 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
private final EMICalculator emiCalculator;
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final InterestRefundService interestRefundService;
private final LoanScheduleComponent loanSchedule;
public AdvancedPaymentScheduleTransactionProcessor(EMICalculator emiCalculator, LoanRepositoryWrapper loanRepositoryWrapper,
InterestRefundService interestRefundService, ExternalIdFactory externalIdFactory) {
InterestRefundService interestRefundService, ExternalIdFactory externalIdFactory, LoanScheduleComponent loanSchedule) {
super(externalIdFactory);
this.emiCalculator = emiCalculator;
this.loanRepositoryWrapper = loanRepositoryWrapper;
this.interestRefundService = interestRefundService;
this.loanSchedule = loanSchedule;
}
@Override
@@ -1460,8 +1463,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
}
loan.updateLoanSchedule(installmentsUpToTransactionDate);
loan.updateLoanScheduleDependentDerivedFields();
loanSchedule.updateLoanSchedule(loan, installmentsUpToTransactionDate);
if (transactionCtx instanceof ProgressiveTransactionCtx progressiveTransactionCtx && loan.isInterestRecalculationEnabled()) {
updateRepaymentPeriodsAfterChargeOff(progressiveTransactionCtx, transactionDate, transactionsToBeReprocessed);
@@ -69,6 +69,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
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;
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
@@ -98,6 +99,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
private AdvancedPaymentScheduleTransactionProcessor underTest;
private static final EMICalculator emiCalculator = Mockito.mock(EMICalculator.class);
private static final LoanRepositoryWrapper loanRepositoryWrapper = Mockito.mock(LoanRepositoryWrapper.class);
private static final LoanScheduleComponent loanSchedule = Mockito.mock(LoanScheduleComponent.class);
@BeforeAll
public static void init() {
@@ -112,7 +114,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
@BeforeEach
public void setUp() {
underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper, null, null);
underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper, null, null, loanSchedule);
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
@@ -79,6 +79,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
import org.apache.fineract.useradministration.domain.AppUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -115,6 +116,7 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private final LoanChargeService loanChargeService;
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
private final LoanScheduleComponent loanSchedule;
/**
* create a new instance of the LoanRescheduleRequest object from the JsonCommand object and persist
@@ -423,14 +425,14 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
loanApplicationTerms.getInterestMethod());
final LoanLifecycleStateMachine loanLifecycleStateMachine = null;
loan.setHelpers(loanLifecycleStateMachine);
final LoanScheduleDTO loanSchedule = loanScheduleGenerator.rescheduleNextInstallments(mathContext, loanApplicationTerms, loan,
final LoanScheduleDTO loanScheduleDTO = loanScheduleGenerator.rescheduleNextInstallments(mathContext, loanApplicationTerms, loan,
loanApplicationTerms.getHolidayDetailDTO(), loanRepaymentScheduleTransactionProcessor, rescheduleFromDate);
// Either the installments got recalculated or the model
if (loanSchedule.getInstallments() != null) {
loan.updateLoanSchedule(loanSchedule.getInstallments());
if (loanScheduleDTO.getInstallments() != null) {
loanSchedule.updateLoanSchedule(loan, loanScheduleDTO.getInstallments());
} else {
loan.updateLoanSchedule(loanSchedule.getLoanScheduleModel());
loanSchedule.updateLoanSchedule(loan, loanScheduleDTO.getLoanScheduleModel());
}
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
loanChargeService.recalculateAllCharges(loan);
@@ -93,6 +93,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanSchedu
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleCalculationPlatformService;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanChargeMapper;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanCollateralManagementMapper;
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
@@ -137,6 +138,7 @@ public class LoanAssemblerImpl implements LoanAssembler {
private final LoanDisbursementService loanDisbursementService;
private final LoanChargeService loanChargeService;
private final LoanOfficerService loanOfficerService;
private final LoanScheduleComponent loanSchedule;
@Override
public Loan assembleFrom(final Long accountId) {
@@ -293,6 +295,8 @@ public class LoanAssemblerImpl implements LoanAssembler {
throw new IllegalStateException("No loan application exists for either a client or group (or both).");
}
loanSchedule.updateLoanSchedule(loanApplication, loanScheduleModel);
copyAdvancedPaymentRulesIfApplicable(transactionProcessingStrategyCode, loanProduct, loanApplication);
loanApplication.setHelpers(defaultLoanLifecycleStateMachine);
// TODO: review
@@ -858,8 +862,8 @@ public class LoanAssemblerImpl implements LoanAssembler {
final JsonElement parsedQuery = this.fromApiJsonHelper.parse(command.json());
final JsonQuery query = JsonQuery.from(command.json(), parsedQuery, this.fromApiJsonHelper);
final LoanScheduleModel loanSchedule = this.calculationPlatformService.calculateLoanSchedule(query, false);
loan.updateLoanSchedule(loanSchedule);
final LoanScheduleModel loanScheduleModel = this.calculationPlatformService.calculateLoanSchedule(query, false);
loanSchedule.updateLoanSchedule(loan, loanScheduleModel);
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
loanChargeService.recalculateAllCharges(loan);
}
@@ -34,6 +34,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.RBILoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanInterestRefundServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
@@ -118,8 +119,9 @@ public class LoanAccountAutoStarter {
@Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class)
public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(EMICalculator emiCalculator,
LoanRepositoryWrapper loanRepositoryWrapper,
@Lazy ProgressiveLoanInterestRefundServiceImpl progressiveLoanInterestRefundService, ExternalIdFactory externalIdFactory) {
@Lazy ProgressiveLoanInterestRefundServiceImpl progressiveLoanInterestRefundService, ExternalIdFactory externalIdFactory,
LoanScheduleComponent loanSchedule) {
return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper, progressiveLoanInterestRefundService,
externalIdFactory);
externalIdFactory, loanSchedule);
}
}
@@ -146,6 +146,7 @@ import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusi
import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
import org.apache.fineract.portfolio.loanaccount.service.adjustment.LoanAdjustmentService;
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
import org.apache.fineract.portfolio.loanproduct.service.LoanDropdownReadPlatformService;
import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService;
@@ -240,14 +241,15 @@ public class LoanAccountConfiguration {
LoanCollateralAssembler loanCollateralAssembler, LoanScheduleCalculationPlatformService calculationPlatformService,
LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler, LoanChargeMapper loanChargeMapper,
LoanCollateralManagementMapper loanCollateralManagementMapper, LoanAccrualsProcessingService loanAccrualsProcessingService,
LoanDisbursementService loanDisbursementService, LoanChargeService loanChargeService, LoanOfficerService loanOfficerService) {
LoanDisbursementService loanDisbursementService, LoanChargeService loanChargeService, LoanOfficerService loanOfficerService,
LoanScheduleComponent loanSchedule) {
return new LoanAssemblerImpl(fromApiJsonHelper, loanRepository, loanProductRepository, clientRepository, groupRepository,
fundRepository, staffRepository, codeValueRepository, loanScheduleAssembler, loanChargeAssembler, collateralAssembler,
loanRepaymentScheduleTransactionProcessorFactory, holidayRepository, configurationDomainService, workingDaysRepository,
rateAssembler, defaultLoanLifecycleStateMachine, externalIdFactory, accountNumberFormatRepository, glimRepository,
accountNumberGenerator, glimAccountInfoWritePlatformService, loanCollateralAssembler, calculationPlatformService,
loanDisbursementDetailsAssembler, loanChargeMapper, loanCollateralManagementMapper, loanAccrualsProcessingService,
loanDisbursementService, loanChargeService, loanOfficerService);
loanDisbursementService, loanChargeService, loanOfficerService, loanSchedule);
}
@Bean
@@ -461,8 +463,8 @@ public class LoanAccountConfiguration {
@ConditionalOnMissingBean(LoanScheduleService.class)
public LoanScheduleService loanScheduleService(LoanChargeService loanChargeService,
ReprocessLoanTransactionsService reprocessLoanTransactionsService,
LoanTransactionProcessingService loanTransactionProcessingService) {
return new LoanScheduleService(loanChargeService, reprocessLoanTransactionsService, loanTransactionProcessingService);
LoanTransactionProcessingService loanTransactionProcessingService, LoanScheduleComponent loanSchedule) {
return new LoanScheduleService(loanChargeService, reprocessLoanTransactionsService, loanTransactionProcessingService, loanSchedule);
}
@Bean
@@ -5310,8 +5310,8 @@ public class ClientLoanIntegrationTest extends BaseLoanIntegrationTest {
ArrayList<HashMap> loanSchedule = LOAN_TRANSACTION_HELPER.getLoanRepaymentSchedule(REQUEST_SPEC, RESPONSE_SPEC, loanID);
assertEquals(2, loanSchedule.size());
assertEquals(0, loanSchedule.get(1).get("penaltyChargesDue"));
assertEquals(0, loanSchedule.get(1).get("penaltyChargesOutstanding"));
assertEquals(0.0f, loanSchedule.get(1).get("penaltyChargesDue"));
assertEquals(0.0f, loanSchedule.get(1).get("penaltyChargesOutstanding"));
assertEquals(1000.0f, loanSchedule.get(1).get("totalDueForPeriod"));
assertEquals(1000.0f, loanSchedule.get(1).get("totalOutstandingForPeriod"));
LocalDate targetDate = LocalDate.of(2022, 9, 7);
@@ -5906,10 +5906,10 @@ public class ClientLoanIntegrationTest extends BaseLoanIntegrationTest {
ArrayList<HashMap> loanSchedule = LOAN_TRANSACTION_HELPER.getLoanRepaymentSchedule(REQUEST_SPEC, RESPONSE_SPEC, loanID);
assertEquals(2, loanSchedule.size());
assertEquals(0, loanSchedule.get(1).get("feeChargesDue"));
assertEquals(0, loanSchedule.get(1).get("feeChargesOutstanding"));
assertEquals(0, loanSchedule.get(1).get("penaltyChargesDue"));
assertEquals(0, loanSchedule.get(1).get("penaltyChargesOutstanding"));
assertEquals(0.0f, loanSchedule.get(1).get("feeChargesDue"));
assertEquals(0.0f, loanSchedule.get(1).get("feeChargesOutstanding"));
assertEquals(0.0f, loanSchedule.get(1).get("penaltyChargesDue"));
assertEquals(0.0f, loanSchedule.get(1).get("penaltyChargesOutstanding"));
assertEquals(1000.0f, loanSchedule.get(1).get("totalDueForPeriod"));
assertEquals(1000.0f, loanSchedule.get(1).get("totalOutstandingForPeriod"));
LocalDate targetDate = LocalDate.of(2022, 9, 7);
@@ -173,10 +173,10 @@ public class LoanSpecificDueDateChargeAfterMaturityTest extends BaseLoanIntegrat
ArrayList<HashMap> loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID);
assertEquals(2, loanSchedule.size());
assertEquals(0, loanSchedule.get(1).get("feeChargesDue"));
assertEquals(0, loanSchedule.get(1).get("feeChargesOutstanding"));
assertEquals(0, loanSchedule.get(1).get("penaltyChargesDue"));
assertEquals(0, loanSchedule.get(1).get("penaltyChargesOutstanding"));
assertEquals(0.0f, loanSchedule.get(1).get("feeChargesDue"));
assertEquals(0.0f, loanSchedule.get(1).get("feeChargesOutstanding"));
assertEquals(0.0f, loanSchedule.get(1).get("penaltyChargesDue"));
assertEquals(0.0f, loanSchedule.get(1).get("penaltyChargesOutstanding"));
assertEquals(10000.0f, loanSchedule.get(1).get("totalDueForPeriod"));
assertEquals(10000.0f, loanSchedule.get(1).get("totalOutstandingForPeriod"));
targetDate = LocalDate.of(2011, 4, 5);
@@ -306,10 +306,10 @@ public class LoanSpecificDueDateChargeAfterMaturityTest extends BaseLoanIntegrat
ArrayList<HashMap> loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID);
assertEquals(2, loanSchedule.size());
assertEquals(0, loanSchedule.get(1).get("feeChargesDue"));
assertEquals(0, loanSchedule.get(1).get("feeChargesOutstanding"));
assertEquals(0, loanSchedule.get(1).get("penaltyChargesDue"));
assertEquals(0, loanSchedule.get(1).get("penaltyChargesOutstanding"));
assertEquals(0.0f, loanSchedule.get(1).get("feeChargesDue"));
assertEquals(0.0f, loanSchedule.get(1).get("feeChargesOutstanding"));
assertEquals(0.0f, loanSchedule.get(1).get("penaltyChargesDue"));
assertEquals(0.0f, loanSchedule.get(1).get("penaltyChargesOutstanding"));
assertEquals(10000.0f, loanSchedule.get(1).get("totalDueForPeriod"));
assertEquals(10000.0f, loanSchedule.get(1).get("totalOutstandingForPeriod"));
@@ -844,7 +844,7 @@ public class SchedulerJobsTestResults extends IntegrationTest {
ArrayList<HashMap> repaymentScheduleDataAfter = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec,
loanID);
Assertions.assertEquals(0, (Integer) repaymentScheduleDataAfter.get(1).get("penaltyChargesDue"),
Assertions.assertEquals(0.0f, repaymentScheduleDataAfter.get(1).get("penaltyChargesDue"),
"Verifying From Penalty Charges due fot first Repayment after Successful completion of Scheduler Job");
}
@@ -1085,7 +1085,7 @@ public class SchedulerJobsTestResults extends IntegrationTest {
this.schedulerJobHelper.executeAndAwaitJob(jobName);
List<HashMap> repaymentScheduleDataAfter = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec,
loanID);
Assertions.assertEquals(0, (Integer) repaymentScheduleDataAfter.get(1).get("penaltyChargesDue"),
Assertions.assertEquals(0.0f, repaymentScheduleDataAfter.get(1).get("penaltyChargesDue"),
"Verifying From Penalty Charges due fot first Repayment after Successful completion of Scheduler Job");
LocalDate lastBusinessDateBeforeFastForward = LocalDate.of(2019, 4, 2);
@@ -1153,7 +1153,7 @@ public class SchedulerJobsTestResults extends IntegrationTest {
this.schedulerJobHelper.executeAndAwaitJob(jobName);
List<HashMap> repaymentScheduleDataAfter = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec,
loanID2);
Assertions.assertEquals(0, (Integer) repaymentScheduleDataAfter.get(1).get("penaltyChargesDue"),
Assertions.assertEquals(0.0f, repaymentScheduleDataAfter.get(1).get("penaltyChargesDue"),
"Verifying From Penalty Charges due fot first Repayment after Successful completion of Scheduler Job");
BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.COB_DATE, LocalDate.of(2020, 5, 3));