MIFOSX-1418 : added regeneration of accrual entries

This commit is contained in:
Pramod Nuthakki
2014-08-16 14:28:53 +05:30
parent e2645b6461
commit 87217fb29e
10 changed files with 207 additions and 23 deletions
@@ -810,7 +810,7 @@ public class AccountingScenarioIntegrationTest {
.withRepaymentAfterEvery(this.LP_REPAYMENT_PERIOD).withNumberOfRepayments(this.LP_REPAYMENTS).withRepaymentTypeAsMonth() .withRepaymentAfterEvery(this.LP_REPAYMENT_PERIOD).withNumberOfRepayments(this.LP_REPAYMENTS).withRepaymentTypeAsMonth()
.withinterestRatePerPeriod(this.LP_INTEREST_RATE).withInterestRateFrequencyTypeAsMonths() .withinterestRatePerPeriod(this.LP_INTEREST_RATE).withInterestRateFrequencyTypeAsMonths()
.withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat().withAccountingRulePeriodicAccrual(accounts) .withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat().withAccountingRulePeriodicAccrual(accounts)
.build(null); .withDaysInMonth("30").withDaysInYear("365").build(null);
return this.loanTransactionHelper.getLoanProductId(loanProductJSON); return this.loanTransactionHelper.getLoanProductId(loanProductJSON);
} }
@@ -31,7 +31,7 @@ public class LoanProductTestBuilder {
public static final String MIFOS_STANDARD_STRATEGY = "1"; public static final String MIFOS_STANDARD_STRATEGY = "1";
// private static final String HEAVENS_FAMILY_STRATEGY ="2"; // private static final String HEAVENS_FAMILY_STRATEGY ="2";
// private static final String CREO_CORE_STRATEGY ="3"; // private static final String CREO_CORE_STRATEGY ="3";
public static final String RBI_INDIA_STRATEGY ="4"; public static final String RBI_INDIA_STRATEGY = "4";
private static final String NONE = "1"; private static final String NONE = "1";
private static final String CASH_BASED = "2"; private static final String CASH_BASED = "2";
private static final String ACCRUAL_PERIODIC = "3"; private static final String ACCRUAL_PERIODIC = "3";
@@ -63,7 +63,7 @@ public class LoanProductTestBuilder {
private Boolean multiDisburseLoan = false; private Boolean multiDisburseLoan = false;
private String outstandingLoanBalance = "35000"; private String outstandingLoanBalance = "35000";
private String maxTrancheCount = "35000"; private String maxTrancheCount = "35000";
private Boolean isInterestRecalculationEnabled = false; private Boolean isInterestRecalculationEnabled = false;
private String daysInYearType = "1"; private String daysInYearType = "1";
private String daysInMonthType = "1"; private String daysInMonthType = "1";
@@ -328,4 +328,13 @@ public class LoanProductTestBuilder {
return this; return this;
} }
public LoanProductTestBuilder withDaysInMonth(final String daysInMonthType) {
this.daysInMonthType = daysInMonthType;
return this;
}
public LoanProductTestBuilder withDaysInYear(final String daysInYearType) {
this.daysInYearType = daysInYearType;
return this;
}
} }
@@ -984,9 +984,36 @@ public class Loan extends AbstractPersistable<Long> {
updateLoanScheduleDependentDerivedFields(); updateLoanScheduleDependentDerivedFields();
updateLoanSummaryDerivedFields(); updateLoanSummaryDerivedFields();
resetAccruals();
} }
/**
* method to reverse all the accrual entries
*/
private void resetAccruals() {
if (!this.repaymentScheduleDetail().isInterestRecalculationEnabled()) { return; }
List<LoanTransaction> transactions = this.getLoanTransactions();
for (final LoanTransaction transaction : transactions) {
if (transaction.isAccrual()) {
transaction.reverse();
}
}
}
public boolean isAccrualsPresent() {
boolean isAccrualsPresent = false;
if (!this.repaymentScheduleDetail().isInterestRecalculationEnabled()) { return isAccrualsPresent; }
List<LoanTransaction> transactions = this.getLoanTransactions();
for (final LoanTransaction transaction : transactions) {
if (transaction.isAccrual() && transaction.isNotReversed()) {
isAccrualsPresent = true;
break;
}
}
return isAccrualsPresent;
}
private void updateLoanScheduleDependentDerivedFields() { private void updateLoanScheduleDependentDerivedFields() {
this.expectedMaturityDate = determineExpectedMaturityDate().toDate(); this.expectedMaturityDate = determineExpectedMaturityDate().toDate();
this.actualMaturityDate = determineExpectedMaturityDate().toDate(); this.actualMaturityDate = determineExpectedMaturityDate().toDate();
@@ -3218,7 +3245,7 @@ public class Loan extends AbstractPersistable<Long> {
return matchesCurrentLoanOfficer; return matchesCurrentLoanOfficer;
} }
private LocalDate getInterestChargedFromDate() { public LocalDate getInterestChargedFromDate() {
LocalDate interestChargedFrom = null; LocalDate interestChargedFrom = null;
if (this.interestChargedFromDate != null) { if (this.interestChargedFromDate != null) {
interestChargedFrom = new LocalDate(this.interestChargedFromDate); interestChargedFrom = new LocalDate(this.interestChargedFromDate);
@@ -3415,7 +3442,7 @@ public class Loan extends AbstractPersistable<Long> {
return this.loanProduct.isPeriodicAccrualAccountingEnabled(); return this.loanProduct.isPeriodicAccrualAccountingEnabled();
} }
private Long productId() { public Long productId() {
return this.loanProduct.getId(); return this.loanProduct.getId();
} }
@@ -4170,4 +4197,12 @@ public class Loan extends AbstractPersistable<Long> {
public List<LoanRepaymentScheduleInstallment> fetchRepaymentScheduleInstallments() { public List<LoanRepaymentScheduleInstallment> fetchRepaymentScheduleInstallments() {
return this.repaymentScheduleInstallments; return this.repaymentScheduleInstallments;
} }
public LocalDate getAccruedTill() {
LocalDate accruedTill = null;
if (this.accruedTill != null) {
accruedTill = new LocalDate(this.accruedTill);
}
return accruedTill;
}
} }
@@ -30,4 +30,13 @@ public interface LoanAccountDomainService {
PaymentDetail paymentDetail, String noteText, String txnExternalId); PaymentDetail paymentDetail, String noteText, String txnExternalId);
LocalDate getCalculatedRepaymentsStartingFromDate(LocalDate actualDisbursementDate, Loan loan, CalendarInstance calendarInstance); LocalDate getCalculatedRepaymentsStartingFromDate(LocalDate actualDisbursementDate, Loan loan, CalendarInstance calendarInstance);
/**
* This method is to recalculate and accrue the income till the last accrued
* date. this method is used when the schedule changes due to interest
* recalculation
*
* @param loan
*/
void recalculateAccruals(Loan loan);
} }
@@ -8,8 +8,10 @@ package org.mifosplatform.portfolio.loanaccount.domain;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.joda.time.LocalDate; import org.joda.time.LocalDate;
@@ -22,6 +24,7 @@ import org.mifosplatform.infrastructure.core.exception.PlatformApiDataValidation
import org.mifosplatform.organisation.holiday.domain.Holiday; import org.mifosplatform.organisation.holiday.domain.Holiday;
import org.mifosplatform.organisation.holiday.domain.HolidayRepository; import org.mifosplatform.organisation.holiday.domain.HolidayRepository;
import org.mifosplatform.organisation.holiday.domain.HolidayStatusType; import org.mifosplatform.organisation.holiday.domain.HolidayStatusType;
import org.mifosplatform.organisation.monetary.data.CurrencyData;
import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency; import org.mifosplatform.organisation.monetary.domain.ApplicationCurrency;
import org.mifosplatform.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; import org.mifosplatform.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
@@ -37,9 +40,14 @@ import org.mifosplatform.portfolio.calendar.domain.CalendarInstanceRepository;
import org.mifosplatform.portfolio.calendar.service.CalendarUtils; import org.mifosplatform.portfolio.calendar.service.CalendarUtils;
import org.mifosplatform.portfolio.client.domain.Client; import org.mifosplatform.portfolio.client.domain.Client;
import org.mifosplatform.portfolio.client.exception.ClientNotActiveException; import org.mifosplatform.portfolio.client.exception.ClientNotActiveException;
import org.mifosplatform.portfolio.common.domain.DaysInMonthType;
import org.mifosplatform.portfolio.common.domain.DaysInYearType;
import org.mifosplatform.portfolio.common.domain.PeriodFrequencyType;
import org.mifosplatform.portfolio.group.domain.Group; import org.mifosplatform.portfolio.group.domain.Group;
import org.mifosplatform.portfolio.group.exception.GroupNotActiveException; import org.mifosplatform.portfolio.group.exception.GroupNotActiveException;
import org.mifosplatform.portfolio.loanaccount.data.LoanScheduleAccrualData;
import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory;
import org.mifosplatform.portfolio.loanaccount.service.LoanAccrualWritePlatformService;
import org.mifosplatform.portfolio.loanaccount.service.LoanAssembler; import org.mifosplatform.portfolio.loanaccount.service.LoanAssembler;
import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.mifosplatform.portfolio.note.domain.Note; import org.mifosplatform.portfolio.note.domain.Note;
@@ -68,6 +76,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository; private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository;
private final CalendarInstanceRepository calendarInstanceRepository; private final CalendarInstanceRepository calendarInstanceRepository;
private final LoanRepaymentScheduleInstallmentRepository repaymentScheduleInstallmentRepository; private final LoanRepaymentScheduleInstallmentRepository repaymentScheduleInstallmentRepository;
private final LoanAccrualWritePlatformService accrualWritePlatformService;
@Autowired @Autowired
public LoanAccountDomainServiceJpa(final LoanAssembler loanAccountAssembler, final LoanRepository loanRepository, public LoanAccountDomainServiceJpa(final LoanAssembler loanAccountAssembler, final LoanRepository loanRepository,
@@ -79,7 +88,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
final AccountTransferRepository accountTransferRepository, final LoanScheduleGeneratorFactory loanScheduleFactory, final AccountTransferRepository accountTransferRepository, final LoanScheduleGeneratorFactory loanScheduleFactory,
final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository, final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository,
final CalendarInstanceRepository calendarInstanceRepository, final CalendarInstanceRepository calendarInstanceRepository,
final LoanRepaymentScheduleInstallmentRepository repaymentScheduleInstallmentRepository) { final LoanRepaymentScheduleInstallmentRepository repaymentScheduleInstallmentRepository,
final LoanAccrualWritePlatformService accrualWritePlatformService) {
this.loanAccountAssembler = loanAccountAssembler; this.loanAccountAssembler = loanAccountAssembler;
this.loanRepository = loanRepository; this.loanRepository = loanRepository;
this.loanTransactionRepository = loanTransactionRepository; this.loanTransactionRepository = loanTransactionRepository;
@@ -94,6 +104,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
this.applicationCurrencyRepository = applicationCurrencyRepository; this.applicationCurrencyRepository = applicationCurrencyRepository;
this.calendarInstanceRepository = calendarInstanceRepository; this.calendarInstanceRepository = calendarInstanceRepository;
this.repaymentScheduleInstallmentRepository = repaymentScheduleInstallmentRepository; this.repaymentScheduleInstallmentRepository = repaymentScheduleInstallmentRepository;
this.accrualWritePlatformService = accrualWritePlatformService;
} }
@Transactional @Transactional
@@ -187,6 +198,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer);
recalculateAccruals(loan);
builderResult.withEntityId(newRepaymentTransaction.getId()) // builderResult.withEntityId(newRepaymentTransaction.getId()) //
.withOfficeId(loan.getOfficeId()) // .withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) // .withClientId(loan.getClientId()) //
@@ -210,7 +222,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
"Validation errors exist.", dataValidationErrors); } "Validation errors exist.", dataValidationErrors); }
} }
} }
private void saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan loan) { private void saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan loan) {
try { try {
List<LoanRepaymentScheduleInstallment> installments = loan.fetchRepaymentScheduleInstallments(); List<LoanRepaymentScheduleInstallment> installments = loan.fetchRepaymentScheduleInstallments();
@@ -270,6 +282,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
} }
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer);
recalculateAccruals(loan);
return newPaymentTransaction; return newPaymentTransaction;
} }
@@ -364,7 +377,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
} }
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer);
return disbursementTransaction; return disbursementTransaction;
} }
@@ -402,6 +414,67 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
return calculatedRepaymentsStartingFromDate; return calculatedRepaymentsStartingFromDate;
} }
/* (non-Javadoc)
* @see org.mifosplatform.portfolio.loanaccount.domain.LoanAccountDomainService#recalculateAccruals(org.mifosplatform.portfolio.loanaccount.domain.Loan)
*/
@Override
public void recalculateAccruals(Loan loan) {
LocalDate accruedTill = loan.getAccruedTill();
if (!loan.repaymentScheduleDetail().isInterestRecalculationEnabled() || accruedTill == null || loan.isAccrualsPresent()) { return; }
Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas = new ArrayList<>();
List<LoanRepaymentScheduleInstallment> installments = loan.fetchRepaymentScheduleInstallments();
Long loanId = loan.getId();
DaysInMonthType daysInMonth = loan.repaymentScheduleDetail().fetchDaysInMonthType();
DaysInYearType daysInYear = loan.repaymentScheduleDetail().fetchDaysInYearType();
Long officeId = loan.getOfficeId();
LocalDate accrualStartDate = null;
PeriodFrequencyType repaymentFrequency = loan.repaymentScheduleDetail().getRepaymentPeriodFrequencyType();
Integer repayEvery = loan.repaymentScheduleDetail().getRepayEvery();
LocalDate interestCalculatedFrom = loan.getInterestChargedFromDate();
Long loanProductId = loan.productId();
MonetaryCurrency currency = loan.getCurrency();
ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
CurrencyData currencyData = applicationCurrency.toData();
Set<LoanCharge> loanCharges = loan.charges();
BigDecimal accruedInterestIncome = null;
BigDecimal accruedFeeIncome = null;
BigDecimal accruedPenaltyIncome = null;
for (LoanRepaymentScheduleInstallment installment : installments) {
if (!accruedTill.isBefore(installment.getDueDate())
|| (accruedTill.isAfter(installment.getFromDate()) && !accruedTill.isAfter(installment.getDueDate()))) {
BigDecimal dueDateFeeIncome = BigDecimal.ZERO;
BigDecimal dueDatePenaltyIncome = BigDecimal.ZERO;
LocalDate chargesTillDate = installment.getDueDate();
if (!accruedTill.isAfter(installment.getDueDate())) {
chargesTillDate = accruedTill;
}
for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(installment.getFromDate(), chargesTillDate)) {
if (loanCharge.isFeeCharge()) {
dueDateFeeIncome = dueDateFeeIncome.add(loanCharge.amount());
} else if (loanCharge.isPenaltyCharge()) {
dueDatePenaltyIncome = dueDatePenaltyIncome.add(loanCharge.amount());
}
}
}
LoanScheduleAccrualData accrualData = new LoanScheduleAccrualData(loanId, officeId, accrualStartDate,
daysInMonth.getValue(), daysInYear.getValue(), repaymentFrequency, repayEvery, installment.getDueDate(),
installment.getFromDate(), installment.getId(), loanProductId,
installment.getInterestCharged(currency).getAmount(), installment.getFeeChargesCharged(currency).getAmount(),
installment.getPenaltyChargesCharged(currency).getAmount(), accruedInterestIncome, accruedFeeIncome,
accruedPenaltyIncome, currencyData, dueDateFeeIncome, dueDatePenaltyIncome, interestCalculatedFrom);
loanScheduleAccrualDatas.add(accrualData);
}
}
if (!loanScheduleAccrualDatas.isEmpty()) {
this.accrualWritePlatformService.addPeriodicAccruals(accruedTill, loanScheduleAccrualDatas);
}
}
private void updateLoanTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) { private void updateLoanTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) {
final AccountTransferTransaction transferTransaction = this.accountTransferRepository.findByToLoanTransactionId(loanTransactionId); final AccountTransferTransaction transferTransaction = this.accountTransferRepository.findByToLoanTransactionId(loanTransactionId);
if (transferTransaction != null) { if (transferTransaction != null) {
@@ -146,6 +146,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
loanApplicationTerms.getInterestChargedFromLocalDate(), loanApplicationTerms.getLoanTermPeriodFrequencyType(), loanApplicationTerms.getInterestChargedFromLocalDate(), loanApplicationTerms.getLoanTermPeriodFrequencyType(),
loanApplicationTerms.getRepaymentEvery()); loanApplicationTerms.getRepaymentEvery());
Money balanceForcalculation = outstandingBalance; Money balanceForcalculation = outstandingBalance;
// reduce principal is the early payment, will processed as per the
// reschedule strategy
if (reducePrincipal.isGreaterThanZero()) { if (reducePrincipal.isGreaterThanZero()) {
switch (loanApplicationTerms.getRescheduleStrategyMethod()) { switch (loanApplicationTerms.getRescheduleStrategyMethod()) {
case REDUCE_EMI_AMOUNT: case REDUCE_EMI_AMOUNT:
@@ -211,10 +213,14 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
fixedEmiAmount = principalForThisPeriod.plus(interestForThisinstallment); fixedEmiAmount = principalForThisPeriod.plus(interestForThisinstallment);
} }
// this block is to identify interest based on late/early payment
if (diffAmt != null && !diffAmt.isEmpty()) { if (diffAmt != null && !diffAmt.isEmpty()) {
BigDecimal interestDueToLatePayment = BigDecimal.ZERO; BigDecimal interestDueToLatePayment = BigDecimal.ZERO;
BigDecimal interestReducedDueToEarlyPayment = BigDecimal.ZERO; BigDecimal interestReducedDueToEarlyPayment = BigDecimal.ZERO;
for (RecalculationDetail detail : diffAmt) { for (RecalculationDetail detail : diffAmt) {
// will increase the principal portion and reduces interest
// as per the number of days. and also identifies reduce
// principal for reschedule strategy
if (!detail.isLatePayment() && detail.getStartDate().isAfter(periodStartDate) if (!detail.isLatePayment() && detail.getStartDate().isAfter(periodStartDate)
&& !detail.getStartDate().isAfter(scheduledDueDate)) { && !detail.getStartDate().isAfter(scheduledDueDate)) {
reducePrincipal = reducePrincipal.plus(detail.getAmount()); reducePrincipal = reducePrincipal.plus(detail.getAmount());
@@ -234,7 +240,10 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
interestReducedDueToEarlyPayment = interestReducedDueToEarlyPayment.add(loanApplicationTerms.interestRateFor( interestReducedDueToEarlyPayment = interestReducedDueToEarlyPayment.add(loanApplicationTerms.interestRateFor(
this.paymentPeriodsInOneYearCalculator, mc, detail.getAmount(), detail.getStartDate(), scheduledDueDate)); this.paymentPeriodsInOneYearCalculator, mc, detail.getAmount(), detail.getStartDate(), scheduledDueDate));
} else if (detail.isLatePayment() && detail.isOverlapping(periodStartDate, scheduledDueDate) }
// calculates the interest for late payment and increase
// the interest for the installment
else if (detail.isLatePayment() && detail.isOverlapping(periodStartDate, scheduledDueDate)
&& periodStartDate.isBefore(LocalDate.now())) { && periodStartDate.isBefore(LocalDate.now())) {
LocalDate fromDate = periodStartDate; LocalDate fromDate = periodStartDate;
LocalDate toDate = scheduledDueDate; LocalDate toDate = scheduledDueDate;
@@ -312,6 +321,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
} }
} }
// this block is to add extra re-payment schedules with interest portion
// if the last payment is missed
if (diffAmt != null && !diffAmt.isEmpty() && !periodStartDate.isAfter(LocalDate.now())) { if (diffAmt != null && !diffAmt.isEmpty() && !periodStartDate.isAfter(LocalDate.now())) {
Map<LocalDate, RecalculationDetail> processDetails = new TreeMap<>(); Map<LocalDate, RecalculationDetail> processDetails = new TreeMap<>();
for (RecalculationDetail detail : diffAmt) { for (RecalculationDetail detail : diffAmt) {
@@ -518,6 +529,11 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
return cumulative; return cumulative;
} }
/**
* 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 )
*/
@Override @Override
public LoanScheduleModel rescheduleNextInstallments(final MathContext mc, final ApplicationCurrency applicationCurrency, public LoanScheduleModel rescheduleNextInstallments(final MathContext mc, final ApplicationCurrency applicationCurrency,
final LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges, final boolean isHolidayEnabled, final LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges, final boolean isHolidayEnabled,
@@ -570,6 +586,11 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
return recalculateFrom; return recalculateFrom;
} }
/**
* Method calls regenerate schedule for a particular set of transactions
* till all the installments are processed to identify early or late
* payments and will be used in regeneration of schedule
*/
private LoanScheduleModel recalculateInstallment(final MathContext mc, final ApplicationCurrency applicationCurrency, private LoanScheduleModel recalculateInstallment(final MathContext mc, final ApplicationCurrency applicationCurrency,
final LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges, final boolean isHolidayEnabled, final LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges, final boolean isHolidayEnabled,
final List<Holiday> holidays, final WorkingDays workingDays, final List<Holiday> holidays, final WorkingDays workingDays,
@@ -593,6 +614,9 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
return loanScheduleModel; return loanScheduleModel;
} }
/**
* Method identifies late or early payments for schedule recalculation.
*/
private RecalculatedSchedule recalculateInterest(final MathContext mc, final ApplicationCurrency applicationCurrency, private RecalculatedSchedule recalculateInterest(final MathContext mc, final ApplicationCurrency applicationCurrency,
final LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges, final boolean isHolidayEnabled, final LoanApplicationTerms loanApplicationTerms, final Set<LoanCharge> loanCharges, final boolean isHolidayEnabled,
final List<Holiday> holidays, final WorkingDays workingDays, final List<LoanTransaction> transactions, final List<Holiday> holidays, final WorkingDays workingDays, final List<LoanTransaction> transactions,
@@ -638,6 +662,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
Map<LocalDate, Money> earlyPaymentMap = loanRepaymentScheduleTransactionProcessor.handleRepaymentSchedule( Map<LocalDate, Money> earlyPaymentMap = loanRepaymentScheduleTransactionProcessor.handleRepaymentSchedule(
transactionsForInstallment, currency, processinstallmets, installment, recalculationDates); transactionsForInstallment, currency, processinstallmets, installment, recalculationDates);
// this block is to create early payment entries for schedule
// generation
for (Map.Entry<LocalDate, Money> entry : earlyPaymentMap.entrySet()) { for (Map.Entry<LocalDate, Money> entry : earlyPaymentMap.entrySet()) {
LocalDate startDate = entry.getKey(); LocalDate startDate = entry.getKey();
if (unpaidPricipal.isGreaterThan(entry.getValue())) { if (unpaidPricipal.isGreaterThan(entry.getValue())) {
@@ -655,6 +681,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
Money totalOutstanding = installment.getTotalOutstanding(currency); Money totalOutstanding = installment.getTotalOutstanding(currency);
boolean reduceStartDate = false; boolean reduceStartDate = false;
// this block is to identify late payment based on the rest
// calculation date
while (totalOutstanding.isGreaterThanZero() && startDate.isBefore(LocalDate.now())) { while (totalOutstanding.isGreaterThanZero() && startDate.isBefore(LocalDate.now())) {
LocalDate recalculateFrom = getNextRestScheduleDate(startDate.minusDays(1), loanApplicationTerms, isHolidayEnabled, LocalDate recalculateFrom = getNextRestScheduleDate(startDate.minusDays(1), loanApplicationTerms, isHolidayEnabled,
holidays, workingDays); holidays, workingDays);
@@ -725,6 +753,10 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
return installments; return installments;
} }
/**
* Method to identify which transaction did the payment for current
* installment
*/
private void applyRest(List<LoanTransaction> loanTransactions, LocalDate from, LocalDate to, private void applyRest(List<LoanTransaction> loanTransactions, LocalDate from, LocalDate to,
LoanRepaymentScheduleInstallment installment, LoanRepaymentScheduleInstallment installment,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, MonetaryCurrency currency, final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, MonetaryCurrency currency,
@@ -760,6 +792,9 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
return nextScheduleDate; return nextScheduleDate;
} }
/**
* Method returns the amount payable to close the loan account as of today.
*/
@Override @Override
public Money fetchPrepaymentAmount(final List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency, public Money fetchPrepaymentAmount(final List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency,
final LoanApplicationTerms applicationTerms, final MathContext mc) { final LoanApplicationTerms applicationTerms, final MathContext mc) {
@@ -5,8 +5,11 @@
*/ */
package org.mifosplatform.portfolio.loanaccount.service; package org.mifosplatform.portfolio.loanaccount.service;
import java.util.Collection;
import org.joda.time.LocalDate; import org.joda.time.LocalDate;
import org.mifosplatform.infrastructure.jobs.exception.JobExecutionException; import org.mifosplatform.infrastructure.jobs.exception.JobExecutionException;
import org.mifosplatform.portfolio.loanaccount.data.LoanScheduleAccrualData;
public interface LoanAccrualWritePlatformService { public interface LoanAccrualWritePlatformService {
@@ -17,4 +20,6 @@ public interface LoanAccrualWritePlatformService {
String addPeriodicAccruals(LocalDate tilldate); String addPeriodicAccruals(LocalDate tilldate);
String addPeriodicAccruals(LocalDate tilldate, Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas);
} }
@@ -95,7 +95,8 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
return addPeriodicAccruals(tilldate, loanScheduleAccrualDatas); return addPeriodicAccruals(tilldate, loanScheduleAccrualDatas);
} }
private String addPeriodicAccruals(final LocalDate tilldate, Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas) { @Override
public String addPeriodicAccruals(final LocalDate tilldate, Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
Set<Long> loansIds = new HashSet<>(); Set<Long> loansIds = new HashSet<>();
LocalDate accruredTill = null; LocalDate accruredTill = null;
@@ -129,16 +130,18 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
private void addAccrualTillSpecificDate(final LocalDate tilldate, final LoanScheduleAccrualData accrualData, private void addAccrualTillSpecificDate(final LocalDate tilldate, final LoanScheduleAccrualData accrualData,
final LocalDate accruredTill) throws Exception { final LocalDate accruredTill) throws Exception {
LocalDate interestStartDate = accrualData.getFromDateAsLocaldate(); LocalDate interestStartDate = accrualData.getFromDateAsLocaldate();
boolean isInterestStartDateEqulsInstallmentStartdate = true;
if (accrualData.getInterestCalculatedFrom() != null if (accrualData.getInterestCalculatedFrom() != null
&& accrualData.getFromDateAsLocaldate().isBefore(accrualData.getInterestCalculatedFrom())) { && accrualData.getFromDateAsLocaldate().isBefore(accrualData.getInterestCalculatedFrom())) {
isInterestStartDateEqulsInstallmentStartdate = false;
if (accrualData.getInterestCalculatedFrom().isBefore(accrualData.getDueDateAsLocaldate())) { if (accrualData.getInterestCalculatedFrom().isBefore(accrualData.getDueDateAsLocaldate())) {
interestStartDate = accrualData.getInterestCalculatedFrom(); interestStartDate = accrualData.getInterestCalculatedFrom();
} else { } else {
interestStartDate = accrualData.getDueDateAsLocaldate(); interestStartDate = accrualData.getDueDateAsLocaldate();
} }
} }
int totalNumberOfDays = Days.daysBetween(interestStartDate, accrualData.getDueDateAsLocaldate()).getDays(); int totalNumberOfDays = Days.daysBetween(interestStartDate, accrualData.getDueDateAsLocaldate()).getDays();
LocalDate startDate = accruredTill; LocalDate startDate = accruredTill;
if (startDate == null) { if (startDate == null) {
@@ -156,10 +159,16 @@ public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlat
int daysInSchedule = Days.daysBetween(interestStartDate, tilldate).getDays(); int daysInSchedule = Days.daysBetween(interestStartDate, tilldate).getDays();
switch (accrualData.getRepaymentFrequency()) { switch (accrualData.getRepaymentFrequency()) {
case MONTHS: case MONTHS:
totalNumberOfDays = calculateTotalNumberOfDaysForMonth(totalNumberOfDays, accrualData); int numberOfDaysInMonth = calculateTotalNumberOfDaysForMonth(totalNumberOfDays, accrualData);
if (!(!isInterestStartDateEqulsInstallmentStartdate && totalNumberOfDays < numberOfDaysInMonth)) {
totalNumberOfDays = numberOfDaysInMonth;
}
break; break;
case YEARS: case YEARS:
totalNumberOfDays = calculateTotalNumberOfDaysForYear(totalNumberOfDays, accrualData); int numberOfDaysinYear = calculateTotalNumberOfDaysForYear(totalNumberOfDays, accrualData);
if (!(!isInterestStartDateEqulsInstallmentStartdate && totalNumberOfDays < numberOfDaysinYear)) {
totalNumberOfDays = numberOfDaysinYear;
}
break; break;
default: default:
break; break;
@@ -1424,6 +1424,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
.append("loan.id as loanId ,if(loan.client_id is null,mg.office_id,mc.office_id) as officeId,") .append("loan.id as loanId ,if(loan.client_id is null,mg.office_id,mc.office_id) as officeId,")
.append("loan.accrued_till as accruedTill, loan.repayment_period_frequency_enum as frequencyEnum, ") .append("loan.accrued_till as accruedTill, loan.repayment_period_frequency_enum as frequencyEnum, ")
.append("loan.interest_calculated_from_date as interestCalculatedFrom, ") .append("loan.interest_calculated_from_date as interestCalculatedFrom, ")
.append("loan.days_in_month_enum as daysInMonth, ")
.append("loan.days_in_year_enum as daysInYear, ")
.append("loan.repay_every as repayEvery,") .append("loan.repay_every as repayEvery,")
.append("(select sum(lc.amount) as dueamount from m_loan_charge lc where lc.loan_id = loan.id and lc.is_active = 1 and lc.waived = 0 and lc.is_penalty = 0 and lc.due_for_collection_as_of_date > ls.fromdate and lc.due_for_collection_as_of_date<= if(:tilldate < ls.duedate, :tilldate , ls.duedate)) as feedueIn,") .append("(select sum(lc.amount) as dueamount from m_loan_charge lc where lc.loan_id = loan.id and lc.is_active = 1 and lc.waived = 0 and lc.is_penalty = 0 and lc.due_for_collection_as_of_date > ls.fromdate and lc.due_for_collection_as_of_date<= if(:tilldate < ls.duedate, :tilldate , ls.duedate)) as feedueIn,")
.append("(select sum(lc.amount) as dueamount from m_loan_charge lc where lc.loan_id = loan.id and lc.is_active = 1 and lc.waived = 0 and lc.is_penalty = 1 and lc.due_for_collection_as_of_date > ls.fromdate and lc.due_for_collection_as_of_date<= if(:tilldate < ls.duedate, :tilldate , ls.duedate)) as penalitydueIn,") .append("(select sum(lc.amount) as dueamount from m_loan_charge lc where lc.loan_id = loan.id and lc.is_active = 1 and lc.waived = 0 and lc.is_penalty = 1 and lc.due_for_collection_as_of_date > ls.fromdate and lc.due_for_collection_as_of_date<= if(:tilldate < ls.duedate, :tilldate , ls.duedate)) as penalitydueIn,")
@@ -1448,8 +1450,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final Long officeId = rs.getLong("officeId"); final Long officeId = rs.getLong("officeId");
final LocalDate accruedTill = JdbcSupport.getLocalDate(rs, "accruedTill"); final LocalDate accruedTill = JdbcSupport.getLocalDate(rs, "accruedTill");
final LocalDate interestCalculatedFrom = JdbcSupport.getLocalDate(rs, "interestCalculatedFrom"); final LocalDate interestCalculatedFrom = JdbcSupport.getLocalDate(rs, "interestCalculatedFrom");
final Integer numberOfDaysInMonth = 30; final Integer numberOfDaysInMonth = JdbcSupport.getInteger(rs, "daysInMonth");;
final Integer numberOfDaysInYear = 360; final Integer numberOfDaysInYear = JdbcSupport.getInteger(rs, "daysInYear");;
final Integer frequencyEnum = JdbcSupport.getInteger(rs, "frequencyEnum"); final Integer frequencyEnum = JdbcSupport.getInteger(rs, "frequencyEnum");
final Integer repayEvery = JdbcSupport.getInteger(rs, "repayEvery"); final Integer repayEvery = JdbcSupport.getInteger(rs, "repayEvery");
final PeriodFrequencyType frequency = PeriodFrequencyType.fromInt(frequencyEnum); final PeriodFrequencyType frequency = PeriodFrequencyType.fromInt(frequencyEnum);
@@ -315,6 +315,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
} }
final Set<LoanCharge> loanCharges = loan.charges(); final Set<LoanCharge> loanCharges = loan.charges();
@@ -340,7 +341,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
updateRecurringCalendarDatesForInterestRecalculation(loan); updateRecurringCalendarDatesForInterestRecalculation(loan);
this.loanAccountDomainService.recalculateAccruals(loan);
return new CommandProcessingResultBuilder() // return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) // .withCommandId(command.commandId()) //
.withEntityId(loan.getId()) // .withEntityId(loan.getId()) //
@@ -526,7 +527,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
} }
updateRecurringCalendarDatesForInterestRecalculation(loan); updateRecurringCalendarDatesForInterestRecalculation(loan);
this.loanAccountDomainService.recalculateAccruals(loan);
} }
return changes; return changes;
} }
@@ -766,6 +769,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan);
return new CommandProcessingResultBuilder() // return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) // .withCommandId(command.commandId()) //
.withEntityId(transactionId) // .withEntityId(transactionId) //
@@ -857,7 +862,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan);
return new CommandProcessingResultBuilder() // return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) // .withCommandId(command.commandId()) //
.withEntityId(waiveInterestTransaction.getId()) // .withEntityId(waiveInterestTransaction.getId()) //
@@ -925,7 +930,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan);
return new CommandProcessingResultBuilder() // return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) // .withCommandId(command.commandId()) //
.withEntityId(writeoff.getId()) // .withEntityId(writeoff.getId()) //
@@ -998,7 +1003,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (possibleClosingTransaction != null) { if (possibleClosingTransaction != null) {
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
} }
this.loanAccountDomainService.recalculateAccruals(loan);
CommandProcessingResult result = null; CommandProcessingResult result = null;
if (possibleClosingTransaction != null) { if (possibleClosingTransaction != null) {
@@ -2043,6 +2048,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
saveLoanWithDataIntegrityViolationChecks(loan); saveLoanWithDataIntegrityViolationChecks(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan);
return new CommandProcessingResultBuilder() // return new CommandProcessingResultBuilder() //
.withOfficeId(loan.getOfficeId()) // .withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) // .withClientId(loan.getClientId()) //
@@ -2091,7 +2097,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan);
return new CommandProcessingResultBuilder() // return new CommandProcessingResultBuilder() //
.withOfficeId(loan.getOfficeId()) // .withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) // .withClientId(loan.getClientId()) //
@@ -2205,8 +2211,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), loan.getDisbursementDate().toDate()); holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), loan.getDisbursementDate().toDate());
workingDays = this.workingDaysRepository.findOne(); workingDays = this.workingDaysRepository.findOne();
} }
ChangedTransactionDetail changedTransactionDetail = loan.recalculateScheduleFromLastTransaction(loanScheduleFactory, applicationCurrency, ChangedTransactionDetail changedTransactionDetail = loan.recalculateScheduleFromLastTransaction(loanScheduleFactory,
calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays, restCalendarInstance, applicationCurrency, calculatedRepaymentsStartingFromDate, isHolidayEnabled, holidays, workingDays, restCalendarInstance,
existingTransactionIds, existingReversedTransactionIds); existingTransactionIds, existingReversedTransactionIds);
saveLoanWithDataIntegrityViolationChecks(loan); saveLoanWithDataIntegrityViolationChecks(loan);
@@ -2218,6 +2224,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
} }
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
this.loanAccountDomainService.recalculateAccruals(loan);
} }
private void updateLoanTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) { private void updateLoanTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) {