/* * Copyright (c) 2005-2011 Grameen Foundation USA * All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or * implied. See the License for the specific language governing * permissions and limitations under the License. * * See also http://www.apache.org/licenses/LICENSE-2.0.html for an * explanation of the license and how it is applied. */ package org.mifos.accounts.loan.business.service; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Locale; import org.joda.time.LocalDate; import org.mifos.accounts.business.AccountActionDateEntity; import org.mifos.accounts.business.AccountFeesActionDetailEntity; import org.mifos.accounts.business.AccountPaymentEntity; import org.mifos.accounts.business.service.AccountBusinessService; import org.mifos.accounts.loan.business.LoanBO; import org.mifos.accounts.loan.business.LoanScheduleEntity; import org.mifos.accounts.loan.business.OriginalLoanFeeScheduleEntity; import org.mifos.accounts.loan.business.OriginalLoanScheduleEntity; import org.mifos.accounts.loan.business.ScheduleCalculatorAdaptor; import org.mifos.accounts.loan.persistance.LegacyLoanDao; import org.mifos.accounts.loan.persistance.LoanDao; import org.mifos.accounts.loan.util.helpers.LoanConstants; import org.mifos.accounts.loan.util.helpers.RepaymentScheduleInstallment; import org.mifos.accounts.penalties.business.AmountPenaltyBO; import org.mifos.accounts.penalties.business.PenaltyBO; import org.mifos.accounts.penalties.business.RatePenaltyBO; import org.mifos.accounts.productdefinition.util.helpers.InterestType; import org.mifos.accounts.util.helpers.AccountConstants; import org.mifos.accounts.util.helpers.AccountExceptionConstants; import org.mifos.accounts.util.helpers.PaymentData; import org.mifos.application.holiday.business.service.HolidayService; import org.mifos.application.servicefacade.ApplicationContextProvider; import org.mifos.config.AccountingRules; import org.mifos.config.business.service.ConfigurationBusinessService; import org.mifos.core.MifosRuntimeException; import org.mifos.customers.business.CustomerBO; import org.mifos.customers.personnel.business.PersonnelBO; import org.mifos.dto.domain.ApplicableCharge; import org.mifos.framework.business.AbstractBusinessObject; import org.mifos.framework.business.service.BusinessService; import org.mifos.framework.exceptions.PersistenceException; import org.mifos.framework.exceptions.ServiceException; import org.mifos.framework.util.LocalizationConverter; import org.mifos.framework.util.helpers.DateUtils; import org.mifos.framework.util.helpers.Money; import org.mifos.platform.validations.Errors; import org.mifos.security.util.UserContext; import org.springframework.beans.factory.annotation.Autowired; public class LoanBusinessService implements BusinessService { private LegacyLoanDao legacyLoanDao = ApplicationContextProvider.getBean(LegacyLoanDao.class); private ConfigurationBusinessService configService = new ConfigurationBusinessService(); @Autowired private AccountBusinessService accountBusinessService; @Autowired private HolidayService holidayService; @Autowired private ScheduleCalculatorAdaptor scheduleCalculatorAdaptor; public LegacyLoanDao getlegacyLoanDao() { if (legacyLoanDao == null) { legacyLoanDao = ApplicationContextProvider.getBean(LegacyLoanDao.class); } return legacyLoanDao; } public ConfigurationBusinessService getConfigService() { if (configService == null) { configService = new ConfigurationBusinessService(); } return configService; } public AccountBusinessService getAccountBusinessService() { if (accountBusinessService == null) { accountBusinessService = new AccountBusinessService(); } return accountBusinessService; } // Spring requires this // Autowired can be used at the constructor but // constructor is possing some non bean dependencies so for now // we use property bean injection instead of construct-arg protected LoanBusinessService() { } public LoanBusinessService(LegacyLoanDao legacyLoanDao, ConfigurationBusinessService configService, AccountBusinessService accountBusinessService, HolidayService holidayService, ScheduleCalculatorAdaptor scheduleCalculatorAdaptor) { this.legacyLoanDao = legacyLoanDao; this.configService = configService; this.accountBusinessService = accountBusinessService; this.holidayService = holidayService; this.scheduleCalculatorAdaptor = scheduleCalculatorAdaptor; } @Override public AbstractBusinessObject getBusinessObject(@SuppressWarnings("unused") final UserContext userContext) { return null; } /** * @deprecated use {@link LoanDao#findByGlobalAccountNum(String)} */ @Deprecated public LoanBO findBySystemId(final String accountGlobalNum) throws ServiceException { try { return getlegacyLoanDao().findBySystemId(accountGlobalNum); } catch (PersistenceException e) { throw new ServiceException(AccountExceptionConstants.FINDBYGLOBALACCNTEXCEPTION, e, new Object[] { accountGlobalNum }); } } /** * use loanDao implementation */ @Deprecated public List<LoanBO> findIndividualLoans(final String accountId) throws ServiceException { try { return getlegacyLoanDao().findIndividualLoans(accountId); } catch (PersistenceException e) { throw new ServiceException(AccountExceptionConstants.FINDBYGLOBALACCNTEXCEPTION, e, new Object[] { accountId }); } } /** * @deprecated - use {@link LoanDao#findById(Integer)} */ @Deprecated public LoanBO getAccount(final Integer accountId) throws ServiceException { try { return getlegacyLoanDao().getAccount(accountId); } catch (PersistenceException e) { throw new ServiceException(e); } } public List<LoanBO> getLoanAccountsActiveInGoodBadStanding(final Integer customerId) throws ServiceException { try { return getlegacyLoanDao().getLoanAccountsActiveInGoodBadStanding(customerId); } catch (PersistenceException e) { throw new ServiceException(e); } } public Short getLastPaymentAction(final Integer accountId) throws ServiceException { try { return getlegacyLoanDao().getLastPaymentAction(accountId); } catch (PersistenceException e) { throw new ServiceException(e); } } public List<LoanBO> getSearchResults(final String officeId, final String personnelId, final String currentStatus) throws ServiceException { try { return getlegacyLoanDao().getSearchResults(officeId, personnelId, currentStatus); } catch (PersistenceException he) { throw new ServiceException(he); } } public List<LoanBO> getAllLoanAccounts() throws ServiceException { try { return getlegacyLoanDao().getAllLoanAccounts(); } catch (PersistenceException pe) { throw new ServiceException(pe); } } public List<LoanBO> getAllChildrenForParentGlobalAccountNum(final String globalAccountNum) throws ServiceException { return findIndividualLoans(findBySystemId(globalAccountNum).getAccountId().toString()); } public List<LoanBO> getActiveLoansForAllClientsAssociatedWithGroupLoan(final LoanBO loan) throws ServiceException { List<LoanBO> activeLoans = new ArrayList<LoanBO>(); Collection<CustomerBO> clients = getClientsAssociatedWithGroupLoan(loan); if (clients != null && clients.size() > 0) { for (CustomerBO client : clients) { try { List<LoanBO> clientLoans = getlegacyLoanDao().getLoanAccountsActiveInGoodBadStanding(client.getCustomerId()); activeLoans.addAll(clientLoans); } catch (PersistenceException e) { throw new MifosRuntimeException(e); } } } return activeLoans; } private Collection<CustomerBO> getClientsAssociatedWithGroupLoan(final LoanBO loan) throws ServiceException { Collection<CustomerBO> clients; if (getConfigService().isGlimEnabled() || getConfigService().isNewGlimEnabled()) { clients = getAccountBusinessService().getCoSigningClientsForGlim(loan.getAccountId()); } else { clients = loan.getCustomer().getChildren(); } return clients; } public List<RepaymentScheduleInstallment> applyDailyInterestRatesWhereApplicable(LoanScheduleGenerationDto loanScheduleGenerationDto, Locale locale) { LoanBO loanBO = loanScheduleGenerationDto.getLoanBO(); List<RepaymentScheduleInstallment> installments = loanBO.toRepaymentScheduleDto(locale); return applyDailyInterestRatesWhereApplicable(loanScheduleGenerationDto, installments); } private boolean dailyInterestRatesApplicable(LoanScheduleGenerationDto loanScheduleGenerationDto, LoanBO loanBO) { return loanScheduleGenerationDto.isVariableInstallmentsAllowed() || loanBO.isDecliningBalanceInterestRecalculation(); } public void applyDailyInterestRates(LoanScheduleGenerationDto loanScheduleGenerationDto, boolean flatInterestType) { Double dailyInterestFactor = loanScheduleGenerationDto.getInterestRate() / (AccountingRules.getNumberOfInterestDays() * 100d); Money principalOutstanding = loanScheduleGenerationDto.getLoanAmountValue(); Money runningPrincipal = new Money(loanScheduleGenerationDto.getLoanAmountValue().getCurrency()); Date initialDueDate = loanScheduleGenerationDto.getDisbursementDate(); int installmentIndex, numInstallments; for (installmentIndex = 0, numInstallments = loanScheduleGenerationDto.getInstallments().size(); installmentIndex < numInstallments - 1; installmentIndex++) { RepaymentScheduleInstallment installment = loanScheduleGenerationDto.getInstallments().get(installmentIndex); Date currentDueDate = installment.getDueDateValue(); long duration = DateUtils.getNumberOfDaysBetweenTwoDates(currentDueDate, initialDueDate); Money fees = installment.getFees(); Money interest = computeInterestAmount(dailyInterestFactor, principalOutstanding, installment, duration); Money miscFee = installment.getMiscFees(); Money miscPenality = installment.getMiscPenalty(); Money total = installment.getTotalValue(); Money principal = total.subtract(interest.add(fees).add(miscFee).add(miscPenality)); installment.setPrincipalAndInterest(interest, principal); initialDueDate = currentDueDate; if (!flatInterestType) { principalOutstanding = principalOutstanding.subtract(principal); } runningPrincipal = runningPrincipal.add(principal); } RepaymentScheduleInstallment lastInstallment = loanScheduleGenerationDto.getInstallments().get(installmentIndex); long duration = DateUtils.getNumberOfDaysBetweenTwoDates(lastInstallment.getDueDateValue(), initialDueDate); Money interest = computeInterestAmount(dailyInterestFactor, principalOutstanding, lastInstallment, duration); Money fees = lastInstallment.getFees(); Money principal = loanScheduleGenerationDto.getLoanAmountValue().subtract(runningPrincipal); Money total = principal.add(interest).add(fees); lastInstallment.setTotalAndTotalValue(total); lastInstallment.setPrincipalAndInterest(interest, principal); } private Money computeInterestAmount(Double dailyInterestFactor, Money principalOutstanding, RepaymentScheduleInstallment installment, long duration) { Double interestForInstallment = dailyInterestFactor * duration * principalOutstanding.getAmountDoubleValue(); return new Money(installment.getCurrency(), interestForInstallment); } public void adjustInstallmentGapsPostDisbursal(List<RepaymentScheduleInstallment> installments, Date oldDisbursementDate, Date newDisbursementDate, Short officeId) { Date oldPrevDate = oldDisbursementDate, newPrevDate = newDisbursementDate; for (RepaymentScheduleInstallment installment : installments) { Date currentDueDate = installment.getDueDateValue(); long delta = DateUtils.getNumberOfDaysBetweenTwoDates(currentDueDate, oldPrevDate); Date newDueDate = DateUtils.addDays(newPrevDate, (int) delta); if (holidayService.isFutureRepaymentHoliday(DateUtils.getCalendar(newDueDate), officeId)) { installment.setDueDateValue(holidayService.getNextWorkingDay(newDueDate, officeId)); } else { installment.setDueDateValue(newDueDate); } oldPrevDate = currentDueDate; newPrevDate = installment.getDueDateValue(); } } public void adjustDatesForVariableInstallments(boolean variableInstallmentsAllowed, boolean fixedRepaymentSchedule, List<RepaymentScheduleInstallment> originalInstallments, Date oldDisbursementDate, Date newDisbursementDate, Short officeId) { if (!fixedRepaymentSchedule && variableInstallmentsAllowed) { adjustInstallmentGapsPostDisbursal(originalInstallments, oldDisbursementDate, newDisbursementDate, officeId); } } public List<RepaymentScheduleInstallment> applyDailyInterestRatesWhereApplicable( LoanScheduleGenerationDto loanScheduleGenerationDto, List<RepaymentScheduleInstallment> installments) { LoanBO loanBO = loanScheduleGenerationDto.getLoanBO(); if (dailyInterestRatesApplicable(loanScheduleGenerationDto, loanBO)) { loanScheduleGenerationDto.setInstallments(installments); applyDailyInterestRates(loanScheduleGenerationDto, loanBO.getInterestType() != null && InterestType.isFlatInterestType(loanBO.getInterestType().getId())); loanBO.updateInstallmentSchedule(installments); } return installments; } public void applyPayment(PaymentData paymentData, LoanBO loanBO, AccountPaymentEntity accountPaymentEntity) { Money balance = paymentData.getTotalAmount(); PersonnelBO personnel = paymentData.getPersonnel(); Date transactionDate = paymentData.getTransactionDate(); if(configService.isRecalculateInterestEnabled() && loanBO.isDecliningBalanceEqualPrincipleCalculation()) scheduleCalculatorAdaptor.applyPayment(loanBO, balance, transactionDate, personnel, accountPaymentEntity, paymentData.isAdjustment()); else if (loanBO.isDecliningBalanceInterestRecalculation()) { scheduleCalculatorAdaptor.applyPayment(loanBO, balance, transactionDate, personnel, accountPaymentEntity, paymentData.isAdjustment()); } else { if (AccountingRules.isOverdueInterestPaidFirst()) { for (AccountActionDateEntity accountActionDate : loanBO.getDetailsOfInstallmentsInArrearsOn(new LocalDate(transactionDate))) { balance = ((LoanScheduleEntity) accountActionDate).applyPaymentToInterest(accountPaymentEntity, balance, personnel, transactionDate); } } for (AccountActionDateEntity accountActionDate : loanBO.getAccountActionDatesSortedByInstallmentId()) { balance = ((LoanScheduleEntity) accountActionDate).applyPayment(accountPaymentEntity, balance, personnel, transactionDate); } } } public void persistOriginalSchedule(LoanBO loan) throws PersistenceException { Collection<LoanScheduleEntity> loanScheduleEntities = loan.getLoanScheduleEntities(); Collection<OriginalLoanScheduleEntity> originalLoanScheduleEntities = new ArrayList<OriginalLoanScheduleEntity>(); for (LoanScheduleEntity loanScheduleEntity : loanScheduleEntities) { originalLoanScheduleEntities.add(new OriginalLoanScheduleEntity(loanScheduleEntity)); } this.getlegacyLoanDao().saveOriginalSchedule(originalLoanScheduleEntities); } public void clearAndPersistOriginalSchedule(LoanBO loan) throws PersistenceException { List<OriginalLoanScheduleEntity> originalLoanScheduleEntities = this.getlegacyLoanDao().getOriginalLoanScheduleEntity(loan.getAccountId()); Collection<LoanScheduleEntity> loanScheduleEntities = loan.getLoanScheduleEntities(); Iterator<LoanScheduleEntity> loanScheduleEntitiesIterator = loanScheduleEntities.iterator(); for (OriginalLoanScheduleEntity originalLoanScheduleEntity : originalLoanScheduleEntities) { Iterator<AccountFeesActionDetailEntity> accountFeesIterator = loanScheduleEntitiesIterator.next().getAccountFeesActionDetails().iterator(); for(OriginalLoanFeeScheduleEntity originalLoanFeeScheduleEntity : originalLoanScheduleEntity.getAccountFeesActionDetails()) { originalLoanFeeScheduleEntity.updateFeeAmount(accountFeesIterator.next().getFeeAmount().getAmount()); } } this.getlegacyLoanDao().saveOriginalSchedule(originalLoanScheduleEntities); } public List<OriginalLoanScheduleEntity> retrieveOriginalLoanSchedule(Integer accountId) throws PersistenceException { return legacyLoanDao.getOriginalLoanScheduleEntity(accountId); } public Errors computeExtraInterest(LoanBO loan, Date asOfDate) { Errors errors = new Errors(); validateForComputeExtraInterestDate(loan, asOfDate, errors); if (!errors.hasErrors()) { scheduleCalculatorAdaptor.computeExtraInterest(loan, asOfDate); } return errors; } private void validateForComputeExtraInterestDate(LoanBO loan, Date extraInterestDate, Errors errors) { if(loan.isDecliningBalanceInterestRecalculation()) { AccountPaymentEntity mostRecentNonzeroPayment = loan.findMostRecentNonzeroPaymentByPaymentDate(); if (mostRecentNonzeroPayment != null) { Date lastPaymentDate = mostRecentNonzeroPayment.getPaymentDate(); if(DateUtils.dateFallsBeforeDate(extraInterestDate, lastPaymentDate)) { errors.addError(LoanConstants.CANNOT_VIEW_REPAYMENT_SCHEDULE, new String[]{extraInterestDate.toString()}); } } } } public List<ApplicableCharge> getAppllicablePenalties(Integer accountId, UserContext userContext) throws ServiceException { List<ApplicableCharge> applicableChargeList = null; try { LoanBO loan = getlegacyLoanDao().getAccount(accountId); applicableChargeList = getLoanApplicablePenalties(getlegacyLoanDao().getAllApplicablePenalties(accountId), userContext, loan); } catch (PersistenceException pe) { throw new ServiceException(pe); } addMiscPenalty(applicableChargeList); return applicableChargeList; } private void addMiscPenalty(List<ApplicableCharge> applicableChargeList) { ApplicableCharge applicableCharge = new ApplicableCharge(); applicableCharge.setFeeId(AccountConstants.MISC_PENALTY); applicableCharge.setFeeName("Misc Penalty"); applicableCharge.setIsRateType(false); applicableCharge.setIsPenaltyType(false); applicableChargeList.add(applicableCharge); } private List<ApplicableCharge> getLoanApplicablePenalties(List<PenaltyBO> penaltyList, UserContext userContext, LoanBO loanBO) { List<ApplicableCharge> applicableChargeList = new ArrayList<ApplicableCharge>(); if (penaltyList != null && !penaltyList.isEmpty()) { if(AccountingRules.isMultiCurrencyEnabled()){ filterBasedOnCurrencyOfLoan(penaltyList, loanBO); } populaleApplicableCharge(applicableChargeList, penaltyList, userContext); } return applicableChargeList; } private void filterBasedOnCurrencyOfLoan(List<PenaltyBO> penaltyList, LoanBO loanBO) { // remove penalties where the currency of penalty doesn't match the currency of loan. for (Iterator<PenaltyBO> iter = penaltyList.iterator(); iter.hasNext();) { PenaltyBO penalty = iter.next(); if (penalty instanceof AmountPenaltyBO) { if (!((AmountPenaltyBO) penalty).getAmount().getCurrency().equals(loanBO.getCurrency())) { iter.remove(); } } } } private void populaleApplicableCharge(List<ApplicableCharge> applicableChargeList, List<PenaltyBO> penaltyList, UserContext userContext) { for (PenaltyBO penalty : penaltyList) { ApplicableCharge applicableCharge = new ApplicableCharge(); applicableCharge.setFeeId(penalty.getPenaltyId().toString()); applicableCharge.setFeeName(penalty.getPenaltyName()); applicableCharge.setIsPenaltyType(true); if (penalty instanceof RatePenaltyBO) { applicableCharge.setAmountOrRate(new LocalizationConverter().getDoubleStringForInterest(((RatePenaltyBO) penalty).getRate())); applicableCharge.setFormula(((RatePenaltyBO) penalty).getFormula().getFormulaStringThatHasName()); applicableCharge.setIsRateType(true); } else { applicableCharge.setAmountOrRate(((AmountPenaltyBO) penalty).getAmount().toString()); applicableCharge.setIsRateType(false); } if(!penalty.isOneTime()) { applicableCharge.setPeriodicity("true"); applicableCharge.setFormula(penalty.getPenaltyFrequency().getName()); } applicableChargeList.add(applicableCharge); } } }