/* * 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.api; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import org.apache.commons.collections.CollectionUtils; import org.hibernate.NonUniqueResultException; import org.joda.time.LocalDate; import org.mifos.accounts.acceptedpaymenttype.persistence.LegacyAcceptedPaymentTypeDao; import org.mifos.accounts.business.AccountBO; import org.mifos.accounts.business.AccountFeesEntity; import org.mifos.accounts.business.AccountOverpaymentEntity; import org.mifos.accounts.business.AccountPaymentEntity; import org.mifos.accounts.business.AccountTrxnEntity; import org.mifos.accounts.exceptions.AccountException; import org.mifos.accounts.loan.business.LoanBO; import org.mifos.accounts.loan.business.service.LoanBusinessService; import org.mifos.accounts.loan.business.service.LoanScheduleGenerationDto; import org.mifos.accounts.loan.persistance.LegacyLoanDao; import org.mifos.accounts.loan.util.helpers.RepaymentScheduleInstallment; import org.mifos.accounts.persistence.LegacyAccountDao; import org.mifos.accounts.savings.business.SavingsBO; import org.mifos.accounts.servicefacade.UserContextFactory; import org.mifos.accounts.util.helpers.AccountState; import org.mifos.accounts.util.helpers.AccountTypes; import org.mifos.accounts.util.helpers.OverpaymentStatus; import org.mifos.accounts.util.helpers.PaymentData; import org.mifos.application.admin.servicefacade.MonthClosingServiceFacade; import org.mifos.application.master.business.PaymentTypeEntity; import org.mifos.application.master.persistence.LegacyMasterDao; import org.mifos.application.servicefacade.ApplicationContextProvider; import org.mifos.application.servicefacade.GroupLoanAccountServiceFacade; import org.mifos.application.servicefacade.SavingsServiceFacade; import org.mifos.application.util.helpers.TrxnTypes; import org.mifos.config.Localization; import org.mifos.config.business.MifosConfigurationManager; import org.mifos.config.persistence.ConfigurationPersistence; import org.mifos.core.MifosRuntimeException; import org.mifos.customers.business.CustomerAccountBO; import org.mifos.customers.persistence.CustomerDao; import org.mifos.customers.persistence.CustomerPersistence; import org.mifos.customers.personnel.business.PersonnelBO; import org.mifos.customers.personnel.persistence.LegacyPersonnelDao; import org.mifos.customers.personnel.persistence.PersonnelDao; import org.mifos.dto.domain.AccountPaymentDto; import org.mifos.dto.domain.AccountPaymentParametersDto; import org.mifos.dto.domain.AccountPaymentParametersDto.PaymentOptions; import org.mifos.dto.domain.AccountReferenceDto; import org.mifos.dto.domain.AccountTrxDto; import org.mifos.dto.domain.GroupIndividualLoanDto; import org.mifos.dto.domain.OverpaymentDto; import org.mifos.dto.domain.PaymentDto; import org.mifos.dto.domain.PaymentTypeDto; import org.mifos.dto.domain.SavingsAccountDetailDto; import org.mifos.dto.domain.SavingsWithdrawalDto; import org.mifos.dto.domain.UserReferenceDto; import org.mifos.framework.exceptions.PersistenceException; import org.mifos.framework.hibernate.helper.HibernateTransactionHelper; import org.mifos.framework.hibernate.helper.StaticHibernateUtil; import org.mifos.framework.util.helpers.Money; import org.mifos.security.MifosUser; import org.mifos.security.util.SecurityConstants; import org.mifos.security.util.UserContext; import org.mifos.service.BusinessRuleException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.context.SecurityContextHolder; /** * A service class implementation to expose basic functions on loans. As an external API, this class should not expose * business objects, only DTOs. */ public class StandardAccountService implements AccountService { private LegacyAccountDao legacyAccountDao; private LegacyLoanDao legacyLoanDao; private LegacyAcceptedPaymentTypeDao acceptedPaymentTypePersistence; private PersonnelDao personnelDao; private CustomerDao customerDao; private LoanBusinessService loanBusinessService; private HibernateTransactionHelper transactionHelper; private MonthClosingServiceFacade monthClosingServiceFacade; private SavingsServiceFacade savingsServiceFacade; private GroupLoanAccountServiceFacade groupLoanAccountServiceFacade; private LegacyMasterDao legacyMasterDao; @Autowired public StandardAccountService(LegacyAccountDao legacyAccountDao, LegacyLoanDao legacyLoanDao, LegacyAcceptedPaymentTypeDao acceptedPaymentTypePersistence, PersonnelDao personnelDao, CustomerDao customerDao, LoanBusinessService loanBusinessService, HibernateTransactionHelper transactionHelper, LegacyMasterDao legacyMasterDao, MonthClosingServiceFacade monthClosingServiceFacade, SavingsServiceFacade savingsServiceFacade, GroupLoanAccountServiceFacade groupLoanAccountServiceFacade) { this.legacyAccountDao = legacyAccountDao; this.legacyLoanDao = legacyLoanDao; this.acceptedPaymentTypePersistence = acceptedPaymentTypePersistence; this.personnelDao = personnelDao; this.customerDao = customerDao; this.loanBusinessService = loanBusinessService; this.transactionHelper = transactionHelper; this.legacyMasterDao = legacyMasterDao; this.monthClosingServiceFacade = monthClosingServiceFacade; this.savingsServiceFacade = savingsServiceFacade; this.groupLoanAccountServiceFacade = groupLoanAccountServiceFacade; } @Override public void makePayment(AccountPaymentParametersDto accountPaymentParametersDto) { try { transactionHelper.startTransaction(); makePaymentNoCommit(accountPaymentParametersDto); transactionHelper.commitTransaction(); } catch (PersistenceException e) { transactionHelper.rollbackTransaction(); throw new MifosRuntimeException(e); } catch (AccountException e) { transactionHelper.rollbackTransaction(); throw new BusinessRuleException(e.getKey(), e); } finally { transactionHelper.closeSession(); } } @Override public void makePaymentFromSavings(AccountPaymentParametersDto accountPaymentParametersDto, String savingsGlobalAccNum) { transactionHelper.flushAndClearSession(); SavingsAccountDetailDto savingsAcc = savingsServiceFacade.retrieveSavingsAccountDetails(savingsGlobalAccNum); MifosUser user = (MifosUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); Long savingsId = savingsAcc.getAccountId().longValue(); Long customerId = accountPaymentParametersDto.getCustomer().getCustomerId().longValue(); LocalDate dateOfWithdrawal = accountPaymentParametersDto.getPaymentDate(); Double amount = accountPaymentParametersDto.getPaymentAmount().doubleValue(); Integer modeOfPayment = accountPaymentParametersDto.getPaymentType().getValue().intValue(); String receiptId = accountPaymentParametersDto.getReceiptId(); LocalDate dateOfReceipt = accountPaymentParametersDto.getReceiptDate(); Locale preferredLocale = Localization.getInstance().getLocaleById(user.getPreferredLocaleId()); SavingsWithdrawalDto savingsWithdrawalDto = new SavingsWithdrawalDto(savingsId, customerId, dateOfWithdrawal, amount, modeOfPayment, receiptId, dateOfReceipt, preferredLocale); try { transactionHelper.startTransaction(); PaymentDto withdrawal = this.savingsServiceFacade.withdraw(savingsWithdrawalDto, true); makePaymentNoCommit(accountPaymentParametersDto, withdrawal.getPaymentId(), null); transactionHelper.commitTransaction(); } catch (AccountException e) { transactionHelper.rollbackTransaction(); throw new BusinessRuleException(e.getKey(), e); } catch (BusinessRuleException e) { transactionHelper.rollbackTransaction(); throw new BusinessRuleException(e.getMessageKey(), e); } catch (Exception e) { transactionHelper.rollbackTransaction(); throw new MifosRuntimeException(e); } } @Override public void makePayments(List<AccountPaymentParametersDto> accountPaymentParametersDtoList) throws PersistenceException, AccountException { /* * We're counting on rollback on exception behavior in BaseAction. If we want to expose makePayments via a * non-Mifos-Web-UI service, we'll need to handle the rollback here. */ StaticHibernateUtil.startTransaction(); for (AccountPaymentParametersDto accountPaymentParametersDTO : accountPaymentParametersDtoList) { makePaymentNoCommit(accountPaymentParametersDTO); } StaticHibernateUtil.commitTransaction(); } public void makePaymentNoCommit(AccountPaymentParametersDto accountPaymentParametersDto) throws PersistenceException, AccountException { makePaymentNoCommit(accountPaymentParametersDto, null, null); } public void makePaymentNoCommit(AccountPaymentParametersDto accountPaymentParametersDto, Integer savingsPaymentId, AccountPaymentEntity parentPayment) throws PersistenceException, AccountException { final int accountId = accountPaymentParametersDto.getAccountId(); final AccountBO account = this.legacyAccountDao.getAccount(accountId); MifosUser mifosUser = (MifosUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); UserContext userContext = new UserContextFactory().create(mifosUser); try { personnelDao.checkAccessPermission(userContext, account.getOfficeId(), account.getCustomer().getLoanOfficerId()); } catch (AccountException e) { throw new MifosRuntimeException(SecurityConstants.KEY_ACTIVITY_NOT_ALLOWED, e); } monthClosingServiceFacade.validateTransactionDate(accountPaymentParametersDto.getPaymentDate().toDateMidnight().toDate()); /** * Handle member payment if parent payment data not provided. * Situation may occur when payment is executed directly on group member account (e.g. from transaction import). * Loan Group Member payments should be posted after parent payment. */ if (account.isGroupLoanAccountMember() && parentPayment == null){ AccountPaymentParametersDto parentPaymentParametersDto = this.createParentLoanPaymentData(account, accountPaymentParametersDto); makePaymentNoCommit(parentPaymentParametersDto, savingsPaymentId, null); return; } PersonnelBO loggedInUser = ApplicationContextProvider.getBean(LegacyPersonnelDao.class).findPersonnelById(accountPaymentParametersDto.getUserMakingPayment().getUserId()); List<InvalidPaymentReason> validationErrors = validatePayment(accountPaymentParametersDto); if (!(account instanceof CustomerAccountBO) && validationErrors.contains(InvalidPaymentReason.INVALID_DATE)) { throw new AccountException("errors.invalidTxndate"); } Money overpaymentAmount = null; Money amount = new Money(account.getCurrency(), accountPaymentParametersDto.getPaymentAmount()); if (account instanceof LoanBO && accountPaymentParametersDto.getPaymentOptions().contains(AccountPaymentParametersDto.PaymentOptions.ALLOW_OVERPAYMENTS) && amount.isGreaterThan(((LoanBO) account).getTotalRepayableAmount())) { overpaymentAmount = amount.subtract(((LoanBO) account).getTotalRepayableAmount()); amount = ((LoanBO) account).getTotalRepayableAmount(); } Date receiptDate = null; if (accountPaymentParametersDto.getReceiptDate() != null) { receiptDate = accountPaymentParametersDto.getReceiptDate().toDateMidnight().toDate(); } PaymentData paymentData = account.createPaymentData(amount, accountPaymentParametersDto.getPaymentDate().toDateMidnight().toDate(), accountPaymentParametersDto.getReceiptId(), receiptDate, accountPaymentParametersDto.getPaymentType() .getValue(), loggedInUser); if (savingsPaymentId != null) { AccountPaymentEntity withdrawal = legacyAccountDao.findPaymentById(savingsPaymentId); paymentData.setOtherTransferPayment(withdrawal); } if (accountPaymentParametersDto.getCustomer() != null) { paymentData.setCustomer(customerDao.findCustomerById( accountPaymentParametersDto.getCustomer().getCustomerId())); } paymentData.setComment(accountPaymentParametersDto.getComment()); paymentData.setOverpaymentAmount(overpaymentAmount); if (account instanceof LoanBO && account.isGroupLoanAccountMember() && parentPayment != null) { paymentData.setParentPayment(parentPayment); } AccountPaymentEntity paymentEntity = account.applyPayment(paymentData); handleParentGroupLoanPayment(account, accountPaymentParametersDto, savingsPaymentId, paymentEntity); this.legacyAccountDao.createOrUpdate(account); } /** * Handles parent NOT-GLIM group loan payment. */ private void handleParentGroupLoanPayment(AccountBO account, AccountPaymentParametersDto parentPaymentParametersDto, Integer savingsPaymentId, AccountPaymentEntity paymentEntity) throws PersistenceException, AccountException{ if (account instanceof LoanBO && account.isParentGroupLoanAccount()) { if (parentPaymentParametersDto.getMemberInfo() == null || parentPaymentParametersDto.getMemberInfo().isEmpty()){ createMembersLoanPaymentsData(parentPaymentParametersDto); } for (Map.Entry<Integer, String> member : parentPaymentParametersDto.getMemberInfo().entrySet()) { AccountBO memberAcc = this.legacyAccountDao.getAccount(member.getKey()); if (null == parentPaymentParametersDto.getMemberAccountIdToRepay() || (null != parentPaymentParametersDto.getMemberAccountIdToRepay()&& !parentPaymentParametersDto.getMemberAccountIdToRepay().equals(memberAcc.getAccountId()))) { AccountPaymentParametersDto memberAccountPaymentParametersDto = new AccountPaymentParametersDto(parentPaymentParametersDto.getUserMakingPayment(), new AccountReferenceDto(memberAcc.getAccountId()), new BigDecimal(member.getValue()), parentPaymentParametersDto.getPaymentDate(), parentPaymentParametersDto.getPaymentType(), parentPaymentParametersDto.getComment(), parentPaymentParametersDto.getReceiptDate(), parentPaymentParametersDto.getReceiptId(), memberAcc.getCustomer().toCustomerDto()); if (parentPaymentParametersDto.getPaymentOptions().contains(AccountPaymentParametersDto.PaymentOptions.ALLOW_OVERPAYMENTS)) { memberAccountPaymentParametersDto.addPaymentOption(AccountPaymentParametersDto.PaymentOptions.ALLOW_OVERPAYMENTS); } makePaymentNoCommit(memberAccountPaymentParametersDto, savingsPaymentId, paymentEntity); } else { AccountPaymentDto paymentDto = new AccountPaymentDto(Double.valueOf(member.getValue()), parentPaymentParametersDto.getPaymentDate().toDateMidnight().toDate(), parentPaymentParametersDto.getReceiptId(), reciptDateNullValidation(parentPaymentParametersDto.getReceiptDate()), parentPaymentParametersDto.getPaymentType().getValue()); ((LoanBO)memberAcc).makeEarlyRepayment(paymentDto, parentPaymentParametersDto.getUserMakingPayment().getUserId(), parentPaymentParametersDto.getRepayLoanInfoDto().isWaiveInterest(), new Money(account.getCurrency(),parentPaymentParametersDto.getInterestDueForCurrentInstalmanet())); } } } } private Date reciptDateNullValidation(LocalDate reciptDate){ return null == reciptDate ? null : reciptDate.toDateMidnight().toDate(); } /** * Create default members payments data. */ private void createMembersLoanPaymentsData(AccountPaymentParametersDto parentPaymentParametersDto){ List<GroupIndividualLoanDto> membersPayments = this.groupLoanAccountServiceFacade.getMemberLoansAndDefaultPayments( parentPaymentParametersDto.getAccountId(), parentPaymentParametersDto.getPaymentAmount()); parentPaymentParametersDto.setMemberInfo(new HashMap<Integer, String>()); for (GroupIndividualLoanDto memberPayment : membersPayments){ parentPaymentParametersDto.getMemberInfo().put(memberPayment.getAccountId(), memberPayment.getDefaultAmount().toString()); } } /** * Create parent payment data with member payment data. * */ private AccountPaymentParametersDto createParentLoanPaymentData(AccountBO memberAccount, AccountPaymentParametersDto memberPaymentParametersDto){ AccountPaymentParametersDto parentPaymentParametersDto = new AccountPaymentParametersDto(memberPaymentParametersDto.getUserMakingPayment(), new AccountReferenceDto(((LoanBO)memberAccount).getParentAccount().getAccountId()), memberPaymentParametersDto.getPaymentAmount(), memberPaymentParametersDto.getPaymentDate(), memberPaymentParametersDto.getPaymentType(), memberPaymentParametersDto.getComment(), memberPaymentParametersDto.getReceiptDate(), memberPaymentParametersDto.getReceiptId(), memberAccount.getCustomer().toCustomerDto()); Map<Integer, String> membersPaymentsData = new HashMap<Integer, String>(); for (AccountBO member : ((LoanBO)memberAccount).getParentAccount().getMemberAccounts()){ if (member.isActiveLoanAccount()) { if (member.getAccountId().equals(memberPaymentParametersDto.getAccountId())){ membersPaymentsData.put(member.getAccountId(), memberPaymentParametersDto.getPaymentAmount().toString()); } else { membersPaymentsData.put(member.getAccountId(), BigDecimal.ZERO.toString()); } } } parentPaymentParametersDto.setMemberInfo(membersPaymentsData); return parentPaymentParametersDto; } /** * method created for undo transaction import ability MIFOS-5702 * changed return type * */ @Override public List<AccountTrxDto> makePaymentsForImport(List<AccountPaymentParametersDto> accountPaymentParametersDtoList) throws PersistenceException, AccountException { /* * We're counting on rollback on exception behavior in BaseAction. If we want to expose makePayments via a * non-Mifos-Web-UI service, we'll need to handle the rollback here. */ List<AccountTrxDto> trxIds = new ArrayList<AccountTrxDto>(); List <AccountBO> accounts = new ArrayList<AccountBO>(); StaticHibernateUtil.startTransaction(); int i = 0; for (AccountPaymentParametersDto accountPaymentParametersDTO : accountPaymentParametersDtoList) { CollectionUtils.addIgnoreNull(accounts, makeImportedPayments(accountPaymentParametersDTO)); if (i%30 == 0) { StaticHibernateUtil.getSessionTL().flush(); StaticHibernateUtil.getSessionTL().clear(); } i++; } StaticHibernateUtil.getSessionTL().flush(); StaticHibernateUtil.getSessionTL().clear(); StaticHibernateUtil.commitTransaction(); for (AccountBO account : accounts) { trxIds.add(new AccountTrxDto(getAccTrxId(account))); } return trxIds; } /** * method created for undo transaction import ability MIFOS-5702 * changed return type * */ public AccountBO makeImportedPayments(AccountPaymentParametersDto accountPaymentParametersDto) throws PersistenceException, AccountException { return makeImportedPayments(accountPaymentParametersDto, null); } /** * method created for undo transaction import ability MIFOS-5702 * returns Id of transaction * */ public AccountBO makeImportedPayments(AccountPaymentParametersDto accountPaymentParametersDto, Integer savingsPaymentId) throws PersistenceException, AccountException { final int accountId = accountPaymentParametersDto.getAccountId(); final AccountBO account = this.legacyAccountDao.getAccount(accountId); MifosUser mifosUser = (MifosUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); UserContext userContext = new UserContextFactory().create(mifosUser); try { personnelDao.checkAccessPermission(userContext, account.getOfficeId(), account.getCustomer().getLoanOfficerId()); } catch (AccountException e) { throw new MifosRuntimeException(SecurityConstants.KEY_ACTIVITY_NOT_ALLOWED, e); } monthClosingServiceFacade.validateTransactionDate(accountPaymentParametersDto.getPaymentDate().toDateMidnight().toDate()); /** * Handle member payment if parent payment data not provided. * Situation may occur when payment is executed directly on group member account (e.g. from transaction import). * Loan Group Member payments should be posted after parent payment. */ if (account.isGroupLoanAccountMember()){ AccountPaymentParametersDto parentPaymentParametersDto = this.createParentLoanPaymentData(account, accountPaymentParametersDto); return makeImportedPayments(parentPaymentParametersDto, savingsPaymentId); } PersonnelBO loggedInUser = ApplicationContextProvider.getBean(LegacyPersonnelDao.class).findPersonnelById(accountPaymentParametersDto.getUserMakingPayment().getUserId()); List<InvalidPaymentReason> validationErrors = validatePayment(accountPaymentParametersDto); if (!(account instanceof CustomerAccountBO) && validationErrors.contains(InvalidPaymentReason.INVALID_DATE)) { throw new AccountException("errors.invalidTxndate"); } Money overpaymentAmount = null; Money amount = new Money(account.getCurrency(), accountPaymentParametersDto.getPaymentAmount()); if (account instanceof LoanBO && accountPaymentParametersDto.getPaymentOptions().contains(AccountPaymentParametersDto.PaymentOptions.ALLOW_OVERPAYMENTS) && amount.isGreaterThan(((LoanBO) account).getTotalRepayableAmount())) { overpaymentAmount = amount.subtract(((LoanBO) account).getTotalRepayableAmount()); amount = ((LoanBO) account).getTotalRepayableAmount(); } Date receiptDate = null; if (accountPaymentParametersDto.getReceiptDate() != null) { receiptDate = accountPaymentParametersDto.getReceiptDate().toDateMidnight().toDate(); } PaymentData paymentData = account.createPaymentData(amount, accountPaymentParametersDto.getPaymentDate().toDateMidnight().toDate(), accountPaymentParametersDto.getReceiptId(), receiptDate, accountPaymentParametersDto.getPaymentType() .getValue(), loggedInUser); if (savingsPaymentId != null) { AccountPaymentEntity withdrawal = legacyAccountDao.findPaymentById(savingsPaymentId); paymentData.setOtherTransferPayment(withdrawal); } if (accountPaymentParametersDto.getCustomer() != null) { paymentData.setCustomer(customerDao.findCustomerById( accountPaymentParametersDto.getCustomer().getCustomerId())); } paymentData.setComment(accountPaymentParametersDto.getComment()); paymentData.setOverpaymentAmount(overpaymentAmount); if (accountPaymentParametersDto.getPaymentOptions().contains(PaymentOptions.ALLOW_OVERPAYMENTS)) { paymentData.setAllowOverpayment(true); } AccountPaymentEntity paymentEntity = account.applyPayment(paymentData); handleParentGroupLoanPayment(account, accountPaymentParametersDto, savingsPaymentId, paymentEntity); this.legacyAccountDao.createOrUpdate(account); /* * Return null when only overpayment is being apply */ if (amount.isZero() && overpaymentAmount != null && overpaymentAmount.isGreaterThanZero()) { return null; } return account; } private Integer getAccTrxId(AccountBO account) { Integer ID = null; Set<AccountTrxnEntity> accountTrxns = account.getAccountPayments().get(account.getAccountPayments().size() - 1).getAccountTrxns(); for (AccountTrxnEntity trx : accountTrxns) { ID = trx.getAccountTrxnId(); } return ID; } @Override public void disburseLoans(List<AccountPaymentParametersDto> accountPaymentParametersDtoList, Locale locale, Short paymentTypeIdForFees, Integer accountForTransferId) throws Exception { StaticHibernateUtil.startTransaction(); for (AccountPaymentParametersDto accountPaymentParametersDto : accountPaymentParametersDtoList) { LoanBO loan = this.legacyLoanDao.getAccount(accountPaymentParametersDto.getAccountId()); PersonnelBO personnelBO = personnelDao.findPersonnelById(accountPaymentParametersDto.getUserMakingPayment() .getUserId()); BigDecimal paymentAmount = accountPaymentParametersDto.getPaymentAmount(); handleLoanDisbursal(locale, loan, personnelBO, paymentAmount, accountPaymentParametersDto.getPaymentType(), accountPaymentParametersDto.getReceiptDate(), accountPaymentParametersDto.getPaymentDate(), accountPaymentParametersDto.getReceiptId(), paymentTypeIdForFees, accountForTransferId); } StaticHibernateUtil.commitTransaction(); } @Override public void disburseLoans(List<AccountPaymentParametersDto> accountPaymentParametersDtoList, Locale locale) throws Exception { disburseLoans(accountPaymentParametersDtoList, locale, null, null); } public void handleLoanDisbursal(Locale locale, LoanBO loan, PersonnelBO personnelBO, BigDecimal paymentAmount, PaymentTypeDto paymentType, LocalDate receiptLocalDate, LocalDate paymentLocalDate, String receiptId, Short paymentTypeIdForFees, Integer accountForTransferId) throws PersistenceException, AccountException { if ("MPESA".equals(paymentType.getName())) { paymentAmount = computeWithdrawnForMPESA(paymentAmount, loan); } PaymentTypeEntity paymentTypeEntity = legacyMasterDao.getPersistentObject( PaymentTypeEntity.class, paymentType.getValue()); Money amount = new Money(loan.getCurrency(), paymentAmount); Date receiptDate = null; if (null != receiptLocalDate) { receiptDate = receiptLocalDate.toDateMidnight().toDate(); } Date transactionDate = paymentLocalDate.toDateMidnight().toDate(); AccountPaymentEntity disbursalPayment = new AccountPaymentEntity(loan, amount, receiptId, receiptDate, paymentTypeEntity, transactionDate); disbursalPayment.setCreatedByUser(personnelBO); Double interestRate = loan.getInterestRate(); Date oldDisbursementDate = loan.getDisbursementDate(); List<RepaymentScheduleInstallment> originalInstallments = loan.toRepaymentScheduleDto(locale); loan.disburseLoan(disbursalPayment, paymentTypeIdForFees, accountForTransferId); if (!loan.isVariableInstallmentsAllowed()) { originalInstallments = loan.toRepaymentScheduleDto(locale); } Date newDisbursementDate = loan.getDisbursementDate(); boolean variableInstallmentsAllowed = loan.isVariableInstallmentsAllowed(); loanBusinessService.adjustDatesForVariableInstallments(variableInstallmentsAllowed, loan.isFixedRepaymentSchedule(), originalInstallments, oldDisbursementDate, newDisbursementDate, loan.getOfficeId()); Date today = new LocalDate().toDateMidnight().toDate(); Date disburseDay = new LocalDate(oldDisbursementDate).toDateMidnight().toDate(); if (!today.equals(disburseDay)) { loanBusinessService.applyDailyInterestRatesWhereApplicable(new LoanScheduleGenerationDto(newDisbursementDate, loan, variableInstallmentsAllowed, amount, interestRate), originalInstallments); } loanBusinessService.persistOriginalSchedule(loan); } @Override public AccountReferenceDto lookupLoanAccountReferenceFromId(Integer id) throws PersistenceException { LoanBO loan = this.legacyLoanDao.getAccount(id); if (null == loan) { throw new PersistenceException("loan not found for id " + id); } return new AccountReferenceDto(loan.getAccountId()); } @Override public AccountReferenceDto lookupLoanAccountReferenceFromExternalId(String externalId) throws PersistenceException { LoanBO loan = this.legacyLoanDao.findByExternalId(externalId); if (null == loan) { throw new PersistenceException("loan not found for external id " + externalId); } return new AccountReferenceDto(loan.getAccountId()); } /** * Note that, since we don't store or otherwise utilize the amount disbursed (passed in * AccountPaymentParametersDto.paymentAmount) we <em>do not</em> validate that digits after decimal for the amount * disbursed fit in an allowed range. We <em>do</em> check that the amount disbursed matches the full amount of the * loan. */ @Override public List<InvalidPaymentReason> validateLoanDisbursement(AccountPaymentParametersDto payment) throws Exception { List<InvalidPaymentReason> errors = new ArrayList<InvalidPaymentReason>(); LoanBO loanAccount = this.legacyLoanDao.getAccount(payment.getAccountId()); if ((loanAccount.getState() != AccountState.LOAN_APPROVED) && (loanAccount.getState() != AccountState.LOAN_DISBURSED_TO_LOAN_OFFICER)) { errors.add(InvalidPaymentReason.INVALID_LOAN_STATE); } BigDecimal paymentAmount = payment.getPaymentAmount(); if ("MPESA".equals(payment.getPaymentType().getName())) { paymentAmount = computeWithdrawnForMPESA(paymentAmount, loanAccount); } disbursalAmountMatchesFullLoanAmount(paymentAmount, errors, loanAccount); Date meetingDate = new CustomerPersistence().getLastMeetingDateForCustomer(loanAccount.getCustomer().getCustomerId()); boolean repaymentIndependentOfMeetingEnabled = new ConfigurationPersistence().isRepaymentIndepOfMeetingEnabled(); if (!loanAccount.isTrxnDateValid(payment.getPaymentDate().toDateMidnight().toDate(), meetingDate, repaymentIndependentOfMeetingEnabled)) { errors.add(InvalidPaymentReason.INVALID_DATE); } if (!getLoanDisbursementTypes().contains(payment.getPaymentType())) { errors.add(InvalidPaymentReason.UNSUPPORTED_PAYMENT_TYPE); } if (!loanAccount.paymentAmountIsValid(new Money(loanAccount.getCurrency(), payment.getPaymentAmount()), payment.getPaymentOptions())) { errors.add(InvalidPaymentReason.INVALID_PAYMENT_AMOUNT); } if (loanAccount.getCustomer().isDisbursalPreventedDueToAnyExistingActiveLoansForTheSameProduct(loanAccount.getLoanOffering())) { errors.add(InvalidPaymentReason.OTHER_ACTIVE_LOANS_FOR_THE_SAME_PRODUCT); } return errors; } void disbursalAmountMatchesFullLoanAmount(BigDecimal paymentAmount, List<InvalidPaymentReason> errors, LoanBO loanAccount) { /* BigDecimal.compareTo() ignores scale, .equals() was explicitly avoided */ if (loanAccount.getLoanAmount().getAmount().compareTo(paymentAmount) != 0) { errors.add(InvalidPaymentReason.INVALID_LOAN_DISBURSAL_AMOUNT); } } @Override public List<InvalidPaymentReason> validatePayment(AccountPaymentParametersDto payment) throws PersistenceException, AccountException { List<InvalidPaymentReason> errors = new ArrayList<InvalidPaymentReason>(); AccountBO accountBo = this.legacyAccountDao.getAccount(payment.getAccountId()); Date meetingDate = new CustomerPersistence().getLastMeetingDateForCustomer(accountBo.getCustomer().getCustomerId()); boolean repaymentIndependentOfMeetingEnabled = new ConfigurationPersistence().isRepaymentIndepOfMeetingEnabled(); if (!accountBo.isTrxnDateValid(payment.getPaymentDate().toDateMidnight().toDate(), meetingDate, repaymentIndependentOfMeetingEnabled)) { errors.add(InvalidPaymentReason.INVALID_DATE); } if (accountBo instanceof LoanBO) { if (((LoanBO)accountBo).paymentsNotAllowed()) { errors.add(InvalidPaymentReason.INVALID_LOAN_STATE); } } if (accountBo instanceof SavingsBO) { if (!accountBo.getState().equals(AccountState.SAVINGS_ACTIVE)) { errors.add(InvalidPaymentReason.INVALID_LOAN_STATE); } } if (AccountTypes.getAccountType(accountBo.getAccountType().getAccountTypeId()) == AccountTypes.LOAN_ACCOUNT) { if (!getLoanPaymentTypes().contains(payment.getPaymentType())) { errors.add(InvalidPaymentReason.UNSUPPORTED_PAYMENT_TYPE); } } else if (AccountTypes.getAccountType(accountBo.getAccountType().getAccountTypeId()) == AccountTypes.SAVINGS_ACCOUNT) { if (!getSavingsPaymentTypes().contains(payment.getPaymentType())) { errors.add(InvalidPaymentReason.UNSUPPORTED_PAYMENT_TYPE); } } else if (AccountTypes.getAccountType(accountBo.getAccountType().getAccountTypeId()) == AccountTypes.CUSTOMER_ACCOUNT) { if (!getFeePaymentTypes().contains(payment.getPaymentType())) { errors.add(InvalidPaymentReason.UNSUPPORTED_PAYMENT_TYPE); } } if (!accountBo.paymentAmountIsValid(new Money(accountBo.getCurrency(), payment.getPaymentAmount()), payment.getPaymentOptions())) { errors.add(InvalidPaymentReason.INVALID_PAYMENT_AMOUNT); } return errors; } @Override public List<AccountPaymentParametersDto> lookupPayments(AccountReferenceDto accountRef) throws PersistenceException { final int accountId = accountRef.getAccountId(); final AccountBO account = this.legacyAccountDao.getAccount(accountId); List<AccountPaymentParametersDto> paymentDtos = new ArrayList<AccountPaymentParametersDto>(); for (AccountPaymentEntity paymentEntity : account.getAccountPayments()) { paymentDtos.add(makePaymentDto(paymentEntity)); } return paymentDtos; } public AccountPaymentParametersDto makePaymentDto(AccountPaymentEntity paymentEntity) { AccountPaymentParametersDto paymentDto = new AccountPaymentParametersDto( paymentEntity.getCreatedByUser() == null ? new UserReferenceDto(paymentEntity.getAccountTrxns().iterator().next(). getPersonnel().getPersonnelId()) : new UserReferenceDto(paymentEntity.getCreatedByUser().getPersonnelId()), new AccountReferenceDto(paymentEntity.getAccount().getAccountId()), paymentEntity.getAmount().getAmount(), LocalDate.fromDateFields(paymentEntity.getPaymentDate()), new PaymentTypeDto(paymentEntity.getPaymentType().getId(), paymentEntity.getPaymentType().toString()), paymentEntity.getComment() == null ? paymentEntity.toString() : paymentEntity.getComment(), paymentEntity.getReceiptDate() == null ? null : LocalDate.fromDateFields(paymentEntity.getReceiptDate()), paymentEntity.getReceiptNumber(), null); return paymentDto; } public List<PaymentTypeDto> getSavingsPaymentTypes() throws PersistenceException { return getPaymentTypes(TrxnTypes.savings_deposit.getValue()); } public List<PaymentTypeDto> getSavingsWithdrawalTypes() throws PersistenceException { return getPaymentTypes(TrxnTypes.savings_withdrawal.getValue()); } @Override public List<PaymentTypeDto> getFeePaymentTypes() throws PersistenceException { return getPaymentTypes(TrxnTypes.fee.getValue()); } @Override public List<PaymentTypeDto> getLoanPaymentTypes() throws PersistenceException { return getPaymentTypes(TrxnTypes.loan_repayment.getValue()); } @Override public List<PaymentTypeDto> getLoanDisbursementTypes() throws PersistenceException { return getPaymentTypes(TrxnTypes.loan_disbursement.getValue()); } private List<PaymentTypeDto> getPaymentTypes(short transactionType) throws PersistenceException { final Short IGNORED_LOCALE_ID = 1; List<PaymentTypeEntity> paymentTypeEntities = this.acceptedPaymentTypePersistence .getAcceptedPaymentTypesForATransaction(IGNORED_LOCALE_ID, transactionType); List<PaymentTypeDto> paymentTypeDtos = new ArrayList<PaymentTypeDto>(); for (PaymentTypeEntity paymentTypeEntity : paymentTypeEntities) { paymentTypeDtos.add(new PaymentTypeDto(paymentTypeEntity.getId(), paymentTypeEntity.getName())); } return paymentTypeDtos; } @Override public AccountReferenceDto lookupLoanAccountReferenceFromGlobalAccountNumber(String globalAccountNumber) throws PersistenceException { AccountBO accountBo = this.legacyAccountDao.findBySystemId(globalAccountNumber); if (null == accountBo) { throw new PersistenceException("loan not found for global account number " + globalAccountNumber); } return new AccountReferenceDto(accountBo.getAccountId()); } @Override public AccountReferenceDto lookupLoanAccountReferenceFromClientGovernmentIdAndLoanProductShortName( String clientGovernmentId, String loanProductShortName) throws Exception { AccountBO accountBo = this.legacyAccountDao.findLoanByClientGovernmentIdAndProductShortName( clientGovernmentId, loanProductShortName); if (null == accountBo) { throw new PersistenceException("loan not found for client government id " + clientGovernmentId + " and loan product short name " + loanProductShortName); } return new AccountReferenceDto(accountBo.getAccountId()); } @Override public AccountReferenceDto lookupSavingsAccountReferenceFromClientGovernmentIdAndSavingsProductShortName( String clientGovernmentId, String savingsProductShortName) throws Exception { AccountBO accountBo = this.legacyAccountDao.findSavingsByClientGovernmentIdAndProductShortName( clientGovernmentId, savingsProductShortName); if (null == accountBo) { throw new PersistenceException("savings not found for client government id " + clientGovernmentId + " and savings product short name " + savingsProductShortName); } return new AccountReferenceDto(accountBo.getAccountId()); } @Override public AccountReferenceDto lookupLoanAccountReferenceFromClientPhoneNumberAndLoanProductShortName( String phoneNumber, String loanProductShortName) throws Exception { AccountBO accountBo = this.legacyAccountDao.findLoanByClientPhoneNumberAndProductShortName( phoneNumber, loanProductShortName); if (null == accountBo) { throw new PersistenceException("loan not found for client phone number " + phoneNumber + " and loan product short name " + loanProductShortName); } return new AccountReferenceDto(accountBo.getAccountId()); } @Override public boolean existsMoreThanOneLoanAccount(String phoneNumber, String loanProductShortName) { try { lookupLoanAccountReferenceFromClientPhoneNumberAndLoanProductShortName(phoneNumber, loanProductShortName); } catch (Exception e) { if (e.getCause() instanceof NonUniqueResultException) { return true; } } return false; } @Override public AccountReferenceDto lookupSavingsAccountReferenceFromClientPhoneNumberAndSavingsProductShortName( String phoneNumber, String savingsProductShortName) throws Exception { AccountBO accountBo = this.legacyAccountDao.findSavingsByClientPhoneNumberAndProductShortName( phoneNumber, savingsProductShortName); if (null == accountBo) { throw new PersistenceException("savings not found for client phone number " + phoneNumber + " and savings product short name " + savingsProductShortName); } return new AccountReferenceDto(accountBo.getAccountId()); } @Override public boolean existsMoreThanOneSavingsAccount(String phoneNumber, String loanProductShortName) { try { lookupSavingsAccountReferenceFromClientPhoneNumberAndSavingsProductShortName(phoneNumber, loanProductShortName); } catch (Exception e) { if (e.getCause() instanceof NonUniqueResultException) { return true; } } return false; } @Override public BigDecimal getTotalPaymentDueAmount(AccountReferenceDto account) throws Exception { AccountBO accountBo = this.legacyAccountDao.getAccount(account.getAccountId()); return accountBo.getTotalAmountDue().getAmount(); } @Override public Object getMifosConfiguration(String propertyKey) { MifosConfigurationManager cfgMng = MifosConfigurationManager.getInstance(); return cfgMng.getProperty(propertyKey); } @Override public boolean receiptExists(String receiptNumber) throws Exception { List<AccountPaymentEntity> existentPaymentsWIthGivenReceiptNumber = this.legacyAccountDao.findAccountPaymentsByReceiptNumber(receiptNumber); return existentPaymentsWIthGivenReceiptNumber != null && !existentPaymentsWIthGivenReceiptNumber.isEmpty(); } /** * Warning - this should be only used from MPESA plugin! */ @Override public List<AccountReferenceDto> lookupLoanAccountReferencesFromClientPhoneNumberAndWithdrawAmount( String phoneNumber, BigDecimal withdrawAmount) throws Exception { List<AccountBO> accounts = this.legacyAccountDao.findApprovedLoansForClientWithPhoneNumber(phoneNumber); List<AccountReferenceDto> result = new ArrayList<AccountReferenceDto>(); for (AccountBO account : accounts) { LoanBO loanAccount = (LoanBO)account; if (loanAccount.getLoanAmount().getAmount().compareTo(computeWithdrawnForMPESA(withdrawAmount, loanAccount)) == 0) { result.add(new AccountReferenceDto(account.getAccountId())); } } return result; } @Override public OverpaymentDto getOverpayment(String overpaymentId) throws PersistenceException { AccountOverpaymentEntity overpaymentEntity = this.legacyAccountDao.findOverpaymentById(Integer.valueOf(overpaymentId)); if (overpaymentEntity == null) { throw new PersistenceException("Overpayment not found for id " + overpaymentId); } return new OverpaymentDto(overpaymentEntity.getOverpaymentId().toString(), overpaymentEntity.getOriginalOverpaymentAmount().getAmount(), overpaymentEntity.getActualOverpaymentAmount().getAmount()); } private BigDecimal computeWithdrawnForMPESA(BigDecimal withdrawAmount, LoanBO loanAccount) { Set<AccountFeesEntity> fees = loanAccount.getAccountFees(); for (AccountFeesEntity fee : fees) { if (fee.isActive() && fee.isOneTime() && fee.isTimeOfDisbursement()) { withdrawAmount = withdrawAmount.add(new BigDecimal(fee.getFeeAmount())); } } return withdrawAmount; } public boolean isAccountGroupLoanMember(Integer accountId) throws Exception { AccountBO account = this.legacyAccountDao.getAccount(accountId); return account.isGroupLoanAccountMember(); } public boolean doesTransactionIntroduceOverpayment(AccountPaymentParametersDto payment) throws Exception { AccountBO account = this.legacyAccountDao.getAccount(payment.getAccountId()); Money amount = new Money(account.getCurrency(), payment.getPaymentAmount()); return amount.isGreaterThan(((LoanBO) account).getTotalRepayableAmount()); } }