/** * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mifosplatform.portfolio.loanaccount.service; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.sql.DataSource; import org.joda.time.Days; import org.joda.time.LocalDate; import org.mifosplatform.accounting.journalentry.service.JournalEntryWritePlatformService; import org.mifosplatform.infrastructure.core.service.DateUtils; import org.mifosplatform.infrastructure.core.service.RoutingDataSource; import org.mifosplatform.organisation.monetary.domain.MoneyHelper; import org.mifosplatform.portfolio.loanaccount.data.LoanChargeData; import org.mifosplatform.portfolio.loanaccount.data.LoanInstallmentChargeData; import org.mifosplatform.portfolio.loanaccount.data.LoanScheduleAccrualData; import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionData; import org.mifosplatform.portfolio.loanaccount.data.LoanTransactionEnumData; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionType; import org.mifosplatform.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; import org.mifosplatform.portfolio.loanproduct.service.LoanEnumerations; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlatformService { private final LoanReadPlatformService loanReadPlatformService; private final LoanChargeReadPlatformService loanChargeReadPlatformService; private final JdbcTemplate jdbcTemplate; private final DataSource dataSource; private final JournalEntryWritePlatformService journalEntryWritePlatformService; @Autowired public LoanAccrualWritePlatformServiceImpl(final RoutingDataSource dataSource, final LoanReadPlatformService loanReadPlatformService, final JournalEntryWritePlatformService journalEntryWritePlatformService, final LoanChargeReadPlatformService loanChargeReadPlatformService) { this.loanReadPlatformService = loanReadPlatformService; this.dataSource = dataSource; this.jdbcTemplate = new JdbcTemplate(this.dataSource); this.journalEntryWritePlatformService = journalEntryWritePlatformService; this.loanChargeReadPlatformService = loanChargeReadPlatformService; } @Override @Transactional public void addAccrualAccounting(final Long loanId, final Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas) throws Exception { Collection<LoanChargeData> chargeData = this.loanChargeReadPlatformService.retrieveLoanChargesForAccural(loanId); Collection<LoanSchedulePeriodData> loanWaiverScheduleData = new ArrayList<>(1); Collection<LoanTransactionData> loanWaiverTansactionData = new ArrayList<>(1); for (final LoanScheduleAccrualData accrualData : loanScheduleAccrualDatas) { if (accrualData.getWaivedInterestIncome() != null && loanWaiverScheduleData.isEmpty()) { loanWaiverScheduleData = this.loanReadPlatformService.fetchWaiverInterestRepaymentData(accrualData.getLoanId()); loanWaiverTansactionData = this.loanReadPlatformService.retrieveWaiverLoanTransactions(accrualData.getLoanId()); } updateCharges(chargeData, accrualData, accrualData.getFromDateAsLocaldate(), accrualData.getDueDateAsLocaldate()); updateInterestIncome(accrualData, loanWaiverTansactionData, loanWaiverScheduleData, accrualData.getDueDateAsLocaldate()); addAccrualAccounting(accrualData); } } @Override @Transactional public void addPeriodicAccruals(final LocalDate tilldate, Long loanId, Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas) throws Exception { boolean firstTime = true; LocalDate accruredTill = null; Collection<LoanChargeData> chargeData = this.loanChargeReadPlatformService.retrieveLoanChargesForAccural(loanId); Collection<LoanSchedulePeriodData> loanWaiverScheduleData = new ArrayList<>(1); Collection<LoanTransactionData> loanWaiverTansactionData = new ArrayList<>(1); for (final LoanScheduleAccrualData accrualData : loanScheduleAccrualDatas) { if (accrualData.getWaivedInterestIncome() != null && loanWaiverScheduleData.isEmpty()) { loanWaiverScheduleData = this.loanReadPlatformService.fetchWaiverInterestRepaymentData(accrualData.getLoanId()); loanWaiverTansactionData = this.loanReadPlatformService.retrieveWaiverLoanTransactions(accrualData.getLoanId()); } if (accrualData.getDueDateAsLocaldate().isAfter(tilldate)) { if (accruredTill == null || firstTime) { accruredTill = accrualData.getAccruedTill(); firstTime = false; } if (accruredTill == null || accruredTill.isBefore(tilldate)) { updateCharges(chargeData, accrualData, accrualData.getFromDateAsLocaldate(), tilldate); updateInterestIncome(accrualData, loanWaiverTansactionData, loanWaiverScheduleData, tilldate); addAccrualTillSpecificDate(tilldate, accrualData); } } else { updateCharges(chargeData, accrualData, accrualData.getFromDateAsLocaldate(), accrualData.getDueDateAsLocaldate()); updateInterestIncome(accrualData, loanWaiverTansactionData, loanWaiverScheduleData, tilldate); addAccrualAccounting(accrualData); accruredTill = accrualData.getDueDateAsLocaldate(); } } } private void addAccrualTillSpecificDate(final LocalDate tilldate, final LoanScheduleAccrualData accrualData) throws Exception { LocalDate interestStartDate = accrualData.getFromDateAsLocaldate(); if (accrualData.getInterestCalculatedFrom() != null && accrualData.getFromDateAsLocaldate().isBefore(accrualData.getInterestCalculatedFrom())) { if (accrualData.getInterestCalculatedFrom().isBefore(accrualData.getDueDateAsLocaldate())) { interestStartDate = accrualData.getInterestCalculatedFrom(); } else { interestStartDate = accrualData.getDueDateAsLocaldate(); } } int totalNumberOfDays = Days.daysBetween(interestStartDate, accrualData.getDueDateAsLocaldate()).getDays(); LocalDate startDate = accrualData.getFromDateAsLocaldate(); if (accrualData.getInterestCalculatedFrom() != null && startDate.isBefore(accrualData.getInterestCalculatedFrom())) { if (accrualData.getInterestCalculatedFrom().isBefore(tilldate)) { startDate = accrualData.getInterestCalculatedFrom(); } else { startDate = tilldate; } } int daysToBeAccrued = Days.daysBetween(startDate, tilldate).getDays(); double interestPerDay = accrualData.getAccruableIncome().doubleValue() / totalNumberOfDays; BigDecimal amount = BigDecimal.ZERO; BigDecimal interestportion = null; BigDecimal feeportion = accrualData.getDueDateFeeIncome(); BigDecimal penaltyportion = accrualData.getDueDatePenaltyIncome(); if (daysToBeAccrued >= totalNumberOfDays) { interestportion = accrualData.getAccruableIncome(); } else { double iterest = interestPerDay * daysToBeAccrued; interestportion = BigDecimal.valueOf(iterest); } interestportion = interestportion.setScale(accrualData.getCurrencyData().decimalPlaces(), MoneyHelper.getRoundingMode()); BigDecimal totalAccInterest = accrualData.getAccruedInterestIncome(); BigDecimal totalAccPenalty = accrualData.getAccruedPenaltyIncome(); BigDecimal totalAccFee = accrualData.getAccruedFeeIncome(); if (interestportion != null) { if (totalAccInterest == null) { totalAccInterest = BigDecimal.ZERO; } interestportion = interestportion.subtract(totalAccInterest); amount = amount.add(interestportion); totalAccInterest = totalAccInterest.add(interestportion); if (interestportion.compareTo(BigDecimal.ZERO) == 0) { interestportion = null; } } if (feeportion != null) { if (totalAccFee == null) { totalAccFee = BigDecimal.ZERO; } feeportion = feeportion.subtract(totalAccFee); amount = amount.add(feeportion); totalAccFee = totalAccFee.add(feeportion); if (feeportion.compareTo(BigDecimal.ZERO) == 0) { feeportion = null; } } if (penaltyportion != null) { if (totalAccPenalty == null) { totalAccPenalty = BigDecimal.ZERO; } penaltyportion = penaltyportion.subtract(totalAccPenalty); amount = amount.add(penaltyportion); totalAccPenalty = totalAccPenalty.add(penaltyportion); if (penaltyportion.compareTo(BigDecimal.ZERO) == 0) { penaltyportion = null; } } if (amount.compareTo(BigDecimal.ZERO) == 1) { addAccrualAccounting(accrualData, amount, interestportion, totalAccInterest, feeportion, totalAccFee, penaltyportion, totalAccPenalty, tilldate); } } @Transactional public void addAccrualAccounting(LoanScheduleAccrualData scheduleAccrualData) throws Exception { BigDecimal amount = BigDecimal.ZERO; BigDecimal interestportion = null; BigDecimal totalAccInterest = null; if (scheduleAccrualData.getAccruableIncome() != null) { interestportion = scheduleAccrualData.getAccruableIncome(); totalAccInterest = interestportion; if (scheduleAccrualData.getAccruedInterestIncome() != null) { interestportion = interestportion.subtract(scheduleAccrualData.getAccruedInterestIncome()); } amount = amount.add(interestportion); if (interestportion.compareTo(BigDecimal.ZERO) == 0) { interestportion = null; } } BigDecimal feeportion = null; BigDecimal totalAccFee = null; if (scheduleAccrualData.getDueDateFeeIncome() != null) { feeportion = scheduleAccrualData.getDueDateFeeIncome(); totalAccFee = feeportion; if (scheduleAccrualData.getAccruedFeeIncome() != null) { feeportion = feeportion.subtract(scheduleAccrualData.getAccruedFeeIncome()); } amount = amount.add(feeportion); if (feeportion.compareTo(BigDecimal.ZERO) == 0) { feeportion = null; } } BigDecimal penaltyportion = null; BigDecimal totalAccPenalty = null; if (scheduleAccrualData.getDueDatePenaltyIncome() != null) { penaltyportion = scheduleAccrualData.getDueDatePenaltyIncome(); totalAccPenalty = penaltyportion; if (scheduleAccrualData.getAccruedPenaltyIncome() != null) { penaltyportion = penaltyportion.subtract(scheduleAccrualData.getAccruedPenaltyIncome()); } amount = amount.add(penaltyportion); if (penaltyportion.compareTo(BigDecimal.ZERO) == 0) { penaltyportion = null; } } if (amount.compareTo(BigDecimal.ZERO) == 1) { addAccrualAccounting(scheduleAccrualData, amount, interestportion, totalAccInterest, feeportion, totalAccFee, penaltyportion, totalAccPenalty, scheduleAccrualData.getDueDateAsLocaldate()); } } private void addAccrualAccounting(LoanScheduleAccrualData scheduleAccrualData, BigDecimal amount, BigDecimal interestportion, BigDecimal totalAccInterest, BigDecimal feeportion, BigDecimal totalAccFee, BigDecimal penaltyportion, BigDecimal totalAccPenalty, final LocalDate accruedTill) throws Exception { String transactionSql = "INSERT INTO m_loan_transaction (loan_id,office_id,is_reversed,transaction_type_enum,transaction_date,amount,interest_portion_derived," + "fee_charges_portion_derived,penalty_charges_portion_derived, submitted_on_date) VALUES (?, ?, 0, ?, ?, ?, ?, ?, ?, ?)"; this.jdbcTemplate.update(transactionSql, scheduleAccrualData.getLoanId(), scheduleAccrualData.getOfficeId(), LoanTransactionType.ACCRUAL.getValue(), accruedTill.toDate(), amount, interestportion, feeportion, penaltyportion, DateUtils.getDateOfTenant()); @SuppressWarnings("deprecation") final Long transactonId = this.jdbcTemplate.queryForLong("SELECT LAST_INSERT_ID()"); Map<LoanChargeData, BigDecimal> applicableCharges = scheduleAccrualData.getApplicableCharges(); String chargespaidSql = "INSERT INTO m_loan_charge_paid_by (loan_transaction_id, loan_charge_id, amount,installment_number) VALUES (?,?,?,?)"; for (Map.Entry<LoanChargeData, BigDecimal> entry : applicableCharges.entrySet()) { LoanChargeData chargeData = entry.getKey(); this.jdbcTemplate.update(chargespaidSql, transactonId, chargeData.getId(), entry.getValue(), scheduleAccrualData.getInstallmentNumber()); } Map<String, Object> transactionMap = toMapData(transactonId, amount, interestportion, feeportion, penaltyportion, scheduleAccrualData, accruedTill); String repaymetUpdatesql = "UPDATE m_loan_repayment_schedule SET accrual_interest_derived=?, accrual_fee_charges_derived=?, " + "accrual_penalty_charges_derived=? WHERE id=?"; this.jdbcTemplate.update(repaymetUpdatesql, totalAccInterest, totalAccFee, totalAccPenalty, scheduleAccrualData.getRepaymentScheduleId()); String updateLoan = "UPDATE m_loan SET accrued_till=? WHERE id=?"; this.jdbcTemplate.update(updateLoan, accruedTill.toDate(), scheduleAccrualData.getLoanId()); final Map<String, Object> accountingBridgeData = deriveAccountingBridgeData(scheduleAccrualData, transactionMap); this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); } public Map<String, Object> deriveAccountingBridgeData(final LoanScheduleAccrualData loanScheduleAccrualData, final Map<String, Object> transactionMap) { final Map<String, Object> accountingBridgeData = new LinkedHashMap<>(); accountingBridgeData.put("loanId", loanScheduleAccrualData.getLoanId()); accountingBridgeData.put("loanProductId", loanScheduleAccrualData.getLoanProductId()); accountingBridgeData.put("officeId", loanScheduleAccrualData.getOfficeId()); accountingBridgeData.put("currency", loanScheduleAccrualData.getCurrencyData()); accountingBridgeData.put("cashBasedAccountingEnabled", false); accountingBridgeData.put("upfrontAccrualBasedAccountingEnabled", false); accountingBridgeData.put("periodicAccrualBasedAccountingEnabled", true); accountingBridgeData.put("isAccountTransfer", false); final List<Map<String, Object>> newLoanTransactions = new ArrayList<>(); newLoanTransactions.add(transactionMap); accountingBridgeData.put("newLoanTransactions", newLoanTransactions); return accountingBridgeData; } public Map<String, Object> toMapData(final Long id, final BigDecimal amount, final BigDecimal interestportion, final BigDecimal feeportion, final BigDecimal penaltyportion, final LoanScheduleAccrualData loanScheduleAccrualData, final LocalDate accruredTill) { final Map<String, Object> thisTransactionData = new LinkedHashMap<>(); final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.ACCRUAL); thisTransactionData.put("id", id); thisTransactionData.put("officeId", loanScheduleAccrualData.getOfficeId()); thisTransactionData.put("type", transactionType); thisTransactionData.put("reversed", false); thisTransactionData.put("date", accruredTill); thisTransactionData.put("currency", loanScheduleAccrualData.getCurrencyData()); thisTransactionData.put("amount", amount); thisTransactionData.put("principalPortion", null); thisTransactionData.put("interestPortion", interestportion); thisTransactionData.put("feeChargesPortion", feeportion); thisTransactionData.put("penaltyChargesPortion", penaltyportion); thisTransactionData.put("overPaymentPortion", null); Map<LoanChargeData, BigDecimal> applicableCharges = loanScheduleAccrualData.getApplicableCharges(); if (applicableCharges != null && !applicableCharges.isEmpty()) { final List<Map<String, Object>> loanChargesPaidData = new ArrayList<>(); for (Map.Entry<LoanChargeData, BigDecimal> entry : applicableCharges.entrySet()) { LoanChargeData chargeData = entry.getKey(); final Map<String, Object> loanChargePaidData = new LinkedHashMap<>(); loanChargePaidData.put("chargeId", chargeData.getChargeId()); loanChargePaidData.put("isPenalty", chargeData.isPenalty()); loanChargePaidData.put("loanChargeId", chargeData.getId()); loanChargePaidData.put("amount", entry.getValue()); loanChargesPaidData.add(loanChargePaidData); } thisTransactionData.put("loanChargesPaid", loanChargesPaidData); } return thisTransactionData; } private void updateCharges(final Collection<LoanChargeData> chargesData, final LoanScheduleAccrualData accrualData, final LocalDate startDate, final LocalDate endDate) { final Map<LoanChargeData, BigDecimal> applicableCharges = new HashMap<>(); BigDecimal dueDateFeeIncome = BigDecimal.ZERO; BigDecimal dueDatePenaltyIncome = BigDecimal.ZERO; for (LoanChargeData loanCharge : chargesData) { BigDecimal chargeAmount = BigDecimal.ZERO; if (loanCharge.getDueDate() == null) { if (loanCharge.isInstallmentFee() && accrualData.getDueDateAsLocaldate().isEqual(endDate)) { Collection<LoanInstallmentChargeData> installmentData = loanCharge.getInstallmentChargeData(); for (LoanInstallmentChargeData installmentChargeData : installmentData) { if (installmentChargeData.getInstallmentNumber().equals(accrualData.getInstallmentNumber())) { BigDecimal accruableForInstallment = installmentChargeData.getAmount(); if (installmentChargeData.getAmountUnrecognized() != null) { accruableForInstallment = accruableForInstallment.subtract(installmentChargeData.getAmountUnrecognized()); } chargeAmount = accruableForInstallment; boolean canAddCharge = chargeAmount.compareTo(BigDecimal.ZERO) == 1; if (canAddCharge && (installmentChargeData.getAmountAccrued() == null || chargeAmount.compareTo(installmentChargeData .getAmountAccrued()) != 0)) { BigDecimal amountForAccrual = chargeAmount; if (installmentChargeData.getAmountAccrued() != null) { amountForAccrual = chargeAmount.subtract(installmentChargeData.getAmountAccrued()); } applicableCharges.put(loanCharge, amountForAccrual); BigDecimal amountAccrued = chargeAmount; if (loanCharge.getAmountAccrued() != null) { amountAccrued = amountAccrued.add(loanCharge.getAmountAccrued()); } loanCharge.updateAmountAccrued(amountAccrued); } break; } } } } else if (loanCharge.getDueDate().isAfter(startDate) && !loanCharge.getDueDate().isAfter(endDate)) { chargeAmount = loanCharge.getAmount(); if (loanCharge.getAmountUnrecognized() != null) { chargeAmount = chargeAmount.subtract(loanCharge.getAmountUnrecognized()); } boolean canAddCharge = chargeAmount.compareTo(BigDecimal.ZERO) == 1; if (canAddCharge && (loanCharge.getAmountAccrued() == null || chargeAmount.compareTo(loanCharge.getAmountAccrued()) != 0)) { BigDecimal amountForAccrual = chargeAmount; if (loanCharge.getAmountAccrued() != null) { amountForAccrual = chargeAmount.subtract(loanCharge.getAmountAccrued()); } applicableCharges.put(loanCharge, amountForAccrual); } } if (loanCharge.isPenalty()) { dueDatePenaltyIncome = dueDatePenaltyIncome.add(chargeAmount); } else { dueDateFeeIncome = dueDateFeeIncome.add(chargeAmount); } } if (dueDateFeeIncome.compareTo(BigDecimal.ZERO) == 0) { dueDateFeeIncome = null; } if (dueDatePenaltyIncome.compareTo(BigDecimal.ZERO) == 0) { dueDatePenaltyIncome = null; } accrualData.updateChargeDetails(applicableCharges, dueDateFeeIncome, dueDatePenaltyIncome); } private void updateInterestIncome(final LoanScheduleAccrualData accrualData, final Collection<LoanTransactionData> loanWaiverTansactions, final Collection<LoanSchedulePeriodData> loanSchedulePeriodDatas, final LocalDate tilldate) { BigDecimal interestIncome = accrualData.getInterestIncome(); if (accrualData.getWaivedInterestIncome() != null) { BigDecimal recognized = BigDecimal.ZERO; BigDecimal unrecognized = BigDecimal.ZERO; BigDecimal remainingAmt = BigDecimal.ZERO; Collection<LoanTransactionData> loanTransactionDatas = new ArrayList<>(); for (LoanTransactionData loanTransactionData : loanWaiverTansactions) { if (!loanTransactionData.dateOf().isAfter(accrualData.getFromDateAsLocaldate()) || (loanTransactionData.dateOf().isAfter(accrualData.getFromDateAsLocaldate()) && !loanTransactionData.dateOf().isAfter(accrualData.getDueDateAsLocaldate()) && !loanTransactionData .dateOf().isAfter(tilldate))) { loanTransactionDatas.add(loanTransactionData); } } Iterator<LoanTransactionData> iterator = loanTransactionDatas.iterator(); for (LoanSchedulePeriodData loanSchedulePeriodData : loanSchedulePeriodDatas) { if (recognized.compareTo(BigDecimal.ZERO) != 1 && unrecognized.compareTo(BigDecimal.ZERO) != 1 && iterator.hasNext()) { LoanTransactionData loanTransactionData = iterator.next(); recognized = recognized.add(loanTransactionData.getInterestPortion()); unrecognized = unrecognized.add(loanTransactionData.getUnrecognizedIncomePortion()); } if (loanSchedulePeriodData.periodDueDate().isBefore(accrualData.getDueDateAsLocaldate())) { remainingAmt = remainingAmt.add(loanSchedulePeriodData.interestWaived()); if (recognized.compareTo(remainingAmt) == 1) { recognized = recognized.subtract(remainingAmt); remainingAmt = BigDecimal.ZERO; } else { remainingAmt = remainingAmt.subtract(recognized); recognized = BigDecimal.ZERO; if (unrecognized.compareTo(remainingAmt) >= 0) { unrecognized = unrecognized.subtract(remainingAmt); remainingAmt = BigDecimal.ZERO; } else if (iterator.hasNext()) { remainingAmt = remainingAmt.subtract(unrecognized); unrecognized = BigDecimal.ZERO; } } } } BigDecimal interestWaived = accrualData.getWaivedInterestIncome(); if (interestWaived.compareTo(recognized) == 1) { interestIncome = interestIncome.subtract(interestWaived.subtract(recognized)); } } accrualData.updateAccruableIncome(interestIncome); } }