From 498e81a734acdb1b2b511608f8fbacd03009e8fa Mon Sep 17 00:00:00 2001 From: taskain7 Date: Mon, 28 Aug 2023 11:17:08 +0200 Subject: [PATCH] [FINERACT-1958] Charge cannot be added to down payment installment --- .../LoanRepaymentScheduleInstallment.java | 4 + ...oanRepaymentScheduleProcessingWrapper.java | 51 +++--- ...nRepaymentScheduleWithDownPaymentTest.java | 166 +++++++++++++++++- 3 files changed, 198 insertions(+), 23 deletions(-) diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index 44bf43e47..69852054f 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -932,4 +932,8 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa return this.loanTransactionToRepaymentScheduleMappings; } + public boolean isDownPayment() { + return isDownPayment; + } + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java index 04ded2c13..2d73f5295 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java @@ -42,37 +42,45 @@ public class LoanRepaymentScheduleProcessingWrapper { LocalDate startDate = disbursementDate; for (final LoanRepaymentScheduleInstallment period : repaymentPeriods) { - final Money feeChargesDueForRepaymentPeriod = cumulativeFeeChargesDueWithin(startDate, period.getDueDate(), loanCharges, - currency, period, totalPrincipal, totalInterest, !period.isRecalculatedInterestComponent()); - final Money feeChargesWaivedForRepaymentPeriod = cumulativeFeeChargesWaivedWithin(startDate, period.getDueDate(), loanCharges, - currency, !period.isRecalculatedInterestComponent(), period.isFirstPeriod()); - final Money feeChargesWrittenOffForRepaymentPeriod = cumulativeFeeChargesWrittenOffWithin(startDate, period.getDueDate(), - loanCharges, currency, !period.isRecalculatedInterestComponent(), period.isFirstPeriod()); + if (!period.isDownPayment()) { - final Money penaltyChargesDueForRepaymentPeriod = cumulativePenaltyChargesDueWithin(startDate, period.getDueDate(), loanCharges, - currency, period, totalPrincipal, totalInterest, !period.isRecalculatedInterestComponent()); - final Money penaltyChargesWaivedForRepaymentPeriod = cumulativePenaltyChargesWaivedWithin(startDate, period.getDueDate(), - loanCharges, currency, !period.isRecalculatedInterestComponent(), period.isFirstPeriod()); - final Money penaltyChargesWrittenOffForRepaymentPeriod = cumulativePenaltyChargesWrittenOffWithin(startDate, - period.getDueDate(), loanCharges, currency, !period.isRecalculatedInterestComponent(), period.isFirstPeriod()); + boolean isFirstNonDownPaymentPeriod = repaymentPeriods.stream() + .filter(repaymentPeriod -> repaymentPeriod.getInstallmentNumber() < period.getInstallmentNumber()) + .allMatch(LoanRepaymentScheduleInstallment::isDownPayment); - period.updateChargePortion(feeChargesDueForRepaymentPeriod, feeChargesWaivedForRepaymentPeriod, - feeChargesWrittenOffForRepaymentPeriod, penaltyChargesDueForRepaymentPeriod, penaltyChargesWaivedForRepaymentPeriod, - penaltyChargesWrittenOffForRepaymentPeriod); + final Money feeChargesDueForRepaymentPeriod = cumulativeFeeChargesDueWithin(startDate, period.getDueDate(), loanCharges, + currency, period, totalPrincipal, totalInterest, !period.isRecalculatedInterestComponent(), + isFirstNonDownPaymentPeriod); + final Money feeChargesWaivedForRepaymentPeriod = cumulativeFeeChargesWaivedWithin(startDate, period.getDueDate(), + loanCharges, currency, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod); + final Money feeChargesWrittenOffForRepaymentPeriod = cumulativeFeeChargesWrittenOffWithin(startDate, period.getDueDate(), + loanCharges, currency, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod); - startDate = period.getDueDate(); + final Money penaltyChargesDueForRepaymentPeriod = cumulativePenaltyChargesDueWithin(startDate, period.getDueDate(), + loanCharges, currency, period, totalPrincipal, totalInterest, !period.isRecalculatedInterestComponent(), + isFirstNonDownPaymentPeriod); + final Money penaltyChargesWaivedForRepaymentPeriod = cumulativePenaltyChargesWaivedWithin(startDate, period.getDueDate(), + loanCharges, currency, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod); + final Money penaltyChargesWrittenOffForRepaymentPeriod = cumulativePenaltyChargesWrittenOffWithin(startDate, + period.getDueDate(), loanCharges, currency, !period.isRecalculatedInterestComponent(), isFirstNonDownPaymentPeriod); + + period.updateChargePortion(feeChargesDueForRepaymentPeriod, feeChargesWaivedForRepaymentPeriod, + feeChargesWrittenOffForRepaymentPeriod, penaltyChargesDueForRepaymentPeriod, penaltyChargesWaivedForRepaymentPeriod, + penaltyChargesWrittenOffForRepaymentPeriod); + + startDate = period.getDueDate(); + } } } private Money cumulativeFeeChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set loanCharges, final MonetaryCurrency monetaryCurrency, LoanRepaymentScheduleInstallment period, final Money totalPrincipal, - final Money totalInterest, boolean isInstallmentChargeApplicable) { + final Money totalInterest, boolean isInstallmentChargeApplicable, boolean isFirstPeriod) { Money cumulative = Money.zero(monetaryCurrency); for (final LoanCharge loanCharge : loanCharges) { if (loanCharge.isFeeCharge() && !loanCharge.isDueAtDisbursement()) { - boolean isDue = period.isFirstPeriod() - ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, periodEnd) + boolean isDue = isFirstPeriod ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, periodEnd) : loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd); if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) { if (loanCharge.getChargeCalculation().isPercentageBased()) { @@ -175,14 +183,13 @@ public class LoanRepaymentScheduleProcessingWrapper { private Money cumulativePenaltyChargesDueWithin(final LocalDate periodStart, final LocalDate periodEnd, final Set loanCharges, final MonetaryCurrency currency, LoanRepaymentScheduleInstallment period, - final Money totalPrincipal, final Money totalInterest, boolean isInstallmentChargeApplicable) { + final Money totalPrincipal, final Money totalInterest, boolean isInstallmentChargeApplicable, boolean isFirstPeriod) { Money cumulative = Money.zero(currency); for (final LoanCharge loanCharge : loanCharges) { if (loanCharge.isPenaltyCharge()) { - boolean isDue = period.isFirstPeriod() - ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, periodEnd) + boolean isDue = isFirstPeriod ? loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, periodEnd) : loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd); if (loanCharge.isInstalmentFee() && isInstallmentChargeApplicable) { if (loanCharge.getChargeCalculation().isPercentageBased()) { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRepaymentScheduleWithDownPaymentTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRepaymentScheduleWithDownPaymentTest.java index 99d0d7194..0c7c2a3bb 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRepaymentScheduleWithDownPaymentTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRepaymentScheduleWithDownPaymentTest.java @@ -36,8 +36,11 @@ import org.apache.fineract.client.models.GetDelinquencyBucketsResponse; import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod; import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.PostChargesResponse; +import org.apache.fineract.client.models.PostLoansLoanIdChargesResponse; import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.charges.ChargesHelper; import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder; import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; @@ -455,6 +458,167 @@ public class LoanRepaymentScheduleWithDownPaymentTest { assertEquals(outstandingBalanceOnThirdRepayment, thirdRepaymentPeriod.getPrincipalLoanBalanceOutstanding()); } + @Test + public void loanRepaymentScheduleWithChargeAndDownPayment() { + String loanExternalIdStr = UUID.randomUUID().toString(); + + final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec); + final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec, + delinquencyBucketId); + + Boolean enableDownPayment = true; + BigDecimal disbursedAmountPercentageForDownPayment = BigDecimal.valueOf(25); + Boolean enableAutoRepaymentForDownPayment = false; + + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + + Integer loanProductId = createLoanProductWithDownPaymentConfiguration(loanTransactionHelper, delinquencyBucketId, enableDownPayment, + "25", enableAutoRepaymentForDownPayment, true); + + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = loanTransactionHelper.getLoanProduct(loanProductId); + assertNotNull(getLoanProductsProductResponse); + assertEquals(enableDownPayment, getLoanProductsProductResponse.getEnableDownPayment()); + assertEquals(0, getLoanProductsProductResponse.getDisbursedAmountPercentageForDownPayment() + .compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, getLoanProductsProductResponse.getEnableAutoRepaymentForDownPayment()); + + final Integer loanId = createApproveAndDisburseLoanAccount(clientId, loanProductId.longValue(), loanExternalIdStr, "1"); + + final Double feeAmount = 10.00; + String payloadJSON = ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, feeAmount.toString(), + false); + final PostChargesResponse postChargesResponse = ChargesHelper.createLoanCharge(requestSpec, responseSpec, payloadJSON); + assertNotNull(postChargesResponse); + final Long loanChargeId = postChargesResponse.getResourceId(); + + payloadJSON = LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(loanChargeId.toString(), "03 September 2022", + feeAmount.toString()); + PostLoansLoanIdChargesResponse postLoansLoanIdChargesResponse = loanTransactionHelper.addChargeForLoan(loanId, payloadJSON, + responseSpec); + assertNotNull(postLoansLoanIdChargesResponse); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); + + assertNotNull(loanDetails); + assertEquals(enableDownPayment, loanDetails.getEnableDownPayment()); + assertEquals(0, loanDetails.getDisbursedAmountPercentageForDownPayment().compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, loanDetails.getEnableAutoRepaymentForDownPayment()); + + List periods = loanDetails.getRepaymentSchedule().getPeriods(); + Double expectedDownPaymentAmount = 250.00; + LocalDate expectedDownPaymentDueDate = LocalDate.of(2022, 9, 3); + Double expectedRepaymentAmount = 750.00; + Double expectedTotalDueForRepaymentInstallment = 760.0; + LocalDate expectedRepaymentDueDate = LocalDate.of(2022, 10, 3); + + assertTrue(periods.stream() // + .anyMatch(period -> expectedDownPaymentAmount.equals(period.getTotalDueForPeriod()) // + && expectedDownPaymentDueDate.equals(period.getDueDate()) // + && Double.valueOf(0.00).equals(period.getFeeChargesDue()))); + assertTrue(periods.stream() // + .anyMatch(period -> expectedTotalDueForRepaymentInstallment.equals(period.getTotalDueForPeriod()) // + && expectedRepaymentAmount.equals(period.getPrincipalDue()) // + && expectedRepaymentDueDate.equals(period.getDueDate()) // + && feeAmount.equals(period.getFeeChargesDue()))); + } + + @Test + public void loanRepaymentScheduleWithMultiDisbursementProductTwoDisbursementAndThreeRepaymentsAndDownPaymentAndCharge() { + String loanExternalIdStr = UUID.randomUUID().toString(); + + final Integer delinquencyBucketId = DelinquencyBucketsHelper.createDelinquencyBucket(requestSpec, responseSpec); + final GetDelinquencyBucketsResponse delinquencyBucket = DelinquencyBucketsHelper.getDelinquencyBucket(requestSpec, responseSpec, + delinquencyBucketId); + + Boolean enableDownPayment = true; + BigDecimal disbursedAmountPercentageForDownPayment = BigDecimal.valueOf(25); + Boolean enableAutoRepaymentForDownPayment = true; + + final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + + Integer loanProductId = createLoanProductWithDownPaymentConfiguration(loanTransactionHelper, delinquencyBucketId, enableDownPayment, + "25", enableAutoRepaymentForDownPayment, true); + + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = loanTransactionHelper.getLoanProduct(loanProductId); + assertNotNull(getLoanProductsProductResponse); + assertEquals(enableDownPayment, getLoanProductsProductResponse.getEnableDownPayment()); + assertEquals(0, getLoanProductsProductResponse.getDisbursedAmountPercentageForDownPayment() + .compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, getLoanProductsProductResponse.getEnableAutoRepaymentForDownPayment()); + + final Integer loanId = createApproveAndDisburseTwiceLoanAccount(clientId, loanProductId.longValue(), loanExternalIdStr, "4"); + + final Double feeAmount = 10.00; + String payloadJSON = ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, feeAmount.toString(), + false); + final PostChargesResponse postChargesResponse = ChargesHelper.createLoanCharge(requestSpec, responseSpec, payloadJSON); + assertNotNull(postChargesResponse); + final Long loanChargeId = postChargesResponse.getResourceId(); + + payloadJSON = LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(loanChargeId.toString(), "04 September 2022", + feeAmount.toString()); + PostLoansLoanIdChargesResponse postLoansLoanIdChargesResponse = loanTransactionHelper.addChargeForLoan(loanId, payloadJSON, + responseSpec); + assertNotNull(postLoansLoanIdChargesResponse); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); + + assertNotNull(loanDetails); + assertEquals(enableDownPayment, loanDetails.getEnableDownPayment()); + assertEquals(0, loanDetails.getDisbursedAmountPercentageForDownPayment().compareTo(disbursedAmountPercentageForDownPayment)); + assertEquals(enableAutoRepaymentForDownPayment, loanDetails.getEnableAutoRepaymentForDownPayment()); + + loanTransactionHelper.printRepaymentSchedule(loanDetails); + + List periods = loanDetails.getRepaymentSchedule().getPeriods(); + Double expectedOutstandingLoanBalanceOnFirstDisbursement = 700.00; + Double expectedFirstDownPaymentAmount = 175.00; + LocalDate expectedFirstDownPaymentDueDate = LocalDate.of(2022, 9, 3); + Double expectedOutstandingLoanBalanceOnSecondDisbursement = 300.00; + Double expectedSecondDownPaymentAmount = 75.00; + LocalDate expectedSecondDownPaymentDueDate = LocalDate.of(2022, 9, 4); + Double expectedRepaymentAmount = 250.00; + Double expectedRepaymentTotalDueWithCharge = 260.0; + LocalDate expectedFirstRepaymentDueDate = LocalDate.of(2022, 10, 3); + Double outstandingBalanceOnFirstRepayment = 500.00; + LocalDate expectedSecondRepaymentDueDate = LocalDate.of(2022, 11, 3); + Double outstandingBalanceOnSecondRepayment = 250.00; + LocalDate expectedThirdRepaymentDueDate = LocalDate.of(2022, 12, 3); + Double outstandingBalanceOnThirdRepayment = 0.00; + + GetLoansLoanIdRepaymentPeriod firstDisbursementPeriod = periods.get(0); + assertEquals(expectedFirstDownPaymentDueDate, firstDisbursementPeriod.getDueDate()); + assertEquals(expectedOutstandingLoanBalanceOnFirstDisbursement, firstDisbursementPeriod.getPrincipalLoanBalanceOutstanding()); + + GetLoansLoanIdRepaymentPeriod firstDownPaymentPeriod = periods.get(1); + assertEquals(expectedFirstDownPaymentAmount, firstDownPaymentPeriod.getTotalDueForPeriod()); + assertEquals(expectedFirstDownPaymentDueDate, firstDownPaymentPeriod.getDueDate()); + + GetLoansLoanIdRepaymentPeriod secondDisbursementPeriod = periods.get(2); + assertEquals(expectedSecondDownPaymentDueDate, secondDisbursementPeriod.getDueDate()); + assertEquals(expectedOutstandingLoanBalanceOnSecondDisbursement, secondDisbursementPeriod.getPrincipalLoanBalanceOutstanding()); + + GetLoansLoanIdRepaymentPeriod secondDownPaymentPeriod = periods.get(3); + assertEquals(expectedSecondDownPaymentAmount, secondDownPaymentPeriod.getTotalDueForPeriod()); + assertEquals(expectedSecondDownPaymentDueDate, secondDownPaymentPeriod.getDueDate()); + + GetLoansLoanIdRepaymentPeriod firstRepaymentPeriod = periods.get(4); + assertEquals(expectedRepaymentAmount, firstRepaymentPeriod.getPrincipalDue()); + assertEquals(expectedRepaymentTotalDueWithCharge, firstRepaymentPeriod.getTotalDueForPeriod()); + assertEquals(expectedFirstRepaymentDueDate, firstRepaymentPeriod.getDueDate()); + assertEquals(outstandingBalanceOnFirstRepayment, firstRepaymentPeriod.getPrincipalLoanBalanceOutstanding()); + + GetLoansLoanIdRepaymentPeriod secondRepaymentPeriod = periods.get(5); + assertEquals(expectedRepaymentAmount, secondRepaymentPeriod.getTotalDueForPeriod()); + assertEquals(expectedSecondRepaymentDueDate, secondRepaymentPeriod.getDueDate()); + assertEquals(outstandingBalanceOnSecondRepayment, secondRepaymentPeriod.getPrincipalLoanBalanceOutstanding()); + + GetLoansLoanIdRepaymentPeriod thirdRepaymentPeriod = periods.get(6); + assertEquals(expectedRepaymentAmount, thirdRepaymentPeriod.getTotalDueForPeriod()); + assertEquals(expectedThirdRepaymentDueDate, thirdRepaymentPeriod.getDueDate()); + assertEquals(outstandingBalanceOnThirdRepayment, thirdRepaymentPeriod.getPrincipalLoanBalanceOutstanding()); + } + private Integer createLoanProductWithDownPaymentConfiguration(final LoanTransactionHelper loanTransactionHelper, final Integer delinquencyBucketId, Boolean enableDownPayment, String disbursedAmountPercentageForDownPayment, Boolean enableAutoRepaymentForDownPayment, boolean multiDisbursement) { @@ -488,7 +652,7 @@ public class LoanRepaymentScheduleWithDownPaymentTest { final Integer loanId = loanTransactionHelper.getLoanId(loanApplicationJSON); loanTransactionHelper.approveLoan("02 September 2022", "1000", loanId, null); - HashMap hashMap = loanTransactionHelper.disburseLoanWithTransactionAmount("03 September 2022", loanId, "1000"); + loanTransactionHelper.disburseLoanWithTransactionAmount("03 September 2022", loanId, "1000"); return loanId; }