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 1ac524f9bf..655069f2ff 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 @@ -132,6 +132,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod; import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType; import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; @@ -3816,7 +3817,8 @@ private void validateActivityNotBeforeClientOrGroupTransferDate(final LoanEvent } private void validateActivityNotBeforeLastTransactionDate(final LoanEvent event, final LocalDate activityDate) { - if (!(this.repaymentScheduleDetail().isInterestRecalculationEnabled() || this.loanProduct().isHoldGuaranteeFunds())) { + if (!(this.repaymentScheduleDetail().isInterestRecalculationEnabled() || this.loanProduct().isHoldGuaranteeFunds()) + || !this.getLoanRepaymentScheduleDetail().getLoanScheduleType().equals(LoanScheduleType.CUMULATIVE)) { return; } LocalDate lastTransactionDate = getLastUserTransactionDate(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java index 9ab94e1c93..219a006a41 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java @@ -164,6 +164,8 @@ import org.apache.fineract.portfolio.savings.domain.SavingsAccountStatusType; import org.glassfish.jersey.media.multipart.FormDataContentDisposition; import org.glassfish.jersey.media.multipart.FormDataParam; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; @@ -223,6 +225,8 @@ @RequiredArgsConstructor public class LoansApiResource { + private Logger logger = LoggerFactory.getLogger(this.getClass()); + private static final Set LOAN_DATA_PARAMETERS = new HashSet<>(Arrays.asList("id", "accountNo", "status", "externalId", "clientId", "group", "loanProductId", "loanProductName", "loanProductDescription", "isLoanProductLinkedToFloatingRate", "fundId", "fundName", "loanPurposeId", "loanPurposeName", "loanOfficerId", "loanOfficerName", "currency", "principal", @@ -970,9 +974,11 @@ private String retrieveLoan(final Long loanId, final String loanExternalIdStr, b } } + logger.error("## LOADING CHARGES? associationParameters: {}", associationParameters); if (associationParameters.contains(DataTableApiConstant.chargesAssociateParamName)) { mandatoryResponseParameters.add(DataTableApiConstant.chargesAssociateParamName); charges = this.loanChargeReadPlatformService.retrieveLoanCharges(resolvedLoanId); + logger.error("## LOADED CHARGES: {}", charges); if (CollectionUtils.isEmpty(charges)) { charges = null; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java index 773116fdb2..6420362805 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java @@ -125,6 +125,7 @@ import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.DefaultScheduledDateGenerator; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator; import org.apache.fineract.portfolio.loanproduct.data.LoanOverdueDTO; @@ -998,7 +999,9 @@ private void validateAddLoanCharge(final Loan loan, final Charge chargeDefinitio chargeDefinition.getName()); } else if (loanCharge.getDueLocalDate() != null) { // TODO: Review, error message seems not valid if interest recalculation is not enabled. - LocalDate validationDate = loan.repaymentScheduleDetail().isInterestRecalculationEnabled() ? loan.getLastUserTransactionDate() + boolean isCumulative = loan.getLoanRepaymentScheduleDetail().getLoanScheduleType().equals(LoanScheduleType.CUMULATIVE); + LocalDate validationDate = loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && isCumulative + ? loan.getLastUserTransactionDate() : loan.getDisbursementDate(); if (DateUtils.isBefore(loanCharge.getDueLocalDate(), validationDate)) { final String defaultUserMessage = "charge with date before last transaction date can not be added to loan."; diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionBackdatedProgressiveTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionBackdatedProgressiveTest.java new file mode 100644 index 0000000000..f36114b117 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionBackdatedProgressiveTest.java @@ -0,0 +1,162 @@ +/** + * 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.integrationtests; + +import com.google.gson.Gson; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.GetLoansLoanIdTransactions; +import org.apache.fineract.client.models.PostLoanProductsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdRequest; +import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdResponse; +import org.apache.fineract.client.models.PostLoansLoanIdChargesRequest; +import org.apache.fineract.client.models.PostLoansLoanIdChargesResponse; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; +import org.apache.fineract.client.models.PostLoansResponse; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.charges.ChargesHelper; +import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@Slf4j +@ExtendWith({ LoanTestLifecycleExtension.class }) +public class LoanTransactionBackdatedProgressiveTest extends BaseLoanIntegrationTest { + + private Long clientId; + private Long loanId; + + @BeforeEach + public void beforeEach() { + runAt("01 July 2024", () -> { + clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive()); + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan( + applyPin4ProgressiveLoanRequest(clientId, loanProductsResponse.getResourceId(), "01 June 2024", 1000.0, 10.0, 4, null)); + loanId = postLoansResponse.getLoanId(); + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(1000.0, "01 June 2024")); + disburseLoan(loanId, BigDecimal.valueOf(250.0), "01 June 2024"); + addRepaymentForLoan(loanId, 100.0, "10 June 2024"); + }); + } + + @Test + public void testProgressiveBackdatedDisbursement() { + runAt("01 July 2024", () -> { + disburseLoan(loanId, BigDecimal.valueOf(250.0), "5 June 2024"); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertEquals(loanDetails.getDisbursementDetails().size(), 2); + }); + } + + @Test + public void testProgressiveBackdatedRepayment() { + runAt("01 July 2024", () -> { + addRepaymentForLoan(loanId, 100.0, "5 June 2024"); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertTrue(loanDetails.getTransactions().size() >= 2); + }); + } + + @Test + public void testProgressiveBackdatedMerchantIssuedRefund() { + runAt("01 July 2024", () -> { + loanTransactionHelper.makeMerchantIssuedRefund(loanId, new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN) + .transactionDate("5 June 2024").locale("en").transactionAmount(100.0)); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertTrue(loanDetails.getTransactions().size() >= 2); + }); + } + + @Test + public void testProgressiveBackdatedPayoutRefund() { + runAt("01 July 2024", () -> { + loanTransactionHelper.makePayoutRefund(loanId, new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN) + .transactionDate("5 June 2024").locale("en").transactionAmount(100.0)); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertTrue(loanDetails.getTransactions().size() >= 2); + }); + } + + @Test + public void testProgressiveBackdatedGoodwillCredit() { + runAt("01 July 2024", () -> { + loanTransactionHelper.makeGoodwillCredit(loanId, new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN) + .transactionDate("5 June 2024").locale("en").transactionAmount(100.0)); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertTrue(loanDetails.getTransactions().size() >= 2); + }); + } + + @Test + public void testProgressiveBackdatedCharge() { + runAt("01 July 2024", () -> { + Integer feeCharge = ChargesHelper.createCharges(requestSpec, responseSpec, + createEurCharge(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", false)); + + loanTransactionHelper.addLoanCharge(loanId, new PostLoansLoanIdChargesRequest().dateFormat(DATETIME_PATTERN) + .dueDate("5 June 2024").locale("en").chargeId((long) feeCharge).amount(10.0)); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertFalse(loanDetails.getCharges().isEmpty()); + }); + } + + @Test + public void testProgressiveBackdatedChargeAdjustment() { + runAt("01 July 2024", () -> { + Integer feeCharge = ChargesHelper.createCharges(requestSpec, responseSpec, + createEurCharge(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "10", false)); + + final PostLoansLoanIdChargesResponse addLoanChargeResponse = loanTransactionHelper.addLoanCharge(loanId, + new PostLoansLoanIdChargesRequest().dateFormat(DATETIME_PATTERN).dueDate("5 June 2024").locale("en") + .chargeId((long) feeCharge).amount(10.0)); + + final PostLoansLoanIdChargesChargeIdResponse chargeAdjustmentResponse = loanTransactionHelper.chargeAdjustment(loanId, + addLoanChargeResponse.getResourceId(), new PostLoansLoanIdChargesChargeIdRequest().locale("en").amount(1.0)); + chargeAdjustmentResponse.getLoanId(); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + final Optional optionalChargeAdjustmentTransaction = loanDetails.getTransactions().stream() + .filter(transaction -> transaction.getType().getCode().equals("loanTransactionType.chargeAdjustment")).findFirst(); + Assertions.assertTrue(optionalChargeAdjustmentTransaction.isPresent()); + }); + } + + private String createEurCharge(final Integer chargeCalculationType, final String amount, final boolean penalty) { + final HashMap map = ChargesHelper.populateDefaultsForLoan(); + map.put("currencyCode", "EUR"); + map.put("chargeTimeType", 2); + map.put("chargePaymentMode", 0); + map.put("penalty", penalty); + map.put("amount", amount); + map.put("chargeCalculationType", chargeCalculationType); + return new Gson().toJson(map); + } +}