diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java index 5c34901bc..68f50c177 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/DefaultLoanLifecycleStateMachine.java @@ -37,17 +37,20 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi @Override public LoanStatus dryTransition(final LoanEvent loanEvent, final Loan loan) { - return getNextState(loanEvent, loan); + LoanStatus nextStatus = getNextState(loanEvent, loan); + return nextStatus != null ? nextStatus : loan.status(); } @Override public void transition(final LoanEvent loanEvent, final Loan loan) { LoanStatus newState = getNextState(loanEvent, loan); - loan.setLoanStatus(newState.getValue()); + if (newState != null) { + loan.setLoanStatus(newState.getValue()); - if (isNotLoanCreation(loanEvent)) { - // in case of Loan creation, a LoanCreatedBusinessEvent is also raised, no need to send a status change - businessEventNotifierService.notifyPostBusinessEvent(new LoanStatusChangedBusinessEvent(loan)); + if (isNotLoanCreation(loanEvent)) { + // in case of Loan creation, a LoanCreatedBusinessEvent is also raised, no need to send a status change + businessEventNotifierService.notifyPostBusinessEvent(new LoanStatusChangedBusinessEvent(loan)); + } } } @@ -62,7 +65,7 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi } LoanStatus from = loan.status(); - LoanStatus newState = from; + LoanStatus newState = null; switch (loanEvent) { case LOAN_REJECTED: @@ -97,7 +100,7 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi break; case LOAN_CHARGE_PAYMENT: case LOAN_REPAYMENT_OR_WAIVER: - if (anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE, LoanStatus.CLOSED_OBLIGATIONS_MET, LoanStatus.OVERPAID)) { + if (anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED_OBLIGATIONS_MET, LoanStatus.OVERPAID)) { newState = activeTransition(); } break; @@ -116,11 +119,6 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi newState = closedRescheduleOutstandingAmountTransition(); } break; - case INTERST_REBATE_OWED: - if (anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED_OBLIGATIONS_MET)) { - newState = closeObligationsMetTransition(); - } - break; case LOAN_OVERPAYMENT: if (anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED_OBLIGATIONS_MET, LoanStatus.ACTIVE)) { newState = overpaidTransition(); @@ -141,8 +139,6 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi case LOAN_WITHDRAW_TRANSFER: newState = activeTransition(); break; - case LOAN_CLOSED: - break; case WRITE_OFF_OUTSTANDING_UNDO: if (anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED_WRITTEN_OFF)) { newState = activeTransition(); @@ -151,6 +147,11 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi case LOAN_CREDIT_BALANCE_REFUND: newState = closeObligationsMetTransition(); break; + case LOAN_CHARGE_ADDED: + if (anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED_OBLIGATIONS_MET)) { + newState = activeTransition(); + } + break; case LOAN_CHARGEBACK: if (anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED_OBLIGATIONS_MET, LoanStatus.OVERPAID)) { newState = activeTransition(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index c84784782..3e2042365 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -572,8 +572,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { public void addLoanCharge(final LoanCharge loanCharge) { - validateLoanIsNotClosed(loanCharge); - if (isChargesAdditionAllowed() && loanCharge.isDueAtDisbursement()) { // Note: added this constraint to restrict adding disbursement // charges to a loan @@ -596,7 +594,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { loanCharge.update(this); final BigDecimal amount = calculateAmountPercentageAppliedTo(loanCharge); - BigDecimal chargeAmt = BigDecimal.ZERO; + BigDecimal chargeAmt; BigDecimal totalChargeAmt = BigDecimal.ZERO; if (loanCharge.getChargeCalculation().isPercentageBased()) { chargeAmt = loanCharge.getPercentage(); @@ -626,6 +624,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { wrapper.reprocess(getCurrency(), getDisbursementDate(), getRepaymentScheduleInstallments(), charges()); updateLoanSummaryDerivedFields(); + loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGE_ADDED, this); } public ChangedTransactionDetail reprocessTransactions() { @@ -743,7 +742,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { private void validateChargeHasValidSpecifiedDateIfApplicable(final LoanCharge loanCharge, final LocalDate disbursementDate, final LocalDate lastRepaymentPeriodDueDate) { - if (loanCharge.isSpecifiedDueDate() + if (isInterestBearing() && loanCharge.isSpecifiedDueDate() && !loanCharge.isDueForCollectionFromAndUpToAndIncluding(disbursementDate, lastRepaymentPeriodDueDate)) { final String defaultUserMessage = "This charge with specified due date cannot be added as the it is not in schedule range."; throw new LoanChargeCannotBeAddedException("loanCharge", "specified.due.date.outside.range", defaultUserMessage, @@ -5242,6 +5241,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return null; } + public boolean isInterestBearing() { + return getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled() + && BigDecimal.ZERO.compareTo(getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate()) < 0; + } + public LocalDate getExpectedMaturityDate() { return this.expectedMaturityDate; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanEvent.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanEvent.java index e0c11e21f..f814dae82 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanEvent.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanEvent.java @@ -49,5 +49,6 @@ public enum LoanEvent { LOAN_REJECT_TRANSFER, // LOAN_WITHDRAW_TRANSFER, // LOAN_CREDIT_BALANCE_REFUND, // - LOAN_CHARGEBACK; + LOAN_CHARGEBACK, // + LOAN_CHARGE_ADDED; } 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 acf79de72..8e8ed91e3 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 @@ -142,7 +142,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen } for (final LoanTransaction loanTransaction : transactionstoBeProcessed) { - + // TODO: analyze and remove this if (!loanTransaction.getTypeOf().equals(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN)) { final Comparator byDate = new Comparator() { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 0ee373287..ee6ba0cd8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -1773,6 +1773,22 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } } + if (!loan.isInterestBearing() && loanCharge.isSpecifiedDueDate() && loanCharge.getDueDate().isAfter(loan.getMaturityDate())) { + LoanRepaymentScheduleInstallment latestRepaymentScheduleInstalment = loan.getRepaymentScheduleInstallments() + .get(loan.getLoanRepaymentScheduleInstallmentsSize() - 1); + if (loanCharge.getDueDate().isAfter(latestRepaymentScheduleInstalment.getDueDate())) { + if (latestRepaymentScheduleInstalment.isAdditional()) { + latestRepaymentScheduleInstalment.updateDueDate(loanCharge.getDueDate()); + } else { + final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, + (loan.getLoanRepaymentScheduleInstallmentsSize() + 1), latestRepaymentScheduleInstalment.getDueDate(), + loanCharge.getDueDate(), BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null); + installment.markAsAdditional(); + loan.addLoanRepaymentScheduleInstallment(installment); + } + } + } + loan.addLoanCharge(loanCharge); this.loanChargeRepository.saveAndFlush(loanCharge); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanSpecificDueDateChargeAfterMaturityTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanSpecificDueDateChargeAfterMaturityTest.java new file mode 100644 index 000000000..05f7f11ed --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanSpecificDueDateChargeAfterMaturityTest.java @@ -0,0 +1,482 @@ +/** + * 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 io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.path.json.JsonPath; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import org.apache.fineract.integrationtests.common.ClientHelper; +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.accounting.PeriodicAccrualAccountingHelper; +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.LoanStatusChecker; +import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; +import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@SuppressWarnings({ "unchecked" }) +public class LoanSpecificDueDateChargeAfterMaturityTest { + + public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL"; + public static final String MINIMUM_OPENING_BALANCE = "1000.0"; + public static final String DEPOSIT_AMOUNT = "7000"; + private static final Logger LOG = LoggerFactory.getLogger(LoanSpecificDueDateChargeAfterMaturityTest.class); + private static final String DATE_OF_JOINING = "01 January 2011"; + private static final Float LP_PRINCIPAL = 10000.0f; + private static final String LP_REPAYMENTS = "1"; + private static final String LP_REPAYMENT_PERIOD = "1"; + private static final String EXPECTED_DISBURSAL_DATE = "04 March 2011"; + private static final String LOAN_APPLICATION_SUBMISSION_DATE = "03 March 2011"; + private static final String LOAN_TERM_FREQUENCY = "1"; + private static final String INDIVIDUAL_LOAN = "individual"; + private static RequestSpecification requestSpec; + private static ResponseSpecification responseSpec; + private LoanTransactionHelper loanTransactionHelper; + private AccountHelper accountHelper; + private PeriodicAccrualAccountingHelper periodicAccrualAccountingHelper; + private DateTimeFormatter dateFormatter = new DateTimeFormatterBuilder().appendPattern("dd MMMM yyyy").toFormatter(); + + public static Integer createSavingsProduct(final String minOpenningBalance, final Account... accounts) { + LOG.info("------------------------------CREATING NEW SAVINGS PRODUCT ---------------------------------------"); + final String savingsProductJSON = new SavingsProductHelper().withInterestCompoundingPeriodTypeAsDaily() // + .withInterestPostingPeriodTypeAsQuarterly() // + .withInterestCalculationPeriodTypeAsDailyBalance() // + .withMinimumOpenningBalance(minOpenningBalance).withAccountingRuleAsCashBased(accounts).build(); + return SavingsProductHelper.createSavingsProduct(savingsProductJSON, requestSpec, responseSpec); + } + + @BeforeEach + public void setup() { + Utils.initializeRESTAssured(); + requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + requestSpec.header("Fineract-Platform-TenantId", "default"); + responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + + this.loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec); + this.accountHelper = new AccountHelper(requestSpec, responseSpec); + this.periodicAccrualAccountingHelper = new PeriodicAccrualAccountingHelper(requestSpec, responseSpec); + } + + @Test + public void onlyNonInterestBearingLoanCanAcceptChargeAfterMaturity() { + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + + final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingWithInterestRecalculationEnabled(assetAccount, + incomeAccount, expenseAccount, overpaymentAccount); + + final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, DATE_OF_JOINING); + + final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1"); + + final float PENALTY_PORTION = 100.0f; + + Integer flatSpecifiedDueDate = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper + .getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, String.valueOf(PENALTY_PORTION), true)); + + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + loanStatusHashMap = this.loanTransactionHelper.approveLoan(EXPECTED_DISBURSAL_DATE, loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + LocalDate targetDate = LocalDate.of(2011, 3, 4); + final String loanDisbursementDate = dateFormatter.format(targetDate); + + String loanDetails = this.loanTransactionHelper.getLoanDetails(requestSpec, responseSpec, loanID); + loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithNetDisbursalAmount(loanDisbursementDate, loanID, + JsonPath.from(loanDetails).get("netDisbursalAmount").toString()); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + targetDate = LocalDate.of(2011, 4, 5); + final String penaltyCharge1AddedDate = dateFormatter.format(targetDate); + + final String ADD_CHARGES_URL = "/fineract-provider/api/v1/loans/" + loanID + "/charges?" + Utils.TENANT_IDENTIFIER; + ResponseSpecification response403Spec = new ResponseSpecBuilder().expectStatusCode(403).build(); + final HashMap response = Utils.performServerPost(requestSpec, response403Spec, ADD_CHARGES_URL, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(flatSpecifiedDueDate), penaltyCharge1AddedDate, + String.valueOf(PENALTY_PORTION)), + ""); + assertEquals("validation.msg.domain.rule.violation", response.get("userMessageGlobalisationCode")); + + } + + @Test + public void checkPeriodicAccrualAccountingAPIFlow() { + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + + final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingNoInterest(assetAccount, incomeAccount, expenseAccount, + overpaymentAccount); + + final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, DATE_OF_JOINING); + + final Integer loanID = applyForLoanApplication(clientID, loanProductID, "0"); + + final float FEE_PORTION = 50.0f; + final float PENALTY_PORTION = 100.0f; + final float NEXT_FEE_PORTION = 55.0f; + final float NEXT_PENALTY_PORTION = 105.0f; + + Integer flat = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, String.valueOf(FEE_PORTION), false)); + Integer flatSpecifiedDueDate = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper + .getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, String.valueOf(PENALTY_PORTION), true)); + + Integer flatNext = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper + .getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, String.valueOf(NEXT_FEE_PORTION), false)); + Integer flatSpecifiedDueDateNext = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper + .getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, String.valueOf(NEXT_PENALTY_PORTION), true)); + + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + loanStatusHashMap = this.loanTransactionHelper.approveLoan(EXPECTED_DISBURSAL_DATE, loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + LocalDate targetDate = LocalDate.of(2011, 3, 4); + final String loanDisbursementDate = dateFormatter.format(targetDate); + + String loanDetails = this.loanTransactionHelper.getLoanDetails(requestSpec, responseSpec, loanID); + loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithNetDisbursalAmount(loanDisbursementDate, loanID, + JsonPath.from(loanDetails).get("netDisbursalAmount").toString()); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + ArrayList loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(2, loanSchedule.size()); + assertEquals(0, loanSchedule.get(1).get("feeChargesDue")); + assertEquals(0, loanSchedule.get(1).get("feeChargesOutstanding")); + assertEquals(0, loanSchedule.get(1).get("penaltyChargesDue")); + assertEquals(0, loanSchedule.get(1).get("penaltyChargesOutstanding")); + assertEquals(10000.0f, loanSchedule.get(1).get("totalDueForPeriod")); + assertEquals(10000.0f, loanSchedule.get(1).get("totalOutstandingForPeriod")); + targetDate = LocalDate.of(2011, 4, 5); + final String penaltyCharge1AddedDate = dateFormatter.format(targetDate); + Integer penalty1LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanID, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(flatSpecifiedDueDate), penaltyCharge1AddedDate, + String.valueOf(PENALTY_PORTION))); + + this.loanTransactionHelper.noAccrualTransactionForRepayment(loanID); + + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(3, loanSchedule.size()); + assertEquals(0, loanSchedule.get(2).get("feeChargesDue")); + assertEquals(0, loanSchedule.get(2).get("feeChargesOutstanding")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("penaltyChargesDue")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("penaltyChargesOutstanding")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("totalDueForPeriod")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("totalOutstandingForPeriod")); + assertEquals(LocalDate.of(2011, 4, 5), LocalDate.of((int) ((List) loanSchedule.get(2).get("dueDate")).get(0), + (int) ((List) loanSchedule.get(2).get("dueDate")).get(1), (int) ((List) loanSchedule.get(2).get("dueDate")).get(2))); + + String runOnDateStr = penaltyCharge1AddedDate; + LocalDate runOnDate = targetDate; + this.periodicAccrualAccountingHelper.runPeriodicAccrualAccounting(runOnDateStr); + + this.loanTransactionHelper.checkAccrualTransactionForRepayment(runOnDate, 0.0f, 0.0f, PENALTY_PORTION, loanID); + + targetDate = LocalDate.of(2011, 4, 6); + final String feeCharge1AddedDate = dateFormatter.format(targetDate); + Integer fee1LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanID, LoanTransactionHelper + .getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(flat), feeCharge1AddedDate, String.valueOf(FEE_PORTION))); + + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(3, loanSchedule.size()); + assertEquals(FEE_PORTION, loanSchedule.get(2).get("feeChargesDue")); + assertEquals(FEE_PORTION, loanSchedule.get(2).get("feeChargesOutstanding")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("penaltyChargesDue")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("penaltyChargesOutstanding")); + assertEquals(PENALTY_PORTION + FEE_PORTION, loanSchedule.get(2).get("totalDueForPeriod")); + assertEquals(PENALTY_PORTION + FEE_PORTION, loanSchedule.get(2).get("totalOutstandingForPeriod")); + assertEquals(LocalDate.of(2011, 4, 6), LocalDate.of((int) ((List) loanSchedule.get(2).get("dueDate")).get(0), + (int) ((List) loanSchedule.get(2).get("dueDate")).get(1), (int) ((List) loanSchedule.get(2).get("dueDate")).get(2))); + + targetDate = LocalDate.of(2011, 4, 7); + final String penaltyCharge2AddedDate = dateFormatter.format(targetDate); + Integer penalty2LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanID, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(flatSpecifiedDueDateNext), + penaltyCharge2AddedDate, String.valueOf(NEXT_PENALTY_PORTION))); + + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(3, loanSchedule.size()); + assertEquals(FEE_PORTION, loanSchedule.get(2).get("feeChargesDue")); + assertEquals(FEE_PORTION, loanSchedule.get(2).get("feeChargesOutstanding")); + assertEquals(PENALTY_PORTION + NEXT_PENALTY_PORTION, loanSchedule.get(2).get("penaltyChargesDue")); + assertEquals(PENALTY_PORTION + NEXT_PENALTY_PORTION, loanSchedule.get(2).get("penaltyChargesOutstanding")); + assertEquals(PENALTY_PORTION + FEE_PORTION + NEXT_PENALTY_PORTION, loanSchedule.get(2).get("totalDueForPeriod")); + assertEquals(PENALTY_PORTION + FEE_PORTION + NEXT_PENALTY_PORTION, loanSchedule.get(2).get("totalOutstandingForPeriod")); + assertEquals(LocalDate.of(2011, 4, 7), LocalDate.of((int) ((List) loanSchedule.get(2).get("dueDate")).get(0), + (int) ((List) loanSchedule.get(2).get("dueDate")).get(1), (int) ((List) loanSchedule.get(2).get("dueDate")).get(2))); + + targetDate = LocalDate.of(2011, 4, 8); + final String feeCharge2AddedDate = dateFormatter.format(targetDate); + Integer fee2LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanID, LoanTransactionHelper + .getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(flatNext), feeCharge2AddedDate, String.valueOf(NEXT_FEE_PORTION))); + + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(3, loanSchedule.size()); + assertEquals(FEE_PORTION + NEXT_FEE_PORTION, loanSchedule.get(2).get("feeChargesDue")); + assertEquals(FEE_PORTION + NEXT_FEE_PORTION, loanSchedule.get(2).get("feeChargesOutstanding")); + assertEquals(PENALTY_PORTION + NEXT_PENALTY_PORTION, loanSchedule.get(2).get("penaltyChargesDue")); + assertEquals(PENALTY_PORTION + NEXT_PENALTY_PORTION, loanSchedule.get(2).get("penaltyChargesOutstanding")); + assertEquals(PENALTY_PORTION + FEE_PORTION + NEXT_PENALTY_PORTION + NEXT_FEE_PORTION, loanSchedule.get(2).get("totalDueForPeriod")); + assertEquals(PENALTY_PORTION + FEE_PORTION + NEXT_PENALTY_PORTION + NEXT_FEE_PORTION, + loanSchedule.get(2).get("totalOutstandingForPeriod")); + assertEquals(LocalDate.of(2011, 4, 8), LocalDate.of((int) ((List) loanSchedule.get(2).get("dueDate")).get(0), + (int) ((List) loanSchedule.get(2).get("dueDate")).get(1), (int) ((List) loanSchedule.get(2).get("dueDate")).get(2))); + + runOnDateStr = penaltyCharge2AddedDate; + runOnDate = LocalDate.of(2011, 4, 7); + this.periodicAccrualAccountingHelper.runPeriodicAccrualAccounting(runOnDateStr); + this.loanTransactionHelper.checkAccrualTransactionForRepayment(runOnDate, 0.0f, FEE_PORTION, NEXT_PENALTY_PORTION, loanID); + } + + @Test + public void reopenClosedLoan() { + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + + final Integer loanProductID = createLoanProductWithPeriodicAccrualAccountingNoInterest(assetAccount, incomeAccount, expenseAccount, + overpaymentAccount); + + final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, DATE_OF_JOINING); + + final Integer loanID = applyForLoanApplication(clientID, loanProductID, "0"); + + final float FEE_PORTION = 50.0f; + final float PENALTY_PORTION = 100.0f; + final float NEXT_FEE_PORTION = 55.0f; + final float NEXT_PENALTY_PORTION = 105.0f; + + Integer flat = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, String.valueOf(FEE_PORTION), false)); + Integer flatSpecifiedDueDate = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper + .getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, String.valueOf(PENALTY_PORTION), true)); + + Integer flatNext = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper + .getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, String.valueOf(NEXT_FEE_PORTION), false)); + Integer flatSpecifiedDueDateNext = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper + .getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, String.valueOf(NEXT_PENALTY_PORTION), true)); + + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + loanStatusHashMap = this.loanTransactionHelper.approveLoan(EXPECTED_DISBURSAL_DATE, loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + LocalDate targetDate = LocalDate.of(2011, 3, 4); + final String loanDisbursementDate = dateFormatter.format(targetDate); + + String loanDetails = this.loanTransactionHelper.getLoanDetails(requestSpec, responseSpec, loanID); + loanStatusHashMap = this.loanTransactionHelper.disburseLoanWithNetDisbursalAmount(loanDisbursementDate, loanID, + JsonPath.from(loanDetails).get("netDisbursalAmount").toString()); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + ArrayList loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(2, loanSchedule.size()); + assertEquals(0, loanSchedule.get(1).get("feeChargesDue")); + assertEquals(0, loanSchedule.get(1).get("feeChargesOutstanding")); + assertEquals(0, loanSchedule.get(1).get("penaltyChargesDue")); + assertEquals(0, loanSchedule.get(1).get("penaltyChargesOutstanding")); + assertEquals(10000.0f, loanSchedule.get(1).get("totalDueForPeriod")); + assertEquals(10000.0f, loanSchedule.get(1).get("totalOutstandingForPeriod")); + + targetDate = LocalDate.of(2011, 3, 10); + String repaymentDateStr = dateFormatter.format(targetDate); + loanTransactionHelper.makeRepayment(repaymentDateStr, 10000.0f, loanID); + + loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanAccountIsClosed(loanStatusHashMap); + + targetDate = LocalDate.of(2011, 4, 13); + final String penaltyCharge1AddedDate = dateFormatter.format(targetDate); + Integer penalty1LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanID, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(flatSpecifiedDueDate), penaltyCharge1AddedDate, + String.valueOf(PENALTY_PORTION))); + + loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(3, loanSchedule.size()); + assertEquals(0, loanSchedule.get(2).get("feeChargesDue")); + assertEquals(0, loanSchedule.get(2).get("feeChargesOutstanding")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("penaltyChargesDue")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("penaltyChargesOutstanding")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("totalDueForPeriod")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("totalOutstandingForPeriod")); + assertEquals(LocalDate.of(2011, 4, 13), LocalDate.of((int) ((List) loanSchedule.get(2).get("dueDate")).get(0), + (int) ((List) loanSchedule.get(2).get("dueDate")).get(1), (int) ((List) loanSchedule.get(2).get("dueDate")).get(2))); + + targetDate = LocalDate.of(2011, 4, 14); + String runOnDateStr = dateFormatter.format(targetDate); + this.periodicAccrualAccountingHelper.runPeriodicAccrualAccounting(runOnDateStr); + + // Transaction date will be the due date of the instalment (in case of N+1 scenario) + this.loanTransactionHelper.checkAccrualTransactionForRepayment(LocalDate.of(2011, 4, 13), 0.0f, 0.0f, PENALTY_PORTION, loanID); + + loanTransactionHelper.waiveChargesForLoan(loanID, penalty1LoanChargeId, + LoanTransactionHelper.getWaiveChargeJSON(String.valueOf(2))); + + loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanAccountIsClosed(loanStatusHashMap); + + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(3, loanSchedule.size()); + assertEquals(0, loanSchedule.get(2).get("feeChargesDue")); + assertEquals(0, loanSchedule.get(2).get("feeChargesOutstanding")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("penaltyChargesDue")); + assertEquals(0.0f, loanSchedule.get(2).get("penaltyChargesOutstanding")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("penaltyChargesWaived")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("totalDueForPeriod")); + assertEquals(0.0f, loanSchedule.get(2).get("totalOutstandingForPeriod")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("totalWaivedForPeriod")); + assertEquals(LocalDate.of(2011, 4, 13), LocalDate.of((int) ((List) loanSchedule.get(2).get("dueDate")).get(0), + (int) ((List) loanSchedule.get(2).get("dueDate")).get(1), (int) ((List) loanSchedule.get(2).get("dueDate")).get(2))); + + targetDate = LocalDate.of(2011, 4, 14); + String penaltyCharge2AddedDate = dateFormatter.format(targetDate); + Integer penalty2LoanChargeId = this.loanTransactionHelper.addChargesForLoan(loanID, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(flatSpecifiedDueDate), penaltyCharge2AddedDate, + String.valueOf(PENALTY_PORTION))); + + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(3, loanSchedule.size()); + assertEquals(0, loanSchedule.get(2).get("feeChargesDue")); + assertEquals(0, loanSchedule.get(2).get("feeChargesOutstanding")); + assertEquals(PENALTY_PORTION + PENALTY_PORTION, loanSchedule.get(2).get("penaltyChargesDue")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("penaltyChargesOutstanding")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("penaltyChargesWaived")); + assertEquals(PENALTY_PORTION + PENALTY_PORTION, loanSchedule.get(2).get("totalDueForPeriod")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("totalOutstandingForPeriod")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("totalWaivedForPeriod")); + assertEquals(LocalDate.of(2011, 4, 14), LocalDate.of((int) ((List) loanSchedule.get(2).get("dueDate")).get(0), + (int) ((List) loanSchedule.get(2).get("dueDate")).get(1), (int) ((List) loanSchedule.get(2).get("dueDate")).get(2))); + + loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + targetDate = LocalDate.of(2011, 4, 15); + repaymentDateStr = dateFormatter.format(targetDate); + loanTransactionHelper.makeRepayment(repaymentDateStr, PENALTY_PORTION, loanID); + + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(3, loanSchedule.size()); + assertEquals(0, loanSchedule.get(2).get("feeChargesDue")); + assertEquals(0, loanSchedule.get(2).get("feeChargesOutstanding")); + assertEquals(PENALTY_PORTION + PENALTY_PORTION, loanSchedule.get(2).get("penaltyChargesDue")); + assertEquals(0.0f, loanSchedule.get(2).get("penaltyChargesOutstanding")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("penaltyChargesWaived")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("penaltyChargesPaid")); + assertEquals(PENALTY_PORTION + PENALTY_PORTION, loanSchedule.get(2).get("totalDueForPeriod")); + assertEquals(0.0f, loanSchedule.get(2).get("totalOutstandingForPeriod")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("totalWaivedForPeriod")); + // Might need to change if refund should update the due date of N+1 instalment + assertEquals(LocalDate.of(2011, 4, 14), LocalDate.of((int) ((List) loanSchedule.get(2).get("dueDate")).get(0), + (int) ((List) loanSchedule.get(2).get("dueDate")).get(1), (int) ((List) loanSchedule.get(2).get("dueDate")).get(2))); + + loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanAccountIsClosed(loanStatusHashMap); + + loanTransactionHelper.loanChargeRefund(penalty2LoanChargeId, null, PENALTY_PORTION, null, loanID, "resourceId"); + + loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanAccountIsOverPaid(loanStatusHashMap); + + loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, responseSpec, loanID); + assertEquals(3, loanSchedule.size()); + assertEquals(0, loanSchedule.get(2).get("feeChargesDue")); + assertEquals(0, loanSchedule.get(2).get("feeChargesOutstanding")); + assertEquals(PENALTY_PORTION + PENALTY_PORTION, loanSchedule.get(2).get("penaltyChargesDue")); + assertEquals(0.0f, loanSchedule.get(2).get("penaltyChargesOutstanding")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("penaltyChargesWaived")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("penaltyChargesPaid")); + assertEquals(PENALTY_PORTION + PENALTY_PORTION, loanSchedule.get(2).get("totalDueForPeriod")); + assertEquals(0.0f, loanSchedule.get(2).get("totalOutstandingForPeriod")); + assertEquals(PENALTY_PORTION, loanSchedule.get(2).get("totalWaivedForPeriod")); + // Might need to change if refund should update the due date of N+1 instalment + assertEquals(LocalDate.of(2011, 4, 14), LocalDate.of((int) ((List) loanSchedule.get(2).get("dueDate")).get(0), + (int) ((List) loanSchedule.get(2).get("dueDate")).get(1), (int) ((List) loanSchedule.get(2).get("dueDate")).get(2))); + } + + private Integer applyForLoanApplication(final Integer clientID, final Integer loanProductID, String interestRate) { + LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------"); + final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(LP_PRINCIPAL.toString()) + .withLoanTermFrequency(LOAN_TERM_FREQUENCY).withLoanTermFrequencyAsMonths().withNumberOfRepayments(LP_REPAYMENTS) + .withRepaymentEveryAfter(LP_REPAYMENT_PERIOD).withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod(interestRate) + .withInterestTypeAsFlatBalance().withAmortizationTypeAsEqualPrincipalPayments() + .withInterestCalculationPeriodTypeSameAsRepaymentPeriod().withExpectedDisbursementDate(EXPECTED_DISBURSAL_DATE) + .withSubmittedOnDate(LOAN_APPLICATION_SUBMISSION_DATE).withLoanType(INDIVIDUAL_LOAN) + .build(clientID.toString(), loanProductID.toString(), null); + return this.loanTransactionHelper.getLoanId(loanApplicationJSON); + } + + private Integer createLoanProductWithPeriodicAccrualAccountingWithInterestRecalculationEnabled(final Account... accounts) { + LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------"); + final String loanProductJSON = new LoanProductTestBuilder().withPrincipal("10000.0").withNumberOfRepayments("1") + .withinterestRatePerPeriod("1").withInterestRateFrequencyTypeAsYear().withInterestTypeAsDecliningBalance() + .withInterestCalculationPeriodTypeAsDays() + .withInterestRecalculationDetails(LoanProductTestBuilder.RECALCULATION_COMPOUNDING_METHOD_NONE, + LoanProductTestBuilder.RECALCULATION_STRATEGY_REDUCE_NUMBER_OF_INSTALLMENTS, + LoanProductTestBuilder.INTEREST_APPLICABLE_STRATEGY_ON_PRE_CLOSE_DATE) + .withInterestRecalculationRestFrequencyDetails(LoanProductTestBuilder.RECALCULATION_FREQUENCY_TYPE_DAILY, "0", null, null) + .withInterestRecalculationCompoundingFrequencyDetails(null, null, null, null).withMoratorium("0", "0") + .withInterestCalculationPeriodTypeAsRepaymentPeriod(true).withAccountingRulePeriodicAccrual(accounts).build(null); + return this.loanTransactionHelper.getLoanProductId(loanProductJSON); + } + + private Integer createLoanProductWithPeriodicAccrualAccountingNoInterest(final Account... accounts) { + LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------"); + final String loanProductJSON = new LoanProductTestBuilder().withPrincipal(LP_PRINCIPAL.toString()).withRepaymentTypeAsMonth() + .withRepaymentAfterEvery(LP_REPAYMENT_PERIOD).withNumberOfRepayments(LP_REPAYMENTS).withRepaymentTypeAsMonth() + .withinterestRatePerPeriod("0").withInterestRateFrequencyTypeAsMonths().withAmortizationTypeAsEqualPrincipalPayment() + .withInterestTypeAsFlat().withAccountingRulePeriodicAccrual(accounts).withDaysInMonth("30").withDaysInYear("365") + .withMoratorium("0", "0").build(null); + return this.loanTransactionHelper.getLoanProductId(loanProductJSON); + } + +} 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 58a1ddf32..7f30e4341 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 @@ -19,6 +19,7 @@ package org.apache.fineract.integrationtests.common.loans; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import com.google.gson.Gson; @@ -951,7 +952,14 @@ public class LoanTransactionHelper { } } assertTrue(isTransactionFound, "No Accrual entries are posted"); + } + public void noAccrualTransactionForRepayment(final Integer loanID) { + ArrayList transactions = (ArrayList) getLoanTransactions(this.requestSpec, this.responseSpec, loanID); + for (HashMap transaction : transactions) { + HashMap transactionType = (HashMap) transaction.get("type"); + assertFalse((Boolean) transactionType.get("accrual"), "Accrual entries are posted!"); + } } public HashMap makeRefundByCash(final String date, final Float amountToBeRefunded, final Integer loanID) {