blob: 79c54ffeff3e1573878e9ac001ae7d6f009186e4 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
package org.apache.fineract.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.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.RoutingDataSource;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
import org.apache.fineract.portfolio.loanaccount.data.LoanInstallmentChargeData;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
import org.joda.time.Days;
import org.joda.time.LocalDate;
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);
}
}