Skip to content

Commit

Permalink
FINERACT-2104: Accrual Activity - Support final Accrual Activity tran…
Browse files Browse the repository at this point in the history
…saction creation at the event of the loan got fully paid
  • Loading branch information
somasorosdpc authored and adamsaghy committed Jul 24, 2024
1 parent 602ad4e commit b1798a6
Show file tree
Hide file tree
Showing 11 changed files with 851 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -394,4 +394,13 @@ private static DateTimeFormatter getDateTimeFormatter(String format, Locale loca
return formatter;
}

public static boolean occursOnDayFromExclusiveAndUpToAndIncluding(final LocalDate fromNotInclusive, final LocalDate upToAndInclusive,
final LocalDate target) {
return DateUtils.isAfter(target, fromNotInclusive) && !DateUtils.isAfter(target, upToAndInclusive);
}

public static boolean occursOnDayFromAndUpToAndIncluding(final LocalDate fromAndInclusive, final LocalDate upToAndInclusive,
final LocalDate target) {
return target != null && !DateUtils.isBefore(target, fromAndInclusive) && !DateUtils.isAfter(target, upToAndInclusive);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3877,7 +3877,8 @@ public void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final LoanTra
public LocalDate getLastUserTransactionDate() {
LocalDate currentTransactionDate = getDisbursementDate();
for (final LoanTransaction previousTransaction : this.loanTransactions) {
if (!(previousTransaction.isReversed() || previousTransaction.isAccrual() || previousTransaction.isIncomePosting())
if (!(previousTransaction.isReversed() || previousTransaction.isAccrual() || previousTransaction.isIncomePosting()
|| previousTransaction.isAccrualActivity())
&& DateUtils.isBefore(currentTransactionDate, previousTransaction.getTransactionDate())) {
currentTransactionDate = previousTransaction.getTransactionDate();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,22 +618,12 @@ public boolean hasLoanIdentifiedBy(final Long loanId) {

public boolean isDueForCollectionFromAndUpToAndIncluding(final LocalDate fromNotInclusive, final LocalDate upToAndInclusive) {
final LocalDate dueDate = getDueLocalDate();
return occursOnDayFromExclusiveAndUpToAndIncluding(fromNotInclusive, upToAndInclusive, dueDate);
return DateUtils.occursOnDayFromExclusiveAndUpToAndIncluding(fromNotInclusive, upToAndInclusive, dueDate);
}

public boolean isDueForCollectionFromIncludingAndUpToAndIncluding(final LocalDate fromAndInclusive, final LocalDate upToAndInclusive) {
final LocalDate dueDate = getDueLocalDate();
return occursOnDayFromAndUpToAndIncluding(fromAndInclusive, upToAndInclusive, dueDate);
}

private boolean occursOnDayFromExclusiveAndUpToAndIncluding(final LocalDate fromNotInclusive, final LocalDate upToAndInclusive,
final LocalDate target) {
return DateUtils.isAfter(target, fromNotInclusive) && !DateUtils.isAfter(target, upToAndInclusive);
}

private boolean occursOnDayFromAndUpToAndIncluding(final LocalDate fromAndInclusive, final LocalDate upToAndInclusive,
final LocalDate target) {
return target != null && !DateUtils.isBefore(target, fromAndInclusive) && !DateUtils.isAfter(target, upToAndInclusive);
return DateUtils.occursOnDayFromAndUpToAndIncluding(fromAndInclusive, upToAndInclusive, dueDate);
}

public boolean isFeeCharge() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,13 +220,20 @@ protected void calculateAccrualActivity(LoanTransaction loanTransaction, Monetar
Money interestPortion = Money.zero(currency);
Money feeChargesPortion = Money.zero(currency);
Money penaltychargesPortion = Money.zero(currency);
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
if (loanTransaction.getDateOf().isEqual(currentInstallment.getDueDate())) {
interestPortion = interestPortion.plus(currentInstallment.getInterestCharged(currency));
feeChargesPortion = feeChargesPortion.plus(currentInstallment.getFeeChargesCharged(currency));
penaltychargesPortion = penaltychargesPortion.plus(currentInstallment.getPenaltyChargesCharged(currency));
}
}
final int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);

final LoanRepaymentScheduleInstallment currentInstallment = installments.stream()
.filter(installment -> installment.getInstallmentNumber().equals(firstNormalInstallmentNumber)
? DateUtils.occursOnDayFromExclusiveAndUpToAndIncluding(installment.getFromDate(), installment.getDueDate(),
loanTransaction.getTransactionDate())
: DateUtils.occursOnDayFromAndUpToAndIncluding(installment.getFromDate(), installment.getDueDate(),
loanTransaction.getTransactionDate()))
.findFirst().orElseThrow();

interestPortion = interestPortion.plus(currentInstallment.getInterestCharged(currency));
feeChargesPortion = feeChargesPortion.plus(currentInstallment.getFeeChargesCharged(currency));
penaltychargesPortion = penaltychargesPortion.plus(currentInstallment.getPenaltyChargesCharged(currency));

loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltychargesPortion);
}

Expand Down Expand Up @@ -602,7 +609,7 @@ protected Money handleTransactionAndCharges(final LoanTransaction loanTransactio
installmentNumber = installments.get(0).getInstallmentNumber();
}

if (loanTransaction.isNotWaiver() && !loanTransaction.isAccrual()) {
if (loanTransaction.isNotWaiver() && !loanTransaction.isAccrual() && !loanTransaction.isAccrualActivity()) {
Money feeCharges = loanTransaction.getFeeChargesPortion(currency);
Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency);
if (chargeAmountToProcess != null && feeCharges.isGreaterThan(chargeAmountToProcess)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ public interface LoanAccrualActivityProcessingService {

Loan makeAccrualActivityTransaction(Loan loan, LocalDate currentDate);

@Transactional
void processAccrualActivityForLoanClosure(Loan loan);

@Transactional
void processAccrualActivityForLoanReopen(Loan loan);
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,16 @@ CommandProcessingResult makeLoanRepayment(LoanTransactionType repaymentTransacti
CommandProcessingResult makeLoanRepaymentWithChargeRefundChargeType(LoanTransactionType repaymentTransactionType, Long loanId,
JsonCommand command, boolean isRecoveryRepayment, String chargeRefundChargeType);

@Transactional
Loan reverseReplayAccrualActivityTransaction(Loan loan, LoanTransaction loanTransaction, LoanRepaymentScheduleInstallment installment,
LocalDate transactionDate);

@Transactional
Loan makeAccrualActivityTransaction(Loan loan, LoanRepaymentScheduleInstallment installment, LocalDate transactionDate);

@Transactional
Loan makeAccrualActivityTransaction(Loan loan, LoanTransaction accrualActivityTransaction);

@Transactional
CommandProcessingResult makeInterestPaymentWaiver(JsonCommand command);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,23 @@
*/
package org.apache.fineract.portfolio.loanaccount.service;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -38,6 +46,8 @@ public class LoanAccrualActivityProcessingServiceImpl implements LoanAccrualActi

private final LoanRepositoryWrapper loanRepositoryWrapper;
private final LoanWritePlatformService loanWritePlatformService;
private final ExternalIdFactory externalIdFactory;
private final BusinessEventNotifierService businessEventNotifierService;

@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
Expand Down Expand Up @@ -66,4 +76,98 @@ public Loan makeAccrualActivityTransaction(Loan loan, final LocalDate currentDat
return loan;
}

@Override
@Transactional
public void processAccrualActivityForLoanClosure(Loan loan) {
LocalDate date = loan.isOverPaid() ? loan.getOverpaidOnDate() : loan.getClosedOnDate();
List<LoanTransaction> accrualActivityTransaction = loan.getLoanTransactions().stream().filter(LoanTransaction::isNotReversed)
.filter(LoanTransaction::isAccrualActivity).filter(loanTransaction -> loanTransaction.getDateOf().isAfter(date)).toList();
if (!accrualActivityTransaction.isEmpty()) {
accrualActivityTransaction.forEach(this::reverseAccrualActivityTransaction);
}
LoanTransaction loanTransaction = assembleClosingAccrualActivityTransaction(loan, date);
if (loanTransaction.getAmount().compareTo(BigDecimal.ZERO) != 0) {
loanWritePlatformService.makeAccrualActivityTransaction(loan, loanTransaction);
}
}

private void reverseAccrualActivityTransaction(LoanTransaction loanTransaction) {
loanTransaction.reverse();
LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(loanTransaction);
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
}

private LoanTransaction assembleClosingAccrualActivityTransaction(Loan loan, LocalDate date) {
// collect fees
BigDecimal feeChargesPortion = BigDecimal.ZERO;
// collect penalties
BigDecimal penaltyChargesPortion = BigDecimal.ZERO;
// collect interests
BigDecimal interestPortion = BigDecimal.ZERO;
var currency = loan.getCurrency();
// sum up all accruals
for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
feeChargesPortion = installment.getFeeAccrued(currency).getAmount().add(feeChargesPortion);
penaltyChargesPortion = installment.getPenaltyAccrued(currency).getAmount().add(penaltyChargesPortion);
interestPortion = installment.getInterestAccrued(currency).getAmount().add(interestPortion);
}
List<LoanTransaction> accrualActivities = loan.getLoanTransactions().stream().filter(LoanTransaction::isAccrualActivity)
.filter(LoanTransaction::isNotReversed).toList();
// subtract already Posted accruals
for (LoanTransaction accrualActivity : accrualActivities) {
if (accrualActivity.getFeeChargesPortion() != null) {
feeChargesPortion = feeChargesPortion.subtract(accrualActivity.getFeeChargesPortion());
}
if (accrualActivity.getPenaltyChargesPortion() != null) {
penaltyChargesPortion = penaltyChargesPortion.subtract(accrualActivity.getPenaltyChargesPortion());
}
if (accrualActivity.getInterestPortion() != null) {
interestPortion = interestPortion.subtract(accrualActivity.getInterestPortion());
}
}

BigDecimal transactionAmount = feeChargesPortion.add(penaltyChargesPortion).add(interestPortion);
ExternalId externalId = externalIdFactory.create();

return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.ACCRUAL_ACTIVITY.getValue(), date, transactionAmount, null,
interestPortion, feeChargesPortion, penaltyChargesPortion, null, false, null, externalId);
}

@Override
@Transactional
public void processAccrualActivityForLoanReopen(Loan loan) {
LoanTransaction lastAccrualActivityMarkedToReverse = null;
List<LoanTransaction> accrualActivityTransaction = loan.getLoanTransactions().stream()
.filter(loanTransaction -> loanTransaction.isNotReversed() && loanTransaction.isAccrualActivity())
.sorted(Comparator.comparing(LoanTransaction::getDateOf)).toList();
// grab the latest AccrualActivityTransaction
// it does not matter if it is on an installment due date or not because it was posted due to loan close
if (!accrualActivityTransaction.isEmpty()) {
lastAccrualActivityMarkedToReverse = accrualActivityTransaction.get(accrualActivityTransaction.size() - 1);
}
final LocalDate lastAccrualActivityTransactionDate = lastAccrualActivityMarkedToReverse == null ? null
: lastAccrualActivityMarkedToReverse.getDateOf();
LocalDate today = DateUtils.getBusinessLocalDate();
final List<LoanRepaymentScheduleInstallment> installmentsBetweenBusinessDateAndLastAccrualActivityTransactionDate = loan
.getRepaymentScheduleInstallments().stream()
.filter(installment -> installment.getDueDate().isBefore(today) && (lastAccrualActivityTransactionDate == null
|| installment.getDueDate().isAfter(lastAccrualActivityTransactionDate)
// if close event happened on installment due date
// we should reverse replay it to calculate installment related accrual parts only
|| installment.getDueDate().isEqual(lastAccrualActivityTransactionDate)))
.sorted(Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate)).toList();
for (LoanRepaymentScheduleInstallment installment : installmentsBetweenBusinessDateAndLastAccrualActivityTransactionDate) {
if (lastAccrualActivityMarkedToReverse != null) {
loanWritePlatformService.reverseReplayAccrualActivityTransaction(loan, lastAccrualActivityMarkedToReverse, installment,
installment.getDueDate());
lastAccrualActivityMarkedToReverse = null;
} else {
loanWritePlatformService.makeAccrualActivityTransaction(loan, installment, installment.getDueDate());
}
}
if (lastAccrualActivityMarkedToReverse != null) {
reverseAccrualActivityTransaction(lastAccrualActivityMarkedToReverse);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,13 @@ public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlat

private final BusinessEventNotifierService businessEventNotifierService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private final LoanAccrualActivityProcessingService loanAccrualActivityProcessingService;

@PostConstruct
public void addListeners() {
businessEventNotifierService.addPostBusinessEventListener(LoanStatusChangedBusinessEvent.class, new LoanStatusChangedListener());
businessEventNotifierService.addPostBusinessEventListener(LoanStatusChangedBusinessEvent.class,
new LoanAccrualActivityPostingLoanStatusChangedListener());
}

private final class LoanStatusChangedListener implements BusinessEventListener<LoanStatusChangedBusinessEvent> {
Expand All @@ -53,4 +56,21 @@ public void onBusinessEvent(LoanStatusChangedBusinessEvent event) {
}
}
}

private final class LoanAccrualActivityPostingLoanStatusChangedListener
implements BusinessEventListener<LoanStatusChangedBusinessEvent> {

@Override
public void onBusinessEvent(LoanStatusChangedBusinessEvent event) {
final Loan loan = event.get();
if (loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()) {
if (loan.getStatus().isClosedObligationsMet() || loan.getStatus().isOverpaid()) {
loanAccrualActivityProcessingService.processAccrualActivityForLoanClosure(loan);
}
if (loan.isOpen()) {
loanAccrualActivityProcessingService.processAccrualActivityForLoanReopen(loan);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1162,15 +1162,50 @@ private ChangedTransactionDetail reprocessChangedLoanTransactions(Loan loan,
return changedTransactionDetail;
}

@Transactional
@Override
public Loan reverseReplayAccrualActivityTransaction(Loan loan, final LoanTransaction loanTransaction,
final LoanRepaymentScheduleInstallment installment, final LocalDate transactionDate) {

LoanTransaction newLoanTransaction = loanTransactionAssembler.assembleAccrualActivityTransaction(loan, installment,
transactionDate);
if (!newLoanTransaction.getDateOf().isEqual(loanTransaction.getDateOf())
|| !LoanTransaction.transactionAmountsMatch(loan.getCurrency(), loanTransaction, newLoanTransaction)) {
loanTransaction.reverse();
loanTransaction.updateExternalId(null);
newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations());
newLoanTransaction.getLoanTransactionRelations().add(LoanTransactionRelation.linkToTransaction(newLoanTransaction,
loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED));
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(newLoanTransaction);
loan.addLoanTransaction(newLoanTransaction);

LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(loanTransaction);
data.setNewTransactionDetail(newLoanTransaction);
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
}
return loan;
}

@Transactional
@Override
public Loan makeAccrualActivityTransaction(Loan loan, final LoanRepaymentScheduleInstallment installment,
final LocalDate transactionDate) {
businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionAccrualActivityPreBusinessEvent(loan));

LoanTransaction newAccrualActivityTransaction = loanTransactionAssembler.assembleAccrualActivityTransaction(loan, installment,
transactionDate);

if (newAccrualActivityTransaction.getAmount().compareTo(BigDecimal.ZERO) == 0) {
return loan;
}

return makeAccrualActivityTransaction(loan, newAccrualActivityTransaction);
}

@Transactional
@Override
public Loan makeAccrualActivityTransaction(Loan loan, LoanTransaction newAccrualActivityTransaction) {
businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionAccrualActivityPreBusinessEvent(loan));

loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(newAccrualActivityTransaction);

loan.addLoanTransaction(newAccrualActivityTransaction);
Expand Down
Loading

0 comments on commit b1798a6

Please sign in to comment.