diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java index 302a2b461..294966ce1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java @@ -712,6 +712,17 @@ public class LoanCharge extends AbstractPersistableCustom { return target != null && target.isAfter(fromNotInclusive) && !target.isAfter(upToAndInclusive); } + public boolean isDueForCollectionFromIncludingAndUpToAndIncluding(final LocalDate fromNotInclusive, final LocalDate upToAndInclusive) { + final LocalDate dueDate = getDueLocalDate(); + return occursOnDayFromAndIncludingAndUpToAndIncluding(fromNotInclusive, upToAndInclusive, dueDate); + } + + private boolean occursOnDayFromAndIncludingAndUpToAndIncluding(final LocalDate fromAndInclusive, final LocalDate upToAndInclusive, + final LocalDate target) { + return target != null && (target.isEqual(fromAndInclusive) || target.isAfter(fromAndInclusive)) + && !target.isAfter(upToAndInclusive); + } + public boolean isFeeCharge() { return !this.penaltyCharge; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index 47d57fc93..7aea6ea41 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -894,4 +894,8 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa this.additional = true; } + public boolean isFirstPeriod() { + return (this.installmentNumber == 1); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java index 8a0fb8d10..72fec14af 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java @@ -118,6 +118,10 @@ public class LoanRepaymentScheduleProcessingWrapper { cumulative = cumulative.plus(loanChargeAmt); } else if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd)) { cumulative = cumulative.plus(loanCharge.amount()); + // Special case for Loan Charges (Due Date) added the same disbursement date + } else if (period.isFirstPeriod() + && loanCharge.isDueForCollectionFromIncludingAndUpToAndIncluding(periodStart, periodEnd)) { + cumulative = cumulative.plus(loanCharge.amount()); } } } 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 new file mode 100644 index 000000000..37c62dcdf --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargeSpecificDueDateTest.java @@ -0,0 +1,148 @@ +/** + * 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 static org.junit.jupiter.api.Assertions.assertNotNull; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.time.LocalDate; +import java.util.HashMap; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; +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.client.models.PostLoansLoanIdTransactionsResponse; +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; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@Slf4j +public class LoanChargeSpecificDueDateTest { + + private ResponseSpecification responseSpec; + private RequestSpecification requestSpec; + private LoanTransactionHelper loanTransactionHelper; + private static final String principalAmount = "1000.00"; + + @BeforeEach + public void setup() { + Utils.initializeRESTAssured(); + + requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + + loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + } + + @Test + public void testApplyLoanSpecificDueDateChargeWithDisbursementDate() { + + final LocalDate todaysDate = Utils.getLocalDateOfTenant(); + + // 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); + + // Get loan details + GetLoansLoanIdResponse getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + validateLoanAccount(getLoansLoanIdResponse, Double.valueOf(principalAmount), Double.valueOf("0.00")); + + // Apply Loan Charge with specific due date + + final String feeAmount = "10.00"; + String payloadJSON = ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, feeAmount, false); + final PostChargesResponse postChargesResponse = ChargesHelper.createLoanCharge(requestSpec, responseSpec, payloadJSON); + assertNotNull(postChargesResponse); + final Integer loanChargeId = postChargesResponse.getResourceId(); + + payloadJSON = LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(loanChargeId.toString(), operationDate, feeAmount); + PostLoansLoanIdChargesResponse postLoansLoanIdChargesResponse = loanTransactionHelper.addChargeForLoan(loanId, payloadJSON, + responseSpec); + assertNotNull(postLoansLoanIdChargesResponse); + + // Get loan details expecting to have a delinquency classification + getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + validateLoanAccount(getLoansLoanIdResponse, Double.valueOf(principalAmount), Double.valueOf("10.00")); + + // Make a full repayment to close the Loan + Float amount = Float.valueOf("1010.00"); + PostLoansLoanIdTransactionsResponse loanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment(operationDate, amount, + loanId); + assertNotNull(loanIdTransactionsResponse); + log.info("Loan Transaction Id: {} {}", loanId, loanIdTransactionsResponse.getResourceId()); + + getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId); + assertNotNull(getLoansLoanIdResponse); + validateLoanAccount(getLoansLoanIdResponse, Double.valueOf("0.00"), Double.valueOf("0.00")); + + } + + private GetLoanProductsProductIdResponse createLoanProduct(final LoanTransactionHelper loanTransactionHelper, + final Integer delinquencyBucketId) { + final HashMap loanProductMap = new LoanProductTestBuilder().build(null, delinquencyBucketId); + final Integer loanProductId = loanTransactionHelper.getLoanProductId(Utils.convertToJson(loanProductMap)); + return loanTransactionHelper.getLoanProduct(loanProductId); + } + + private Integer createLoanAccount(final LoanTransactionHelper loanTransactionHelper, final String clientId, final String loanProductId, + final String operationDate) { + final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(principalAmount).withLoanTermFrequency("12") + .withLoanTermFrequencyAsMonths().withNumberOfRepayments("12").withRepaymentEveryAfter("1") + .withRepaymentFrequencyTypeAsMonths() // + .withInterestRatePerPeriod("0") // + .withExpectedDisbursementDate(operationDate) // + .withInterestTypeAsDecliningBalance() // + .withSubmittedOnDate(operationDate) // + .build(clientId, loanProductId, null); + final Integer loanId = loanTransactionHelper.getLoanId(loanApplicationJSON); + loanTransactionHelper.approveLoan(operationDate, principalAmount, loanId, null); + loanTransactionHelper.disburseLoanWithNetDisbursalAmount(operationDate, loanId, principalAmount); + return loanId; + } + + private void validateLoanAccount(GetLoansLoanIdResponse getLoansLoanIdResponse, Double principal, Double fees) { + assertNotNull(getLoansLoanIdResponse); + loanTransactionHelper.printRepaymentSchedule(getLoansLoanIdResponse); + loanTransactionHelper.validateLoanPrincipalOustandingBalance(getLoansLoanIdResponse, principal); + loanTransactionHelper.validateLoanFeesOustandingBalance(getLoansLoanIdResponse, fees); + loanTransactionHelper.validateLoanTotalOustandingBalance(getLoansLoanIdResponse, (principal + fees)); + } + +} 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 38119c95d..a10472e75 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 @@ -23,6 +23,8 @@ import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; import java.util.ArrayList; import java.util.HashMap; +import org.apache.fineract.client.models.PostChargesResponse; +import org.apache.fineract.client.util.JSON; import org.apache.fineract.integrationtests.common.CommonConstants; import org.apache.fineract.integrationtests.common.Utils; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; @@ -83,6 +85,8 @@ public final class ChargesHelper { public static final String FEE_ON_MONTH_DAY = "04 March"; private static final String MONTH_DAY_FORMAT = "dd MMM"; + private static final Gson GSON = new JSON().getGson(); + public static String getSavingsSpecifiedDueDateJSON() { final HashMap map = populateDefaultsForSavings(); map.put("chargeTimeType", CHARGE_SPECIFIED_DUE_DATE); @@ -385,6 +389,12 @@ public final class ChargesHelper { return Utils.performServerPost(requestSpec, responseSpec, CREATE_CHARGES_URL, request, "resourceId"); } + public static PostChargesResponse createLoanCharge(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, + final String payload) { + final String response = Utils.performServerPost(requestSpec, responseSpec, CREATE_CHARGES_URL, payload, null); + return GSON.fromJson(response, PostChargesResponse.class); + } + public static ArrayList getCharges(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) { return (ArrayList) Utils.performServerGet(requestSpec, responseSpec, CHARGES_URL + "?" + Utils.TENANT_IDENTIFIER, ""); } 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 e80419c36..1719595e1 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 @@ -48,6 +48,7 @@ import org.apache.fineract.client.models.GetLoansLoanIdRepaymentSchedule; import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdSummary; import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse; +import org.apache.fineract.client.models.PostLoansLoanIdChargesResponse; import org.apache.fineract.client.models.PostLoansLoanIdResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdResponse; @@ -490,6 +491,14 @@ public class LoanTransactionHelper { return (Integer) response.get("resourceId"); } + public PostLoansLoanIdChargesResponse addChargeForLoan(final Integer loanId, final String payload, + final ResponseSpecification responseSpecParam) { + log.info("--------------------------------- ADD CHARGES FOR LOAN --------------------------------"); + final String ADD_CHARGES_URL = LOAN_ACCOUNT_URL + "/" + loanId + "/charges?" + Utils.TENANT_IDENTIFIER; + final String response = Utils.performServerPost(requestSpec, responseSpecParam, ADD_CHARGES_URL, payload); + return GSON.fromJson(response, PostLoansLoanIdChargesResponse.class); + } + public Object addChargesForAllreadyDisursedLoan(final Integer loanId, final String request, final ResponseSpecification responseSpecification) { final String ADD_CHARGES_URL = "/fineract-provider/api/v1/loans/" + loanId + "/charges?" + Utils.TENANT_IDENTIFIER; @@ -1160,4 +1169,20 @@ public class LoanTransactionHelper { } } + public void validateLoanFeesOustandingBalance(GetLoansLoanIdResponse getLoansLoanIdResponse, Double amountExpected) { + GetLoansLoanIdSummary getLoansLoanIdSummary = getLoansLoanIdResponse.getSummary(); + if (getLoansLoanIdSummary != null) { + log.info("Loan with Fees Outstanding Balance {} expected {}", getLoansLoanIdSummary.getFeeChargesOutstanding(), amountExpected); + assertEquals(amountExpected, getLoansLoanIdSummary.getFeeChargesOutstanding()); + } + } + + public void validateLoanTotalOustandingBalance(GetLoansLoanIdResponse getLoansLoanIdResponse, Double amountExpected) { + GetLoansLoanIdSummary getLoansLoanIdSummary = getLoansLoanIdResponse.getSummary(); + if (getLoansLoanIdSummary != null) { + log.info("Loan with Total Outstanding Balance {} expected {}", getLoansLoanIdSummary.getTotalOutstanding(), amountExpected); + assertEquals(amountExpected, getLoansLoanIdSummary.getTotalOutstanding()); + } + } + }