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
@@ -11,6 +11,14 @@
"bigdecimal"
]
},
{
"default": null,
"name": "availableDisbursementAmountWithOverApplied",
"type": [
"null",
"bigdecimal"
]
},
{
"default": null,
"name": "pastDueDays",
@@ -122,6 +122,7 @@ public enum DefaultLoanProduct implements LoanProduct {
LP2_ADV_DP_IR_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL, //
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_DISBURSEMENT_CHARGES, //
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSE_EXPECT_TRANCHE, //
LP2_ADV_PYMNT_INT_DAILY_EMI_360_30_INT_RECALC_DAILY_MULTIDISB_EXPECT_TRANCHE_APPROVED_OVER_APPLIED, //
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSE, //
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CASH_ACCOUNTING_DISBURSEMENT_CHARGES, //
LP2_ADV_PYMNT_INT_DAILY_EMI_ACTUAL_ACTUAL_INT_REFUND_FULL_ZERO_INT_CHARGE_OFF, //
@@ -1066,4 +1066,10 @@ public final class ErrorMessageHelper {
return String.format("%nNumber of lines does not match in Deferred Income tab and expected datatable of resource %s." //
+ "%nNumber of transaction tab lines: %s %nNumber of expected datatable lines: %s%n", resourceId, actual, expected);
}
public static String wrongAvailableDisbursementAmountWithOverApplied(final double actual, final double expected) {
return String.format(
"Wrong value in LoanDetails/availableDisbursementAmountWithOverApplied. %nActual value is: %s %nExpected Value is: %s",
actual, expected);
}
}
@@ -3658,6 +3658,42 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ
TestContext.INSTANCE.set(
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_PROGRESSIVE_ADV_PYMNT_BUYDOWN_FEES_NON_MERCHANT_CHARGE_OFF_REASON,
responseLoanProductsRequestLP2ProgressiveAdvPaymentBuyDownFeesNonMerchantWithChargeOffReason);
// LP2 with progressive loan schedule + horizontal + interest recalculation daily EMI + 360/30 +
// multidisbursement
// Frequency for recalculate Outstanding Principal: Daily, Frequency Interval for recalculation: 1
// allow approved/disbursed amount over applied amount is enabled with percentage type
// (LP2_ADV_PYMNT_INT_DAILY_EMI_360_30_INT_RECALC_DAILY_MULTIDISB_EXPECT_TRANCHE_APPROVED_OVER_APPLIED)
final String name138 = DefaultLoanProduct.LP2_ADV_PYMNT_INT_DAILY_EMI_360_30_INT_RECALC_DAILY_MULTIDISB_EXPECT_TRANCHE_APPROVED_OVER_APPLIED
.getName();
final PostLoanProductsRequest loanProductsRequestLP2AdvEmi36030IntRecalcDailyMultiDisbApprovedOverApplied = loanProductsRequestFactory
.defaultLoanProductsRequestLP2Emi()//
.name(name138)//
.daysInYearType(DaysInYearType.DAYS360.value)//
.daysInMonthType(DaysInMonthType.DAYS30.value)//
.isInterestRecalculationEnabled(true)//
.preClosureInterestCalculationStrategy(1)//
.rescheduleStrategyMethod(4)//
.interestRecalculationCompoundingMethod(0)//
.recalculationRestFrequencyType(2)//
.recalculationRestFrequencyInterval(1)//
.paymentAllocation(List.of(//
createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT"), //
createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"), //
createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION"), //
createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT")))//
.multiDisburseLoan(true)//
.disallowExpectedDisbursements(false)//
.allowApprovedDisbursedAmountsOverApplied(true)//
.overAppliedCalculationType(OverAppliedCalculationType.PERCENTAGE.value)//
.overAppliedNumber(50)//
.maxTrancheCount(10)//
.outstandingLoanBalance(10000.0);//
final Response<PostLoanProductsResponse> responseLoanProductsRequestLP2AdvEmi36030IntRecalcDailyMultiDisbApprovedOverApplied = loanProductsApi
.createLoanProduct(loanProductsRequestLP2AdvEmi36030IntRecalcDailyMultiDisbApprovedOverApplied).execute();
TestContext.INSTANCE.set(
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INT_DAILY_EMI_360_30_INT_RECALC_DAILY_MULTIDISB_EXPECT_TRANCHE_APPROVED_OVER_APPLIED,
responseLoanProductsRequestLP2AdvEmi36030IntRecalcDailyMultiDisbApprovedOverApplied);
}
public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule,
@@ -3293,6 +3293,25 @@ public class LoanStepDef extends AbstractStepDef {
assertThat(fixedLengthactual).as(ErrorMessageHelper.wrongfixedLength(fixedLengthactual, fieldValue)).isEqualTo(fieldValue);
}
@Then("Loan has availableDisbursementAmountWithOverApplied field with value: {double}")
public void checkLoanDetailsAvailableDisbursementAmountWithOverAppliedField(final double fieldValue) throws IOException {
final Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
assert loanResponse.body() != null;
final long loanId = loanResponse.body().getLoanId();
final Response<GetLoansLoanIdResponse> loanDetails = loansApi.retrieveLoan(loanId, false, "", "", "").execute();
ErrorHelper.checkSuccessfulApiCall(loanDetails);
assert loanDetails.body() != null;
assert loanDetails.body().getDelinquent() != null;
assert loanDetails.body().getDelinquent().getAvailableDisbursementAmountWithOverApplied() != null;
final Double availableDisbursementAmountWithOverApplied = loanDetails.body().getDelinquent()
.getAvailableDisbursementAmountWithOverApplied().doubleValue();
assertThat(availableDisbursementAmountWithOverApplied).as(
ErrorMessageHelper.wrongAvailableDisbursementAmountWithOverApplied(availableDisbursementAmountWithOverApplied, fieldValue))
.isEqualTo(fieldValue);
}
@Then("Loan emi amount variations has {int} variation, with the following data:")
public void loanEmiAmountVariationsCheck(final int linesExpected, final DataTable table) throws IOException {
final Response<PostLoansResponse> loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
@@ -152,6 +152,7 @@ public abstract class TestContextKey {
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALC_EMI_360_30_CHARGEBACK_PRINCIPAL_INTEREST_FEE = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalcEmi36030ChargebackPrincipalInterestFee";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_DISBURSEMENT_CHARGES = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationDailyDisbursementCharge";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSE_EXPECT_TRANCHE = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationDailyMultidisburseExpectTranche";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INT_DAILY_EMI_360_30_INT_RECALC_DAILY_MULTIDISB_EXPECT_TRANCHE_APPROVED_OVER_APPLIED = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationDailyMultidisburseExpectTrancheApprovedOVerAppliedPercentage";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSE = "loanProductCreateResponseLP2AdvancedPaymentInterestRecalculationDailyEmi36030Multidisburse";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CASH_ACCOUNTING_DISBURSEMENT_CHARGES = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationDailyCashBasedDisbursementCharge";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_PROGRESSIVE_ADV_PYMNT_CAPITALIZED_INCOME = "loanProductCreateResponseLP2ProgressiveAdvancedPaymentCapitalizedIncome";
@@ -7958,3 +7958,66 @@ Feature: Loan
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| 01 June 2024 | Disbursement | 1200.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1200.0 |
| 01 July 2024 | Repayment | 417.03 | 405.03 | 12.0 | 0.0 | 0.0 | 794.97 |
@TestRailId:C3999
Scenario: Verify availableDisbursementAmountWithOverApplied field calculation
When Admin sets the business date to "1 January 2025"
And Admin creates a client with random data
And Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_PYMNT_INTEREST_DAILY_RECALC_EMI_360_30_MULTIDISB_APPROVED_OVER_APPLIED_CAPITALIZED_INCOME | 01 January 2025 | 1000 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
Then Loan has availableDisbursementAmountWithOverApplied field with value: 1500
And Admin successfully approves the loan on "1 January 2025" with "1000" amount and expected disbursement date on "1 January 2025"
Then Loan has availableDisbursementAmountWithOverApplied field with value: 1500
And Admin successfully disburse the loan on "1 January 2025" with "900" EUR transaction amount
Then Loan has availableDisbursementAmountWithOverApplied field with value: 600
And Admin successfully disburse the loan on "1 January 2025" with "300" EUR transaction amount
Then Loan has availableDisbursementAmountWithOverApplied field with value: 300
And Admin adds capitalized income with "AUTOPAY" payment type to the loan on "1 January 2025" with "100" EUR transaction amount
Then Loan has availableDisbursementAmountWithOverApplied field with value: 200
And Admin adds capitalized income with "AUTOPAY" payment type to the loan on "1 January 2025" with "150" EUR transaction amount
Then Loan has availableDisbursementAmountWithOverApplied field with value: 50
When Loan Pay-off is made on "01 January 2025"
Then Loan's all installments have obligations met
@TestRailId:C4003
Scenario: Verify availableDisbursementAmountWithOverApplied field calculation for progressive multidisbursal loan that expects tranches
When Admin sets the business date to "01 January 2025"
When Admin creates a client with random data
When Admin creates a fully customized loan with three expected disbursements details and following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | 1st_tranche_disb_expected_date | 1st_tranche_disb_principal | 2nd_tranche_disb_expected_date | 2nd_tranche_disb_principal | 3rd_tranche_disb_expected_date | 3rd_tranche_disb_principal |
| LP2_ADV_PYMNT_INT_DAILY_EMI_360_30_INT_RECALC_DAILY_MULTIDISB_EXPECT_TRANCHE_APPROVED_OVER_APPLIED | 01 January 2025 | 1000 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | 01 January 2025 | 300.0 | 02 January 2025 | 200.0 | 03 January 2025 | 500.0 |
Then Loan Tranche Details tab has the following data:
| Expected Disbursement On | Disbursed On | Principal | Net Disbursal Amount |
| 01 January 2025 | | 300.0 | |
| 02 January 2025 | | 200.0 | |
| 03 January 2025 | | 500.0 | |
Then Loan has availableDisbursementAmountWithOverApplied field with value: 500
And Admin successfully approves the loan on "01 January 2025" with "1000" amount and expected disbursement date on "01 January 2025"
Then Loan Tranche Details tab has the following data:
| Expected Disbursement On | Disbursed On | Principal | Net Disbursal Amount |
| 01 January 2025 | | 300.0 | |
| 02 January 2025 | | 200.0 | |
| 03 January 2025 | | 500.0 | |
Then Loan has availableDisbursementAmountWithOverApplied field with value: 500
When Admin successfully disburse the loan on "01 January 2025" with "300" EUR transaction amount
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2025 | Disbursement | 300.0 | 0.0 | 0.0 | 0.0 | 0.0 | 300.0 | false | false |
Then Loan has availableDisbursementAmountWithOverApplied field with value: 500
When Admin sets the business date to "02 January 2025"
When Admin successfully disburse the loan on "02 January 2025" with "200" EUR transaction amount
Then Loan has availableDisbursementAmountWithOverApplied field with value: 500
When Admin sets the business date to "03 January 2025"
When Admin successfully disburse the loan on "03 January 2025" with "1000" EUR transaction amount
Then Loan has availableDisbursementAmountWithOverApplied field with value: 0
Then Loan Tranche Details tab has the following data:
| Expected Disbursement On | Disbursed On | Principal | Net Disbursal Amount |
| 01 January 2025 | 01 January 2025 | 300.0 | |
| 02 January 2025 | 02 January 2025 | 200.0 | |
| 03 January 2025 | 03 January 2025 | 1000.0 | |
And Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
| 01 January 2025 | Disbursement | 300.0 | 0.0 | 0.0 | 0.0 | 0.0 | 300.0 | false | false |
| 02 January 2025 | Disbursement | 200.0 | 0.0 | 0.0 | 0.0 | 0.0 | 500.0 | false | false |
| 03 January 2025 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1500.0 | false | false |
@@ -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();
}
}
}
@@ -1020,6 +1020,8 @@ final class LoansApiResourceSwagger {
@Schema(example = "100.000000")
public BigDecimal availableDisbursementAmount;
@Schema(example = "150.000000")
public BigDecimal availableDisbursementAmountWithOverApplied;
@Schema(example = "12")
public Integer pastDueDays;
@Schema(example = "[2022, 07, 01]")
@@ -643,8 +643,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
public LoanApprovalData retrieveApprovalTemplate(final Long loanId) {
final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
final ApplicationCurrency appCurrency = applicationCurrencyRepository.findOneWithNotFoundDetection(loan.getCurrency());
final BigDecimal availableDisbursementAmountWithOverApplied = delinquencyReadPlatformService
.calculateAvailableDisbursementAmountWithOverApplied(loan);
return new LoanApprovalData(loan.getProposedPrincipal(), DateUtils.getBusinessLocalDate(), loan.getNetDisbursalAmount(),
appCurrency.toData());
appCurrency.toData(), availableDisbursementAmountWithOverApplied);
}
@Override
@@ -656,9 +658,12 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
paymentOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes();
}
final ApplicationCurrency appCurrency = applicationCurrencyRepository.findOneWithNotFoundDetection(loan.getCurrency());
final BigDecimal availableDisbursementAmountWithOverApplied = delinquencyReadPlatformService
.calculateAvailableDisbursementAmountWithOverApplied(loan);
return LoanTransactionData.loanTransactionDataForDisbursalTemplate(transactionType,
loan.getExpectedDisbursedOnLocalDateForTemplate(), loan.getDisburseAmountForTemplate(), loan.getNetDisbursalAmount(),
paymentOptions, loan.retriveLastEmiAmount(), loan.getNextPossibleRepaymentDateForRescheduling(), appCurrency.toData());
paymentOptions, loan.retriveLastEmiAmount(), loan.getNextPossibleRepaymentDateForRescheduling(), appCurrency.toData(),
availableDisbursementAmountWithOverApplied);
}
@Override
@@ -186,8 +186,8 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
loanForProcessing);
final BigDecimal zero = BigDecimal.ZERO;
CollectionData collectionData = new CollectionData(zero, 2L, null, zero, 2L, overDueSinceDate, zero, null, null, null, null, null,
null, zero, zero, zero, zero);
CollectionData collectionData = new CollectionData(zero, zero, 2L, null, zero, 2L, overDueSinceDate, zero, null, null, null, null,
null, null, zero, zero, zero, zero);
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
@@ -240,10 +240,10 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(2);
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
loanForProcessing);
CollectionData collectionData = new CollectionData(zeroAmount, 2L, null, zeroAmount, 2L, overDueSinceDate, zeroAmount, null, null,
null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
CollectionData collectionData = new CollectionData(zeroAmount, zeroAmount, 2L, null, zeroAmount, 2L, overDueSinceDate, zeroAmount,
null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
CollectionData installmentCollectionData = new CollectionData(zeroAmount, 2L, null, zeroAmount, 2L, overDueSinceDate,
CollectionData installmentCollectionData = new CollectionData(zeroAmount, zeroAmount, 2L, null, zeroAmount, 2L, overDueSinceDate,
installmentPrincipalAmount, null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
@@ -369,10 +369,10 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
loanForProcessing);
CollectionData collectionData = new CollectionData(zeroAmount, 2L, null, zeroAmount, 2L, overDueSinceDate, zeroAmount, null, null,
null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
CollectionData collectionData = new CollectionData(zeroAmount, zeroAmount, 2L, null, zeroAmount, 2L, overDueSinceDate, zeroAmount,
null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
CollectionData installmentCollectionData = new CollectionData(zeroAmount, 2L, null, zeroAmount, 2L, overDueSinceDate,
CollectionData installmentCollectionData = new CollectionData(zeroAmount, zeroAmount, 2L, null, zeroAmount, 2L, overDueSinceDate,
installmentPrincipalAmount, null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
@@ -446,10 +446,10 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(29);
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
loanForProcessing);
CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 29L, null, zeroAmount, 29L, overDueSinceDate, zeroAmount, null,
null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
CollectionData collectionData = new CollectionData(zeroAmount, zeroAmount, 29L, null, zeroAmount, 29L, overDueSinceDate, zeroAmount,
null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
CollectionData installmentCollectionData = new CollectionData(zeroAmount, 29L, null, zeroAmount, 29L, overDueSinceDate,
CollectionData installmentCollectionData = new CollectionData(zeroAmount, zeroAmount, 29L, null, zeroAmount, 29L, overDueSinceDate,
installmentPrincipalAmount, null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
@@ -534,13 +534,14 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
LocalDate overDueSinceDate = DateUtils.getBusinessLocalDate().minusDays(29);
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
loanForProcessing);
CollectionData collectionData = new CollectionData(zeroAmount, 29L, null, zeroAmount, 29L, overDueSinceDate, zeroAmount, null, null,
null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
CollectionData collectionData = new CollectionData(zeroAmount, zeroAmount, 29L, null, zeroAmount, 29L, overDueSinceDate, zeroAmount,
null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
CollectionData installmentCollectionData_1 = new CollectionData(zeroAmount, 29L, null, zeroAmount, 29L, overDueSinceDate,
installmentPrincipalAmount, null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
CollectionData installmentCollectionData_1 = new CollectionData(zeroAmount, zeroAmount, 29L, null, zeroAmount, 29L,
overDueSinceDate, installmentPrincipalAmount, null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount,
zeroAmount);
CollectionData installmentCollectionData_2 = new CollectionData(zeroAmount, 0L, null, zeroAmount, 0L, null,
CollectionData installmentCollectionData_2 = new CollectionData(zeroAmount, zeroAmount, 0L, null, zeroAmount, 0L, null,
installmentPrincipalAmount, null, null, null, null, null, null, zeroAmount, zeroAmount, zeroAmount, zeroAmount);
Map<Long, CollectionData> installmentsCollection = new HashMap<>();