Skip to content

Commit

Permalink
FINERACT-1981: EMICalculator for Advanced payment transaction processor
Browse files Browse the repository at this point in the history
  • Loading branch information
ruchiD committed Jul 16, 2024
1 parent 6c4932b commit fe61d0d
Show file tree
Hide file tree
Showing 7 changed files with 342 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,11 @@ public void resetBalances() {
resetDerivedComponents();
resetPrincipalDue();
resetChargesCharged();
resetInterestCharged();
}

private void resetInterestCharged() {
this.interestCharged = null;
}

public void resetPrincipalDue() {
Expand All @@ -1064,4 +1069,12 @@ public enum PaymentAction {
public boolean isReAged() {
return isReAged;
}

public void addToInterestCharged(final Money interestAmount) {
if (this.interestCharged == null) {
this.interestCharged = interestAmount.getAmount();
} else {
this.interestCharged = this.interestCharged.add(interestAmount.getAmount());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.HashSet;
import java.util.Set;
import lombok.Data;
import lombok.experimental.Accessors;
Expand All @@ -43,6 +44,7 @@ public class PreGeneratedLoanSchedulePeriod implements LoanScheduleModelPeriod {
private BigDecimal rescheduleInterestPortion;
private boolean isRecalculatedInterestComponent;
private boolean isEMIFixedSpecificToInstallment;
private Set<LoanInterestRecalcualtionAdditionalDetails> loanCompoundingDetails;

public PreGeneratedLoanSchedulePeriod(Integer periodNumber, LocalDate periodFromDate, LocalDate periodDueDate) {
this.periodNumber = periodNumber;
Expand All @@ -57,6 +59,7 @@ public PreGeneratedLoanSchedulePeriod(Integer periodNumber, LocalDate periodFrom
this.rescheduleInterestPortion = BigDecimal.ZERO;
this.isRecalculatedInterestComponent = false;
this.isEMIFixedSpecificToInstallment = false;
this.loanCompoundingDetails = new HashSet<>();
}

@Override
Expand All @@ -81,7 +84,7 @@ public void addInterestAmount(Money interestDue) {

@Override
public Set<LoanInterestRecalcualtionAdditionalDetails> getLoanCompoundingDetails() {
return Set.of();
return this.loanCompoundingDetails;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* 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.loanschedule.service;

import java.math.BigDecimal;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.PreGeneratedLoanSchedulePeriod;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;

@Mapper(config = MapstructMapperConfig.class)
public interface LoanRepaymentSchedulePeriodMapper {

@Mapping(target = "periodNumber", source = "installmentNumber")
@Mapping(target = "periodFromDate", source = "fromDate")
@Mapping(target = "periodDueDate", source = "dueDate")
@Mapping(target = "principalDue", source = "source", qualifiedByName = "toPrincipalDue")
@Mapping(target = "interestDue", source = "source", qualifiedByName = "toInterestDue")
@Mapping(target = "feeChargesDue", source = "source", qualifiedByName = "toFeeChargesDue")
@Mapping(target = "penaltyChargesDue", source = "source", qualifiedByName = "toPenaltyChargesDue")
@Mapping(target = "rescheduleInterestPortion", source = "source", qualifiedByName = "toRescheduleInterestPortion")
@Mapping(target = "isRecalculatedInterestComponent", source = "source", qualifiedByName = "toIsRecalculatedInterestComponent")
@Mapping(target = "isEMIFixedSpecificToInstallment", source = "source", qualifiedByName = "toIsEMIFixedSpecificToInstallment")
@Mapping(target = "isRepaymentPeriod", source = "source", qualifiedByName = "toIsRepaymentPeriod")
@Mapping(target = "isDownPaymentPeriod", source = "source", qualifiedByName = "toIsDownPaymentPeriod")
@Mapping(target = "loanCompoundingDetails", source = "source", qualifiedByName = "toLoanCompoundingDetails")

PreGeneratedLoanSchedulePeriod map(LoanRepaymentScheduleInstallment source);

List<PreGeneratedLoanSchedulePeriod> map(List<LoanRepaymentScheduleInstallment> source);

@Named("toPrincipalDue")
default BigDecimal toPrincipalDue(LoanRepaymentScheduleInstallment source) {
return BigDecimal.ZERO;
}

@Named("toInterestDue")
default BigDecimal toInterestDue(LoanRepaymentScheduleInstallment source) {
return BigDecimal.ZERO;
}

@Named("toFeeChargesDue")
default BigDecimal toFeeChargesDue(LoanRepaymentScheduleInstallment source) {
return BigDecimal.ZERO;
}

@Named("toPenaltyChargesDue")
default BigDecimal toPenaltyChargesDue(LoanRepaymentScheduleInstallment source) {
return BigDecimal.ZERO;
}

@Named("toIsRecalculatedInterestComponent")
default Boolean toIsRecalculatedInterestComponent(LoanRepaymentScheduleInstallment source) {
return source.isRecalculatedInterestComponent();
}

@Named("toRescheduleInterestPortion")
default BigDecimal toRescheduleInterestPortion(LoanRepaymentScheduleInstallment source) {
return BigDecimal.ZERO;
}

@Named("toIsEMIFixedSpecificToInstallment")
default Boolean toIsEMIFixedSpecificToInstallment(LoanRepaymentScheduleInstallment source) {
return Boolean.FALSE;
}

@Named("toIsRepaymentPeriod")
default Boolean toIsRepaymentPeriod(LoanRepaymentScheduleInstallment source) {
return !source.isDownPayment();
}

@Named("toIsDownPaymentPeriod")
default Boolean toIsDownPaymentPeriod(LoanRepaymentScheduleInstallment source) {
return source.isDownPayment();
}

@Named("toLoanCompoundingDetails")
default Set toLoanCompoundingDetails(LoanRepaymentScheduleInstallment source) {
return new HashSet<>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.PreGeneratedLoanSchedulePeriod;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.PrincipalInterest;
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanRepaymentSchedulePeriodMapper;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculationResult;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType;
import org.apache.fineract.portfolio.loanproduct.domain.DueType;
Expand All @@ -97,6 +102,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep

private final LoanReAgingParameterRepository reAgingParameterRepository;

private final EMICalculator emiCalculator;

private final LoanRepaymentSchedulePeriodMapper repaymentSchedulePeriodMapper;

@Override
public String getCode() {
return ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
Expand Down Expand Up @@ -644,25 +653,33 @@ private void updateLoanSchedule(LoanTransaction disbursementTransaction, Monetar
Money amortizableAmount = disbursementTransaction.getAmount(currency).minus(downPaymentAmount);

if (amortizableAmount.isGreaterThanZero()) {
Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, mc.getRoundingMode());
if (installmentAmountInMultiplesOf != null) {
increasePrincipalBy = Money.roundToMultiplesOf(increasePrincipalBy, installmentAmountInMultiplesOf);
}

Money remainingAmount = amortizableAmount
.minus(increasePrincipalBy.multiplyRetainScale(noCandidateRepaymentInstallments, mc.getRoundingMode()));

Money finalIncreasePrincipalBy = increasePrincipalBy;
candidateRepaymentInstallments
.forEach(i -> i.addToPrincipal(disbursementTransaction.getTransactionDate(), finalIncreasePrincipalBy));
// Hence the rounding, we might need to amend the last installment amount
candidateRepaymentInstallments.get(noCandidateRepaymentInstallments - 1)
.addToPrincipal(disbursementTransaction.getTransactionDate(), remainingAmount);
List<PreGeneratedLoanSchedulePeriod> repaymentScheduleFromInstallments = repaymentSchedulePeriodMapper
.map(candidateRepaymentInstallments);
EMICalculationResult emiCalculationResult = emiCalculator.calculateEMIValueAndRateFactors(amortizableAmount,
loanProductRelatedDetail, repaymentScheduleFromInstallments, 1, noCandidateRepaymentInstallments, mc);
updatePrincipalAndInterestAmountsForInstallments(disbursementTransaction, candidateRepaymentInstallments, emiCalculationResult,
amortizableAmount, installmentAmountInMultiplesOf, mc);
}

allocateOverpayment(disbursementTransaction, currency, installments, overpaymentHolder);
}

private void updatePrincipalAndInterestAmountsForInstallments(LoanTransaction disbursementTransaction,
List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments, EMICalculationResult emiCalculationResult,
Money amortizableAmount, Integer installmentAmountInMultiplesOf, MathContext mc) {
Money outstandingAmount = amortizableAmount;
int installmentNumber = 1;
for (LoanRepaymentScheduleInstallment installment : candidateRepaymentInstallments) {
PrincipalInterest principalInterestForThisPeriod = emiCalculator.calculatePrincipalInterestComponentsForPeriod(
emiCalculationResult, outstandingAmount, installmentAmountInMultiplesOf, installmentNumber,
candidateRepaymentInstallments.size(), mc);
installment.addToPrincipal(disbursementTransaction.getTransactionDate(), principalInterestForThisPeriod.principal());
installment.addToInterestCharged(principalInterestForThisPeriod.interest());
outstandingAmount = outstandingAmount.minus(principalInterestForThisPeriod.principal());
++installmentNumber;
}
}

private void allocateOverpayment(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, MoneyHolder overpaymentHolder) {
if (overpaymentHolder.getMoneyObject().isGreaterThanZero()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanRepaymentSchedulePeriodMapper;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
Expand All @@ -91,6 +93,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
private static final MockedStatic<MoneyHelper> MONEY_HELPER = mockStatic(MoneyHelper.class);
private AdvancedPaymentScheduleTransactionProcessor underTest;
private LoanReAgingParameterRepository reAgingParameterRepository = Mockito.mock(LoanReAgingParameterRepository.class);
private EMICalculator emiCalculator = Mockito.mock(EMICalculator.class);
private LoanRepaymentSchedulePeriodMapper loanRepaymentSchedulePeriodMapper = Mockito.mock(LoanRepaymentSchedulePeriodMapper.class);

@BeforeAll
public static void init() {
Expand All @@ -104,7 +108,8 @@ public static void destruct() {

@BeforeEach
public void setUp() {
underTest = new AdvancedPaymentScheduleTransactionProcessor(reAgingParameterRepository);
underTest = new AdvancedPaymentScheduleTransactionProcessor(reAgingParameterRepository, emiCalculator,
loanRepaymentSchedulePeriodMapper);

ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
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.loanschedule.service.LoanRepaymentSchedulePeriodMapper;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
Expand Down Expand Up @@ -105,8 +107,9 @@ public LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTra
@Bean
@Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class)
public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(
LoanReAgingParameterRepository reAgingParameterRepository) {
return new AdvancedPaymentScheduleTransactionProcessor(reAgingParameterRepository);
LoanReAgingParameterRepository reAgingParameterRepository, EMICalculator emiCalculator,
LoanRepaymentSchedulePeriodMapper repaymentSchedulePeriodMapper) {
return new AdvancedPaymentScheduleTransactionProcessor(reAgingParameterRepository, emiCalculator, repaymentSchedulePeriodMapper);
}

}
Loading

0 comments on commit fe61d0d

Please sign in to comment.