From 52068ebf35732c938cabcae4e14a2fa409aac450 Mon Sep 17 00:00:00 2001 From: Ruchi Dhamankar Date: Thu, 7 Dec 2023 18:50:14 +0530 Subject: [PATCH] FINERACT-1968: Adding Installment percentage fee Adv Payment allocation --- .../LoanChargeWritePlatformServiceImpl.java | 13 ++ ...lingWithAdvancedPaymentAllocationTest.java | 180 ++++++++++++++++++ .../common/loans/LoanTransactionHelper.java | 6 + 3 files changed, 199 insertions(+) create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargeTypeInstallmentFeeErrorHandlingWithAdvancedPaymentAllocationTest.java 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 5769ca3f4cd..5e96c43d996 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 @@ -72,6 +72,7 @@ import org.apache.fineract.portfolio.accountdetails.domain.AccountType; import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; +import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; import org.apache.fineract.portfolio.charge.exception.ChargeCannotBeAppliedToException; import org.apache.fineract.portfolio.charge.exception.ChargeCannotBeUpdatedException; import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeAddedException; @@ -183,6 +184,18 @@ public CommandProcessingResult addLoanCharge(final Long loanId, final JsonComman final Long chargeDefinitionId = command.longValueOfParameterNamed("chargeId"); final Charge chargeDefinition = this.chargeRepository.findOneWithNotFoundDetection(chargeDefinitionId); + /* + * TODO: remove this check once handling for Installment fee charges is implemented for Advanced Payment + * strategy + */ + if (ChargeTimeType.fromInt(chargeDefinition.getChargeTimeType()).isInstalmentFee() + && AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY + .equals(loan.transactionProcessingStrategy())) { + final String errorMessageInstallmentChargeNotSupported = "Charge with identifier " + chargeDefinition.getId() + + " cannot be applied: Installment fee charges are not supported for Advanced payment allocation strategy"; + throw new ChargeCannotBeAppliedToException("loan", errorMessageInstallmentChargeNotSupported, chargeDefinition.getId()); + } + if (loan.isDisbursed() && chargeDefinition.isDisbursementCharge()) { // validates whether any pending disbursements are available to // apply this charge diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargeTypeInstallmentFeeErrorHandlingWithAdvancedPaymentAllocationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargeTypeInstallmentFeeErrorHandlingWithAdvancedPaymentAllocationTest.java new file mode 100644 index 00000000000..af8a098463a --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargeTypeInstallmentFeeErrorHandlingWithAdvancedPaymentAllocationTest.java @@ -0,0 +1,180 @@ +/** + * 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.assertEquals; +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.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.fineract.client.models.AdvancedPaymentData; +import org.apache.fineract.client.models.PaymentAllocationOrder; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.integrationtests.common.BusinessDateHelper; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.CommonConstants; +import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.accounting.Account; +import org.apache.fineract.integrationtests.common.accounting.AccountHelper; +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.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; +import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class LoanChargeTypeInstallmentFeeErrorHandlingWithAdvancedPaymentAllocationTest { + + private static LoanTransactionHelper LOAN_TRANSACTION_HELPER; + private static ResponseSpecification RESPONSE_SPEC; + private static RequestSpecification REQUEST_SPEC; + private static ClientHelper CLIENT_HELPER; + private static AccountHelper ACCOUNT_HELPER; + + @BeforeAll + public static void setupTests() { + Utils.initializeRESTAssured(); + REQUEST_SPEC = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + REQUEST_SPEC.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + RESPONSE_SPEC = new ResponseSpecBuilder().expectStatusCode(200).build(); + LOAN_TRANSACTION_HELPER = new LoanTransactionHelper(REQUEST_SPEC, RESPONSE_SPEC); + CLIENT_HELPER = new ClientHelper(REQUEST_SPEC, RESPONSE_SPEC); + ACCOUNT_HELPER = new AccountHelper(REQUEST_SPEC, RESPONSE_SPEC); + } + + /* + * TODO: To be disabled when Installment Fee Charges are handled for Advanced Payment Allocation + */ + @Test + public void addingLoanChargeTypeInstallmentFeeForAdvancedPaymentAllocationGivesErrorTest() { + try { + // Set business date + LocalDate businessDate = LocalDate.of(2023, 3, 15); + + GlobalConfigurationHelper.updateIsBusinessDateEnabled(REQUEST_SPEC, RESPONSE_SPEC, Boolean.TRUE); + BusinessDateHelper.updateBusinessDate(REQUEST_SPEC, RESPONSE_SPEC, BusinessDateType.BUSINESS_DATE, businessDate); + + // Accounts oof periodic accrual + final Account assetAccount = ACCOUNT_HELPER.createAssetAccount(); + final Account incomeAccount = ACCOUNT_HELPER.createIncomeAccount(); + final Account expenseAccount = ACCOUNT_HELPER.createExpenseAccount(); + final Account overpaymentAccount = ACCOUNT_HELPER.createLiabilityAccount(); + + final ResponseSpecification errorResponse = new ResponseSpecBuilder().expectStatusCode(403).build(); + final LoanTransactionHelper validationErrorHelper = new LoanTransactionHelper(REQUEST_SPEC, errorResponse); + + // Loan ExternalId + String loanExternalIdStr = UUID.randomUUID().toString(); + + final Integer clientId = CLIENT_HELPER.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + + final Integer loanProductId = createLoanProduct(assetAccount, incomeAccount, expenseAccount, overpaymentAccount); + + final Integer loanId = createLoanAccount(clientId, loanProductId, loanExternalIdStr); + + // disburse principal amount + LOAN_TRANSACTION_HELPER.disburseLoanWithTransactionAmount("15 February 2023", loanId, "1000"); + + // add loan charge + // apply Installment fee + Integer installmentFeeCharge = ChargesHelper.createCharges(REQUEST_SPEC, RESPONSE_SPEC, + ChargesHelper.getLoanInstallmentJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "50", false)); + + List> loanChargeErrorData = (List>) validationErrorHelper + .addChargesForLoanWithError(loanId, + LoanTransactionHelper.getInstallmentChargesForLoanAsJSON(String.valueOf(installmentFeeCharge), "50"), + CommonConstants.RESPONSE_ERROR); + assertNotNull(loanChargeErrorData); + + assertEquals( + "Charge with identifier %d cannot be applied: Installment fee charges are not supported for Advanced payment allocation strategy" + .formatted(installmentFeeCharge), + loanChargeErrorData.get(0).get("defaultUserMessage")); + assertEquals("error.msg.charge.cannot.be.applied.toloan", + loanChargeErrorData.get(0).get(CommonConstants.RESPONSE_ERROR_MESSAGE_CODE)); + + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(REQUEST_SPEC, RESPONSE_SPEC, Boolean.FALSE); + } + } + + private Integer createLoanProduct(final Account... accounts) { + String futureInstallmentAllocationRule = "NEXT_INSTALLMENT"; + AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation(futureInstallmentAllocationRule); + String loanProductCreateJSON = new LoanProductTestBuilder().withPrincipal("15,000.00").withNumberOfRepayments("4") + .withRepaymentAfterEvery("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("0") + .withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualInstallments().withInterestTypeAsDecliningBalance() + .withAccountingRulePeriodicAccrual(accounts).withInterestCalculationPeriodTypeAsRepaymentPeriod(true) + .addAdvancedPaymentAllocation(defaultAllocation).withLoanScheduleType(LoanScheduleType.PROGRESSIVE).withMultiDisburse() + .withDisallowExpectedDisbursements(true).build(); + return LOAN_TRANSACTION_HELPER.getLoanProductId(loanProductCreateJSON); + + } + + private AdvancedPaymentData createDefaultPaymentAllocation(String futureInstallmentAllocationRule) { + AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData(); + advancedPaymentData.setTransactionType("DEFAULT"); + advancedPaymentData.setFutureInstallmentAllocationRule(futureInstallmentAllocationRule); + + List paymentAllocationOrders = getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_PENALTY, + PaymentAllocationType.PAST_DUE_FEE, PaymentAllocationType.PAST_DUE_PRINCIPAL, PaymentAllocationType.PAST_DUE_INTEREST, + PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_PRINCIPAL, + PaymentAllocationType.DUE_INTEREST, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE, + PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST); + + advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders); + return advancedPaymentData; + } + + private List getPaymentAllocationOrder(PaymentAllocationType... paymentAllocationTypes) { + AtomicInteger integer = new AtomicInteger(1); + return Arrays.stream(paymentAllocationTypes).map(pat -> { + PaymentAllocationOrder paymentAllocationOrder = new PaymentAllocationOrder(); + paymentAllocationOrder.setPaymentAllocationRule(pat.name()); + paymentAllocationOrder.setOrder(integer.getAndIncrement()); + return paymentAllocationOrder; + }).toList(); + } + + private Integer createLoanAccount(final Integer clientID, final Integer loanProductID, final String externalId) { + + String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency("60") + .withLoanTermFrequencyAsDays().withNumberOfRepayments("4").withRepaymentEveryAfter("15").withRepaymentFrequencyTypeAsDays() + .withInterestRatePerPeriod("0").withInterestTypeAsFlatBalance().withAmortizationTypeAsEqualPrincipalPayments() + .withInterestCalculationPeriodTypeSameAsRepaymentPeriod().withExpectedDisbursementDate("15 February 2023") + .withSubmittedOnDate("15 February 2023").withLoanType("individual").withExternalId(externalId) + .withRepaymentStrategy("advanced-payment-allocation-strategy").build(clientID.toString(), loanProductID.toString(), null); + + final Integer loanId = LOAN_TRANSACTION_HELPER.getLoanId(loanApplicationJSON); + LOAN_TRANSACTION_HELPER.approveLoan("15 February 2023", "1000", loanId, null); + return loanId; + } +} 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 4794baf703d..ba30cc35b88 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 @@ -1965,4 +1965,10 @@ public PostLoansLoanIdTransactionsResponse writeOffLoanAccount(final String loan final PostLoansLoanIdTransactionsRequest request) { return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "writeoff")); } + + public Object addChargesForLoanWithError(final Integer loanId, final String request, final String jsonAttributeToGetBack) { + log.info("--------------------------------- ADD CHARGES FOR LOAN --------------------------------"); + final String ADD_CHARGES_URL = LOAN_ACCOUNT_URL + "/" + loanId + "/charges?" + Utils.TENANT_IDENTIFIER; + return Utils.performServerPost(requestSpec, responseSpec, ADD_CHARGES_URL, request, jsonAttributeToGetBack); + } }