FINERACT-2060: Accrual reverse replay logic and Handling

This commit is contained in:
Marta Jankovics
2024-11-29 19:17:23 +01:00
committed by Adam Saghy
parent 8d25b0c765
commit d57e72ec2f
80 changed files with 2882 additions and 2940 deletions
@@ -19,17 +19,24 @@
package org.apache.fineract.infrastructure.event.business.domain.loan;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
public class LoanStatusChangedBusinessEvent extends LoanBusinessEvent {
private static final String TYPE = "LoanStatusChangedBusinessEvent";
private final LoanStatus oldStatus;
public LoanStatusChangedBusinessEvent(Loan value) {
public LoanStatusChangedBusinessEvent(Loan value, LoanStatus oldStatus) {
super(value);
this.oldStatus = oldStatus;
}
@Override
public String getType() {
return TYPE;
}
public LoanStatus getOldStatus() {
return oldStatus;
}
}
@@ -0,0 +1,35 @@
/**
* 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.infrastructure.event.business.domain.loan.transaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
public class LoanAccrualAdjustmentTransactionBusinessEvent extends LoanTransactionBusinessEvent {
private static final String TYPE = "LoanAccrualAdjustmentTransactionBusinessEvent";
public LoanAccrualAdjustmentTransactionBusinessEvent(LoanTransaction value) {
super(value);
}
@Override
public String getType() {
return TYPE;
}
}
@@ -0,0 +1,37 @@
/**
* 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 lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
import org.apache.fineract.organisation.monetary.domain.Money;
@Data
@Accessors(chain = true)
@RequiredArgsConstructor
public class AccrualChargeData {
private final Long loanChargeId;
private final Long loanInstallmentChargeId;
private final boolean isPenalty;
private Money chargeAmount;
private Money chargeAccruable;
private Money chargeAccrued;
}
@@ -0,0 +1,88 @@
/**
* 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 java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.Money;
@Data
@Accessors(chain = true)
@RequiredArgsConstructor
public class AccrualPeriodData {
private final Integer installmentNumber;
private final boolean isFirstPeriod;
private final LocalDate startDate;
private final LocalDate dueDate;
private Money interestAmount;
private Money interestAccruable;
private Money interestAccrued;
private Money unrecognizedWaive;
private Money transactionAccrued;
private final List<AccrualChargeData> charges = new ArrayList<>();
public AccrualPeriodData addCharge(AccrualChargeData charge) {
charges.add(charge);
return this;
}
public Money getChargeAmount() {
return charges.stream().map(AccrualChargeData::getChargeAmount).reduce(null, MathUtil::plus);
}
public Money getFeeAmount() {
return charges.stream().filter(charge -> !charge.isPenalty()).map(AccrualChargeData::getChargeAmount).reduce(null, MathUtil::plus);
}
public Money getPenaltyAmount() {
return charges.stream().filter(AccrualChargeData::isPenalty).map(AccrualChargeData::getChargeAmount).reduce(null, MathUtil::plus);
}
public Money getChargeAccrued() {
return charges.stream().map(AccrualChargeData::getChargeAccrued).reduce(null, MathUtil::plus);
}
public Money getFeeAccrued() {
return charges.stream().filter(charge -> !charge.isPenalty()).map(AccrualChargeData::getChargeAccrued).reduce(null, MathUtil::plus);
}
public Money getPenaltyAccrued() {
return charges.stream().filter(AccrualChargeData::isPenalty).map(AccrualChargeData::getChargeAccrued).reduce(null, MathUtil::plus);
}
public Money getChargeAccruable() {
return charges.stream().map(AccrualChargeData::getChargeAccruable).reduce(null, MathUtil::plus);
}
public Money getFeeAccruable() {
return charges.stream().filter(charge -> !charge.isPenalty()).map(AccrualChargeData::getChargeAccruable).reduce(null,
MathUtil::plus);
}
public Money getPenaltyAccruable() {
return charges.stream().filter(AccrualChargeData::isPenalty).map(AccrualChargeData::getChargeAccruable).reduce(null,
MathUtil::plus);
}
}
@@ -0,0 +1,64 @@
/**
* 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, installment.getFromDate(), installment.getDueDate()));
}
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);
}
}
@@ -20,7 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@@ -66,7 +66,7 @@ public class LoanChargeData {
private final BigDecimal amountOrPercentage;
private final Collection<ChargeData> chargeOptions;
private final List<ChargeData> chargeOptions;
private final boolean penalty;
@@ -84,7 +84,7 @@ public class LoanChargeData {
private final BigDecimal maxCap;
private final Collection<LoanInstallmentChargeData> installmentChargeData;
private final List<LoanInstallmentChargeData> installmentChargeData;
private BigDecimal amountAccrued;
@@ -94,7 +94,7 @@ public class LoanChargeData {
private final ExternalId externalLoanId;
public static LoanChargeData template(final Collection<ChargeData> chargeOptions) {
public static LoanChargeData template(final List<ChargeData> chargeOptions) {
return new LoanChargeData(null, null, null, null, null, null, null, null, chargeOptions, false, null, false, false, null,
ExternalId.empty(), null, null, null, null, ExternalId.empty());
}
@@ -116,7 +116,7 @@ public class LoanChargeData {
final LocalDate dueDate, final EnumOptionData chargeCalculationType, final BigDecimal percentage,
final BigDecimal amountPercentageAppliedTo, final boolean penalty, final EnumOptionData chargePaymentMode, final boolean paid,
final boolean waived, final Long loanId, final ExternalId externalLoanId, final BigDecimal minCap, final BigDecimal maxCap,
final BigDecimal amountOrPercentage, Collection<LoanInstallmentChargeData> installmentChargeData, final ExternalId externalId) {
final BigDecimal amountOrPercentage, List<LoanInstallmentChargeData> installmentChargeData, final ExternalId externalId) {
this.id = id;
this.chargeId = chargeId;
this.name = name;
@@ -160,9 +160,9 @@ public class LoanChargeData {
private LoanChargeData(final Long id, final Long chargeId, final String name, final CurrencyData currency, final BigDecimal amount,
final BigDecimal percentage, final EnumOptionData chargeTimeType, final EnumOptionData chargeCalculationType,
final Collection<ChargeData> chargeOptions, final boolean penalty, final EnumOptionData chargePaymentMode, final boolean paid,
final List<ChargeData> chargeOptions, final boolean penalty, final EnumOptionData chargePaymentMode, final boolean paid,
final boolean waived, final Long loanId, final ExternalId externalLoanId, final BigDecimal minCap, final BigDecimal maxCap,
final BigDecimal amountOrPercentage, Collection<LoanInstallmentChargeData> installmentChargeData, final ExternalId externalId) {
final BigDecimal amountOrPercentage, List<LoanInstallmentChargeData> installmentChargeData, final ExternalId externalId) {
this.id = id;
this.chargeId = chargeId;
this.name = name;
@@ -207,7 +207,7 @@ public class LoanChargeData {
public LoanChargeData(final Long id, final LocalDate dueAsOfDate, final LocalDate submittedOnDate, final BigDecimal amountOutstanding,
EnumOptionData chargeTimeType, final Long loanId, final ExternalId externalLoanId,
Collection<LoanInstallmentChargeData> installmentChargeData, final ExternalId externalId) {
List<LoanInstallmentChargeData> installmentChargeData, final ExternalId externalId) {
this.id = id;
this.chargeId = null;
this.name = null;
@@ -308,7 +308,7 @@ public class LoanChargeData {
this.externalId = chargeData.externalId;
}
public LoanChargeData(LoanChargeData chargeData, Collection<LoanInstallmentChargeData> installmentChargeData) {
public LoanChargeData(LoanChargeData chargeData, List<LoanInstallmentChargeData> installmentChargeData) {
this.id = chargeData.id;
this.chargeId = chargeData.chargeId;
this.name = chargeData.name;
@@ -61,6 +61,7 @@ public class LoanTransactionEnumData {
private final boolean reAmortize;
private final boolean accrualActivity;
private final boolean interestRefund;
private final boolean accrualAdjustment;
public LoanTransactionEnumData(final Long id, final String code, final String value) {
this.id = id;
@@ -96,6 +97,7 @@ public class LoanTransactionEnumData {
this.reAge = Long.valueOf(LoanTransactionType.REAGE.getValue()).equals(this.id);
this.reAmortize = Long.valueOf(LoanTransactionType.REAMORTIZE.getValue()).equals(this.id);
this.interestRefund = Long.valueOf(LoanTransactionType.INTEREST_REFUND.getValue()).equals(this.id);
this.accrualAdjustment = Long.valueOf(LoanTransactionType.ACCRUAL_ADJUSTMENT.getValue()).equals(this.id);
}
public boolean isRepaymentType() {
@@ -55,7 +55,7 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
if (isNotLoanCreation(loanEvent)) {
// in case of Loan creation, a LoanCreatedBusinessEvent is also raised, no need to send a status change
businessEventNotifierService.notifyPostBusinessEvent(new LoanStatusChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanStatusChangedBusinessEvent(loan, oldStatus));
}
// set mandatory field states based on new status after the transition
@@ -749,11 +749,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
public ChangedTransactionDetail reprocessTransactions() {
ChangedTransactionDetail changedTransactionDetail;
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor();
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(),
getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
}
@@ -778,10 +778,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
public LoanTransaction handleChargeAppliedTransaction(final LoanCharge loanCharge, final LocalDate suppliedTransactionDate) {
final Money chargeAmount = loanCharge.getAmount(getCurrency());
Money feeCharges = chargeAmount;
Money penaltyCharges = Money.zero(loanCurrency());
Money penaltyCharges = Money.zero(getCurrency());
if (loanCharge.isPenaltyCharge()) {
penaltyCharges = chargeAmount;
feeCharges = Money.zero(loanCurrency());
feeCharges = Money.zero(getCurrency());
}
LocalDate transactionDate;
@@ -898,8 +898,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
removeOrModifyTransactionAssociatedWithLoanChargeIfDueAtDisbursement(loanCharge);
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor();
if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(loanCurrency())) {
if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(getCurrency())) {
/*
* TODO Vishwas Currently we do not allow removing a loan charge after a loan is approved (hence there is no
* need to adjust any loan transactions).
@@ -907,9 +906,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
* Consider removing this block of code or logically completing it for the future by getting the list of
* affected Transactions
*/
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
reprocessTransactions();
}
this.charges.remove(loanCharge);
updateLoanSummaryDerivedFields();
@@ -922,7 +919,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
for (final LoanTransaction transaction : transactions) {
if (transaction.isRepaymentAtDisbursement()
&& doesLoanChargePaidByContainLoanCharge(transaction.getLoanChargesPaid(), loanCharge)) {
final MonetaryCurrency currency = loanCurrency();
final MonetaryCurrency currency = getCurrency();
final Money chargeAmount = Money.of(currency, loanCharge.amount());
if (transaction.isGreaterThan(chargeAmount)) {
final Money principalPortion = Money.zero(currency);
@@ -969,9 +966,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
* Consider removing this block of code or logically completing it for the future by getting the list of
* affected Transactions
*/
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
reprocessTransactions();
} else {
// reprocess loan schedule based on charge been waived.
final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper();
@@ -1067,7 +1062,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
final ExternalId externalId) {
validateLoanIsNotClosed(loanCharge);
final Money amountWaived = loanCharge.waive(loanCurrency(), loanInstallmentNumber);
final Money amountWaived = loanCharge.waive(getCurrency(), loanInstallmentNumber);
changes.put("amount", amountWaived.getAmount());
Money unrecognizedIncome = amountWaived.zero();
@@ -1089,10 +1084,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
}
Money feeChargesWaived = chargeComponent;
Money penaltyChargesWaived = Money.zero(loanCurrency());
Money penaltyChargesWaived = Money.zero(getCurrency());
if (loanCharge.isPenaltyCharge()) {
penaltyChargesWaived = chargeComponent;
feeChargesWaived = Money.zero(loanCurrency());
feeChargesWaived = Money.zero(getCurrency());
}
LocalDate transactionDate = getDisbursementDate();
@@ -1132,7 +1127,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
// Waive of charges whose due date falls after latest 'repayment' transaction don't require entire loan schedule
// to be reprocessed.
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor();
if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(loanCurrency())) {
if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(getCurrency())) {
/*
* TODO Vishwas Currently we do not allow waiving fully paid loan charge and waiving partially paid loan
* charges only waives the remaining amount.
@@ -1140,9 +1135,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
* Consider removing this block of code or logically completing it for the future by getting the list of
* affected Transactions
*/
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
reprocessTransactions();
} else {
// reprocess loan schedule based on charge been waived.
final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper();
@@ -1289,7 +1282,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
addLoanRepaymentScheduleInstallment(installment);
}
}
updateLoanScheduleDependentDerivedFields();
updateLoanSummaryDerivedFields();
}
@@ -1310,7 +1302,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
updateLoanScheduleDependentDerivedFields();
updateLoanSummaryDerivedFields();
}
private LoanRepaymentScheduleInstallment findByInstallmentNumber(Collection<LoanRepaymentScheduleInstallment> installments,
@@ -1347,8 +1338,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.totalRecovered = recoveredAmount.getAmountDefaultedToNullIfZero();
final Money principal = this.loanRepaymentScheduleDetail.getPrincipal();
this.summary.updateSummary(loanCurrency(), principal, getRepaymentScheduleInstallments(), this.loanSummaryWrapper,
this.charges);
this.summary.updateSummary(getCurrency(), principal, getRepaymentScheduleInstallments(), this.loanSummaryWrapper, this.charges);
updateLoanOutstandingBalances();
}
}
@@ -1923,7 +1913,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
public void updateLoanRepaymentPeriodsDerivedFields(final LocalDate actualDisbursementDate) {
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment repaymentPeriod : installments) {
repaymentPeriod.updateObligationsMet(loanCurrency(), actualDisbursementDate);
repaymentPeriod.updateObligationsMet(getCurrency(), actualDisbursementDate);
}
}
@@ -2002,7 +1992,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
* TODO Vishwas: do we need to be able to pass in payment type details for repayments at disbursements too?
*/
final Money totalFeeChargesDueAtDisbursement = this.summary.getTotalFeeChargesDueAtDisbursement(loanCurrency());
final Money totalFeeChargesDueAtDisbursement = this.summary.getTotalFeeChargesDueAtDisbursement(getCurrency());
/*
* all Charges repaid at disbursal is marked as repaid and "APPLY Charge" transactions are created for all other
* fees ( which are created during disbursal but not repaid)
@@ -2221,7 +2211,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
if (charge.isOverdueInstallmentCharge()) {
charge.setActive(false);
} else {
charge.resetToOriginal(loanCurrency());
charge.resetToOriginal(getCurrency());
}
}
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
@@ -2354,7 +2344,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
loanTransaction.updateLoan(this);
if (loanTransaction.isNotZero(loanCurrency())) {
if (loanTransaction.isNotZero()) {
addLoanTransaction(loanTransaction);
}
updateLoanSummaryDerivedFields();
@@ -2371,7 +2361,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
if (loanTransaction.isRecoveryRepayment()
&& loanTransaction.getAmount(loanCurrency()).getAmount().compareTo(getSummary().getTotalWrittenOff()) > 0) {
&& loanTransaction.getAmount(getCurrency()).getAmount().compareTo(getSummary().getTotalWrittenOff()) > 0) {
final String errorMessage = "The transaction amount cannot greater than the remaining written off amount.";
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.greater.than.total.written.off", errorMessage);
}
@@ -2380,7 +2370,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
final boolean isTransactionChronologicallyLatest = isChronologicallyLatestRepaymentOrWaiver(loanTransaction);
if (loanTransaction.isNotZero(loanCurrency())) {
if (loanTransaction.isNotZero()) {
addLoanTransaction(loanTransaction);
}
@@ -2400,12 +2390,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
if (loanTransaction.isInterestWaiver()) {
Money totalInterestOutstandingOnLoan = getTotalInterestOutstandingOnLoan();
if (adjustedTransaction != null) {
totalInterestOutstandingOnLoan = totalInterestOutstandingOnLoan.plus(adjustedTransaction.getAmount(loanCurrency()));
totalInterestOutstandingOnLoan = totalInterestOutstandingOnLoan.plus(adjustedTransaction.getAmount(getCurrency()));
}
if (loanTransaction.getAmount(loanCurrency()).isGreaterThan(totalInterestOutstandingOnLoan)) {
if (loanTransaction.getAmount(getCurrency()).isGreaterThan(totalInterestOutstandingOnLoan)) {
final String errorMessage = "The amount of interest to waive cannot be greater than total interest outstanding on loan.";
throw new InvalidLoanStateTransitionException("waive.interest", "amount.exceeds.total.outstanding.interest", errorMessage,
loanTransaction.getAmount(loanCurrency()), totalInterestOutstandingOnLoan.getAmount());
loanTransaction.getAmount(getCurrency()), totalInterestOutstandingOnLoan.getAmount());
}
}
@@ -2450,17 +2440,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
&& !getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE)) {
regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
}
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
}
/*
* Commented since throwing exception if external id present for one of the transactions. for this need to
* save the reversed transactions first and then new transactions.
*/
this.loanTransactions.addAll(changedTransactionDetail.getNewTransactionMappings().values());
changedTransactionDetail = reprocessTransactions();
}
updateLoanSummaryDerivedFields();
@@ -2531,7 +2511,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
// FIXME - kw - update account balance to negative amount.
handleLoanOverpayment(transactionDate, loanLifecycleStateMachine);
statusChanged = true;
} else if (this.summary.isRepaidInFull(loanCurrency())) {
} else if (this.summary.isRepaidInFull(getCurrency())) {
handleLoanRepaymentInFull(transactionDate, loanLifecycleStateMachine);
statusChanged = true;
} else {
@@ -2645,7 +2625,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
LocalDate lastTransactionDate = null;
for (final LoanTransaction transaction : this.loanTransactions) {
if (transaction.isRepaymentLikeType() && transaction.isNonZero()) {
if (transaction.isRepaymentLikeType() && transaction.isGreaterThanZero()) {
lastTransactionDate = transaction.getTransactionDate();
}
}
@@ -2669,7 +2649,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) {
final Money outstandingForPeriod = scheduledRepayment.getInterestOutstanding(loanCurrency());
final Money outstandingForPeriod = scheduledRepayment.getInterestOutstanding(getCurrency());
if (scheduledRepayment.isOverdueOn(DateUtils.getBusinessLocalDate()) && scheduledRepayment.isNotFullyPaidOff()
&& outstandingForPeriod.isGreaterThanZero()) {
transactionDate = scheduledRepayment.getDueDate();
@@ -2692,16 +2672,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
final LoanTransaction writeOffTransaction = findWriteOffTransaction();
writeOffTransaction.reverse();
loanLifecycleStateMachine.transition(LoanEvent.WRITE_OFF_OUTSTANDING_UNDO, this);
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor();
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
}
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(),
getActiveCharges());
updateLoanSummaryDerivedFields();
return changedTransactionDetail;
return reprocessTransactions();
}
public LoanTransaction findWriteOffTransaction() {
@@ -2718,7 +2689,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
private Money calculateTotalOverpayment() {
Money totalPaidInRepayments = getTotalPaidInRepayments();
final MonetaryCurrency currency = loanCurrency();
final MonetaryCurrency currency = getCurrency();
Money cumulativeTotalPaidOnInstallments = Money.zero(currency);
Money cumulativeTotalWaivedOnInstallments = Money.zero(currency);
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
@@ -2759,10 +2730,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return getTotalRecoveredPayments();
}
public MonetaryCurrency loanCurrency() {
return this.loanRepaymentScheduleDetail.getCurrency();
}
public ChangedTransactionDetail closeAsWrittenOff(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine,
final Map<String, Object> changes, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
final AppUser currentUser, final ScheduleGeneratorDTO scheduleGeneratorDTO) {
@@ -2822,7 +2789,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
addLoanTransaction(loanTransaction);
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(loanCurrency(),
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(getCurrency(),
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()), null));
updateLoanSummaryDerivedFields();
@@ -2844,14 +2811,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
}
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
addLoanTransaction(mapEntry.getValue());
}
updateLoanSummaryDerivedFields();
changedTransactionDetail = reprocessTransactions();
LocalDate lastLoanTransactionDate = getLatestTransactionDate();
doPostLoanTransactionChecks(lastLoanTransactionDate, loanLifecycleStateMachine);
}
@@ -2904,7 +2864,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
LoanTransaction loanTransaction = null;
if (isOpen()) {
final Money totalOutstanding = this.summary.getTotalOutstanding(loanCurrency());
final Money totalOutstanding = this.summary.getTotalOutstanding(getCurrency());
if (totalOutstanding.isGreaterThanZero() && getInArrearsTolerance().isGreaterThanOrEqualTo(totalOutstanding)) {
this.closedOnDate = closureDate;
@@ -2923,7 +2883,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
addLoanTransaction(loanTransaction);
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(loanCurrency(),
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(getCurrency(),
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()), null));
updateLoanSummaryDerivedFields();
@@ -3123,11 +3083,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
private Money getTotalPaidInRepayments() {
Money cumulativePaid = Money.zero(loanCurrency());
Money cumulativePaid = Money.zero(getCurrency());
for (final LoanTransaction repayment : this.loanTransactions) {
if (repayment.isRepaymentLikeType() && !repayment.isReversed()) {
cumulativePaid = cumulativePaid.plus(repayment.getAmount(loanCurrency()));
cumulativePaid = cumulativePaid.plus(repayment.getAmount(getCurrency()));
}
}
@@ -3135,11 +3095,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
public Money getTotalRecoveredPayments() {
Money cumulativePaid = Money.zero(loanCurrency());
Money cumulativePaid = Money.zero(getCurrency());
for (final LoanTransaction recoveredPayment : this.loanTransactions) {
if (recoveredPayment.isRecoveryRepayment()) {
cumulativePaid = cumulativePaid.plus(recoveredPayment.getAmount(loanCurrency()));
cumulativePaid = cumulativePaid.plus(recoveredPayment.getAmount(getCurrency()));
}
}
return cumulativePaid;
@@ -3148,16 +3108,16 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
public Money getTotalPrincipalOutstandingUntil(LocalDate date) {
return getRepaymentScheduleInstallments().stream()
.filter(installment -> installment.getDueDate().isBefore(date) || installment.getDueDate().isEqual(date))
.map(installment -> installment.getPrincipalOutstanding(loanCurrency())).reduce(Money.zero(loanCurrency()), Money::add);
.map(installment -> installment.getPrincipalOutstanding(getCurrency())).reduce(Money.zero(getCurrency()), Money::add);
}
private Money getTotalInterestOutstandingOnLoan() {
Money cumulativeInterest = Money.zero(loanCurrency());
Money cumulativeInterest = Money.zero(getCurrency());
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) {
cumulativeInterest = cumulativeInterest.plus(scheduledRepayment.getInterestOutstanding(loanCurrency()));
cumulativeInterest = cumulativeInterest.plus(scheduledRepayment.getInterestOutstanding(getCurrency()));
}
return cumulativeInterest;
@@ -3169,7 +3129,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) {
final Money interestOutstandingForPeriod = scheduledRepayment.getInterestOutstanding(loanCurrency());
final Money interestOutstandingForPeriod = scheduledRepayment.getInterestOutstanding(getCurrency());
if (scheduledRepayment.isOverdueOn(DateUtils.getBusinessLocalDate())) {
cumulativeInterestOverdue = cumulativeInterestOverdue.plus(interestOutstandingForPeriod);
}
@@ -3515,7 +3475,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
&& !DateUtils.isAfter(transaction.getTransactionDate(), tillDate)) {
if (transaction.isAccrual()) {
receivableInterest = receivableInterest.plus(transaction.getInterestPortion(getCurrency()));
} else if (transaction.isRepaymentLikeType() || transaction.isInterestWaiver()) {
} else if (transaction.isRepaymentLikeType() || transaction.isInterestWaiver() || transaction.isAccrualAdjustment()) {
receivableInterest = receivableInterest.minus(transaction.getInterestPortion(getCurrency()));
}
}
@@ -3841,8 +3801,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
public LocalDate getLastUserTransactionDate() {
LocalDate currentTransactionDate = getDisbursementDate();
for (final LoanTransaction previousTransaction : this.loanTransactions) {
if (!(previousTransaction.isReversed() || previousTransaction.isAccrual() || previousTransaction.isIncomePosting()
|| previousTransaction.isAccrualActivity())
if (!(previousTransaction.isReversed() || previousTransaction.isAccrualRelated() || previousTransaction.isIncomePosting())
&& DateUtils.isBefore(currentTransactionDate, previousTransaction.getTransactionDate())) {
currentTransactionDate = previousTransaction.getTransactionDate();
}
@@ -4131,17 +4090,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
regenerateRepaymentSchedule(scheduleGeneratorDTO);
}
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor();
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(),
getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
addLoanTransaction(mapEntry.getValue());
}
return changedTransactionDetail;
return reprocessTransactions();
}
public BigDecimal getPrincipalAmountForRepaymentSchedule() {
@@ -4219,7 +4168,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
} else {
regenerateRepaymentSchedule(generatorDTO);
}
return processTransactions();
return reprocessTransactions();
}
@@ -4229,33 +4178,13 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
} else {
regenerateRepaymentSchedule(generatorDTO);
}
return processTransactions();
return reprocessTransactions();
}
public ChangedTransactionDetail handleRegenerateRepaymentScheduleWithInterestRecalculation(final ScheduleGeneratorDTO generatorDTO) {
regenerateRepaymentScheduleWithInterestRecalculation(generatorDTO);
return processTransactions();
}
public ChangedTransactionDetail processTransactions() {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor();
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(),
getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
}
/*
* Commented since throwing exception if external id present for one of the transactions. for this need to save
* the reversed transactions first and then new transactions.
*/
this.loanTransactions.addAll(changedTransactionDetail.getNewTransactionMappings().values());
updateLoanSummaryDerivedFields();
return changedTransactionDetail;
return reprocessTransactions();
}
public void regenerateRepaymentScheduleWithInterestRecalculation(final ScheduleGeneratorDTO generatorDTO) {
@@ -4417,17 +4346,17 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
private OutstandingAmountsDTO getTotalOutstandingOnLoan() {
Money totalPrincipal = Money.zero(loanCurrency());
Money totalInterest = Money.zero(loanCurrency());
Money feeCharges = Money.zero(loanCurrency());
Money penaltyCharges = Money.zero(loanCurrency());
Money totalPrincipal = Money.zero(getCurrency());
Money totalInterest = Money.zero(getCurrency());
Money feeCharges = Money.zero(getCurrency());
Money penaltyCharges = Money.zero(getCurrency());
final Set<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails = null;
List<LoanRepaymentScheduleInstallment> repaymentSchedule = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment scheduledRepayment : repaymentSchedule) {
totalPrincipal = totalPrincipal.plus(scheduledRepayment.getPrincipalOutstanding(loanCurrency()));
totalInterest = totalInterest.plus(scheduledRepayment.getInterestOutstanding(loanCurrency()));
feeCharges = feeCharges.plus(scheduledRepayment.getFeeChargesOutstanding(loanCurrency()));
penaltyCharges = penaltyCharges.plus(scheduledRepayment.getPenaltyChargesOutstanding(loanCurrency()));
totalPrincipal = totalPrincipal.plus(scheduledRepayment.getPrincipalOutstanding(getCurrency()));
totalInterest = totalInterest.plus(scheduledRepayment.getInterestOutstanding(getCurrency()));
feeCharges = feeCharges.plus(scheduledRepayment.getFeeChargesOutstanding(getCurrency()));
penaltyCharges = penaltyCharges.plus(scheduledRepayment.getPenaltyChargesOutstanding(getCurrency()));
}
return new OutstandingAmountsDTO(totalPrincipal.getCurrency()).principal(totalPrincipal).interest(totalInterest)
.feeCharges(feeCharges).penaltyCharges(penaltyCharges);
@@ -4533,6 +4462,16 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return getRepaymentScheduleInstallments().stream().filter(predicate).findFirst().orElse(null);
}
/**
* @param predicate
* filter of the installments
* @return the installments matching the filter
**/
public List<LoanRepaymentScheduleInstallment> getRepaymentScheduleInstallments(
@NotNull Predicate<LoanRepaymentScheduleInstallment> predicate) {
return getRepaymentScheduleInstallments().stream().filter(predicate).toList();
}
/**
* @return loan disbursement data
**/
@@ -4691,17 +4630,18 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return this.createStandingInstructionAtDisbursement != null && this.createStandingInstructionAtDisbursement;
}
public Collection<LoanCharge> getLoanCharges(LocalDate dueDate) {
Collection<LoanCharge> loanCharges = new ArrayList<>();
public List<LoanCharge> getLoanChargesByDueDate(LocalDate dueDate) {
return getLoanCharges(
loanCharge -> loanCharge.getDueLocalDate() != null && DateUtils.isEqual(loanCharge.getDueLocalDate(), dueDate));
}
for (LoanCharge loanCharge : charges) {
if (loanCharge.getDueLocalDate() != null && loanCharge.getDueLocalDate().equals(dueDate)) {
loanCharges.add(loanCharge);
}
}
return loanCharges;
/**
* @param predicate
* filter of the charges
* @return the loan charges matching the filter
**/
public List<LoanCharge> getLoanCharges(@NotNull Predicate<LoanCharge> predicate) {
return getLoanCharges().stream().filter(predicate).toList();
}
public void setGuaranteeAmount(BigDecimal guaranteeAmountDerived) {
@@ -4796,7 +4736,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
throw new InvalidLoanStateTransitionException("transaction", "no.payment.yet.made.for.loan", errorMessage);
}
if (loanTransaction.isNotZero(loanCurrency())) {
if (loanTransaction.isNotZero()) {
addLoanTransaction(loanTransaction);
}
if (loanTransaction.isNotRefundForActiveLoan()) {
@@ -4828,13 +4768,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(getCurrency(),
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()), null));
} else {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
}
changedTransactionDetail = reprocessTransactions();
}
updateLoanSummaryDerivedFields();
@@ -4869,7 +4803,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
LocalDate lastTransactionDate = null;
for (final LoanTransaction transaction : this.loanTransactions) {
if ((transaction.isRepaymentLikeType() || transaction.isRefundForActiveLoan() || transaction.isCreditBalanceRefund())
&& transaction.isNonZero() && transaction.isNotReversed()) {
&& transaction.isGreaterThanZero() && transaction.isNotReversed()) {
lastTransactionDate = transaction.getTransactionDate();
}
}
@@ -4979,7 +4913,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
charge.setActive(false);
} else if (charge.isTrancheDisbursementCharge() && disbursementDetail.getDisbursementDate()
.equals(charge.getTrancheDisbursementCharge().getloanDisbursementDetails().actualDisbursementDate())) {
charge.resetToOriginal(loanCurrency());
charge.resetToOriginal(getCurrency());
}
}
this.loanRepaymentScheduleDetail.setPrincipal(getDisbursedAmount().subtract(disbursementDetail.principal()));
@@ -5211,7 +5145,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
public void validateForForeclosure(final LocalDate transactionDate) {
if (isInterestRecalculationEnabledForProduct()) {
if (getLoanProductRelatedDetail().isInterestRecalculationEnabled()) {
final String defaultUserMessage = "The loan with interest recalculation enabled cannot be foreclosed.";
throw new LoanForeclosureException("loan.with.interest.recalculation.enabled.cannot.be.foreclosured", defaultUserMessage,
getId());
@@ -5461,8 +5395,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
public LoanTransaction getLastUserTransaction() {
return getLoanTransactions().stream() //
.filter(LoanTransaction::isNotReversed) //
.filter(t -> !(t.isAccrualTransaction() || t.isIncomePosting())) //
.filter(t -> t.isNotReversed() && !(t.isAccrual() || t.isAccrualAdjustment() || t.isIncomePosting())) //
.reduce((first, second) -> second) //
.orElse(null);
}
@@ -28,6 +28,7 @@ import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
@@ -40,7 +41,8 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.function.Predicate;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
@@ -58,6 +60,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidDetail;
import org.apache.fineract.portfolio.loanaccount.data.LoanInstallmentChargeData;
@Getter
@Entity
@Table(name = "m_loan_charge", uniqueConstraints = { @UniqueConstraint(columnNames = { "external_id" }, name = "external_id") })
public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@@ -305,10 +308,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
public BigDecimal getAmountPercentageAppliedTo() {
return this.amountPercentageAppliedTo;
}
private BigDecimal calculateAmountOutstanding(final MonetaryCurrency currency) {
return getAmount(currency).minus(getAmountWaived(currency)).minus(getAmountPaid(currency)).getAmount();
}
@@ -509,15 +508,7 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
public LocalDate getDueLocalDate() {
return this.dueDate;
}
public LocalDate getDueDate() {
return this.dueDate;
}
public LocalDate getSubmittedOnDate() {
return submittedOnDate;
return this.dueDate; // TODO delete duplicated method
}
private boolean determineIfFullyPaid() {
@@ -597,11 +588,11 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
public BigDecimal amount() {
return this.amount;
return this.amount; // TODO delete duplicated method
}
public BigDecimal amountOutstanding() {
return this.amountOutstanding;
return this.amountOutstanding; // TODO delete duplicated method
}
public Money getAmountOutstanding(final MonetaryCurrency currency) {
@@ -644,14 +635,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return this.waived;
}
public BigDecimal getMinCap() {
return this.minCap;
}
public BigDecimal getMaxCap() {
return this.maxCap;
}
public boolean isPaidOrPartiallyPaid(final MonetaryCurrency currency) {
final Money amountWaivedOrWrittenOff = getAmountWaived(currency).plus(getAmountWrittenOff(currency));
@@ -729,10 +712,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return this.charge.getCurrencyCode();
}
public Charge getCharge() {
return this.charge;
}
/*
* @Override public boolean equals(final Object obj) { if (obj == null) { return false; } if (obj == this) { return
* true; } if (obj.getClass() != getClass()) { return false; } final LoanCharge rhs = (LoanCharge) obj; return new
@@ -756,10 +735,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return ChargeCalculationType.fromInt(this.chargeCalculation);
}
public BigDecimal getPercentage() {
return this.percentage;
}
public void updateAmount(final BigDecimal amount) {
this.amount = amount;
calculateOutstanding();
@@ -851,7 +826,7 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
public BigDecimal amountOrPercentage() {
return this.amountOrPercentage;
return this.amountOrPercentage; // TODO delete duplicated method
}
public BigDecimal chargeAmount() {
@@ -906,12 +881,8 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
public LoanOverdueInstallmentCharge getOverdueInstallmentCharge() {
return this.overdueInstallmentCharge;
}
public LoanTrancheDisbursementCharge getTrancheDisbursementCharge() {
return this.loanTrancheDisbursementCharge;
return this.loanTrancheDisbursementCharge; // TODO delete duplicated method
}
public Money undoPaidOrPartiallyAmountBy(final Money incrementBy, final Integer installmentNumber, final Money feeAmount) {
@@ -960,14 +931,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return paidChargePerInstallment;
}
public Set<LoanChargePaidBy> getLoanChargePaidBySet() {
return this.loanChargePaidBySet;
}
public Loan getLoan() {
return this.loan;
}
public boolean isDisbursementCharge() {
return ChargeTimeType.fromInt(this.chargeTime).equals(ChargeTimeType.DISBURSEMENT);
}
@@ -988,10 +951,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.waived = false;
}
public ExternalId getExternalId() {
return externalId;
}
public ChargeTimeType getChargeTimeType() {
return ChargeTimeType.fromInt(this.chargeTime);
}
@@ -1013,6 +972,11 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return dueDate;
}
@NotNull
public List<LoanChargePaidBy> getLoanChargePaidBy(@NotNull Predicate<LoanChargePaidBy> filter) {
return getLoanChargePaidBySet().stream().filter(filter).toList();
}
public LoanChargeData toData() {
EnumOptionData chargeTimeTypeData = new EnumOptionData((long) getChargeTimeType().ordinal(), getChargeTimeType().getCode(),
String.valueOf(getChargeTimeType().getValue()));
@@ -1020,8 +984,8 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
getChargeCalculation().getCode(), String.valueOf(getChargeCalculation().getValue()));
EnumOptionData chargePaymentModeData = new EnumOptionData((long) getChargePaymentMode().ordinal(), getChargePaymentMode().getCode(),
String.valueOf(getChargePaymentMode().getValue()));
Set<LoanInstallmentChargeData> loanInstallmentChargeDataSet = installmentCharges().stream().map(LoanInstallmentCharge::toData)
.collect(Collectors.toSet());
List<LoanInstallmentChargeData> loanInstallmentChargeDataList = installmentCharges().stream().map(LoanInstallmentCharge::toData)
.toList();
return LoanChargeData.builder().id(getId()).chargeId(getCharge().getId()).name(getCharge().getName())
.currency(getCharge().toData().getCurrency()).amount(amount).amountPaid(amountPaid).amountWaived(amountWaived)
@@ -1029,6 +993,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
.submittedOnDate(submittedOnDate).dueDate(dueDate).chargeCalculationType(chargeCalculationTypeData).percentage(percentage)
.amountPercentageAppliedTo(amountPercentageAppliedTo).amountOrPercentage(amountOrPercentage).penalty(penaltyCharge)
.chargePaymentMode(chargePaymentModeData).paid(paid).waived(waived).loanId(loan.getId()).minCap(minCap).maxCap(maxCap)
.installmentChargeData(loanInstallmentChargeDataSet).externalId(externalId).build();
.installmentChargeData(loanInstallmentChargeDataList).externalId(externalId).build();
}
}
@@ -188,7 +188,7 @@ public class LoanInterestRecalculationDetails extends AbstractPersistableCustom<
}
public boolean isCompoundingToBePostedAsTransaction() {
return null == this.isCompoundingToBePostedAsTransaction ? false : this.isCompoundingToBePostedAsTransaction;
return this.isCompoundingToBePostedAsTransaction != null && this.isCompoundingToBePostedAsTransaction;
}
public boolean allowCompoundingOnEod() {
@@ -34,6 +34,7 @@ import java.util.Set;
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;
@@ -692,29 +693,32 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
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) {
@@ -253,6 +253,15 @@ public class LoanRepaymentScheduleProcessingWrapper {
: DateUtils.isDateInRangeFromExclusiveToInclusive(targetDate, fromDate, toDate);
}
public static boolean isBeforePeriod(LocalDate targetDate, LoanRepaymentScheduleInstallment installment, boolean isFirstPeriod) {
LocalDate fromDate = installment.getFromDate();
return isFirstPeriod ? DateUtils.isBefore(targetDate, fromDate) : !DateUtils.isAfter(targetDate, fromDate);
}
public static boolean isAfterPeriod(LocalDate targetDate, LoanRepaymentScheduleInstallment installment) {
return DateUtils.isAfter(targetDate, installment.getDueDate());
}
public static Optional<LoanRepaymentScheduleInstallment> findInPeriod(LocalDate targetDate,
List<LoanRepaymentScheduleInstallment> installments) {
int firstNumber = fetchFirstNormalInstallmentNumber(installments);
@@ -101,6 +101,18 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
String FIND_ALL_LOAN_IDS_BY_STATUS_ID = "SELECT loan.id FROM Loan loan WHERE loan.loanStatus = :statusId";
String LOANS_FOR_ACCRUAL = "select l from Loan l left join l.loanInterestRecalculationDetails recalcDetails "
+ "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 ((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)) ";
String FIND_LOANS_FOR_PERIODIC_ACCRUAL = LOANS_FOR_ACCRUAL
+ "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))))";
String FIND_LOANS_FOR_ADD_ACCRUAL = LOANS_FOR_ACCRUAL + "and (:futureCharges = true or ls.dueDate <= :tillDate)))";
@Query(FIND_GROUP_LOANS_DISBURSED_AFTER)
List<Loan> getGroupLoansDisbursedAfter(@Param("disbursementDate") LocalDate disbursementDate, @Param("groupId") Long groupId,
@Param("loanType") Integer loanType);
@@ -227,4 +239,12 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
@Query(FIND_ALL_LOAN_IDS_BY_STATUS_ID)
List<Long> findLoanIdByStatusId(@Param("statusId") Integer statusId);
@Query(FIND_LOANS_FOR_PERIODIC_ACCRUAL)
List<Loan> findLoansForPeriodicAccrual(@Param("accountingType") Integer accountingType, @Param("tillDate") LocalDate tillDate,
@Param("futureCharges") boolean futureCharges);
@Query(FIND_LOANS_FOR_ADD_ACCRUAL)
List<Loan> findLoansForAddAccrual(@Param("accountingType") Integer accountingType, @Param("tillDate") LocalDate tillDate,
@Param("futureCharges") boolean futureCharges);
}
@@ -263,4 +263,12 @@ public class LoanRepositoryWrapper {
return repository.findLoanIdByStatusId(statusId);
}
public List<Loan> findLoansForPeriodicAccrual(Integer accountingType, LocalDate tillDate, boolean futureCharges) {
return repository.findLoansForPeriodicAccrual(accountingType, tillDate, futureCharges);
}
public List<Loan> findLoansForAddAccrual(Integer accountingType, LocalDate tillDate, boolean futureCharges) {
return repository.findLoansForAddAccrual(accountingType, tillDate, futureCharges);
}
}
@@ -252,26 +252,29 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
public static LoanTransaction accrueInterest(final Office office, final Loan loan, final Money amount,
final LocalDate interestAppliedDate, final ExternalId externalId) {
BigDecimal principalPortion = null;
BigDecimal feesPortion = null;
BigDecimal penaltiesPortion = null;
BigDecimal interestPortion = amount.getAmount();
BigDecimal overPaymentPortion = null;
boolean reversed = false;
PaymentDetail paymentDetail = null;
return new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL.getValue(), interestAppliedDate, interestPortion,
principalPortion, interestPortion, feesPortion, penaltiesPortion, overPaymentPortion, reversed, paymentDetail, externalId);
return accrueTransaction(loan, office, interestAppliedDate, interestPortion, interestPortion, null, null, externalId);
}
public static LoanTransaction accrueLoanCharge(final Loan loan, final Office office, final Money amount, final LocalDate applyDate,
final Money feeCharges, final Money penaltyCharges, final ExternalId externalId) {
final LoanTransaction applyCharge = new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL, amount.getAmount(), applyDate,
externalId);
applyCharge.updateChargesComponents(feeCharges, penaltyCharges);
return applyCharge;
}
public static LoanTransaction accrueTransaction(final Loan loan, final Office office, final LocalDate dateOf, final BigDecimal amount,
final BigDecimal interestPortion, final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion,
final ExternalId externalId) {
BigDecimal principalPortion = null;
BigDecimal overPaymentPortion = null;
boolean reversed = false;
PaymentDetail paymentDetail = null;
return new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL.getValue(), dateOf, amount, principalPortion, interestPortion,
feeChargesPortion, penaltyChargesPortion, overPaymentPortion, reversed, paymentDetail, externalId);
return new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL.getValue(), dateOf, amount, null, interestPortion,
feeChargesPortion, penaltyChargesPortion, null, false, null, externalId);
}
public static LoanTransaction accrualAdjustment(final Loan loan, final Office office, final LocalDate dateOf, final BigDecimal amount,
final BigDecimal interestPortion, final BigDecimal feePortion, final BigDecimal penaltyPortion, final ExternalId externalId) {
return new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL_ADJUSTMENT.getValue(), dateOf, amount, null, interestPortion,
feePortion, penaltyPortion, null, false, null, externalId);
}
public static LoanTransaction initiateTransfer(final Office office, final Loan loan, final LocalDate transferDate,
@@ -325,14 +328,6 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
return newTransaction;
}
public static LoanTransaction accrueLoanCharge(final Loan loan, final Office office, final Money amount, final LocalDate applyDate,
final Money feeCharges, final Money penaltyCharges, final ExternalId externalId) {
final LoanTransaction applyCharge = new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL, amount.getAmount(), applyDate,
externalId);
applyCharge.updateChargesComponents(feeCharges, penaltyCharges);
return applyCharge;
}
public static LoanTransaction creditBalanceRefund(final Loan loan, final Office office, final Money amount, final LocalDate paymentDate,
final ExternalId externalId, PaymentDetail paymentDetail) {
return new LoanTransaction(loan, office, LoanTransactionType.CREDIT_BALANCE_REFUND.getValue(), paymentDate, amount.getAmount(),
@@ -492,47 +487,41 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
* penaltyCharges
*/
public void updateComponents(final Money principal, final Money interest, final Money feeCharges, final Money penaltyCharges) {
final MonetaryCurrency currency = principal.getCurrency();
this.principalPortion = defaultToNullIfZero(getPrincipalPortion(currency).plus(principal).getAmount());
this.interestPortion = defaultToNullIfZero(getInterestPortion(currency).plus(interest).getAmount());
this.principalPortion = MathUtil.zeroToNull(MathUtil.add(getPrincipalPortion(), MathUtil.toBigDecimal(principal)));
this.interestPortion = MathUtil.zeroToNull(MathUtil.add(getInterestPortion(), MathUtil.toBigDecimal(interest)));
updateChargesComponents(feeCharges, penaltyCharges);
}
public void updateChargesComponents(final Money feeCharges, final Money penaltyCharges) {
final MonetaryCurrency currency = feeCharges.getCurrency();
this.feeChargesPortion = defaultToNullIfZero(getFeeChargesPortion(currency).plus(feeCharges).getAmount());
this.penaltyChargesPortion = defaultToNullIfZero(getPenaltyChargesPortion(currency).plus(penaltyCharges).getAmount());
this.feeChargesPortion = MathUtil.zeroToNull(MathUtil.add(getFeeChargesPortion(), MathUtil.toBigDecimal(feeCharges)));
this.penaltyChargesPortion = MathUtil.zeroToNull(MathUtil.add(getPenaltyChargesPortion(), MathUtil.toBigDecimal(penaltyCharges)));
}
private void updateChargesComponents(final Money feeCharges, final Money penaltyCharges, final Money unrecognizedCharges) {
final MonetaryCurrency currency = feeCharges.getCurrency();
this.feeChargesPortion = defaultToNullIfZero(getFeeChargesPortion(currency).plus(feeCharges).getAmount());
this.penaltyChargesPortion = defaultToNullIfZero(getPenaltyChargesPortion(currency).plus(penaltyCharges).getAmount());
this.unrecognizedIncomePortion = defaultToNullIfZero(getUnrecognizedIncomePortion(currency).plus(unrecognizedCharges).getAmount());
this.feeChargesPortion = MathUtil.zeroToNull(MathUtil.add(getFeeChargesPortion(), MathUtil.toBigDecimal(feeCharges)));
this.penaltyChargesPortion = MathUtil.zeroToNull(MathUtil.add(getPenaltyChargesPortion(), MathUtil.toBigDecimal(penaltyCharges)));
this.unrecognizedIncomePortion = MathUtil
.zeroToNull(MathUtil.add(getUnrecognizedIncomePortion(), MathUtil.toBigDecimal(unrecognizedCharges)));
}
private void updateInterestComponent(final Money interest, final Money unrecognizedInterest) {
final MonetaryCurrency currency = interest.getCurrency();
this.interestPortion = defaultToNullIfZero(getInterestPortion(currency).plus(interest).getAmount());
this.unrecognizedIncomePortion = defaultToNullIfZero(getUnrecognizedIncomePortion(currency).plus(unrecognizedInterest).getAmount());
this.interestPortion = MathUtil.zeroToNull(MathUtil.add(getInterestPortion(), MathUtil.toBigDecimal(interest)));
this.unrecognizedIncomePortion = MathUtil
.zeroToNull(MathUtil.add(getUnrecognizedIncomePortion(), MathUtil.toBigDecimal(unrecognizedInterest)));
}
public void adjustInterestComponent(final MonetaryCurrency currency) {
this.interestPortion = defaultToNullIfZero(getInterestPortion(currency).minus(getUnrecognizedIncomePortion(currency)).getAmount());
public void adjustInterestComponent() {
this.interestPortion = MathUtil.zeroToNull(MathUtil.subtract(getInterestPortion(), getUnrecognizedIncomePortion()));
}
public void updateComponentsAndTotal(final Money principal, final Money interest, final Money feeCharges, final Money penaltyCharges) {
updateComponents(principal, interest, feeCharges, penaltyCharges);
final MonetaryCurrency currency = principal.getCurrency();
this.amount = getPrincipalPortion(currency).plus(getInterestPortion(currency)).plus(getFeeChargesPortion(currency))
.plus(getPenaltyChargesPortion(currency)).getAmount();
this.amount = MathUtil
.nullToZero(MathUtil.add(getPrincipalPortion(), getInterestPortion(), getFeeChargesPortion(), getPenaltyChargesPortion()));
}
public void setOverPayments(final Money overPayment) {
if (overPayment != null) {
this.overPaymentPortion = defaultToNullIfZero(overPayment.getAmount());
}
this.overPaymentPortion = MathUtil.zeroToNull(MathUtil.toBigDecimal(overPayment));
}
public Money getPrincipalPortion(final MonetaryCurrency currency) {
@@ -720,48 +709,20 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
return getTypeOf().isReAmortize() && isNotReversed();
}
public boolean isAccrualActivity() {
return getTypeOf().isAccrualActivity();
}
public boolean isIdentifiedBy(final Long identifier) {
return getId().equals(identifier);
}
public boolean isBelongingToLoanOf(final Loan check) {
return this.loan.getId().equals(check.getId());
}
public boolean isNotBelongingToLoanOf(final Loan check) {
return !isBelongingToLoanOf(check);
}
public boolean isNonZero() {
return this.amount.subtract(BigDecimal.ZERO).doubleValue() > 0;
}
public boolean isGreaterThan(final Money monetaryAmount) {
return getAmount(monetaryAmount.getCurrency()).isGreaterThan(monetaryAmount);
return MathUtil.isGreaterThan(amount, MathUtil.toBigDecimal(monetaryAmount));
}
public boolean isGreaterThanZero(final MonetaryCurrency currency) {
return getAmount(currency).isGreaterThanZero();
public boolean isGreaterThanZero() {
return MathUtil.isGreaterThanZero(amount);
}
public boolean isGreaterThanZeroAndLessThanOrEqualTo(BigDecimal totalOverpaid) {
return isNonZero() && this.amount.compareTo(totalOverpaid) <= 0;
return isGreaterThanZero() && !MathUtil.isGreaterThan(amount, totalOverpaid);
}
public boolean isNotZero(final MonetaryCurrency currency) {
return !getAmount(currency).isZero();
}
private BigDecimal defaultToNullIfZero(final BigDecimal value) {
BigDecimal result = value;
if (BigDecimal.ZERO.compareTo(value) == 0) {
result = null;
}
return result;
public boolean isNotZero() {
return !MathUtil.isEmpty(amount);
}
public LoanTransactionData toData(final CurrencyData currencyData, final AccountTransferData transfer) {
@@ -785,7 +746,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
thisTransactionData.put("id", getId());
thisTransactionData.put("officeId", this.office.getId());
thisTransactionData.put("type", transactionType);
thisTransactionData.put("reversed", Boolean.valueOf(isReversed()));
thisTransactionData.put("reversed", isReversed());
thisTransactionData.put("date", getTransactionDate());
thisTransactionData.put("currencyCode", currencyCode);
thisTransactionData.put("amount", this.amount);
@@ -871,16 +832,37 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
}
public boolean isAccrual() {
return LoanTransactionType.ACCRUAL.equals(getTypeOf()) && isNotReversed();
return getTypeOf().isAccrual();
}
public boolean isAccrualAdjustment() {
return getTypeOf().isAccrualAdjustment();
}
public boolean isAccrualActivity() {
return getTypeOf().isAccrualActivity();
}
public boolean isAccrualRelated() {
return isAccrual() || isAccrualAdjustment() || isAccrualActivity();
}
public boolean isWaiveCharge() {
return getTypeOf().isWaiveCharges();
}
public boolean isWaiveInterest() {
return getTypeOf().isWaiveInterest();
}
public boolean isNonMonetaryTransaction() {
return isNotReversed() && (LoanTransactionType.CONTRA.equals(getTypeOf())
|| LoanTransactionType.MARKED_FOR_RESCHEDULING.equals(getTypeOf()) || LoanTransactionType.ACCRUAL.equals(getTypeOf())
|| LoanTransactionType.ACCRUAL_ACTIVITY.equals(getTypeOf()) || LoanTransactionType.APPROVE_TRANSFER.equals(getTypeOf())
|| LoanTransactionType.INITIATE_TRANSFER.equals(getTypeOf()) || LoanTransactionType.REJECT_TRANSFER.equals(getTypeOf())
|| LoanTransactionType.WITHDRAW_TRANSFER.equals(getTypeOf()) || LoanTransactionType.CHARGE_OFF.equals(getTypeOf())
|| LoanTransactionType.REAMORTIZE.equals(getTypeOf()) || LoanTransactionType.REAGE.equals(getTypeOf()));
LoanTransactionType type = getTypeOf();
return isNotReversed() && (type == LoanTransactionType.CONTRA || type == LoanTransactionType.MARKED_FOR_RESCHEDULING
|| type == LoanTransactionType.ACCRUAL || type == LoanTransactionType.ACCRUAL_ADJUSTMENT
|| type == LoanTransactionType.ACCRUAL_ACTIVITY || type == LoanTransactionType.APPROVE_TRANSFER
|| type == LoanTransactionType.INITIATE_TRANSFER || type == LoanTransactionType.REJECT_TRANSFER
|| type == LoanTransactionType.WITHDRAW_TRANSFER || type == LoanTransactionType.CHARGE_OFF
|| type == LoanTransactionType.REAMORTIZE || type == LoanTransactionType.REAGE);
}
public void updateOutstandingLoanBalance(BigDecimal outstandingLoanBalance) {
@@ -985,11 +967,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
}
public Boolean isAllowTypeTransactionAtTheTimeOfLastUndo() {
return isDisbursement() || isAccrual() || isRepaymentAtDisbursement() || isRepayment() || isAccrualActivity();
}
public boolean isAccrualTransaction() {
return isAccrual();
return isNotReversed() && (isDisbursement() || isAccrualActivity() || isRepaymentAtDisbursement() || isRepayment());
}
public Money getOutstandingLoanBalanceMoney(final MonetaryCurrency currency) {
@@ -24,12 +24,12 @@ import jakarta.persistence.Entity;
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;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
@Entity
@Table(name = "m_loan_transaction_repayment_schedule_mapping")
@@ -86,21 +86,13 @@ public class LoanTransactionToRepaymentScheduleMapping extends AbstractPersistab
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,
@@ -112,8 +104,7 @@ public class LoanTransactionToRepaymentScheduleMapping extends AbstractPersistab
}
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,
@@ -146,6 +137,15 @@ public class LoanTransactionToRepaymentScheduleMapping extends AbstractPersistab
return Money.of(currency, this.penaltyChargesPortion);
}
public BigDecimal getPortion(AllocationType allocationType) {
return switch (allocationType) {
case PRINCIPAL -> getPrincipalPortion();
case INTEREST -> getInterestPortion();
case FEE -> getFeeChargesPortion();
case PENALTY -> getPenaltyChargesPortion();
};
}
public LoanTransaction getLoanTransaction() {
return loanTransaction;
}
@@ -66,6 +66,7 @@ public enum LoanTransactionType {
INTEREST_PAYMENT_WAIVER(31, "loanTransactionType.interestPaymentWaiver"), //
ACCRUAL_ACTIVITY(32, "loanTransactionType.accrualActivity"), //
INTEREST_REFUND(33, "loanTransactionType.interestRefund"), //
ACCRUAL_ADJUSTMENT(34, "loanTransactionType.accrualAdjustment"), //
;
private final Integer value;
@@ -115,6 +116,7 @@ public enum LoanTransactionType {
case 31 -> LoanTransactionType.INTEREST_PAYMENT_WAIVER;
case 32 -> LoanTransactionType.ACCRUAL_ACTIVITY;
case 33 -> LoanTransactionType.INTEREST_REFUND;
case 34 -> LoanTransactionType.ACCRUAL_ADJUSTMENT;
default -> LoanTransactionType.INVALID;
};
}
@@ -228,4 +230,7 @@ public enum LoanTransactionType {
return this.equals(LoanTransactionType.INTEREST_REFUND);
}
public boolean isAccrualAdjustment() {
return this == LoanTransactionType.ACCRUAL_ADJUSTMENT;
}
}
@@ -45,14 +45,14 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapper {
totalInterest = totalInterest.plus(installment.getInterestCharged(currency));
totalPrincipal = totalPrincipal.plus(installment.getPrincipal(currency));
}
LoanChargePaidBy accrualBy = null;
List<LoanChargePaidBy> accruals = null;
if (loanCharge.isSpecifiedDueDate()) {
LoanRepaymentScheduleInstallment addedPeriod = addChargeOnlyRepaymentInstallmentIfRequired(loanCharge, installments);
if (addedPeriod != null) {
addedPeriod.updateObligationsMet(currency, disbursementDate);
}
accrualBy = loanCharge.getLoanChargePaidBySet().stream().filter(e -> e.getLoanTransaction().isAccrual()).findFirst()
.orElse(null);
accruals = loanCharge.getLoanChargePaidBySet().stream().filter(e -> !e.getLoanTransaction().isReversed()
&& (e.getLoanTransaction().isAccrual() || e.getLoanTransaction().isAccrualAdjustment())).toList();
}
LocalDate startDate = disbursementDate;
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
@@ -61,7 +61,8 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapper {
continue;
}
boolean installmentChargeApplicable = !installment.isRecalculatedInterestComponent();
boolean isFirstPeriod = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber);
Integer installmentNumber = installment.getInstallmentNumber();
boolean isFirstPeriod = installmentNumber.equals(firstNormalInstallmentNumber);
Predicate<LoanCharge> feePredicate = e -> e.isFeeCharge() && !e.isDueAtDisbursement();
LocalDate dueDate = installment.getDueDate();
final Money feeChargesDue = calcChargeDue(startDate, dueDate, loanCharge, currency, installment, totalPrincipal, totalInterest,
@@ -82,13 +83,19 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapper {
installment.addToChargePortion(feeChargesDue, feeChargesWaived, feeChargesWrittenOff, penaltyChargesDue, penaltyChargesWaived,
penaltyChargesWrittenOff);
if (accrualBy != null && installment.isAdditional() && loanCharge.isDueInPeriod(startDate, dueDate, isFirstPeriod)) {
Money amount = Money.of(currency, accrualBy.getAmount());
if (accruals != null && !accruals.isEmpty() && installment.isAdditional()
&& loanCharge.isDueInPeriod(startDate, dueDate, isFirstPeriod)) {
BigDecimal amount = null;
for (LoanChargePaidBy accrual : accruals) {
accrual.setInstallmentNumber(installmentNumber);
amount = accrual.getLoanTransaction().isAccrual() ? MathUtil.add(amount, accrual.getAmount())
: MathUtil.subtract(amount, accrual.getAmount());
}
Money accruedAmount = Money.of(currency, MathUtil.negativeToZero(amount));
boolean isFee = loanCharge.isFeeCharge();
installment.updateAccrualPortion(installment.getInterestAccrued(currency),
MathUtil.plus(installment.getFeeAccrued(currency), (isFee ? amount : null)),
MathUtil.plus(installment.getPenaltyAccrued(currency), (isFee ? null : amount)));
accrualBy.setInstallmentNumber(installment.getInstallmentNumber());
MathUtil.plus(installment.getFeeAccrued(currency), (isFee ? accruedAmount : null)),
MathUtil.plus(installment.getPenaltyAccrued(currency), (isFee ? null : accruedAmount)));
}
startDate = dueDate;
}
@@ -166,7 +166,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
// pass through for new transactions
if (loanTransaction.getId() == null) {
processLatestTransaction(loanTransaction, new TransactionCtx(currency, installments, charges, overpaymentHolder, null));
loanTransaction.adjustInterestComponent(currency);
loanTransaction.adjustInterestComponent();
} else {
/**
* For existing transactions, check if the re-payment breakup (principal, interest, fees, penalties)
@@ -178,7 +178,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
// re-process transaction
processLatestTransaction(newLoanTransaction,
new TransactionCtx(currency, installments, charges, overpaymentHolder, null));
newLoanTransaction.adjustInterestComponent(currency);
newLoanTransaction.adjustInterestComponent();
/**
* Check if the transaction amounts have changed. If so, reverse the original transaction and update
* changedTransactionDetail accordingly
@@ -18,6 +18,10 @@
*/
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isAfterPeriod;
import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isBeforePeriod;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
@@ -2805,4 +2809,39 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
.feeCharges(feeCharges) //
.penaltyCharges(penaltyCharges);
}
@Override
public Money getPeriodInterestTillDate(@NotNull LoanRepaymentScheduleInstallment installment, @NotNull LocalDate targetDate) {
if (installment.isAdditional() || installment.isDownPayment() || installment.isReAged()) {
return null;
}
if (isBeforePeriod(targetDate, installment, false)) {
return null;
}
Loan loan = installment.getLoan();
MonetaryCurrency currency = loan.getLoanProductRelatedDetail().getCurrency();
BigDecimal interest = installment.getInterestCharged();
Money zero = Money.zero(currency);
if (MathUtil.isEmpty(interest)) {
return zero;
}
if (isAfterPeriod(targetDate, installment) || DateUtils.isEqual(targetDate, installment.getDueDate())) {
return installment.getInterestCharged(currency);
}
BigDecimal interestPortion;
LocalDate startDate = installment.getFromDate();
LocalDate dueDate = installment.getDueDate();
if (DateUtils.isBefore(startDate, loan.getInterestChargedFromDate())) {
startDate = loan.getInterestChargedFromDate();
}
if (!DateUtils.isBefore(startDate, dueDate) || !DateUtils.isBefore(startDate, targetDate)) {
return zero;
}
int totalNumberOfDays = DateUtils.getExactDifferenceInDays(startDate, dueDate);
int daysToBeAccrued = DateUtils.getExactDifferenceInDays(startDate, targetDate);
double interestPerDay = interest.doubleValue() / totalNumberOfDays;
interestPortion = BigDecimal.valueOf(interestPerDay * daysToBeAccrued);
return Money.of(currency, interestPortion);
}
}
@@ -18,14 +18,17 @@
*/
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
import jakarta.validation.constraints.NotNull;
import java.math.MathContext;
import java.time.LocalDate;
import java.util.Set;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
@@ -42,4 +45,5 @@ public interface LoanScheduleGenerator {
MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO,
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor);
Money getPeriodInterestTillDate(@NotNull LoanRepaymentScheduleInstallment installment, @NotNull LocalDate targetDate);
}
@@ -18,30 +18,33 @@
*/
package org.apache.fineract.portfolio.loanaccount.service;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
import org.apache.fineract.infrastructure.core.exception.MultiException;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
public interface LoanAccrualsProcessingService {
void addPeriodicAccruals(LocalDate tilldate) throws MultiException;
void addPeriodicAccruals(@NotNull LocalDate tilldate) throws MultiException;
void addPeriodicAccruals(LocalDate tilldate, Loan loan) throws MultiException;
void addPeriodicAccruals(@NotNull LocalDate tilldate, @NotNull Loan loan) throws MultiException;
void addAccrualAccounting(Long loanId, Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas) throws Exception;
void addAccruals(@NotNull LocalDate tilldate) throws MultiException;
void reprocessExistingAccruals(@NotNull Loan loan);
void processAccrualsForInterestRecalculation(@NotNull Loan loan, boolean isInterestRecalculationEnabled);
void processIncomePostingAndAccruals(@NotNull Loan loan);
void addIncomeAndAccrualTransactions(Long loanId) throws Exception;
void reprocessExistingAccruals(Loan loan);
void processAccrualsOnLoanClosure(@NotNull Loan loan);
void processAccrualsForInterestRecalculation(Loan loan, boolean isInterestRecalculationEnabled);
void processAccrualsOnLoanReopen(@NotNull Loan loan);
void processIncomePostingAndAccruals(Loan loan);
void processAccrualsForLoanClosure(Loan loan);
void processAccrualsForLoanForeClosure(Loan loan, LocalDate foreClosureDate, Collection<LoanTransaction> newAccrualTransactions);
void processAccrualsOnLoanForeClosure(@NotNull Loan loan, @NotNull LocalDate foreClosureDate,
@NotNull List<LoanTransaction> newAccrualTransactions);
}
@@ -0,0 +1,50 @@
/**
* 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.util.Map;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.organisation.staff.domain.Staff;
import org.apache.fineract.portfolio.fund.domain.Fund;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.useradministration.domain.AppUser;
public interface LoanAssembler {
Loan assembleFrom(Long accountId);
Loan assembleFrom(JsonCommand command);
void setHelpers(Loan loanAccount);
void accountNumberGeneration(JsonCommand command, Loan loan);
CodeValue findCodeValueByIdIfProvided(Long codeValueId);
Fund findFundByIdIfProvided(Long fundId);
Staff findLoanOfficerByIdIfProvided(Long loanOfficerId);
Map<String, Object> updateFrom(JsonCommand command, Loan loan);
Map<String, Object> updateLoanApplicationAttributesForWithdrawal(Loan loan, JsonCommand command, AppUser currentUser);
Map<String, Object> updateLoanApplicationAttributesForRejection(Loan loan, JsonCommand command, AppUser currentUser);
}
@@ -19,6 +19,7 @@
package org.apache.fineract.portfolio.loanaccount.service;
import java.util.Collection;
import java.util.List;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.portfolio.charge.data.ChargeData;
import org.apache.fineract.portfolio.charge.domain.Charge;
@@ -38,12 +39,10 @@ public interface LoanChargeReadPlatformService {
Collection<LoanChargeData> retrieveLoanChargesForFeePayment(Integer paymentMode, Integer loanStatus);
Collection<LoanInstallmentChargeData> retrieveInstallmentLoanCharges(Long loanChargeId, boolean onlyPaymentPendingCharges);
List<LoanInstallmentChargeData> retrieveInstallmentLoanCharges(Long loanChargeId, boolean onlyPaymentPendingCharges);
Collection<Integer> retrieveOverdueInstallmentChargeFrequencyNumber(Loan loan, Charge charge, Integer periodNumber);
Collection<LoanChargeData> retrieveLoanChargesForAccrual(Long loanId);
Collection<LoanChargePaidByData> retrieveLoanChargesPaidBy(Long chargeId, LoanTransactionType transactionType,
Integer installmentNumber);
@@ -323,6 +323,8 @@ public final class LoanEnumerations {
LoanTransactionType.ACCRUAL_ACTIVITY.getCode(), "Accrual Activity");
case INTEREST_REFUND -> new LoanTransactionEnumData(LoanTransactionType.INTEREST_REFUND.getValue().longValue(),
LoanTransactionType.INTEREST_REFUND.getCode(), "Interest Refund");
case ACCRUAL_ADJUSTMENT -> new LoanTransactionEnumData(LoanTransactionType.ACCRUAL_ADJUSTMENT.getValue().longValue(),
LoanTransactionType.ACCRUAL_ADJUSTMENT.getCode(), "Accrual Adjustment");
};
}
@@ -46,4 +46,5 @@
<include relativeToChangelogFile="true" file="parts/1021_add_loan_status_change_history.xml"/>
<include relativeToChangelogFile="true" file="parts/1022_add_interest_refund_support.xml"/>
<include relativeToChangelogFile="true" file="parts/1023_add_charge_off_behaviour.xml"/>
<include relativeToChangelogFile="true" file="parts/1024_accrual_adjustment_transaction_type.xml"/>
</databaseChangeLog>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
<changeSet author="fineract" id="1">
<insert tableName="m_external_event_configuration">
<column name="type" value="LoanAccrualAdjustmentTransactionBusinessEvent"/>
<column name="enabled" valueBoolean="false"/>
</insert>
</changeSet>
<changeSet author="fineract" id="2">
<insert tableName="r_enum_value">
<column name="enum_name" value="transaction_type_enum"/>
<column name="enum_id" valueNumeric="34"/>
<column name="enum_message_property" value="Accrual Adjustment"/>
<column name="enum_value" value="Accrual Adjustment"/>
<column name="enum_type" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>