FINERACT-2326: Add additional field which would return available disbursement amount with over applied amount

This commit is contained in:
mariiaKraievska
2025-08-28 11:09:45 +02:00
committed by Adam Saghy
parent 8bd9444689
commit 2cba4430c7
16 changed files with 250 additions and 34 deletions
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.delinquency.service;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData;
@@ -26,6 +27,8 @@ import org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryD
import org.apache.fineract.portfolio.delinquency.data.LoanInstallmentDelinquencyTagData;
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction;
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.springframework.lang.NonNull;
public interface DelinquencyReadPlatformService {
@@ -43,6 +46,8 @@ public interface DelinquencyReadPlatformService {
CollectionData calculateLoanCollectionData(Long loanId);
BigDecimal calculateAvailableDisbursementAmountWithOverApplied(@NonNull Loan loan);
Collection<LoanInstallmentDelinquencyTagData> retrieveLoanInstallmentsCurrentDelinquencyTag(Long loanId);
List<LoanDelinquencyAction> retrieveLoanDelinquencyActions(Long loanId);
@@ -34,6 +34,7 @@ import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDoma
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData;
import org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryData;
@@ -56,11 +57,14 @@ import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
import org.apache.fineract.portfolio.loanaccount.data.DelinquencyPausePeriod;
import org.apache.fineract.portfolio.loanaccount.data.InstallmentLevelDelinquency;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException;
import org.springframework.lang.NonNull;
import org.springframework.transaction.annotation.Transactional;
@@ -138,7 +142,12 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
// If the Loan is not Active yet or is cancelled (rejected or withdrawn), return template data
if (loan.isSubmittedAndPendingApproval() || loan.isApproved() || loan.isCancelled()) {
return CollectionData.template();
if (loan.getLoanProduct() != null && !loan.getLoanProduct().isAllowApprovedDisbursedAmountsOverApplied()) {
return collectionData;
} else {
collectionData.setAvailableDisbursementAmountWithOverApplied(calculateAvailableDisbursementAmountWithOverApplied(loan));
return collectionData;
}
}
final List<LoanDelinquencyAction> savedDelinquencyList = retrieveLoanDelinquencyActions(loanId);
@@ -152,6 +161,7 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
// loans
collectionData = loanDelinquencyDomainService.getOverdueCollectionData(loan, effectiveDelinquencyList);
collectionData.setAvailableDisbursementAmount(calculateAvailableDisbursementAmount(loan));
collectionData.setAvailableDisbursementAmountWithOverApplied(calculateAvailableDisbursementAmountWithOverApplied(loan));
collectionData.setNextPaymentDueDate(possibleNextRepaymentDate(nextPaymentDueDateConfig, loan));
PossibleNextRepaymentCalculationService possibleNextRepaymentCalculationService = possibleNextRepaymentCalculationServiceDiscovery
.getService(loan);
@@ -182,6 +192,54 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
return collectionData;
}
@Override
public BigDecimal calculateAvailableDisbursementAmountWithOverApplied(@NonNull final Loan loan) {
final LoanProduct loanProduct = loan.getLoanProduct();
// Start with approved principal
BigDecimal approvedWithOverApplied = loan.getApprovedPrincipal();
// If over applied amount is enabled, calculate the maximum allowed amount
if (loanProduct.isAllowApprovedDisbursedAmountsOverApplied()) {
if (loanProduct.getOverAppliedCalculationType() != null) {
if ("percentage".equalsIgnoreCase(loanProduct.getOverAppliedCalculationType())) {
final BigDecimal overAppliedNumber = BigDecimal.valueOf(loanProduct.getOverAppliedNumber());
final BigDecimal totalPercentage = BigDecimal.valueOf(1)
.add(overAppliedNumber.divide(BigDecimal.valueOf(100), MoneyHelper.getMathContext()));
approvedWithOverApplied = loan.getProposedPrincipal().multiply(totalPercentage);
} else {
approvedWithOverApplied = loan.getProposedPrincipal().add(BigDecimal.valueOf(loanProduct.getOverAppliedNumber()));
}
} else {
throw new LoanProductGeneralRuleException("overAppliedCalculationType.must.be.percentage.or.flat",
"Over Applied Calculation Type Must Be 'percentage' or 'flat'");
}
}
// Calculate available amount: (approved + over applied) - expected tranches - disbursed - capitalized income
if (loan.isMultiDisburmentLoan() && loan.getDisbursementDetails() != null) {
final BigDecimal expectedDisbursementAmount = loan.getDisbursementDetails().stream()
.filter(detail -> detail.actualDisbursementDate() == null).map(LoanDisbursementDetails::principal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
approvedWithOverApplied = approvedWithOverApplied.subtract(expectedDisbursementAmount);
}
BigDecimal availableDisbursementAmount = approvedWithOverApplied.subtract(loan.getDisbursedAmount());
if (loan.getLoanRepaymentScheduleDetail().isEnableIncomeCapitalization()) {
final LoanSummary loanSummary = loan.getSummary();
if (loanSummary != null) {
final BigDecimal totalCapitalizedIncome = MathUtil.nullToZero(loanSummary.getTotalCapitalizedIncome());
final BigDecimal totalCapitalizedIncomeAdjustment = MathUtil.nullToZero(loanSummary.getTotalCapitalizedIncomeAdjustment());
final BigDecimal netCapitalizedIncome = totalCapitalizedIncome.subtract(totalCapitalizedIncomeAdjustment);
availableDisbursementAmount = availableDisbursementAmount.subtract(netCapitalizedIncome);
}
}
// Ensure availableDisbursementAmount is never negative
return availableDisbursementAmount.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : availableDisbursementAmount;
}
private BigDecimal calculateAvailableDisbursementAmount(@NonNull final Loan loan) {
BigDecimal availableDisbursementAmount = loan.getApprovedPrincipal().subtract(loan.getDisbursedAmount());
if (loan.getLoanRepaymentScheduleDetail().isEnableIncomeCapitalization()) {
@@ -29,6 +29,7 @@ import lombok.Data;
public final class CollectionData {
private BigDecimal availableDisbursementAmount;
private BigDecimal availableDisbursementAmountWithOverApplied;
private Long pastDueDays;
private LocalDate nextPaymentDueDate;
private BigDecimal nextPaymentAmount;
@@ -51,7 +52,7 @@ public final class CollectionData {
public static CollectionData template() {
final BigDecimal zero = BigDecimal.ZERO;
return new CollectionData(zero, 0L, null, zero, 0L, null, zero, null, zero, null, zero, null, null, zero, zero, zero, zero);
return new CollectionData(zero, zero, 0L, null, zero, 0L, null, zero, null, zero, null, zero, null, null, zero, zero, zero, zero);
}
}
@@ -32,6 +32,7 @@ public class LoanApprovalData {
private final LocalDate approvalDate;
private final BigDecimal approvalAmount;
private final BigDecimal netDisbursalAmount;
private final BigDecimal availableDisbursementAmountWithOverApplied;
// import fields
private LocalDate approvedOnDate;
@@ -54,14 +55,16 @@ public class LoanApprovalData {
this.approvalAmount = null;
this.approvalDate = null;
this.netDisbursalAmount = null;
this.availableDisbursementAmountWithOverApplied = null;
}
public LoanApprovalData(final BigDecimal approvalAmount, final LocalDate approvalDate, final BigDecimal netDisbursalAmount,
final CurrencyData currency) {
final CurrencyData currency, final BigDecimal availableDisbursementAmountWithOverApplied) {
this.approvalDate = approvalDate;
this.approvalAmount = approvalAmount;
this.netDisbursalAmount = netDisbursalAmount;
this.currency = currency;
this.availableDisbursementAmountWithOverApplied = availableDisbursementAmountWithOverApplied;
}
}
@@ -25,6 +25,7 @@ import java.time.LocalDate;
import java.util.Collection;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
@@ -70,6 +71,7 @@ public class LoanTransactionData implements Serializable {
private final LocalDate submittedOnDate;
private final boolean manuallyReversed;
private final LocalDate possibleNextRepaymentDate;
private final BigDecimal availableDisbursementAmountWithOverApplied;
private Collection<LoanChargePaidByData> loanChargePaidByList;
@@ -78,6 +80,7 @@ public class LoanTransactionData implements Serializable {
private Collection<CodeValueData> writeOffReasonOptions = null;
@Setter
private Integer numberOfRepayments = 0;
// import fields
@@ -94,6 +97,7 @@ public class LoanTransactionData implements Serializable {
private Integer bankNumber;
private transient Long accountId;
private transient String transactionType;
@Setter
private List<LoanRepaymentScheduleInstallmentData> loanRepaymentScheduleInstallments;
// Reverse Data
@@ -144,6 +148,7 @@ public class LoanTransactionData implements Serializable {
this.paymentTypeOptions = null;
this.writeOffReasonOptions = null;
this.reversalExternalId = ExternalId.empty();
this.availableDisbursementAmountWithOverApplied = null;
}
public static LoanTransactionData importInstance(BigDecimal repaymentAmount, LocalDate repaymentDate, Long repaymentTypeId,
@@ -196,14 +201,7 @@ public class LoanTransactionData implements Serializable {
this.paymentTypeOptions = null;
this.writeOffReasonOptions = null;
this.reversalExternalId = ExternalId.empty();
}
public void setNumberOfRepayments(Integer numberOfRepayments) {
this.numberOfRepayments = numberOfRepayments;
}
public void setLoanRepaymentScheduleInstallments(final List<LoanRepaymentScheduleInstallmentData> loanRepaymentScheduleInstallments) {
this.loanRepaymentScheduleInstallments = loanRepaymentScheduleInstallments;
this.availableDisbursementAmountWithOverApplied = null;
}
public static LoanTransactionData templateOnTop(final LoanTransactionData loanTransactionData,
@@ -301,6 +299,7 @@ public class LoanTransactionData implements Serializable {
this.possibleNextRepaymentDate = null;
this.reversalExternalId = reversalExternalId;
this.reversedOnDate = reversedOnDate;
this.availableDisbursementAmountWithOverApplied = null;
}
public LoanTransactionData(Long id, LoanTransactionEnumData transactionType, LocalDate date, BigDecimal totalAmount,
@@ -336,17 +335,20 @@ public class LoanTransactionData implements Serializable {
final LocalDate submittedOnDate = null;
final LocalDate possibleNextRepaymentDate = null;
final boolean manuallyReversed = false;
final BigDecimal availableDisbursementAmountWithOverApplied = null;
return new LoanTransactionData(id, officeId, officeName, transactionType, paymentDetailData, currency, transactionDate,
transactionAmount, netDisbursalAmount, principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion,
overpaymentPortion, unrecognizedIncomePortion, paymentOptions, transfer, externalId, fixedEmiAmount, outstandingLoanBalance,
submittedOnDate, manuallyReversed, possibleNextRepaymentDate, loanId, externalLoanId);
submittedOnDate, manuallyReversed, possibleNextRepaymentDate, loanId, externalLoanId,
availableDisbursementAmountWithOverApplied);
}
public static LoanTransactionData loanTransactionDataForDisbursalTemplate(final LoanTransactionEnumData transactionType,
final LocalDate expectedDisbursedOnLocalDateForTemplate, final BigDecimal disburseAmountForTemplate,
final BigDecimal netDisbursalAmount, final Collection<PaymentTypeData> paymentOptions, final BigDecimal retriveLastEmiAmount,
final LocalDate possibleNextRepaymentDate, final CurrencyData currency) {
final LocalDate possibleNextRepaymentDate, final CurrencyData currency,
final BigDecimal availableDisbursementAmountWithOverApplied) {
final Long id = null;
final Long loanId = null;
final ExternalId externalLoanId = ExternalId.empty();
@@ -368,7 +370,7 @@ public class LoanTransactionData implements Serializable {
expectedDisbursedOnLocalDateForTemplate, disburseAmountForTemplate, netDisbursalAmount, principalPortion, interestPortion,
feeChargesPortion, penaltyChargesPortion, overpaymentPortion, unrecognizedIncomePortion, paymentOptions, transfer,
externalId, retriveLastEmiAmount, outstandingLoanBalance, submittedOnDate, manuallyReversed, possibleNextRepaymentDate,
loanId, externalLoanId);
loanId, externalLoanId, availableDisbursementAmountWithOverApplied);
}
@@ -379,7 +381,7 @@ public class LoanTransactionData implements Serializable {
BigDecimal unrecognizedIncomePortion, Collection<PaymentTypeData> paymentOptions, final AccountTransferData transfer,
final ExternalId externalId, final BigDecimal fixedEmiAmount, BigDecimal outstandingLoanBalance,
final LocalDate submittedOnDate, final boolean manuallyReversed, final LocalDate possibleNextRepaymentDate, Long loanId,
ExternalId externalLoanId) {
ExternalId externalLoanId, final BigDecimal availableDisbursementAmountWithOverApplied) {
this.id = id;
this.loanId = loanId;
this.externalLoanId = externalLoanId;
@@ -406,6 +408,7 @@ public class LoanTransactionData implements Serializable {
this.manuallyReversed = manuallyReversed;
this.possibleNextRepaymentDate = possibleNextRepaymentDate;
this.reversalExternalId = ExternalId.empty();
this.availableDisbursementAmountWithOverApplied = availableDisbursementAmountWithOverApplied;
}
public boolean isNotDisbursement() {
@@ -814,7 +814,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
return principal;
} else {
return getNetDisbursalAmount();
if (this.actualDisbursementDate == null) {
return BigDecimal.ZERO;
} else {
return getNetDisbursalAmount();
}
}
}