blob: 27b4d167c385fd4d21c3986588cfd2d3a54c0266 [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 static org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction.accrueTransaction;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
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 lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.office.domain.OfficeRepository;
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.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
public class LoanAccrualWritePlatformServiceImpl implements LoanAccrualWritePlatformService {
private static final String ACCRUAL_ON_CHARGE_DUE_DATE = "due-date";
private static final String ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE = "submitted-date";
private final LoanReadPlatformService loanReadPlatformService;
private final LoanChargeReadPlatformService loanChargeReadPlatformService;
private final JdbcTemplate jdbcTemplate;
private final DatabaseSpecificSQLGenerator sqlGenerator;
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
private final PlatformSecurityContext context;
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final LoanRepository loanRepository;
private final OfficeRepository officeRepository;
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanTransactionRepository loanTransactionRepository;
private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService;
private final ConfigurationDomainService configurationDomainService;
private final ExternalIdFactory externalIdFactory;
@Override
@Transactional
public void addAccrualAccounting(final Long loanId, final Collection<LoanScheduleAccrualData> loanScheduleAccrualData) {
Collection<LoanChargeData> chargeData = this.loanChargeReadPlatformService.retrieveLoanChargesForAccrual(loanId);
Collection<LoanSchedulePeriodData> loanWaiverScheduleData = new ArrayList<>(1);
Collection<LoanTransactionData> loanWaiverTransactionData = new ArrayList<>(1);
for (final LoanScheduleAccrualData accrualData : loanScheduleAccrualData) {
if (accrualData.getWaivedInterestIncome() != null && loanWaiverScheduleData.isEmpty()) {
loanWaiverScheduleData = this.loanReadPlatformService.fetchWaiverInterestRepaymentData(accrualData.getLoanId());
loanWaiverTransactionData = this.loanReadPlatformService.retrieveWaiverLoanTransactions(accrualData.getLoanId());
}
updateCharges(chargeData, accrualData, accrualData.getFromDateAsLocaldate(), accrualData.getDueDateAsLocaldate());
updateInterestIncome(accrualData, loanWaiverTransactionData, loanWaiverScheduleData, accrualData.getDueDateAsLocaldate());
addAccrualAccounting(accrualData);
}
}
@Override
@Transactional
public void addPeriodicAccruals(final LocalDate tillDate, Long loanId, Collection<LoanScheduleAccrualData> loanScheduleAccrualData) {
boolean firstTime = true;
LocalDate accruedTill = null;
Collection<LoanChargeData> chargeData = this.loanChargeReadPlatformService.retrieveLoanChargesForAccrual(loanId);
Collection<LoanSchedulePeriodData> loanWaiverScheduleData = new ArrayList<>(1);
Collection<LoanTransactionData> loanWaiverTransactionData = new ArrayList<>(1);
for (final LoanScheduleAccrualData accrualData : loanScheduleAccrualData) {
if (accrualData.getWaivedInterestIncome() != null && loanWaiverScheduleData.isEmpty()) {
loanWaiverScheduleData = this.loanReadPlatformService.fetchWaiverInterestRepaymentData(accrualData.getLoanId());
loanWaiverTransactionData = this.loanReadPlatformService.retrieveWaiverLoanTransactions(accrualData.getLoanId());
}
if (DateUtils.isAfter(accrualData.getDueDateAsLocaldate(), tillDate)) {
if (accruedTill == null || firstTime) {
accruedTill = accrualData.getAccruedTill();
firstTime = false;
}
if (accruedTill == null || DateUtils.isBefore(accruedTill, tillDate)) {
updateCharges(chargeData, accrualData, accrualData.getFromDateAsLocaldate(), tillDate);
updateInterestIncome(accrualData, loanWaiverTransactionData, loanWaiverScheduleData, tillDate);
addAccrualTillSpecificDate(tillDate, accrualData);
}
} else {
updateCharges(chargeData, accrualData, accrualData.getFromDateAsLocaldate(), accrualData.getDueDateAsLocaldate());
updateInterestIncome(accrualData, loanWaiverTransactionData, loanWaiverScheduleData, tillDate);
addAccrualAccounting(accrualData);
accruedTill = accrualData.getDueDateAsLocaldate();
}
}
}
private void addAccrualTillSpecificDate(final LocalDate tillDate, final LoanScheduleAccrualData accrualData) {
LocalDate interestStartDate = accrualData.getFromDateAsLocaldate();
if (DateUtils.isBefore(accrualData.getFromDateAsLocaldate(), accrualData.getInterestCalculatedFrom())) {
if (DateUtils.isBefore(accrualData.getInterestCalculatedFrom(), accrualData.getDueDateAsLocaldate())) {
interestStartDate = accrualData.getInterestCalculatedFrom();
} else {
interestStartDate = accrualData.getDueDateAsLocaldate();
}
}
int totalNumberOfDays = Math.toIntExact(ChronoUnit.DAYS.between(interestStartDate, accrualData.getDueDateAsLocaldate()));
LocalDate startDate = accrualData.getFromDateAsLocaldate();
if (DateUtils.isBefore(startDate, accrualData.getInterestCalculatedFrom())) {
if (DateUtils.isBefore(accrualData.getInterestCalculatedFrom(), tillDate)) {
startDate = accrualData.getInterestCalculatedFrom();
} else {
startDate = tillDate;
}
}
int daysToBeAccrued = Math.toIntExact(ChronoUnit.DAYS.between(startDate, tillDate));
double interestPerDay = accrualData.getAccruableIncome().doubleValue() / totalNumberOfDays;
BigDecimal amount = BigDecimal.ZERO;
BigDecimal interestPortion;
BigDecimal feePortion = accrualData.getDueDateFeeIncome();
BigDecimal penaltyPortion = accrualData.getDueDatePenaltyIncome();
if (daysToBeAccrued >= totalNumberOfDays) {
interestPortion = accrualData.getAccruableIncome();
} else {
interestPortion = BigDecimal.valueOf(interestPerDay * daysToBeAccrued);
}
interestPortion = interestPortion.setScale(accrualData.getCurrencyData().getDecimalPlaces(), MoneyHelper.getRoundingMode());
BigDecimal totalAccInterest = accrualData.getAccruedInterestIncome();
BigDecimal totalAccPenalty = accrualData.getAccruedPenaltyIncome();
BigDecimal totalAccFee = accrualData.getAccruedFeeIncome();
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) > 0) {
addAccrualAccounting(accrualData, amount, interestPortion, totalAccInterest, feePortion, totalAccFee, penaltyPortion,
totalAccPenalty, tillDate);
}
}
@Transactional
public void addAccrualAccounting(LoanScheduleAccrualData scheduleAccrualData) {
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) > 0) {
final String chargeAccrualDateCriteria = configurationDomainService.getAccrualDateConfigForCharge();
if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_DUE_DATE)) {
addAccrualAccounting(scheduleAccrualData, amount, interestPortion, totalAccInterest, feePortion, totalAccFee,
penaltyPortion, totalAccPenalty, scheduleAccrualData.getDueDateAsLocaldate());
} else if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE)) {
addAccrualAccounting(scheduleAccrualData, amount, interestPortion, totalAccInterest, feePortion, totalAccFee,
penaltyPortion, totalAccPenalty, DateUtils.getBusinessLocalDate());
}
}
}
private void addAccrualAccounting(LoanScheduleAccrualData scheduleAccrualData, BigDecimal amount, BigDecimal interestPortion,
BigDecimal totalAccInterest, BigDecimal feePortion, BigDecimal totalAccFee, BigDecimal penaltyPortion,
BigDecimal totalAccPenalty, final LocalDate accruedTill) throws DataAccessException {
AppUser user = context.authenticatedUser();
Loan loan = loanRepository.getReferenceById(scheduleAccrualData.getLoanId());
Office office = officeRepository.getReferenceById(scheduleAccrualData.getOfficeId());
LoanTransaction loanTransaction = loanTransactionRepository.saveAndFlush(accrueTransaction(loan, office, accruedTill, amount,
interestPortion, feePortion, penaltyPortion, externalIdFactory.create()));
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, loanTransaction.getId(), chargeData.getId(), entry.getValue(),
scheduleAccrualData.getInstallmentNumber());
}
Map<String, Object> transactionMap = toMapData(loanTransaction.getId(), amount, interestPortion, feePortion, penaltyPortion,
scheduleAccrualData, accruedTill);
String repaymentUpdateSql = "UPDATE m_loan_repayment_schedule SET accrual_interest_derived=?, accrual_fee_charges_derived=?, "
+ "accrual_penalty_charges_derived=? WHERE id=?";
this.jdbcTemplate.update(repaymentUpdateSql, totalAccInterest, totalAccFee, totalAccPenalty,
scheduleAccrualData.getRepaymentScheduleId());
String updateLoan = "UPDATE m_loan SET accrued_till=?, last_modified_by=?, last_modified_on_utc=? WHERE id=?";
this.jdbcTemplate.update(updateLoan, accruedTill, user.getId(), DateUtils.getAuditOffsetDateTime(),
scheduleAccrualData.getLoanId());
businessEventNotifierService.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(loanTransaction));
final Map<String, Object> accountingBridgeData = deriveAccountingBridgeData(scheduleAccrualData, transactionMap);
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
}
private 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("currencyCode", loanScheduleAccrualData.getCurrencyData().getCode());
accountingBridgeData.put("cashBasedAccountingEnabled", false);
accountingBridgeData.put("upfrontAccrualBasedAccountingEnabled", false);
accountingBridgeData.put("periodicAccrualBasedAccountingEnabled", true);
accountingBridgeData.put("isAccountTransfer", false);
accountingBridgeData.put("isChargeOff", false);
accountingBridgeData.put("isFraud", 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 accruedTill) {
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", accruedTill);
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 String chargeAccrualDateCriteria = configurationDomainService.getAccrualDateConfigForCharge();
if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_DUE_DATE)) {
updateChargeForDueDate(chargesData, accrualData, startDate, endDate);
} else if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE)) {
updateChargeForSubmittedOnDate(chargesData, accrualData, startDate, endDate);
}
}
private void updateChargeForSubmittedOnDate(Collection<LoanChargeData> chargesData, LoanScheduleAccrualData accrualData,
LocalDate startDate, LocalDate endDate) {
final Map<LoanChargeData, BigDecimal> applicableCharges = new HashMap<>();
BigDecimal submittedDateFeeIncome = BigDecimal.ZERO;
BigDecimal submittedDatePenaltyIncome = BigDecimal.ZERO;
LocalDate scheduleEndDate = accrualData.getDueDateAsLocaldate();
for (LoanChargeData loanCharge : chargesData) {
BigDecimal chargeAmount = BigDecimal.ZERO;
if (((accrualData.getInstallmentNumber() == 1 && DateUtils.isEqual(startDate, loanCharge.getSubmittedOnDate())
&& DateUtils.isEqual(startDate, loanCharge.getDueDate())) || DateUtils.isBefore(startDate, loanCharge.getDueDate()))
&& !DateUtils.isBefore(endDate, loanCharge.getSubmittedOnDate())
&& !DateUtils.isBefore(scheduleEndDate, loanCharge.getDueDate())) {
chargeAmount = loanCharge.getAmount();
if (loanCharge.getAmountUnrecognized() != null) {
chargeAmount = chargeAmount.subtract(loanCharge.getAmountUnrecognized());
}
boolean canAddCharge = chargeAmount.compareTo(BigDecimal.ZERO) > 0;
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()) {
submittedDatePenaltyIncome = submittedDatePenaltyIncome.add(chargeAmount);
} else {
submittedDateFeeIncome = submittedDateFeeIncome.add(chargeAmount);
}
}
if (submittedDateFeeIncome.compareTo(BigDecimal.ZERO) == 0) {
submittedDateFeeIncome = null;
}
if (submittedDatePenaltyIncome.compareTo(BigDecimal.ZERO) == 0) {
submittedDatePenaltyIncome = null;
}
accrualData.updateChargeDetails(applicableCharges, submittedDateFeeIncome, submittedDatePenaltyIncome);
}
private void updateChargeForDueDate(Collection<LoanChargeData> chargesData, LoanScheduleAccrualData accrualData, LocalDate startDate,
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() && DateUtils.isEqual(endDate, accrualData.getDueDateAsLocaldate())) {
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) > 0;
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 (((accrualData.getInstallmentNumber() == 1 && DateUtils.isEqual(loanCharge.getDueDate(), startDate))
|| DateUtils.isAfter(loanCharge.getDueDate(), startDate)) && !DateUtils.isAfter(loanCharge.getDueDate(), endDate)) {
chargeAmount = loanCharge.getAmount();
if (loanCharge.getAmountUnrecognized() != null) {
chargeAmount = chargeAmount.subtract(loanCharge.getAmountUnrecognized());
}
boolean canAddCharge = chargeAmount.compareTo(BigDecimal.ZERO) > 0;
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> loanWaiverTransactions,
final Collection<LoanSchedulePeriodData> loanSchedulePeriodDataList, final LocalDate tillDate) {
BigDecimal interestIncome = BigDecimal.ZERO;
if (accrualData.getInterestIncome() != null) {
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 : loanWaiverTransactions) {
LocalDate transactionDate = loanTransactionData.getDate();
if (!DateUtils.isAfter(transactionDate, accrualData.getFromDateAsLocaldate())
|| (DateUtils.isAfter(transactionDate, accrualData.getFromDateAsLocaldate())
&& !DateUtils.isAfter(transactionDate, accrualData.getDueDateAsLocaldate())
&& !DateUtils.isAfter(transactionDate, tillDate))) {
loanTransactionDatas.add(loanTransactionData);
}
}
Iterator<LoanTransactionData> iterator = loanTransactionDatas.iterator();
for (LoanSchedulePeriodData loanSchedulePeriodData : loanSchedulePeriodDataList) {
if (MathUtil.isLessThanOrEqualZero(recognized) && MathUtil.isLessThanOrEqualZero(unrecognized) && iterator.hasNext()) {
LoanTransactionData loanTransactionData = iterator.next();
recognized = recognized.add(loanTransactionData.getInterestPortion());
unrecognized = unrecognized.add(loanTransactionData.getUnrecognizedIncomePortion());
}
if (DateUtils.isBefore(loanSchedulePeriodData.getDueDate(), accrualData.getDueDateAsLocaldate())) {
remainingAmt = remainingAmt.add(loanSchedulePeriodData.getInterestWaived());
if (recognized.compareTo(remainingAmt) > 0) {
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) > 0) {
interestIncome = interestIncome.subtract(interestWaived.subtract(recognized));
}
}
accrualData.updateAccruableIncome(interestIncome);
}
@Override
@Transactional
public void addIncomeAndAccrualTransactions(Long loanId) throws LoanNotFoundException {
if (loanId != null) {
Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
if (loan == null) {
throw new LoanNotFoundException(loanId);
}
final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds());
loan.processIncomeTransactions();
this.loanRepositoryWrapper.saveAndFlush(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
}
}
private void postJournalEntries(final Loan loan, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds) {
final MonetaryCurrency currency = loan.getCurrency();
boolean isAccountTransfer = false;
final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds,
existingReversedTransactionIds, isAccountTransfer);
journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
}
}