diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 9c62a71a1..7d78c4a1a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -1081,7 +1081,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { transactionDate = loanCharge.getDueLocalDate(); } } else if (loanCharge.isInstalmentFee()) { - transactionDate = loanCharge.getInstallmentLoanCharge(loanInstallmentNumber).getRepaymentInstallment().getDueDate(); + LocalDate repaymentDueDate = loanCharge.getInstallmentLoanCharge(loanInstallmentNumber).getRepaymentInstallment().getDueDate(); + if (repaymentDueDate.isAfter(DateUtils.getBusinessLocalDate())) { + transactionDate = DateUtils.getBusinessLocalDate(); + } else { + transactionDate = repaymentDueDate; + } } scheduleGeneratorDTO.setRecalculateFrom(transactionDate); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java index 1ed06d2a6..5637d5f59 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java @@ -25,6 +25,7 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import java.math.BigDecimal; import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; +import org.apache.fineract.interoperation.util.MathUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.loanaccount.data.LoanInstallmentChargeData; @@ -168,7 +169,7 @@ public class LoanInstallmentCharge extends AbstractPersistableCustom implements } public boolean isPending() { - return !(isPaid() || isWaived()); + return !MathUtil.isZero(this.amountOutstanding); } public boolean isChargeAmountpaid(MonetaryCurrency currency) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index 5be6b1f75..039da3468 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -209,7 +209,6 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen } } reprocessInstallments(installments, currency); - return changedTransactionDetail; } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargeSpecificDueDateTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargeSpecificDueDateTest.java index 1b3695ed3..442354da0 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargeSpecificDueDateTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargeSpecificDueDateTest.java @@ -20,6 +20,7 @@ package org.apache.fineract.integrationtests; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import io.restassured.builder.RequestSpecBuilder; import io.restassured.builder.ResponseSpecBuilder; @@ -30,10 +31,12 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.util.HashMap; import java.util.List; +import java.util.Optional; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse; import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.GetLoansLoanIdTransactions; import org.apache.fineract.client.models.JournalEntryTransactionItem; import org.apache.fineract.client.models.PostChargesResponse; import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdResponse; @@ -233,6 +236,154 @@ public class LoanChargeSpecificDueDateTest { } + @Test + public void testApplyAndWaiveInstallmentFee() { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + final LocalDate todaysDate = Utils.getLocalDateOfTenant(); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, todaysDate); + + // Client and Loan account creation + final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2012"); + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProduct(loanTransactionHelper, null); + assertNotNull(getLoanProductsProductResponse); + + // Older date to have more than one overdue installment + LocalDate transactionDate = todaysDate; + String operationDate = Utils.dateFormatter.format(transactionDate); + log.info("Operation date {}", transactionDate); + + // Create Loan Account + final Integer loanId = createLoanAccount(loanTransactionHelper, clientId.toString(), + getLoanProductsProductResponse.getId().toString(), operationDate, "1", "0"); + + // Get loan details + GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + validateLoanAccount(getLoansLoanIdResponse, Double.valueOf(principalAmount), Double.valueOf("0.00"), false); + + // Apply Loan Charge with specific due date + final String feeAmount = "1.500000"; + String payloadJSON = ChargesHelper.getLoanSpecificInstallmentFeeJSON(); + final PostChargesResponse postChargesResponse = ChargesHelper.createLoanCharge(requestSpec, responseSpec, payloadJSON); + assertNotNull(postChargesResponse); + final Long chargeId = postChargesResponse.getResourceId(); + assertNotNull(chargeId); + + float amount = Float.parseFloat("5.00"); + PostLoansLoanIdTransactionsResponse loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount, + loanId); + + payloadJSON = LoanTransactionHelper.getSpecifiedInstallmentChargesForLoanAsJSON(chargeId.toString(), feeAmount); + PostLoansLoanIdChargesResponse postLoansLoanIdChargesResponse = loanTransactionHelper.addChargeForLoan(loanId, payloadJSON, + responseSpec); + assertNotNull(postLoansLoanIdChargesResponse); + final Long loanChargeId = postLoansLoanIdChargesResponse.getResourceId(); + assertNotNull(loanChargeId); + + getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + validateLoanAccount(getLoansLoanIdResponse, Double.valueOf(principalAmount), Double.valueOf("10.00"), false); + + // Waive the Loan Charge + final PostLoansLoanIdChargesChargeIdResponse postWaiveLoanChargesResponse = loanTransactionHelper.applyLoanChargeCommand(loanId, + loanChargeId, "waive", Utils.emptyJson()); + assertNotNull(postWaiveLoanChargesResponse); + + // evaluate the outstanding + getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + validateLoanAccount(getLoansLoanIdResponse, Double.valueOf(principalAmount), Double.valueOf("0.00"), false); + + Optional waiveTransaction = getLoansLoanIdResponse.getTransactions().stream() + .filter(transaction -> transaction.getType().getWaiveCharges() != null && transaction.getType().getWaiveCharges()) + .findFirst(); + assertTrue(waiveTransaction.isPresent()); + assertEquals(transactionDate, waiveTransaction.get().getDate()); + + // Make a full repayment to close the Loan + amount = Float.parseFloat("1000.00"); + loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount, loanId); + assertNotNull(loanIdTransactionsResponse); + log.info("Loan Transaction Id: {} {}", loanId, loanIdTransactionsResponse.getResourceId()); + + getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + assertNotNull(getLoansLoanIdResponse); + loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.closed.obligations.met"); + + } + + @Test + public void testApplyAndWaiveInstallmentFeeAnotherDueDate() { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + final LocalDate todaysDate = Utils.getLocalDateOfTenant(); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, todaysDate); + + // Client and Loan account creation + final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec, "01 January 2012"); + final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProduct(loanTransactionHelper, null); + assertNotNull(getLoanProductsProductResponse); + + // Older date to have more than one overdue installment + LocalDate transactionDate = todaysDate; + String operationDate = Utils.dateFormatter.format(transactionDate); + log.info("Operation date {}", transactionDate); + + // Create Loan Account + final Integer loanId = createLoanAccount(loanTransactionHelper, clientId.toString(), + getLoanProductsProductResponse.getId().toString(), operationDate, "1", "0"); + + // Get loan details + GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + validateLoanAccount(getLoansLoanIdResponse, Double.valueOf(principalAmount), Double.valueOf("0.00"), false); + + // Apply Loan Charge with specific due date + final String feeAmount = "1.500000"; + String payloadJSON = ChargesHelper.getLoanSpecificInstallmentFeeJSON(); + final PostChargesResponse postChargesResponse = ChargesHelper.createLoanCharge(requestSpec, responseSpec, payloadJSON); + assertNotNull(postChargesResponse); + final Long chargeId = postChargesResponse.getResourceId(); + assertNotNull(chargeId); + + float amount = Float.parseFloat("5.00"); + PostLoansLoanIdTransactionsResponse loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount, + loanId); + transactionDate = todaysDate.plusDays(32); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, transactionDate); + + payloadJSON = LoanTransactionHelper.getSpecifiedInstallmentChargesForLoanAsJSON(chargeId.toString(), feeAmount); + PostLoansLoanIdChargesResponse postLoansLoanIdChargesResponse = loanTransactionHelper.addChargeForLoan(loanId, payloadJSON, + responseSpec); + assertNotNull(postLoansLoanIdChargesResponse); + final Long loanChargeId = postLoansLoanIdChargesResponse.getResourceId(); + assertNotNull(loanChargeId); + + getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + validateLoanAccount(getLoansLoanIdResponse, Double.valueOf(principalAmount), Double.valueOf("10.00"), false); + LocalDate repaymentDueDate = getLoansLoanIdResponse.getRepaymentSchedule().getPeriods().get(1).getDueDate(); + // Waive the Loan Charge + final PostLoansLoanIdChargesChargeIdResponse postWaiveLoanChargesResponse = loanTransactionHelper.applyLoanChargeCommand(loanId, + loanChargeId, "waive", Utils.emptyJson()); + assertNotNull(postWaiveLoanChargesResponse); + + // evaluate the outstanding + getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + validateLoanAccount(getLoansLoanIdResponse, Double.valueOf(principalAmount), Double.valueOf("0.00"), false); + + Optional waiveTransaction = getLoansLoanIdResponse.getTransactions().stream() + .filter(transaction -> transaction.getType().getWaiveCharges() != null && transaction.getType().getWaiveCharges()) + .findFirst(); + assertTrue(waiveTransaction.isPresent()); + assertEquals(repaymentDueDate, waiveTransaction.get().getDate()); + + // Make a full repayment to close the Loan + amount = Float.parseFloat("1000.00"); + loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount, loanId); + assertNotNull(loanIdTransactionsResponse); + log.info("Loan Transaction Id: {} {}", loanId, loanIdTransactionsResponse.getResourceId()); + + getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + assertNotNull(getLoansLoanIdResponse); + loanTransactionHelper.validateLoanStatus(getLoansLoanIdResponse, "loanStatusType.closed.obligations.met"); + + } + @Test public void testApplyAndWaiveLoanSpecificDueDatePenaltyWithDisbursementDate() { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/charges/ChargesHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/charges/ChargesHelper.java index 3d1013a90..d554d3b02 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/charges/ChargesHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/charges/ChargesHelper.java @@ -237,6 +237,19 @@ public final class ChargesHelper extends IntegrationTest { return chargesCreateJson; } + public static String getLoanSpecificInstallmentFeeJSON() { + final HashMap map = populateDefaultsForLoan(); + map.put("chargeTimeType", CHARGE_INSTALLMENT_FEE); + map.put("chargePaymentMode", CHARGE_PAYMENT_MODE_REGULAR); + map.put("penalty", false); + map.put("amount", "1.500000"); + map.put("chargeCalculationType", CHARGE_CALCULATION_TYPE_PERCENTAGE_AMOUNT_AND_INTEREST); + + String chargesCreateJson = new Gson().toJson(map); + LOG.info("{}", chargesCreateJson); + return chargesCreateJson; + } + public static String getLoanSpecifiedDueDateWithAccountTransferJSON(final Integer chargeCalculationType, final String amount, boolean penalty) { return getLoanSpecifiedDueDateJSON(chargeCalculationType, amount, penalty, ChargesHelper.CHARGE_PAYMENT_MODE_ACCOUNT_TRANSFER); @@ -251,6 +264,16 @@ public final class ChargesHelper extends IntegrationTest { return getLoanInstallmentJSON(chargeCalculationType, amount, penalty, ChargesHelper.CHARGE_PAYMENT_MODE_REGULAR); } + public static String getLoanOverdueInstallmentJSON(final String amount) { + final HashMap map = populateDefaultsForLoan(); + map.put("chargeId", CHARGE_OVERDUE_INSTALLMENT_FEE); + map.put("amount", amount); + + String chargesCreateJson = new Gson().toJson(map); + LOG.info("{}", chargesCreateJson); + return chargesCreateJson; + } + public static String getLoanInstallmentJSON(final Integer chargeCalculationType, final String amount, final boolean penalty, final Integer paymentMode) { final HashMap map = populateDefaultsForLoan(); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index 46fa15454..8a774dcc1 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -1206,6 +1206,17 @@ public class LoanTransactionHelper extends IntegrationTest { return json; } + public static String getSpecifiedInstallmentChargesForLoanAsJSON(final String chargeId, final String amount) { + final HashMap map = new HashMap<>(); + map.put("locale", "en_GB"); + map.put("dateFormat", "dd MMMM yyyy"); + map.put("amount", amount); + map.put("chargeId", chargeId); + String json = new Gson().toJson(map); + log.info("{}", json); + return json; + } + public static String getDisbursementChargesForLoanAsJSON(final String chargeId) { return getDisbursementChargesForLoanAsJSON(chargeId, "100"); }