MIFOSX-2228 : added recalcualte from date flexibility for interest recalculation

This commit is contained in:
pramod
2015-10-08 15:51:15 +05:30
parent b732fca525
commit 1c17e73ec2
17 changed files with 866 additions and 142 deletions
@@ -96,6 +96,7 @@ import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentDa
import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerAssignmentException;
import org.mifosplatform.portfolio.loanaccount.exception.LoanOfficerUnassignmentDateException;
import org.mifosplatform.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
@@ -364,6 +365,10 @@ public class Loan extends AbstractPersistable<Long> {
@Column(name = "guarantee_amount_derived", scale = 6, precision = 19, nullable = true)
private BigDecimal guaranteeAmountDerived;
@Temporal(TemporalType.DATE)
@Column(name = "interest_recalcualated_on")
private Date interestRecalculatedOn;
public static Loan newIndividualLoanApplication(final String accountNo, final Client client, final Integer loanType,
final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose,
final LoanTransactionProcessingStrategy transactionProcessingStrategy,
@@ -496,8 +501,8 @@ public class Loan extends AbstractPersistable<Long> {
private Set<LoanCharge> associateChargesWithThisLoan(final Set<LoanCharge> loanCharges) {
for (final LoanCharge loanCharge : loanCharges) {
loanCharge.update(this);
if(loanCharge.getTrancheDisbursementCharge() != null) {
addTrancheLoanCharge(loanCharge.getCharge()) ;
if (loanCharge.getTrancheDisbursementCharge() != null) {
addTrancheLoanCharge(loanCharge.getCharge());
}
}
return loanCharges;
@@ -569,7 +574,7 @@ public class Loan extends AbstractPersistable<Long> {
}
this.charges.add(loanCharge);
this.summary = updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
// store Id's of existing loan transactions and existing reversed loan
@@ -906,9 +911,12 @@ public class Loan extends AbstractPersistable<Long> {
Money chargeComponent = amountWaived;
if (isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
Money receivableCharge = Money.zero(getCurrency());
if(loanInstallmentNumber !=null){
receivableCharge = accruedCharge.minus(loanCharge.getInstallmentLoanCharge(loanInstallmentNumber).getAmountPaid(getCurrency()));
}else{ receivableCharge = accruedCharge.minus(loanCharge.getAmountPaid(getCurrency()));}
if (loanInstallmentNumber != null) {
receivableCharge = accruedCharge.minus(loanCharge.getInstallmentLoanCharge(loanInstallmentNumber).getAmountPaid(
getCurrency()));
} else {
receivableCharge = accruedCharge.minus(loanCharge.getAmountPaid(getCurrency()));
}
if (receivableCharge.isLessThanZero()) {
receivableCharge = amountWaived.zero();
}
@@ -924,10 +932,11 @@ public class Loan extends AbstractPersistable<Long> {
feeChargesWaived = Money.zero(loanCurrency());
}
LocalDate transactionDate = DateUtils.getLocalDateOfTenant();
LocalDate transactionDate = getDisbursementDate();
if (loanCharge.isSpecifiedDueDate()) {
transactionDate = loanCharge.getDueLocalDate();
}
scheduleGeneratorDTO.setRecalculateFrom(transactionDate);
updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
@@ -1107,6 +1116,17 @@ public class Loan extends AbstractPersistable<Long> {
}
public void updateLoanSchedule(final Collection<LoanRepaymentScheduleInstallment> installments, AppUser currentUser) {
this.repaymentScheduleInstallments.clear();
for (final LoanRepaymentScheduleInstallment installment : installments) {
addRepaymentScheduleInstallment(installment);
}
updateLoanScheduleDependentDerivedFields();
updateLoanSummaryDerivedFields();
applyAccurals(currentUser);
}
/**
* method updates accrual derived fields on installments and reverse the
* unprocessed transactions
@@ -1197,8 +1217,8 @@ public class Loan extends AbstractPersistable<Long> {
updateLoanOutstandingBalaces();
}
}
public void updateLoanSummarAndStatus(){
public void updateLoanSummarAndStatus() {
updateLoanSummaryDerivedFields();
doPostLoanTransactionChecks(getLastUserTransactionDate(), loanLifecycleStateMachine);
}
@@ -1485,9 +1505,9 @@ public class Loan extends AbstractPersistable<Long> {
}
public boolean isMultiDisburmentLoan() {
return this.loanProduct.isMultiDisburseLoan() ;
return this.loanProduct.isMultiDisburseLoan();
}
/**
* Update interest recalculation settings if product configuration changes
*/
@@ -1660,12 +1680,13 @@ public class Loan extends AbstractPersistable<Long> {
List<Long> loanChargeIds = fetchLoanChargeIds();
int chargeIdLength = loanChargeIds.size();
String chargeIds = null;
//From modify application page, if user removes all charges, we should get empty array.
//So we need to remove all charges applied for this loan
// From modify application page, if user removes all charges, we should
// get empty array.
// So we need to remove all charges applied for this loan
boolean removeAllChages = false;
if (jsonCommand.parameterExists(LoanApiConstants.chargesParameterName)) {
JsonArray chargesArray = jsonCommand.arrayOfParameterNamed(LoanApiConstants.chargesParameterName);
if (chargesArray.size() == 0) removeAllChages = true;
if (chargesArray.size() == 0) removeAllChages = true;
}
if (jsonCommand.parameterExists(LoanApiConstants.disbursementDataParameterName)) {
@@ -1754,13 +1775,13 @@ public class Loan extends AbstractPersistable<Long> {
this.disbursementDetails.add(disbursementDetails);
for (LoanTrancheCharge trancheCharge : trancheCharges) {
Charge chargeDefinition = trancheCharge.getCharge();
final LoanCharge loanCharge = LoanCharge.createNewWithoutLoan(chargeDefinition, principal, null, null, null,
new LocalDate(expectedDisbursementDate), null, null);
loanCharge.update(this);
LoanTrancheDisbursementCharge loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge,
disbursementDetails);
loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge);
addLoanCharge(loanCharge);
final LoanCharge loanCharge = LoanCharge.createNewWithoutLoan(chargeDefinition, principal, null, null, null, new LocalDate(
expectedDisbursementDate), null, null);
loanCharge.update(this);
LoanTrancheDisbursementCharge loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge,
disbursementDetails);
loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge);
addLoanCharge(loanCharge);
}
actualChanges.put(LoanApiConstants.disbursementDataParameterName, expectedDisbursementDate + "-" + principal);
actualChanges.put("recalculateLoanSchedule", true);
@@ -4857,9 +4878,10 @@ public class Loan extends AbstractPersistable<Long> {
public void regenerateRepaymentScheduleWithInterestRecalculation(final ScheduleGeneratorDTO generatorDTO, final AppUser currentUser) {
LocalDate lastTransactionDate = getLastUserTransactionDate();
final LoanScheduleModel loanSchedule = getRecalculatedSchedule(generatorDTO);
final LoanScheduleDTO loanSchedule = getRecalculatedSchedule(generatorDTO);
if (loanSchedule == null) { return; }
updateLoanSchedule(loanSchedule, currentUser);
updateLoanSchedule(loanSchedule.getInstallments(), currentUser);
this.interestRecalculatedOn = DateUtils.getDateOfTenant();
LocalDate lastRepaymentDate = this.getLastRepaymentPeriodDueDate();
Set<LoanCharge> charges = this.charges();
for (LoanCharge loanCharge : charges) {
@@ -4896,7 +4918,7 @@ public class Loan extends AbstractPersistable<Long> {
}
}
private LoanScheduleModel getRecalculatedSchedule(final ScheduleGeneratorDTO generatorDTO) {
private LoanScheduleDTO getRecalculatedSchedule(final ScheduleGeneratorDTO generatorDTO) {
if (!this.repaymentScheduleDetail().isInterestRecalculationEnabled() || isNpa || !isOpen()) { return null; }
final InterestMethod interestMethod = this.loanRepaymentScheduleDetail.getInterestMethod();
@@ -4912,10 +4934,9 @@ public class Loan extends AbstractPersistable<Long> {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
final LoanScheduleModel loanSchedule = loanScheduleGenerator.rescheduleNextInstallments(mc, loanApplicationTerms, charges(),
generatorDTO.getHolidayDetailDTO(), retreiveListOfTransactionsPostDisbursementExcludeAccruals(),
loanRepaymentScheduleTransactionProcessor);
return loanSchedule;
return loanScheduleGenerator.rescheduleNextInstallments(mc, loanApplicationTerms, charges(), generatorDTO.getHolidayDetailDTO(),
retreiveListOfTransactionsPostDisbursementExcludeAccruals(), loanRepaymentScheduleTransactionProcessor,
repaymentScheduleInstallments, generatorDTO.getRecalculateFrom());
}
public LoanApplicationTerms constructLoanApplicationTerms(final ApplicationCurrency applicationCurrency,
@@ -5017,8 +5038,8 @@ public class Loan extends AbstractPersistable<Long> {
getApprovedPrincipal());
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy);
installment = loanScheduleGenerator.calculatePrepaymentAmount(this.repaymentScheduleInstallments, getCurrency(), onDate,
loanApplicationTerms, mc, charges(), holidayDetailDTO, retreiveListOfTransactionsPostDisbursementExcludeAccruals(),
installment = loanScheduleGenerator.calculatePrepaymentAmount(getCurrency(), onDate, loanApplicationTerms, mc, charges(),
holidayDetailDTO, retreiveListOfTransactionsPostDisbursementExcludeAccruals(),
loanRepaymentScheduleTransactionProcessor);
} else {
installment = this.getTotalOutstandingOnLoan();
@@ -5053,6 +5074,16 @@ public class Loan extends AbstractPersistable<Long> {
return accruedTill;
}
public LocalDate fetchInterestRecalculateFromDate() {
LocalDate interestRecalculatedOn = null;
if (this.interestRecalculatedOn != null) {
interestRecalculatedOn = getDisbursementDate();
} else {
interestRecalculatedOn = new LocalDate(this.interestRecalculatedOn);
}
return interestRecalculatedOn;
}
private void updateLoanOutstandingBalaces() {
Money outstanding = Money.zero(getCurrency());
List<LoanTransaction> loanTransactions = retreiveListOfTransactionsExcludeAccruals();
@@ -5473,8 +5504,8 @@ public class Loan extends AbstractPersistable<Long> {
}
public void addTrancheLoanCharge(Charge charge) {
if(!trancheCharges.contains(charge)) {
trancheCharges.add(new LoanTrancheCharge(charge, this));
if (!trancheCharges.contains(charge)) {
trancheCharges.add(new LoanTrancheCharge(charge, this));
}
}
@@ -186,6 +186,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
overdurPenaltyWaitPeriod = this.configurationDomainService.retrievePenaltyWaitPeriod();
recalculateFrom = transactionDate;
}
HolidayDetailDTO holidayDetailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays, allowTransactionsOnHoliday,
allowTransactionsOnNonWorkingDay);
@@ -319,6 +319,7 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
public void reverse() {
this.reversed = true;
this.loanTransactionToRepaymentScheduleMappings.clear();
}
public void resetDerivedComponents() {
@@ -0,0 +1,38 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.portfolio.loanaccount.loanschedule.data;
import java.util.List;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
/**
* Transfer object to return the schedule after generation of schedule
*/
public class LoanScheduleDTO {
private final List<LoanRepaymentScheduleInstallment> installments;
private final LoanScheduleModel loanScheduleModel;
private LoanScheduleDTO(final List<LoanRepaymentScheduleInstallment> installments, final LoanScheduleModel loanScheduleModel) {
this.installments = installments;
this.loanScheduleModel = loanScheduleModel;
}
public static LoanScheduleDTO from(final List<LoanRepaymentScheduleInstallment> installments, final LoanScheduleModel loanScheduleModel){
return new LoanScheduleDTO(installments, loanScheduleModel);
}
public List<LoanRepaymentScheduleInstallment> getInstallments() {
return this.installments;
}
public LoanScheduleModel getLoanScheduleModel() {
return this.loanScheduleModel;
}
}
@@ -0,0 +1,203 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.portfolio.loanaccount.loanschedule.data;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.joda.time.LocalDate;
import org.mifosplatform.organisation.monetary.domain.Money;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.RecalculationDetail;
public class LoanScheduleRecalculationDTO {
private final int periodNumber;
private final int instalmentNumber;
private final LocalDate periodStartDate;
private final LocalDate actualRepaymentDate;
private final Money totalCumulativePrincipal;
private final Money totalCumulativeInterest;
private final Money totalFeeChargesCharged;
private final Money totalPenaltyChargesCharged;
private final Money totalRepaymentExpected;
private final Money reducePrincipal;
private final Map<LocalDate, Money> principalPortionMap;
private final Map<LocalDate, Money> latePaymentMap;
private final TreeMap<LocalDate, Money> compoundingMap;
private final Map<LocalDate, Money> disburseDetailMap;
private final Money outstandingBalance;
private final Money outstandingBalanceAsPerRest;
private final List<LoanRepaymentScheduleInstallment> installments;
private final Collection<RecalculationDetail> recalculationDetails;
private final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor;
private final LocalDate scheduleTillDate;
private final boolean partialUpdate;
private LoanScheduleRecalculationDTO(final int periodNumber, final int instalmentNumber, LocalDate periodStartDate,
final LocalDate actualRepaymentDate, final Money totalCumulativePrincipal, final Money totalCumulativeInterest,
final Money totalFeeChargesCharged, final Money totalPenaltyChargesCharged, final Money totalRepaymentExpected,
final Money reducePrincipal, final Map<LocalDate, Money> principalPortionMap, final Map<LocalDate, Money> latePaymentMap,
final TreeMap<LocalDate, Money> compoundingMap, final Map<LocalDate, Money> disburseDetailMap, final Money outstandingBalance,
final Money outstandingBalanceAsPerRest, final List<LoanRepaymentScheduleInstallment> installments,
final Collection<RecalculationDetail> recalculationDetails,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final LocalDate scheduleTillDate,
final boolean partialUpdate) {
this.periodNumber = periodNumber;
this.instalmentNumber = instalmentNumber;
this.periodStartDate = periodStartDate;
this.actualRepaymentDate = actualRepaymentDate;
this.totalCumulativePrincipal = totalCumulativePrincipal;
this.totalCumulativeInterest = totalCumulativeInterest;
this.totalFeeChargesCharged = totalFeeChargesCharged;
this.totalPenaltyChargesCharged = totalPenaltyChargesCharged;
this.totalRepaymentExpected = totalRepaymentExpected;
this.reducePrincipal = reducePrincipal;
this.principalPortionMap = principalPortionMap;
this.latePaymentMap = latePaymentMap;
this.compoundingMap = compoundingMap;
this.disburseDetailMap = disburseDetailMap;
this.outstandingBalance = outstandingBalance;
this.outstandingBalanceAsPerRest = outstandingBalanceAsPerRest;
this.installments = installments;
this.recalculationDetails = recalculationDetails;
this.loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessor;
this.scheduleTillDate = scheduleTillDate;
this.partialUpdate = partialUpdate;
}
public static LoanScheduleRecalculationDTO createLoanScheduleDTOForPartialUpdate(final int periodNumber, final int instalmentNumber,
LocalDate periodStartDate, final LocalDate actualRepaymentDate, final Money totalCumulativePrincipal,
final Money totalCumulativeInterest, final Money totalFeeChargesCharged, final Money totalPenaltyChargesCharged,
final Money totalRepaymentExpected, final Money reducePrincipal, final Map<LocalDate, Money> principalPortionMap,
final Map<LocalDate, Money> latePaymentMap, final TreeMap<LocalDate, Money> compoundingMap,
final Map<LocalDate, Money> disburseDetailMap, final Money outstandingBalance, final Money outstandingBalanceAsPerRest,
final List<LoanRepaymentScheduleInstallment> installments, final Collection<RecalculationDetail> recalculationDetails,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final LocalDate scheduleTillDate) {
final boolean partialUpdate = true;
return new LoanScheduleRecalculationDTO(periodNumber, instalmentNumber, periodStartDate, actualRepaymentDate,
totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged, totalPenaltyChargesCharged,
totalRepaymentExpected, reducePrincipal, principalPortionMap, latePaymentMap, compoundingMap, disburseDetailMap,
outstandingBalance, outstandingBalanceAsPerRest, installments, recalculationDetails,
loanRepaymentScheduleTransactionProcessor, scheduleTillDate, partialUpdate);
}
public static LoanScheduleRecalculationDTO createLoanScheduleDTOForCompleteUpdate(
final Collection<RecalculationDetail> recalculationDetails,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final LocalDate scheduleTillDate) {
final int periodNumber = 1;
final int instalmentNumber = 1;
final LocalDate periodStartDate = null;
final LocalDate actualRepaymentDate = null;
final Money totalCumulativePrincipal = null;
final Money totalCumulativeInterest = null;
final Money totalFeeChargesCharged = null;
final Money totalPenaltyChargesCharged = null;
final Money totalRepaymentExpected = null;
final Money reducePrincipal = null;
final Map<LocalDate, Money> principalPortionMap = null;
final Map<LocalDate, Money> latePaymentMap = null;
final TreeMap<LocalDate, Money> compoundingMap = null;
final Map<LocalDate, Money> disburseDetailMap = null;
final Money outstandingBalance = null;
final Money outstandingBalanceAsPerRest = null;
final List<LoanRepaymentScheduleInstallment> installments = null;
final boolean partialUpdate = false;
return new LoanScheduleRecalculationDTO(periodNumber, instalmentNumber, periodStartDate, actualRepaymentDate,
totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged, totalPenaltyChargesCharged,
totalRepaymentExpected, reducePrincipal, principalPortionMap, latePaymentMap, compoundingMap, disburseDetailMap,
outstandingBalance, outstandingBalanceAsPerRest, installments, recalculationDetails,
loanRepaymentScheduleTransactionProcessor, scheduleTillDate, partialUpdate);
}
public int getPeriodNumber() {
return this.periodNumber;
}
public int getInstalmentNumber() {
return this.instalmentNumber;
}
public LocalDate getActualRepaymentDate() {
return this.actualRepaymentDate;
}
public Money getTotalCumulativePrincipal() {
return this.totalCumulativePrincipal;
}
public Money getTotalCumulativeInterest() {
return this.totalCumulativeInterest;
}
public Money getTotalFeeChargesCharged() {
return this.totalFeeChargesCharged;
}
public Money getTotalPenaltyChargesCharged() {
return this.totalPenaltyChargesCharged;
}
public Money getTotalRepaymentExpected() {
return this.totalRepaymentExpected;
}
public Money getReducePrincipal() {
return this.reducePrincipal;
}
public Map<LocalDate, Money> getPrincipalPortionMap() {
return this.principalPortionMap;
}
public Map<LocalDate, Money> getLatePaymentMap() {
return this.latePaymentMap;
}
public TreeMap<LocalDate, Money> getCompoundingMap() {
return this.compoundingMap;
}
public Map<LocalDate, Money> getDisburseDetailMap() {
return this.disburseDetailMap;
}
public Money getOutstandingBalance() {
return this.outstandingBalance;
}
public Money getOutstandingBalanceAsPerRest() {
return this.outstandingBalanceAsPerRest;
}
public List<LoanRepaymentScheduleInstallment> getInstallments() {
return this.installments;
}
public Collection<RecalculationDetail> getRecalculationDetails() {
return this.recalculationDetails;
}
public LoanRepaymentScheduleTransactionProcessor getLoanRepaymentScheduleTransactionProcessor() {
return this.loanRepaymentScheduleTransactionProcessor;
}
public LocalDate getScheduleTillDate() {
return this.scheduleTillDate;
}
public boolean isPartialUpdate() {
return this.partialUpdate;
}
public LocalDate getPeriodStartDate() {
return this.periodStartDate;
}
}
@@ -7,7 +7,15 @@ package org.mifosplatform.portfolio.loanaccount.loanschedule.domain;
import java.math.BigDecimal;
import java.math.MathContext;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.joda.time.Days;
import org.joda.time.LocalDate;
@@ -22,8 +30,14 @@ import org.mifosplatform.portfolio.calendar.service.CalendarUtils;
import org.mifosplatform.portfolio.common.domain.PeriodFrequencyType;
import org.mifosplatform.portfolio.loanaccount.data.DisbursementData;
import org.mifosplatform.portfolio.loanaccount.data.HolidayDetailDTO;
import org.mifosplatform.portfolio.loanaccount.domain.*;
import org.mifosplatform.portfolio.loanaccount.domain.Loan;
import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.mifosplatform.portfolio.loanaccount.domain.LoanSummary;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction;
import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleRecalculationDTO;
import org.mifosplatform.portfolio.loanaccount.loanschedule.exception.MultiDisbursementEmiAmountException;
import org.mifosplatform.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException;
import org.mifosplatform.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleModel;
@@ -42,16 +56,13 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
@Override
public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms,
final Set<LoanCharge> loanCharges, final HolidayDetailDTO holidayDetailDTO) {
final Collection<RecalculationDetail> diffAmt = null;
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = null;
final LocalDate scheduleTillDate = null;
return generate(mc, loanApplicationTerms, loanCharges, holidayDetailDTO, diffAmt, loanRepaymentScheduleTransactionProcessor,
scheduleTillDate);
final LoanScheduleRecalculationDTO loanScheduleRecalculationDTO = null;
return generate(mc, loanApplicationTerms, loanCharges, holidayDetailDTO, loanScheduleRecalculationDTO);
}
private LoanScheduleModel generate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms,
final Set<LoanCharge> loanCharges, final HolidayDetailDTO holidayDetailDTO, final Collection<RecalculationDetail> transactions,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, final LocalDate scheduleTillDate) {
final Set<LoanCharge> loanCharges, final HolidayDetailDTO holidayDetailDTO,
final LoanScheduleRecalculationDTO loanScheduleRecalculationDTO) {
final ApplicationCurrency applicationCurrency = loanApplicationTerms.getApplicationCurrency();
// generate list of proposed schedule due dates
@@ -63,16 +74,17 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
// setup variables for tracking important facts required for loan
// schedule generation.
Money principalToBeScheduled;
if (loanApplicationTerms.isMultiDisburseLoan() && loanApplicationTerms.getApprovedPrincipal().isGreaterThanZero()) {
principalToBeScheduled = loanApplicationTerms.getApprovedPrincipal();
} else {
principalToBeScheduled = loanApplicationTerms.getPrincipal();
}
Money principalToBeScheduled = getPrincipalToBeScheduled(loanApplicationTerms);
final MonetaryCurrency currency = principalToBeScheduled.getCurrency();
final int numberOfRepayments = loanApplicationTerms.getNumberOfRepayments();
final LocalDate scheduleTillDate = loanScheduleRecalculationDTO == null ? null : loanScheduleRecalculationDTO.getScheduleTillDate();
final Collection<RecalculationDetail> transactions = loanScheduleRecalculationDTO == null ? null : loanScheduleRecalculationDTO
.getRecalculationDetails();
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanScheduleRecalculationDTO == null ? null
: loanScheduleRecalculationDTO.getLoanRepaymentScheduleTransactionProcessor();
// variables for cumulative totals
int loanTermInDays = Integer.valueOf(0);
final BigDecimal totalPrincipalPaid = BigDecimal.ZERO;
@@ -107,10 +119,13 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
// Actual period Number plus interest only repayments
int instalmentNumber = 1;
// actual outstanding balance for interest calculation
Money outstandingBalance = principalToBeScheduled;
// Set fixed EMI Amount
updateFixedInstallmentAmount(mc, loanApplicationTerms, actualRepaymentDate, periodNumber, outstandingBalance, holidayDetailDTO);
if (loanApplicationTerms.getFixedEmiAmount() == null) {
updateFixedInstallmentAmount(mc, loanApplicationTerms, actualRepaymentDate, periodNumber, outstandingBalance, holidayDetailDTO);
}
// disbursement map for tranche details(will added to outstanding
// balance as per the start date)
@@ -165,6 +180,34 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
holidayDetailDTO.getWorkingDays().setRepaymentReschedulingType(RepaymentRescheduleType.MOVE_TO_NEXT_WORKING_DAY.getValue());
extendTermForDailyRepayments = true;
}
// this block is to start the schedule generation from specified date
if (loanScheduleRecalculationDTO != null && loanScheduleRecalculationDTO.isPartialUpdate()) {
periodNumber = loanScheduleRecalculationDTO.getPeriodNumber();
instalmentNumber = loanScheduleRecalculationDTO.getInstalmentNumber();
periodStartDate = loanScheduleRecalculationDTO.getPeriodStartDate();
periodStartDateApplicableForInterest = periodStartDate;
actualRepaymentDate = loanScheduleRecalculationDTO.getActualRepaymentDate();
totalCumulativePrincipal = loanScheduleRecalculationDTO.getTotalCumulativePrincipal();
totalCumulativeInterest = loanScheduleRecalculationDTO.getTotalCumulativeInterest();
totalFeeChargesCharged = loanScheduleRecalculationDTO.getTotalFeeChargesCharged().getAmount();
totalPenaltyChargesCharged = loanScheduleRecalculationDTO.getTotalPenaltyChargesCharged().getAmount();
totalRepaymentExpected = loanScheduleRecalculationDTO.getTotalRepaymentExpected().getAmount();
reducePrincipal = loanScheduleRecalculationDTO.getReducePrincipal();
principalPortionMap.clear();
principalPortionMap.putAll(loanScheduleRecalculationDTO.getPrincipalPortionMap());
latePaymentMap.clear();
latePaymentMap.putAll(loanScheduleRecalculationDTO.getLatePaymentMap());
compoundingMap.clear();
compoundingMap.putAll(loanScheduleRecalculationDTO.getCompoundingMap());
disburseDetailMap.clear();
disburseDetailMap.putAll(loanScheduleRecalculationDTO.getDisburseDetailMap());
outstandingBalance = loanScheduleRecalculationDTO.getOutstandingBalance();
outstandingBalanceAsPerRest = loanScheduleRecalculationDTO.getOutstandingBalanceAsPerRest();
installments.clear();
installments.addAll(loanScheduleRecalculationDTO.getInstallments());
}
while (!outstandingBalance.isZero() || !disburseDetailMap.isEmpty()) {
actualRepaymentDate = this.scheduledDateGenerator.generateNextRepaymentDate(actualRepaymentDate, loanApplicationTerms,
@@ -451,9 +494,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
}
}
LocalDate tillDate = getNextRestScheduleDate(currentDate.minusDays(1), loanApplicationTerms,
holidayDetailDTO);
if (arrears.isGreaterThanZero() && applicableDate.isBefore(tillDate)) {
if (arrears.isGreaterThanZero() && applicableDate.isBefore(lastRestDate)) {
updateLatePaidAmountsToPrincipalMap(principalPortionMap, detail.getTransaction(), latePaymentMap,
compoundingMap, loanApplicationTerms, currency, holidayDetailDTO, lastRestDate);
compoundingDateVariations.put(periodStartDateApplicableForInterest, new TreeMap<>(compoundingMap));
@@ -519,8 +560,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
if (earlyPaidAmount.isGreaterThanZero()) {
reducePrincipal = reducePrincipal.plus(earlyPaidAmount);
BigDecimal fixedEmiAmount = loanApplicationTerms.getFixedEmiAmount();
reducePrincipal = applyEarlyPaymentStrategy(loanApplicationTerms, reducePrincipal, totalCumulativePrincipal,
periodNumber + 1, mc, holidayDetailDTO);
reducePrincipal = applyEarlyPaymentStrategy(loanApplicationTerms, reducePrincipal,
totalCumulativePrincipal.plus(principalForThisPeriod).plus(earlyPaidAmount), periodNumber + 1, mc, holidayDetailDTO);
if (loanApplicationTerms.getAmortizationMethod().isEqualInstallment()
&& fixedEmiAmount.compareTo(loanApplicationTerms.getFixedEmiAmount()) != 0) {
isEmiAmountChanged = true;
@@ -740,6 +781,20 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
totalPenaltyChargesCharged, totalRepaymentExpected, totalOutstanding);
}
/**
* this method calculates the principal amount for generating the repayment
* schedule.
*/
private Money getPrincipalToBeScheduled(final LoanApplicationTerms loanApplicationTerms) {
Money principalToBeScheduled;
if (loanApplicationTerms.isMultiDisburseLoan() && loanApplicationTerms.getApprovedPrincipal().isGreaterThanZero()) {
principalToBeScheduled = loanApplicationTerms.getApprovedPrincipal();
} else {
principalToBeScheduled = loanApplicationTerms.getPrincipal();
}
return principalToBeScheduled;
}
private boolean updateFixedInstallmentAmount(final MathContext mc, final LoanApplicationTerms loanApplicationTerms,
LocalDate actualRepaymentDate, int periodNumber, Money outstandingBalance, final HolidayDetailDTO holidayDetailDTO) {
boolean isAmountChanged = false;
@@ -943,14 +998,14 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
case REDUCE_EMI_AMOUNT:
// in this case emi amount will be reduced but number of
// installments won't change
Money principal = getPrincipalToBeScheduled(loanApplicationTerms);
if (loanApplicationTerms.getActualFixedEmiAmount() == null) {
loanApplicationTerms.setFixedEmiAmount(null);
updateFixedInstallmentAmount(mc, loanApplicationTerms, loanApplicationTerms.getExpectedDisbursementDate(),
periodNumber, loanApplicationTerms.getPrincipal().minus(totalCumulativePrincipal), holidayDetailDTO);
periodNumber, principal.minus(totalCumulativePrincipal), holidayDetailDTO);
}
if (loanApplicationTerms.getAmortizationMethod().isEqualPrincipal()) {
loanApplicationTerms.updateFixedPrincipalAmount(mc, periodNumber,
loanApplicationTerms.getPrincipal().minus(totalCumulativePrincipal));
loanApplicationTerms.updateFixedPrincipalAmount(mc, periodNumber, principal.minus(totalCumulativePrincipal));
}
reducePrincipal = reducePrincipal.zero();
break;
@@ -1743,23 +1798,353 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
}
/**
* Method calls schedule regeneration by passing transactions one after
* another(this is done mainly to handle the scenario where interest or fee
* of over due installment should be collected before collecting principal )
* Method preprocess the installments and transactions and sets the required
* fields to generate the schedule
*/
@Override
public LoanScheduleModel rescheduleNextInstallments(final MathContext mc, final LoanApplicationTerms loanApplicationTerms,
public LoanScheduleDTO rescheduleNextInstallments(final MathContext mc, final LoanApplicationTerms loanApplicationTerms,
final Set<LoanCharge> loanCharges, final HolidayDetailDTO holidayDetailDTO, final List<LoanTransaction> transactions,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
final List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments, final LocalDate rescheduleFrom) {
// Loan transactions to process and find the variation on payments
Collection<RecalculationDetail> recalculationDetails = new ArrayList<>();
for (LoanTransaction loanTransaction : transactions) {
recalculationDetails.add(new RecalculationDetail(loanTransaction.getTransactionDate(), LoanTransaction
.copyTransactionProperties(loanTransaction)));
}
// Fixed schedule End Date for generating schedule
final LocalDate scheduleTillDate = null;
return generate(mc, loanApplicationTerms, loanCharges, holidayDetailDTO, recalculationDetails,
loanRepaymentScheduleTransactionProcessor, scheduleTillDate);
LoanScheduleRecalculationDTO loanScheduleRecalculationDTO = null;
Collection<LoanScheduleModelPeriod> periods = new ArrayList<>();
final List<LoanRepaymentScheduleInstallment> retainedInstallments = new ArrayList<>();
// this block is to retain the schedule installments prior to the
// provided date and creates late and early payment details for further
// calculations
if (rescheduleFrom != null) {
Money principalToBeScheduled = getPrincipalToBeScheduled(loanApplicationTerms);
// actual outstanding balance for interest calculation
Money outstandingBalance = principalToBeScheduled;
// total outstanding balance as per rest for interest calculation.
Money outstandingBalanceAsPerRest = outstandingBalance;
// this is required to update total fee amounts in the
// LoanScheduleModel
final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loanCharges);
periods = createNewLoanScheduleListWithDisbursementDetails(loanApplicationTerms.getNumberOfRepayments(), loanApplicationTerms,
chargesDueAtTimeOfDisbursement);
final List<LoanRepaymentScheduleInstallment> newRepaymentScheduleInstallments = new ArrayList<>();
MonetaryCurrency currency = outstandingBalance.getCurrency();
final Map<LocalDate, Money> disburseDetailMap = new HashMap<>();
if (loanApplicationTerms.isMultiDisburseLoan()) {
// fetches the first tranche amount and also updates other
// tranche
// details to map
BigDecimal disburseAmt = getDisbursementAmount(loanApplicationTerms, loanApplicationTerms.getExpectedDisbursementDate(),
periods, chargesDueAtTimeOfDisbursement, disburseDetailMap, true);
outstandingBalance = outstandingBalance.zero().plus(disburseAmt);
outstandingBalanceAsPerRest = outstandingBalance;
}
// early payments will be added here and as per the selected
// strategy
// action will be performed on this value
Money reducePrincipal = outstandingBalanceAsPerRest.zero();
// principal changes will be added along with date(after applying
// rest)
// from when these amounts will effect the outstanding balance for
// interest calculation
final Map<LocalDate, Money> principalPortionMap = new HashMap<>();
// compounding(principal) amounts will be added along with
// date(after applying compounding frequency)
// from when these amounts will effect the outstanding balance for
// interest calculation
final Map<LocalDate, Money> latePaymentMap = new HashMap<>();
// compounding(interest/Fee) amounts will be added along with
// date(after applying compounding frequency)
// from when these amounts will effect the outstanding balance for
// interest calculation
final TreeMap<LocalDate, Money> compoundingMap = new TreeMap<>();
LocalDate currentDate = DateUtils.getLocalDateOfTenant();
LocalDate lastRestDate = currentDate;
if (loanApplicationTerms.getRestCalendarInstance() != null) {
lastRestDate = getNextRestScheduleDate(currentDate.minusDays(1), loanApplicationTerms, holidayDetailDTO);
}
LocalDate actualRepaymentDate = loanApplicationTerms.getExpectedDisbursementDate();
boolean isFirstRepayment = true;
// cumulative fields
Money totalCumulativePrincipal = principalToBeScheduled.zero();
Money totalCumulativeInterest = principalToBeScheduled.zero();
Money totalFeeChargesCharged = principalToBeScheduled.zero().plus(chargesDueAtTimeOfDisbursement);
Money totalPenaltyChargesCharged = principalToBeScheduled.zero();
Money totalRepaymentExpected = principalToBeScheduled.zero();
// Actual period Number as per the schedule
int periodNumber = 1;
// Actual period Number plus interest only repayments
int instalmentNumber = 1;
LocalDate lastInstallmentDate = actualRepaymentDate;
LocalDate periodStartDate = loanApplicationTerms.getExpectedDisbursementDate();
// Set fixed Amortization Amounts(either EMI or Principal )
updateAmortization(mc, loanApplicationTerms, actualRepaymentDate, periodNumber, outstandingBalance, holidayDetailDTO);
// Block process the installment and creates the period if it falls
// before reschedule from date
// This will create the recalculation details by applying the
// transactions
for (LoanRepaymentScheduleInstallment installment : repaymentScheduleInstallments) {
for (Map.Entry<LocalDate, Money> disburseDetail : disburseDetailMap.entrySet()) {
if (disburseDetail.getKey().isAfter(installment.getFromDate())
&& !disburseDetail.getKey().isAfter(installment.getDueDate())) {
// creates and add disbursement detail to the repayments
// period
final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod.disbursement(
disburseDetail.getKey(), disburseDetail.getValue(), chargesDueAtTimeOfDisbursement);
periods.add(disbursementPeriod);
// updates actual outstanding balance with new
// disbursement detail
outstandingBalance = outstandingBalance.plus(disburseDetail.getValue());
}
}
// this will generate the next schedule due date and allows to
// process the installment only if recalculate from date is
// greater than due date
if (installment.getDueDate().isAfter(lastInstallmentDate)) {
LocalDate previousRepaymentDate = actualRepaymentDate;
actualRepaymentDate = this.scheduledDateGenerator.generateNextRepaymentDate(actualRepaymentDate, loanApplicationTerms,
isFirstRepayment, holidayDetailDTO);
isFirstRepayment = false;
lastInstallmentDate = this.scheduledDateGenerator.adjustRepaymentDate(actualRepaymentDate, loanApplicationTerms,
holidayDetailDTO);
if (!lastInstallmentDate.isBefore(rescheduleFrom)) {
actualRepaymentDate = previousRepaymentDate;
break;
}
periodNumber++;
}
// calculation of basic fields to start the schedule generation
// from the middle
periodStartDate = installment.getDueDate();
installment.resetDerivedComponents();
newRepaymentScheduleInstallments.add(installment);
outstandingBalance = outstandingBalance.minus(installment.getPrincipal(currency));
final LoanScheduleModelPeriod loanScheduleModelPeriod = createLoanScheduleModelPeriod(installment, outstandingBalance);
periods.add(loanScheduleModelPeriod);
totalCumulativePrincipal = totalCumulativePrincipal.plus(installment.getPrincipal(currency));
totalCumulativeInterest = totalCumulativeInterest.plus(installment.getInterestCharged(currency));
totalFeeChargesCharged = totalFeeChargesCharged.plus(installment.getFeeChargesCharged(currency));
totalPenaltyChargesCharged = totalPenaltyChargesCharged.plus(installment.getPenaltyChargesCharged(currency));
instalmentNumber++;
// populates the collection with transactions till the due date
// of
// the period for interest recalculation enabled loans
Collection<RecalculationDetail> applicableTransactions = getApplicableTransactionsForPeriod(loanApplicationTerms,
installment.getDueDate(), recalculationDetails);
// calculates the expected principal value for this repayment
// schedule
Money principalPortionCalculated = principalToBeScheduled.zero();
if (!installment.isRecalculatedInterestComponent()) {
principalPortionCalculated = calculateExpectedPrincipalPortion(installment.getInterestCharged(currency),
loanApplicationTerms);
}
// expected principal considering the previously paid excess
// amount
Money actualPrincipalPortion = principalPortionCalculated.minus(reducePrincipal);
if (actualPrincipalPortion.isLessThanZero()) {
actualPrincipalPortion = principalPortionCalculated.zero();
}
Money unprocessed = updateEarlyPaidAmountsToMap(loanApplicationTerms, holidayDetailDTO,
loanRepaymentScheduleTransactionProcessor, newRepaymentScheduleInstallments, currency, principalPortionMap,
installment, applicableTransactions, actualPrincipalPortion);
// this block is to adjust the period number based on the actual
// schedule due date and installment due date
// recalculatedInterestComponent installment shouldn't be
// considered while calculating fixed EMI amounts
int period = periodNumber;
if (!lastInstallmentDate.isEqual(installment.getDueDate())) {
period--;
}
reducePrincipal = fetchEarlyPaidAmount(installment.getPrincipal(currency), principalPortionCalculated, reducePrincipal,
loanApplicationTerms, totalCumulativePrincipal, period, mc, holidayDetailDTO);
// Updates principal paid map with efective date for reducing
// the amount from outstanding balance(interest calculation)
LocalDate amountApplicableDate = getNextRestScheduleDate(installment.getDueDate().minusDays(1), loanApplicationTerms,
holidayDetailDTO);
// updates map with the installment principal amount excluding
// unprocessed amount since this amount is already accounted.
updateMapWithAmount(principalPortionMap, installment.getPrincipal(currency).minus(unprocessed), amountApplicableDate);
// update outstanding balance for interest calculation
outstandingBalanceAsPerRest = updateBalanceForInterestCalculation(principalPortionMap, installment.getDueDate(),
outstandingBalanceAsPerRest, false);
outstandingBalanceAsPerRest = updateBalanceForInterestCalculation(disburseDetailMap, installment.getDueDate(),
outstandingBalanceAsPerRest, true);
}
totalRepaymentExpected = totalCumulativePrincipal.plus(totalCumulativeInterest).plus(totalFeeChargesCharged)
.plus(totalPenaltyChargesCharged);
// updates the map with over due amounts
updateLatePaymentsToMap(loanApplicationTerms, holidayDetailDTO, currency, latePaymentMap, lastInstallmentDate,
newRepaymentScheduleInstallments, true, lastRestDate, compoundingMap);
// for partial schedule generation
if (!newRepaymentScheduleInstallments.isEmpty()) {
loanScheduleRecalculationDTO = LoanScheduleRecalculationDTO
.createLoanScheduleDTOForPartialUpdate(periodNumber, instalmentNumber, periodStartDate, actualRepaymentDate,
totalCumulativePrincipal, totalCumulativeInterest, totalFeeChargesCharged, totalPenaltyChargesCharged,
totalRepaymentExpected, reducePrincipal, principalPortionMap, latePaymentMap, compoundingMap,
disburseDetailMap, outstandingBalance, outstandingBalanceAsPerRest, newRepaymentScheduleInstallments,
recalculationDetails, loanRepaymentScheduleTransactionProcessor, scheduleTillDate);
retainedInstallments.addAll(newRepaymentScheduleInstallments);
}
}
// for complete schedule generation
if (loanScheduleRecalculationDTO == null) {
loanScheduleRecalculationDTO = LoanScheduleRecalculationDTO.createLoanScheduleDTOForCompleteUpdate(recalculationDetails,
loanRepaymentScheduleTransactionProcessor, scheduleTillDate);
}
LoanScheduleModel loanScheduleModel = generate(mc, loanApplicationTerms, loanCharges, holidayDetailDTO,
loanScheduleRecalculationDTO);
for (LoanScheduleModelPeriod loanScheduleModelPeriod : loanScheduleModel.getPeriods()) {
if (loanScheduleModelPeriod.isRepaymentPeriod()) {
// adding newly created repayment periods to installments
addLoanRepaymentScheduleInstallment(retainedInstallments, loanScheduleModelPeriod);
}
}
periods.addAll(loanScheduleModel.getPeriods());
LoanScheduleModel loanScheduleModelwithPeriodChanges = LoanScheduleModel.withLoanScheduleModelPeriods(periods, loanScheduleModel);
return LoanScheduleDTO.from(retainedInstallments, loanScheduleModelwithPeriodChanges);
}
/**
* Method identifies the early paid amounts for a installment and update the
* principal map for further calculations
*/
private Money updateEarlyPaidAmountsToMap(final LoanApplicationTerms loanApplicationTerms, final HolidayDetailDTO holidayDetailDTO,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
final List<LoanRepaymentScheduleInstallment> newRepaymentScheduleInstallments, MonetaryCurrency currency,
final Map<LocalDate, Money> principalPortionMap, LoanRepaymentScheduleInstallment installment,
Collection<RecalculationDetail> applicableTransactions, Money actualPrincipalPortion) {
Money unprocessed = Money.zero(currency);
for (RecalculationDetail detail : applicableTransactions) {
if (!detail.isProcessed()) {
Money principalProcessed = installment.getPrincipalCompleted(currency);
List<LoanTransaction> currentTransactions = new ArrayList<>(2);
currentTransactions.add(detail.getTransaction());
// applies the transaction as per transaction strategy
// on scheduled installments to identify the
// unprocessed(early payment ) amounts
loanRepaymentScheduleTransactionProcessor.handleRepaymentSchedule(currentTransactions, currency,
newRepaymentScheduleInstallments);
// Identifies totalEarlyPayment and early paid amount with this
// transaction
Money principalPaidWithTransaction = installment.getPrincipalCompleted(currency).minus(principalProcessed);
Money totalEarlyPayment = installment.getPrincipalCompleted(currency).minus(actualPrincipalPortion);
if (totalEarlyPayment.isGreaterThanZero()) {
unprocessed = principalPaidWithTransaction;
// will execute this block if partial amount paid as
// early
if (principalPaidWithTransaction.isGreaterThan(totalEarlyPayment)) {
unprocessed = totalEarlyPayment;
}
}
// updates principal portion map with the early
// payment amounts and applicable date as per rest
LocalDate applicableDate = getNextRestScheduleDate(detail.getTransactionDate().minusDays(1), loanApplicationTerms,
holidayDetailDTO);
updateMapWithAmount(principalPortionMap, unprocessed, applicableDate);
}
}
return unprocessed;
}
private void updateAmortization(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, LocalDate actualRepaymentDate,
int periodNumber, Money outstandingBalance, final HolidayDetailDTO holidayDetailDTO) {
if (loanApplicationTerms.getAmortizationMethod().isEqualInstallment()) {
updateFixedInstallmentAmount(mc, loanApplicationTerms, actualRepaymentDate, periodNumber, outstandingBalance, holidayDetailDTO);
} else {
loanApplicationTerms.updateFixedPrincipalAmount(mc, periodNumber, outstandingBalance);
}
}
/**
* Method identifies early paid amount and applies the early payment
* strategy
*/
private Money fetchEarlyPaidAmount(final Money principalPortion, final Money principalPortionCalculated, final Money reducePrincipal,
final LoanApplicationTerms applicationTerms, final Money totalCumulativePrincipal, int periodNumber, final MathContext mc,
final HolidayDetailDTO holidayDetailDTO) {
Money existingEarlyPayment = reducePrincipal.minus(principalPortionCalculated);
Money earlyPaidAmount = principalPortion.plus(existingEarlyPayment);
if (existingEarlyPayment.isLessThanZero()) {
existingEarlyPayment = existingEarlyPayment.zero();
}
boolean isEarlyPaid = earlyPaidAmount.isGreaterThan(existingEarlyPayment);
if (earlyPaidAmount.isLessThanZero()) {
earlyPaidAmount = earlyPaidAmount.zero();
}
if (isEarlyPaid) {
switch (applicationTerms.getRescheduleStrategyMethod()) {
case REDUCE_EMI_AMOUNT:
// in this case emi amount will be reduced but number of
// installments won't change
Money principal = getPrincipalToBeScheduled(applicationTerms);
if (applicationTerms.getActualFixedEmiAmount() == null) {
applicationTerms.setFixedEmiAmount(null);
updateFixedInstallmentAmount(mc, applicationTerms, applicationTerms.getExpectedDisbursementDate(), periodNumber,
principal.minus(totalCumulativePrincipal), holidayDetailDTO);
}
if (applicationTerms.getAmortizationMethod().isEqualPrincipal()) {
applicationTerms.updateFixedPrincipalAmount(mc, periodNumber, principal.minus(totalCumulativePrincipal));
}
earlyPaidAmount = earlyPaidAmount.zero();
break;
case REDUCE_NUMBER_OF_INSTALLMENTS:
// number of installments will reduce but emi amount won't
// get effected
earlyPaidAmount = earlyPaidAmount.zero();
break;
case RESCHEDULE_NEXT_REPAYMENTS:
// will reduce principal from the reduce Principal for each
// installment(means installments will have less emi amount)
// until this
// amount becomes zero
break;
default:
break;
}
}
return earlyPaidAmount;
}
private Money calculateExpectedPrincipalPortion(final Money interestPortion, final LoanApplicationTerms applicationTerms) {
Money principalPortionCalculated = interestPortion.zero();
if (applicationTerms.getAmortizationMethod().isEqualInstallment()) {
principalPortionCalculated = principalPortionCalculated.plus(applicationTerms.getFixedEmiAmount()).minus(interestPortion);
} else {
principalPortionCalculated = principalPortionCalculated.plus(applicationTerms.getFixedPrincipalAmount());
}
return principalPortionCalculated;
}
private List<LoanRepaymentScheduleInstallment> fetchInstallmentsFromScheduleModel(final LoanScheduleModel loanScheduleModel) {
@@ -1770,16 +2155,29 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
return installments;
}
private void addLoanRepaymentScheduleInstallment(final List<LoanRepaymentScheduleInstallment> installments,
private LoanRepaymentScheduleInstallment addLoanRepaymentScheduleInstallment(final List<LoanRepaymentScheduleInstallment> installments,
final LoanScheduleModelPeriod scheduledLoanInstallment) {
LoanRepaymentScheduleInstallment installment = null;
if (scheduledLoanInstallment.isRepaymentPeriod()) {
final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(null,
scheduledLoanInstallment.periodNumber(), scheduledLoanInstallment.periodFromDate(),
scheduledLoanInstallment.periodDueDate(), scheduledLoanInstallment.principalDue(),
scheduledLoanInstallment.interestDue(), scheduledLoanInstallment.feeChargesDue(),
scheduledLoanInstallment.penaltyChargesDue(), scheduledLoanInstallment.isRecalculatedInterestComponent());
installment = new LoanRepaymentScheduleInstallment(null, scheduledLoanInstallment.periodNumber(),
scheduledLoanInstallment.periodFromDate(), scheduledLoanInstallment.periodDueDate(),
scheduledLoanInstallment.principalDue(), scheduledLoanInstallment.interestDue(),
scheduledLoanInstallment.feeChargesDue(), scheduledLoanInstallment.penaltyChargesDue(),
scheduledLoanInstallment.isRecalculatedInterestComponent());
installments.add(installment);
}
return installment;
}
private LoanScheduleModelPeriod createLoanScheduleModelPeriod(final LoanRepaymentScheduleInstallment installment,
final Money outstandingPrincipal) {
final MonetaryCurrency currency = outstandingPrincipal.getCurrency();
LoanScheduleModelPeriod scheduledLoanInstallment = LoanScheduleModelRepaymentPeriod
.repayment(installment.getInstallmentNumber(), installment.getFromDate(), installment.getDueDate(),
installment.getPrincipal(currency), outstandingPrincipal, installment.getInterestCharged(currency),
installment.getFeeChargesCharged(currency), installment.getPenaltyChargesCharged(currency),
installment.getDue(currency), installment.isRecalculatedInterestComponent());
return scheduledLoanInstallment;
}
private LocalDate getNextRestScheduleDate(LocalDate startDate, LoanApplicationTerms loanApplicationTerms,
@@ -1815,9 +2213,9 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
* Method returns the amount payable to close the loan account as of today.
*/
@Override
public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(List<LoanRepaymentScheduleInstallment> installments,
MonetaryCurrency currency, final LocalDate onDate, LoanApplicationTerms loanApplicationTerms, MathContext mc,
Set<LoanCharge> charges, final HolidayDetailDTO holidayDetailDTO, final List<LoanTransaction> loanTransactions,
public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(final MonetaryCurrency currency, final LocalDate onDate,
final LoanApplicationTerms loanApplicationTerms, final MathContext mc, final Set<LoanCharge> charges,
final HolidayDetailDTO holidayDetailDTO, final List<LoanTransaction> loanTransactions,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) {
LocalDate calculateTill = onDate;
@@ -1829,9 +2227,10 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
recalculationDetails.add(new RecalculationDetail(loanTransaction.getTransactionDate(), LoanTransaction
.copyTransactionProperties(loanTransaction)));
}
LoanScheduleModel loanScheduleModel = generate(mc, loanApplicationTerms, charges, holidayDetailDTO, recalculationDetails,
loanRepaymentScheduleTransactionProcessor, calculateTill);
installments = fetchInstallmentsFromScheduleModel(loanScheduleModel);
LoanScheduleRecalculationDTO loanScheduleRecalculationDTO = LoanScheduleRecalculationDTO.createLoanScheduleDTOForCompleteUpdate(
recalculationDetails, loanRepaymentScheduleTransactionProcessor, calculateTill);
LoanScheduleModel loanScheduleModel = generate(mc, loanApplicationTerms, charges, holidayDetailDTO, loanScheduleRecalculationDTO);
List<LoanRepaymentScheduleInstallment> installments = fetchInstallmentsFromScheduleModel(loanScheduleModel);
loanRepaymentScheduleTransactionProcessor.handleTransaction(loanApplicationTerms.getExpectedDisbursementDate(), loanTransactions,
currency, installments, charges);
Money feeCharges = Money.zero(currency);
@@ -1270,4 +1270,9 @@ public final class LoanApplicationTerms {
return loanCalendar;
}
public BigDecimal getFixedPrincipalAmount() {
return this.fixedPrincipalAmount;
}
}
@@ -19,6 +19,7 @@ import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge;
import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction;
import org.mifosplatform.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
import org.mifosplatform.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleModel;
import org.mifosplatform.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest;
@@ -27,14 +28,13 @@ public interface LoanScheduleGenerator {
LoanScheduleModel generate(MathContext mc, LoanApplicationTerms loanApplicationTerms, Set<LoanCharge> loanCharges,
final HolidayDetailDTO holidayDetailDTO);
LoanScheduleModel rescheduleNextInstallments(MathContext mc, LoanApplicationTerms loanApplicationTerms, Set<LoanCharge> loanCharges,
LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicationTerms loanApplicationTerms, Set<LoanCharge> loanCharges,
final HolidayDetailDTO holidayDetailDTO, List<LoanTransaction> transactions,
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor);
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments, LocalDate rescheduleFrom);
LoanRepaymentScheduleInstallment calculatePrepaymentAmount(List<LoanRepaymentScheduleInstallment> installments,
MonetaryCurrency currency, LocalDate onDate, LoanApplicationTerms loanApplicationTerms, MathContext mc,
Set<LoanCharge> charges, HolidayDetailDTO holidayDetailDTO, List<LoanTransaction> loanTransactions,
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor);
LoanRepaymentScheduleInstallment calculatePrepaymentAmount(MonetaryCurrency currency,
LocalDate onDate, LoanApplicationTerms loanApplicationTerms, MathContext mc, Set<LoanCharge> charges,
HolidayDetailDTO holidayDetailDTO, List<LoanTransaction> loanTransactions, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor);
LoanRescheduleModel reschedule(final MathContext mathContext, final LoanRescheduleRequest loanRescheduleRequest,
final ApplicationCurrency applicationCurrency, final HolidayDetailDTO holidayDetailDTO, CalendarInstance restCalendarInstance,
@@ -51,6 +51,15 @@ public final class LoanScheduleModel {
loanScheduleModel.totalRepaymentExpected, loanScheduleModel.totalOutstanding);
}
public static LoanScheduleModel withLoanScheduleModelPeriods(final Collection<LoanScheduleModelPeriod> periods,
final LoanScheduleModel loanScheduleModel) {
return new LoanScheduleModel(periods, loanScheduleModel.applicationCurrency, loanScheduleModel.loanTermInDays,
loanScheduleModel.totalPrincipalDisbursed, loanScheduleModel.totalPrincipalExpected, loanScheduleModel.totalPrincipalPaid,
loanScheduleModel.totalInterestCharged, loanScheduleModel.totalFeeChargesCharged,
loanScheduleModel.totalPenaltyChargesCharged, loanScheduleModel.totalRepaymentExpected, loanScheduleModel.totalOutstanding);
}
private LoanScheduleModel(final Collection<LoanScheduleModelPeriod> periods, final ApplicationCurrency applicationCurrency,
final int loanTermInDays, final Money principalDisbursed, final BigDecimal totalPrincipalExpected,
final BigDecimal totalPrincipalPaid, final BigDecimal totalInterestCharged, final BigDecimal totalFeeChargesCharged,
@@ -95,7 +104,6 @@ public final class LoanScheduleModel {
return this.periods;
}
public BigDecimal getTotalPenaltyChargesCharged() {
return this.totalPenaltyChargesCharged;
}
@@ -231,8 +231,8 @@ public class LoanScheduleAssembler {
final Integer graceOnInterestCharged = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("graceOnInterestCharged", element);
final LocalDate interestChargedFromDate = this.fromApiJsonHelper.extractLocalDateNamed("interestChargedFromDate", element);
final Integer graceOnArrearsAgeing = this.fromApiJsonHelper
.extractIntegerWithLocaleNamed(LoanProductConstants.graceOnArrearsAgeingParameterName, element);
final Integer graceOnArrearsAgeing = this.fromApiJsonHelper.extractIntegerWithLocaleNamed(
LoanProductConstants.graceOnArrearsAgeingParameterName, element);
// other
final BigDecimal inArrearsTolerance = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("inArrearsTolerance", element);
@@ -240,14 +240,14 @@ public class LoanScheduleAssembler {
final BigDecimal emiAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.emiAmountParameterName,
element);
final BigDecimal maxOutstandingBalance = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(LoanApiConstants.maxOutstandingBalanceParameterName, element);
final BigDecimal maxOutstandingBalance = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(
LoanApiConstants.maxOutstandingBalanceParameterName, element);
final List<DisbursementData> disbursementDatas = fetchDisbursementData(element.getAsJsonObject());
final List<LoanTermVariationsData> loanVariationTermsData = new ArrayList<>();
LoanTermVariationsData data = new LoanTermVariationsData(null, LoanEnumerations.loanvariationType(LoanTermVariationType.EMI_AMOUNT),
expectedDisbursementDate, emiAmount);
LoanTermVariationsData data = new LoanTermVariationsData(null,
LoanEnumerations.loanvariationType(LoanTermVariationType.EMI_AMOUNT), expectedDisbursementDate, emiAmount);
loanVariationTermsData.add(data);
/**
@@ -267,8 +267,8 @@ public class LoanScheduleAssembler {
.getProductInterestRecalculationDetails();
recalculationFrequencyType = loanProductInterestRecalculationDetails.getRestFrequencyType();
if (!recalculationFrequencyType.isSameAsRepayment()) {
LocalDate calendarStartDate = this.fromApiJsonHelper
.extractLocalDateNamed(LoanProductConstants.recalculationRestFrequencyDateParamName, element);
LocalDate calendarStartDate = this.fromApiJsonHelper.extractLocalDateNamed(
LoanProductConstants.recalculationRestFrequencyDateParamName, element);
if (calendarStartDate == null) {
calendarStartDate = expectedDisbursementDate;
}
@@ -280,8 +280,8 @@ public class LoanScheduleAssembler {
if (compoundingMethod.isCompoundingEnabled()) {
compoundingFrequencyType = loanProductInterestRecalculationDetails.getCompoundingFrequencyType();
if (!compoundingFrequencyType.isSameAsRepayment()) {
LocalDate calendarStartDate = this.fromApiJsonHelper
.extractLocalDateNamed(LoanProductConstants.recalculationCompoundingFrequencyDateParamName, element);
LocalDate calendarStartDate = this.fromApiJsonHelper.extractLocalDateNamed(
LoanProductConstants.recalculationCompoundingFrequencyDateParamName, element);
if (calendarStartDate == null) {
calendarStartDate = expectedDisbursementDate;
}
@@ -346,12 +346,12 @@ public class LoanScheduleAssembler {
BigDecimal principal = null;
if (jsonObject.has(LoanApiConstants.disbursementDateParameterName)) {
expectedDisbursementDate = this.fromApiJsonHelper
.extractLocalDateNamed(LoanApiConstants.disbursementDateParameterName, jsonObject, dateFormat, locale);
expectedDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed(
LoanApiConstants.disbursementDateParameterName, jsonObject, dateFormat, locale);
}
if (jsonObject.has(LoanApiConstants.disbursementPrincipalParameterName)
&& jsonObject.get(LoanApiConstants.disbursementPrincipalParameterName).isJsonPrimitive() && StringUtils
.isNotBlank((jsonObject.get(LoanApiConstants.disbursementPrincipalParameterName).getAsString()))) {
&& jsonObject.get(LoanApiConstants.disbursementPrincipalParameterName).isJsonPrimitive()
&& StringUtils.isNotBlank((jsonObject.get(LoanApiConstants.disbursementPrincipalParameterName).getAsString()))) {
principal = jsonObject.getAsJsonPrimitive(LoanApiConstants.disbursementPrincipalParameterName).getAsBigDecimal();
}
disbursementDatas.add(new DisbursementData(null, expectedDisbursementDate, null, principal, null, null));
@@ -363,8 +363,9 @@ public class LoanScheduleAssembler {
}
private void validateRepaymentsStartDateWithMeetingDates(final LocalDate repaymentsStartingFromDate, final Calendar calendar) {
if (repaymentsStartingFromDate != null && !CalendarUtils.isValidRedurringDate(calendar.getRecurrence(),
calendar.getStartDateLocalDate(), repaymentsStartingFromDate)) {
if (repaymentsStartingFromDate != null
&& !CalendarUtils.isValidRedurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(),
repaymentsStartingFromDate)) {
final String errorMessage = "First repayment date '" + repaymentsStartingFromDate + "' do not fall on a meeting date";
throw new LoanApplicationDateException("first.repayment.date.do.not.match.meeting.date", errorMessage,
repaymentsStartingFromDate);
@@ -439,9 +440,10 @@ public class LoanScheduleAssembler {
}
public LoanScheduleModel assembleLoanScheduleFrom(final LoanApplicationTerms loanApplicationTerms, final boolean isHolidayEnabled,
final List<Holiday> holidays, final WorkingDays workingDays, final JsonElement element, Set<LoanDisbursementDetails> disbursementDetails) {
final List<Holiday> holidays, final WorkingDays workingDays, final JsonElement element,
Set<LoanDisbursementDetails> disbursementDetails) {
final Set<LoanCharge> loanCharges = this.loanChargeAssembler.fromParsedJson(element,disbursementDetails);
final Set<LoanCharge> loanCharges = this.loanChargeAssembler.fromParsedJson(element, disbursementDetails);
final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getInterestMethod());
@@ -455,37 +457,38 @@ public class LoanScheduleAssembler {
public LoanScheduleModel assembleForInterestRecalculation(final LoanApplicationTerms loanApplicationTerms, final Long officeId,
List<LoanTransaction> transactions, final Set<LoanCharge> loanCharges,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
final List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments, final LocalDate rescheduleFrom) {
final RoundingMode roundingMode = MoneyHelper.getRoundingMode();
final MathContext mc = new MathContext(8, roundingMode);
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(officeId,
loanApplicationTerms.getExpectedDisbursementDate().toDate(), HolidayStatusType.ACTIVE.getValue());
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(officeId, loanApplicationTerms
.getExpectedDisbursementDate().toDate(), HolidayStatusType.ACTIVE.getValue());
final WorkingDays workingDays = this.workingDaysRepository.findOne();
final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getInterestMethod());
HolidayDetailDTO detailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays);
return loanScheduleGenerator.rescheduleNextInstallments(mc, loanApplicationTerms, loanCharges, detailDTO, transactions,
loanRepaymentScheduleTransactionProcessor);
loanRepaymentScheduleTransactionProcessor, repaymentScheduleInstallments, rescheduleFrom).getLoanScheduleModel();
}
public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(List<LoanRepaymentScheduleInstallment> installments,
MonetaryCurrency currency, LocalDate onDate, LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges,
final Long officeId, List<LoanTransaction> loanTransactions,
public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate,
LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges, final Long officeId,
List<LoanTransaction> loanTransactions,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) {
final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getInterestMethod());
final RoundingMode roundingMode = MoneyHelper.getRoundingMode();
final MathContext mc = new MathContext(8, roundingMode);
final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(officeId,
loanApplicationTerms.getExpectedDisbursementDate().toDate(), HolidayStatusType.ACTIVE.getValue());
final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(officeId, loanApplicationTerms
.getExpectedDisbursementDate().toDate(), HolidayStatusType.ACTIVE.getValue());
final WorkingDays workingDays = this.workingDaysRepository.findOne();
HolidayDetailDTO holidayDetailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays);
return loanScheduleGenerator.calculatePrepaymentAmount(installments, currency, onDate, loanApplicationTerms, mc, loanCharges,
holidayDetailDTO, loanTransactions, loanRepaymentScheduleTransactionProcessor);
return loanScheduleGenerator.calculatePrepaymentAmount(currency, onDate, loanApplicationTerms, mc, loanCharges, holidayDetailDTO,
loanTransactions, loanRepaymentScheduleTransactionProcessor);
}
@@ -522,7 +525,7 @@ public class LoanScheduleAssembler {
refernceDateForCalculatingFirstRepaymentDate, repaymentPeriodFrequencyType,
minimumDaysBetweenDisbursalAndFirstRepayment, calendar);
} /*** Individual or group account, or JLG not linked to a meeting ***/
}/*** Individual or group account, or JLG not linked to a meeting ***/
else {
LocalDate dateBasedOnRepaymentFrequency;
// Derive the first repayment date as greater date among
@@ -535,12 +538,12 @@ public class LoanScheduleAssembler {
dateBasedOnRepaymentFrequency = expectedDisbursementDate.plusWeeks(repaymentEvery);
} else if (repaymentPeriodFrequencyType.isMonthly()) {
dateBasedOnRepaymentFrequency = expectedDisbursementDate.plusMonths(repaymentEvery);
} /** yearly loan **/
}/** yearly loan **/
else {
dateBasedOnRepaymentFrequency = expectedDisbursementDate.plusYears(repaymentEvery);
}
derivedFirstRepayment = dateBasedOnRepaymentFrequency.isAfter(dateBasedOnMinimumDaysBetweenDisbursalAndFirstRepayment)
? dateBasedOnRepaymentFrequency : dateBasedOnMinimumDaysBetweenDisbursalAndFirstRepayment;
derivedFirstRepayment = dateBasedOnRepaymentFrequency.isAfter(dateBasedOnMinimumDaysBetweenDisbursalAndFirstRepayment) ? dateBasedOnRepaymentFrequency
: dateBasedOnMinimumDaysBetweenDisbursalAndFirstRepayment;
}
return derivedFirstRepayment;
@@ -554,17 +557,16 @@ public class LoanScheduleAssembler {
final LocalDate derivedFirstRepayment = CalendarUtils.getFirstRepaymentMeetingDate(calendar,
refernceDateForCalculatingFirstRepaymentDate, repaymentEvery, frequency);
final LocalDate minimumFirstRepaymentDate = expectedDisbursementDate.plusDays(minimumDaysBetweenDisbursalAndFirstRepayment);
return minimumFirstRepaymentDate.isBefore(derivedFirstRepayment) ? derivedFirstRepayment
: deriveFirstRepaymentDateForJLGLoans(repaymentEvery, expectedDisbursementDate, derivedFirstRepayment,
repaymentPeriodFrequencyType, minimumDaysBetweenDisbursalAndFirstRepayment, calendar);
return minimumFirstRepaymentDate.isBefore(derivedFirstRepayment) ? derivedFirstRepayment : deriveFirstRepaymentDateForJLGLoans(
repaymentEvery, expectedDisbursementDate, derivedFirstRepayment, repaymentPeriodFrequencyType,
minimumDaysBetweenDisbursalAndFirstRepayment, calendar);
}
private void validateMinimumDaysBetweenDisbursalAndFirstRepayment(final LocalDate disbursalDate, final LocalDate firstRepaymentDate,
final Integer minimumDaysBetweenDisbursalAndFirstRepayment) {
final LocalDate minimumFirstRepaymentDate = disbursalDate.plusDays(minimumDaysBetweenDisbursalAndFirstRepayment);
if (firstRepaymentDate
.isBefore(minimumFirstRepaymentDate)) { throw new MinDaysBetweenDisbursalAndFirstRepaymentViolationException(disbursalDate,
firstRepaymentDate, minimumDaysBetweenDisbursalAndFirstRepayment); }
if (firstRepaymentDate.isBefore(minimumFirstRepaymentDate)) { throw new MinDaysBetweenDisbursalAndFirstRepaymentViolationException(
disbursalDate, firstRepaymentDate, minimumDaysBetweenDisbursalAndFirstRepayment); }
}
}
@@ -167,8 +167,8 @@ public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleC
LoanApplicationTerms loanApplicationTerms = loan.constructLoanApplicationTerms(applicationCurrency,
calculatedRepaymentsStartingFromDate, restCalendarInstance, compoundingCalendarInstance);
LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = this.loanScheduleAssembler.calculatePrepaymentAmount(
loan.fetchRepaymentScheduleInstallments(), currency, today, loanApplicationTerms, loan.charges(), loan.getOfficeId(),
loan.retreiveListOfTransactionsPostDisbursementExcludeAccruals(), loanRepaymentScheduleTransactionProcessor);
currency, today, loanApplicationTerms, loan.charges(), loan.getOfficeId(), loan.retreiveListOfTransactionsPostDisbursementExcludeAccruals(),
loanRepaymentScheduleTransactionProcessor);
Money totalAmount = totalPrincipal.plus(loanRepaymentScheduleInstallment.getFeeChargesOutstanding(currency)).plus(
loanRepaymentScheduleInstallment.getPenaltyChargesOutstanding(currency));
Money interestDue = Money.zero(currency);
@@ -189,7 +189,8 @@ public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleC
}
LoanScheduleModel model = this.loanScheduleAssembler.assembleForInterestRecalculation(loanApplicationTerms, loan.getOfficeId(),
modifiedTransactions, loan.charges(), loanRepaymentScheduleTransactionProcessor);
modifiedTransactions, loan.charges(), loanRepaymentScheduleTransactionProcessor, loan.fetchRepaymentScheduleInstallments(),
loan.fetchInterestRecalculateFromDate());
LoanScheduleData scheduleDate = model.toData();
Collection<LoanSchedulePeriodData> periodDatas = scheduleDate.getPeriods();
for (LoanSchedulePeriodData periodData : periodDatas) {
@@ -21,6 +21,7 @@ import org.mifosplatform.infrastructure.core.api.JsonCommand;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResult;
import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder;
import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException;
import org.mifosplatform.infrastructure.core.service.DateUtils;
import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext;
import org.mifosplatform.organisation.holiday.domain.Holiday;
import org.mifosplatform.organisation.holiday.domain.HolidayRepositoryWrapper;
@@ -463,6 +464,7 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
loan.getDisbursementDate().toDate());
workingDays = this.workingDaysRepository.findOne();
overdurPenaltyWaitPeriod = this.configurationDomainService.retrievePenaltyWaitPeriod();
recalculateFrom = DateUtils.getLocalDateOfTenant();
}
HolidayDetailDTO holidayDetailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays);
@@ -1071,9 +1071,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
for (LoanRepaymentScheduleInstallment installment : installments) {
if (installment.getId() == null) {
this.repaymentScheduleInstallmentRepository.save(installment);
} else {
break;
}
}
}
this.loanRepository.saveAndFlush(loan);
} catch (final DataIntegrityViolationException e) {
@@ -1695,14 +1695,16 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
sqlBuilder.append(" WHERE ml.loan_status_id = ? ");
sqlBuilder.append(" and ml.interest_recalculation_enabled = 1 ");
sqlBuilder.append(" and ml.is_npa = 0 ");
sqlBuilder.append(" and (ml.interest_recalcualated_on is null or ml.interest_recalcualated_on <> ?)");
sqlBuilder.append(" and ((");
sqlBuilder.append(" mr.completed_derived is false ");
sqlBuilder.append(" and mr.duedate < ? )");
sqlBuilder.append(" or dd.expected_disburse_date < ? ) ");
sqlBuilder.append(" group by ml.id");
try {
String currentdate = formatter.print(DateUtils.getLocalDateOfTenant());
return this.jdbcTemplate.queryForList(sqlBuilder.toString(), Long.class,
new Object[] { LoanStatus.ACTIVE.getValue(), formatter.print(LocalDate.now()), formatter.print(LocalDate.now()) });
new Object[] { LoanStatus.ACTIVE.getValue(), currentdate, currentdate, currentdate });
} catch (final EmptyResultDataAccessException e) {
return null;
}
@@ -492,8 +492,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
for (LoanRepaymentScheduleInstallment installment : installments) {
if (installment.getId() == null) {
this.repaymentScheduleInstallmentRepository.save(installment);
} else {
break;
}
}
this.loanRepository.saveAndFlush(loan);
@@ -515,9 +513,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
for (LoanRepaymentScheduleInstallment installment : installments) {
if (installment.getId() == null) {
this.repaymentScheduleInstallmentRepository.save(installment);
} else {
break;
}
}
}
this.loanRepository.save(loan);
} catch (final DataIntegrityViolationException e) {
@@ -893,6 +889,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled();
overdurPenaltyWaitPeriod = this.configurationDomainService.retrievePenaltyWaitPeriod();
recalculateFrom = transactionToAdjust.getTransactionDate().isAfter(transactionDate) ? transactionDate : transactionToAdjust
.getTransactionDate();
}
HolidayDetailDTO holidayDetailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays, allowTransactionsOnHoliday,
@@ -1040,6 +1038,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), loan.getDisbursementDate().toDate());
workingDays = this.workingDaysRepository.findOne();
overdurPenaltyWaitPeriod = this.configurationDomainService.retrievePenaltyWaitPeriod();
recalculateFrom = transactionDate;
}
HolidayDetailDTO holidayDetailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays);
@@ -1135,6 +1134,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
compoundingCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId(
loan.loanInterestRecalculationDetailId(), CalendarEntityType.LOAN_RECALCULATION_COMPOUNDING_DETAIL.getValue());
overdurPenaltyWaitPeriod = this.configurationDomainService.retrievePenaltyWaitPeriod();
recalculateFrom = command.localDateValueOfParameterNamed("transactionDate");
}
HolidayDetailDTO holidayDetailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays);
ScheduleGeneratorDTO scheduleGeneratorDTO = new ScheduleGeneratorDTO(loanScheduleFactory, applicationCurrency,
@@ -1217,6 +1217,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
compoundingCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId(
loan.loanInterestRecalculationDetailId(), CalendarEntityType.LOAN_RECALCULATION_COMPOUNDING_DETAIL.getValue());
overdurPenaltyWaitPeriod = this.configurationDomainService.retrievePenaltyWaitPeriod();
recalculateFrom = command.localDateValueOfParameterNamed("transactionDate");
}
HolidayDetailDTO holidayDetailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays);
@@ -1356,6 +1357,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
boolean isAppliedOnBackDate = false;
LoanCharge loanCharge = null;
LocalDate recalculateFrom =loan.fetchInterestRecalculateFromDate();
if (chargeDefinition.isPercentageOfDisbursementAmount()) {
LoanTrancheDisbursementCharge loanTrancheDisbursementCharge = null;
for (LoanDisbursementDetails disbursementDetail : loanDisburseDetails) {
@@ -1367,7 +1369,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_ADD_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN_CHARGE, loanCharge));
validateAddLoanCharge(loan, chargeDefinition, loanCharge);
isAppliedOnBackDate = addCharge(loan, chargeDefinition, loanCharge);
addCharge(loan, chargeDefinition, loanCharge);
isAppliedOnBackDate = true;
if (recalculateFrom.isAfter(disbursementDetail.expectedDisbursementDateAsLocalDate())) {
recalculateFrom = disbursementDetail.expectedDisbursementDateAsLocalDate();
}
}
}
loan.addTrancheLoanCharge(chargeDefinition);
@@ -1378,12 +1384,17 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
validateAddLoanCharge(loan, chargeDefinition, loanCharge);
isAppliedOnBackDate = addCharge(loan, chargeDefinition, loanCharge);
if (loanCharge.getDueLocalDate() == null || recalculateFrom.isAfter(loanCharge.getDueLocalDate())) {
isAppliedOnBackDate = true;
recalculateFrom = loanCharge.getDueLocalDate();
}
}
boolean reprocessRequired = true;
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
if (isAppliedOnBackDate && loan.isFeeCompoundingEnabledForInterestRecalculation()) {
runScheduleRecalculation(loan);
runScheduleRecalculation(loan, recalculateFrom);
reprocessRequired = false;
}
updateOriginalSchedule(loan);
@@ -1445,7 +1456,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
public void runScheduleRecalculation(Loan loan) {
public void runScheduleRecalculation(final Loan loan, final LocalDate recalculateFrom) {
AppUser currentUser = getAppUserIfPresent();
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
CalendarInstance restCalendarInstance = calendarInstanceRepository.findCalendarInstaneByEntityId(
@@ -1466,7 +1477,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
WorkingDays workingDays = this.workingDaysRepository.findOne();
final Long overdurPenaltyWaitPeriod = this.configurationDomainService.retrievePenaltyWaitPeriod();
LocalDate recalculateFrom = null;
HolidayDetailDTO holidayDetailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays);
ScheduleGeneratorDTO generatorDTO = new ScheduleGeneratorDTO(loanScheduleFactory, applicationCurrency,
calculatedRepaymentsStartingFromDate, holidayDetailDTO, restCalendarInstance, compoundingCalendarInstance,
@@ -2413,6 +2423,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
boolean runInterestRecalculation = false;
LocalDate recalculateFrom = DateUtils.getLocalDateOfTenant();
for (final OverdueLoanScheduleData overdueInstallment : overdueLoanScheduleDatas) {
final JsonElement parsedCommand = this.fromApiJsonHelper.parse(overdueInstallment.toString());
@@ -2422,12 +2433,20 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
overdueInstallment.getPeriodNumber(), command, loan, existingTransactionIds, existingReversedTransactionIds);
loan = overdueDTO.getLoan();
runInterestRecalculation = runInterestRecalculation || overdueDTO.isRunInterestRecalculation();
if (recalculateFrom.isAfter(overdueDTO.getRecalculateFrom())) {
recalculateFrom = overdueDTO.getRecalculateFrom();
}
}
if (loan != null) {
boolean reprocessRequired = true;
LocalDate recalculatedTill = loan.fetchInterestRecalculateFromDate();
if(recalculateFrom.isAfter(recalculatedTill)){
recalculateFrom = recalculatedTill;
}
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
if (runInterestRecalculation && loan.isFeeCompoundingEnabledForInterestRecalculation()) {
runScheduleRecalculation(loan);
runScheduleRecalculation(loan, recalculateFrom);
reprocessRequired = false;
}
updateOriginalSchedule(loan);
@@ -2482,7 +2501,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (feeFrequency == null) {
scheduleDates.put(frequencyNunber++, startDate.minusDays(diff.intValue()));
} else {
while (new LocalDate().isAfter(startDate)) {
while (DateUtils.getLocalDateOfTenant().isAfter(startDate)) {
scheduleDates.put(frequencyNunber++, startDate.minusDays(diff.intValue()));
LocalDate scheduleDate = scheduledDateGenerator.getRepaymentPeriodDate(PeriodFrequencyType.fromInt(feeFrequency),
chargeDefinition.feeInterval(), startDate, null, null);
@@ -2505,6 +2524,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
installment = loan.fetchRepaymentScheduleInstallment(periodNumber);
}
LocalDate recalculateFrom = DateUtils.getLocalDateOfTenant();
if (loan != null) {
this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_APPLY_OVERDUE_CHARGE,
constructEntityMap(BUSINESS_ENTITY.LOAN, loan));
@@ -2518,10 +2538,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
boolean isAppliedOnBackDate = addCharge(loan, chargeDefinition, loanCharge);
runInterestRecalculation = runInterestRecalculation || isAppliedOnBackDate;
if (entry.getValue().isBefore(recalculateFrom)) {
recalculateFrom = entry.getValue();
}
}
}
return new LoanOverdueDTO(loan, runInterestRecalculation);
return new LoanOverdueDTO(loan, runInterestRecalculation, recalculateFrom);
}
@Override
@@ -2845,7 +2868,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
List<Holiday> holidays = null;
boolean isHolidayEnabled = false;
WorkingDays workingDays = null;
LocalDate recalculateFrom = null;
LocalDate recalculateFrom = loan.fetchInterestRecalculateFromDate();
Long overdurPenaltyWaitPeriod = null;
final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>();
@@ -5,16 +5,19 @@
*/
package org.mifosplatform.portfolio.loanproduct.data;
import org.joda.time.LocalDate;
import org.mifosplatform.portfolio.loanaccount.domain.Loan;
public class LoanOverdueDTO {
private final Loan loan;
private final boolean runInterestRecalculation;
private final LocalDate recalculateFrom;
public LoanOverdueDTO(final Loan loan, final boolean runInterestRecalculation) {
public LoanOverdueDTO(final Loan loan, final boolean runInterestRecalculation, final LocalDate recalculateFrom) {
this.loan = loan;
this.runInterestRecalculation = runInterestRecalculation;
this.recalculateFrom = recalculateFrom;
}
public boolean isRunInterestRecalculation() {
@@ -24,4 +27,9 @@ public class LoanOverdueDTO {
public Loan getLoan() {
return this.loan;
}
public LocalDate getRecalculateFrom() {
return this.recalculateFrom;
}
}
@@ -0,0 +1,2 @@
ALTER TABLE `m_loan`
ADD COLUMN `interest_recalcualated_on` DATE NULL DEFAULT NULL AFTER `accrued_till`;