Skip to content

Commit

Permalink
FINERACT-2060: Accrual reverse replay logic and Handling
Browse files Browse the repository at this point in the history
  • Loading branch information
Marta Jankovics committed Nov 15, 2024
1 parent e52f20a commit 6d5074e
Show file tree
Hide file tree
Showing 13 changed files with 468 additions and 495 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,21 @@
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;

@Data
@Accessors(chain = true)
@RequiredArgsConstructor
public class AccrualAmountsData {
public class AccrualPeriodData {

private final Integer installmentNumber;
private final MonetaryCurrency currency;
private final boolean isFirstPeriod;
private Money interestAmount;
private Money interestAccruable;
private Money interestAccrued;
private List<AccrualChargeData> charges;
private final List<AccrualChargeData> charges = new ArrayList<>();

public AccrualAmountsData addCharge(AccrualChargeData charge) {
if (charges == null) {
charges = new ArrayList<>();
}
public AccrualPeriodData addCharge(AccrualChargeData charge) {
charges.add(charge);
return this;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* 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.data;

import jakarta.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;

@Data
@Accessors(chain = true)
@RequiredArgsConstructor
public class AccrualPeriodsData {

private final MonetaryCurrency currency;
private final List<AccrualPeriodData> periods = new ArrayList<>();

public AccrualPeriodsData addPeriod(AccrualPeriodData period) {
periods.add(period);
return this;
}

public static AccrualPeriodsData create(@NotNull List<LoanRepaymentScheduleInstallment> installments, Integer firstInstallmentNumber,
MonetaryCurrency currency) {
AccrualPeriodsData accrualPeriods = new AccrualPeriodsData(currency);
for (LoanRepaymentScheduleInstallment installment : installments) {
Integer installmentNumber = installment.getInstallmentNumber();
boolean isFirst = installmentNumber.equals(firstInstallmentNumber);
accrualPeriods.addPeriod(new AccrualPeriodData(installmentNumber, isFirst));
}
return accrualPeriods;
}

public AccrualPeriodData getPeriodByInstallmentNumber(Integer installmentNumber) {
return installmentNumber == null ? null
: periods.stream().filter(p -> installmentNumber.equals(p.getInstallmentNumber())).findFirst().orElse(null);
}

public Integer getFirstInstallmentNumber() {
return periods.stream().filter(AccrualPeriodData::isFirstPeriod).map(AccrualPeriodData::getInstallmentNumber).findFirst()
.orElse(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import lombok.Getter;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
Expand Down Expand Up @@ -692,29 +693,32 @@ public boolean isOverdueOn(final LocalDate date) {

public void updateChargePortion(final Money feeChargesDue, final Money feeChargesWaived, final Money feeChargesWrittenOff,
final Money penaltyChargesDue, final Money penaltyChargesWaived, final Money penaltyChargesWrittenOff) {
this.feeChargesCharged = defaultToNullIfZero(feeChargesDue.getAmount());
this.feeChargesWaived = defaultToNullIfZero(feeChargesWaived.getAmount());
this.feeChargesWrittenOff = defaultToNullIfZero(feeChargesWrittenOff.getAmount());
this.penaltyCharges = defaultToNullIfZero(penaltyChargesDue.getAmount());
this.penaltyChargesWaived = defaultToNullIfZero(penaltyChargesWaived.getAmount());
this.penaltyChargesWrittenOff = defaultToNullIfZero(penaltyChargesWrittenOff.getAmount());
this.feeChargesCharged = MathUtil.zeroToNull(MathUtil.toBigDecimal(feeChargesDue));
this.feeChargesWaived = MathUtil.zeroToNull(MathUtil.toBigDecimal(feeChargesWaived));
this.feeChargesWrittenOff = MathUtil.zeroToNull(MathUtil.toBigDecimal(feeChargesWrittenOff));
this.penaltyCharges = MathUtil.zeroToNull(MathUtil.toBigDecimal(penaltyChargesDue));
this.penaltyChargesWaived = MathUtil.zeroToNull(MathUtil.toBigDecimal(penaltyChargesWaived));
this.penaltyChargesWrittenOff = MathUtil.zeroToNull(MathUtil.toBigDecimal(penaltyChargesWrittenOff));
}

public void addToChargePortion(final Money feeChargesDue, final Money feeChargesWaived, final Money feeChargesWrittenOff,
final Money penaltyChargesDue, final Money penaltyChargesWaived, final Money penaltyChargesWrittenOff) {
this.feeChargesCharged = defaultToNullIfZero(feeChargesDue.plus(this.feeChargesCharged).getAmount());
this.feeChargesWaived = defaultToNullIfZero(feeChargesWaived.plus(this.feeChargesWaived).getAmount());
this.feeChargesWrittenOff = defaultToNullIfZero(feeChargesWrittenOff.plus(this.feeChargesWrittenOff).getAmount());
this.penaltyCharges = defaultToNullIfZero(penaltyChargesDue.plus(this.penaltyCharges).getAmount());
this.penaltyChargesWaived = defaultToNullIfZero(penaltyChargesWaived.plus(this.penaltyChargesWaived).getAmount());
this.penaltyChargesWrittenOff = defaultToNullIfZero(penaltyChargesWrittenOff.plus(this.penaltyChargesWrittenOff).getAmount());
this.feeChargesCharged = MathUtil.zeroToNull(MathUtil.add(MathUtil.toBigDecimal(feeChargesDue), this.feeChargesCharged));
this.feeChargesWaived = MathUtil.zeroToNull(MathUtil.add(MathUtil.toBigDecimal(feeChargesWaived), this.feeChargesWaived));
this.feeChargesWrittenOff = MathUtil
.zeroToNull(MathUtil.add(MathUtil.toBigDecimal(feeChargesWrittenOff), this.feeChargesWrittenOff));
this.penaltyCharges = MathUtil.zeroToNull(MathUtil.add(MathUtil.toBigDecimal(penaltyChargesDue), this.penaltyCharges));
this.penaltyChargesWaived = MathUtil
.zeroToNull(MathUtil.add(MathUtil.toBigDecimal(penaltyChargesWaived), this.penaltyChargesWaived));
this.penaltyChargesWrittenOff = MathUtil
.zeroToNull(MathUtil.add(MathUtil.toBigDecimal(penaltyChargesWrittenOff), this.penaltyChargesWrittenOff));
checkIfRepaymentPeriodObligationsAreMet(getObligationsMetOnDate(), feeChargesDue.getCurrency());
}

public void updateAccrualPortion(final Money interest, final Money feeCharges, final Money penalityCharges) {
this.interestAccrued = defaultToNullIfZero(interest.getAmount());
this.feeAccrued = defaultToNullIfZero(feeCharges.getAmount());
this.penaltyAccrued = defaultToNullIfZero(penalityCharges.getAmount());
this.interestAccrued = MathUtil.zeroToNull(MathUtil.toBigDecimal(interest));
this.feeAccrued = MathUtil.zeroToNull(MathUtil.toBigDecimal(feeCharges));
this.penaltyAccrued = MathUtil.zeroToNull(MathUtil.toBigDecimal(penalityCharges));
}

public void updateObligationsMet(final MonetaryCurrency currency, final LocalDate transactionDate) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,11 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
+ "where l.loanStatus = 300 and l.isNpa = false and l.chargedOff = false "
+ "and l.loanProduct.accountingRule = :accountingType "
+ "and (recalcDetails.isCompoundingToBePostedAsTransaction is null or recalcDetails.isCompoundingToBePostedAsTransaction = false) "
+ "and exists (select ls.id from LoanRepaymentScheduleInstallment ls where ls.loan.id = l.id and ls.isDownPayment = false "
+ "and ls.fromDate < :tillDate or (ls.installmentNumber = 1 and ls.fromDate = :tillDate) "
+ "and ((ls.interestCharged is not null and ls.interestCharged <> ls.interestAccrued) "
+ "or (ls.feeChargesCharged is not null and ls.feeChargesCharged <> ls.feeAccrued) "
+ "or (ls.penaltyCharges is not null and ls.penaltyCharges <> ls.penaltyAccrued)))";
+ "and (exists (select ls.id from LoanRepaymentScheduleInstallment ls where ls.loan.id = l.id and ls.isDownPayment = false "
+ "and (:futureCharges = true or ls.fromDate < :tillDate or (ls.installmentNumber = (select min(lsi.installmentNumber) from LoanRepaymentScheduleInstallment lsi where lsi.loan.id = l.id and lsi.isDownPayment = false) and ls.fromDate = :tillDate)) "
+ "and ((coalesce(ls.interestCharged, 0) - coalesce(ls.interestWaived, 0)) <> coalesce(ls.interestAccrued, 0) "
+ "or (coalesce(ls.feeChargesCharged, 0) - coalesce(ls.feeChargesWaived, 0)) <> coalesce(ls.feeAccrued, 0) "
+ "or (coalesce(ls.penaltyCharges, 0) - coalesce(ls.penaltyChargesWaived, 0)) <> coalesce(ls.penaltyAccrued, 0))))";

@Query(FIND_GROUP_LOANS_DISBURSED_AFTER)
List<Loan> getGroupLoansDisbursedAfter(@Param("disbursementDate") LocalDate disbursementDate, @Param("groupId") Long groupId,
Expand Down Expand Up @@ -239,5 +239,6 @@ List<Long> findAllNonClosedLoansByLastClosedBusinessDateNotNullAndMinAndMaxLoanI
List<Long> findLoanIdByStatusId(@Param("statusId") Integer statusId);

@Query(FIND_LOANS_FOR_ACCRUAL)
List<Loan> findLoansForAccrual(@Param("accountingType") Integer accountingType, @Param("tillDate") LocalDate tillDate);
List<Loan> findLoansForAccrual(@Param("accountingType") Integer accountingType, @Param("tillDate") LocalDate tillDate,
@Param("futureCharges") boolean futureCharges);
}
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,8 @@ public List<Long> findLoanIdsByStatusId(Integer statusId) {
return repository.findLoanIdByStatusId(statusId);
}

public List<Loan> findLoansForAccrual(Integer accountingType, LocalDate tillDate) {
return repository.findLoansForAccrual(accountingType, tillDate);
public List<Loan> findLoansForAccrual(Integer accountingType, LocalDate tillDate, boolean futureCharges) {
return repository.findLoansForAccrual(accountingType, tillDate, futureCharges);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.service.MathUtil;
Expand Down Expand Up @@ -87,21 +86,13 @@ private static BigDecimal defaultToNullIfZero(final Money value) {
return (value == null || value.isZero()) ? null : value.getAmount();
}

private BigDecimal defaultToZeroIfNull(final BigDecimal value) {
BigDecimal result = value;
if (value == null) {
result = BigDecimal.ZERO;
}
return result;
}

public LoanRepaymentScheduleInstallment getLoanRepaymentScheduleInstallment() {
return this.installment;
}

public void updateComponents(@NotNull Money principal, @NotNull Money interest, @NotNull Money feeCharges,
@NotNull Money penaltyCharges) {
updateComponents(principal.getAmount(), interest.getAmount(), feeCharges.getAmount(), penaltyCharges.getAmount());
public void updateComponents(Money principal, Money interest, Money feeCharges, Money penaltyCharges) {
updateComponents(MathUtil.toBigDecimal(principal), MathUtil.toBigDecimal(interest), MathUtil.toBigDecimal(feeCharges),
MathUtil.toBigDecimal(penaltyCharges));
}

void updateComponents(final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges,
Expand All @@ -113,8 +104,7 @@ void updateComponents(final BigDecimal principal, final BigDecimal interest, fin
}

private void updateAmount() {
this.amount = defaultToZeroIfNull(getPrincipalPortion()).add(defaultToZeroIfNull(getInterestPortion()))
.add(defaultToZeroIfNull(getFeeChargesPortion())).add(defaultToZeroIfNull(getPenaltyChargesPortion()));
this.amount = MathUtil.add(getPrincipalPortion(), getInterestPortion(), getFeeChargesPortion(), getPenaltyChargesPortion());
}

public void setComponents(final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2817,6 +2817,10 @@ public Money getDueInterest(@NotNull Loan loan, @NotNull LoanRepaymentScheduleIn
return null;
}
MonetaryCurrency currency = loan.getLoanProductRelatedDetail().getCurrency();
BigDecimal interest = installment.getInterestCharged();
if (MathUtil.isEmpty(interest)) {
return Money.zero(currency);
}
if (isAfterPeriod(targetDate, installment) || DateUtils.isEqual(targetDate, installment.getDueDate())) {
return installment.getInterestCharged(currency);
}
Expand All @@ -2825,10 +2829,10 @@ public Money getDueInterest(@NotNull Loan loan, @NotNull LoanRepaymentScheduleIn
LocalDate fromDate = installment.getFromDate();
boolean isFirst = installment.getInstallmentNumber()
.equals(fetchFirstNormalInstallmentNumber(loan.getRepaymentScheduleInstallments()));
LocalDate startDate = isFirst ? fromDate.plusDays(1) : fromDate;
LocalDate startDate = isFirst ? fromDate : fromDate.plusDays(1);
int totalNumberOfDays = DateUtils.getExactDifferenceInDays(startDate, installment.getDueDate());
int daysToBeAccrued = DateUtils.getExactDifferenceInDays(startDate, targetDate);
double interestPerDay = installment.getInterestCharged().doubleValue() / totalNumberOfDays;
double interestPerDay = interest.doubleValue() / totalNumberOfDays;
interestPortion = BigDecimal.valueOf(interestPerDay * daysToBeAccrued);
return Money.of(currency, interestPortion);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ public interface LoanAccrualsProcessingService {

void addAccrualAccounting(@NotNull Long loanId, @NotNull List<LoanScheduleAccrualData> loanScheduleAccrualDatas) throws Exception;

void addIncomeAndAccrualTransactions(Long loanId) throws Exception;

void reprocessExistingAccruals(@NotNull Loan loan);

void processAccrualsForInterestRecalculation(@NotNull Loan loan, boolean isInterestRecalculationEnabled);

void processIncomePostingAndAccruals(@NotNull Loan loan);

void addIncomeAndAccrualTransactions(Long loanId) throws Exception;

void processAccrualsForLoanClosure(@NotNull Loan loan);

void processAccrualsForLoanForeClosure(@NotNull Loan loan, @NotNull LocalDate foreClosureDate,
Expand Down
Loading

0 comments on commit 6d5074e

Please sign in to comment.